gargle/0000755000176200001440000000000014456300740011516 5ustar liggesusersgargle/NAMESPACE0000644000176200001440000000363214436207160012741 0ustar liggesusers# Generated by roxygen2: do not edit by hand S3method(format,gargle_oauth_dat) S3method(gargle_map_cli,"NULL") S3method(gargle_map_cli,character) S3method(gargle_map_cli,default) S3method(print,gargle_oauth_client) S3method(print,gargle_oauth_dat) export(AuthState) export(Gargle2.0) export(GceToken) export(WifToken) export(bulletize) export(check_is_service_account) export(cred_funs_add) export(cred_funs_clear) export(cred_funs_list) export(cred_funs_list_default) export(cred_funs_set) export(cred_funs_set_default) export(credentials_app_default) export(credentials_byo_oauth2) export(credentials_external_account) export(credentials_gce) export(credentials_service_account) export(credentials_user_oauth2) export(field_mask) export(gargle2.0_token) export(gargle_api_key) export(gargle_app) export(gargle_client) export(gargle_error_message) export(gargle_map_cli) export(gargle_oauth_cache) export(gargle_oauth_client) export(gargle_oauth_client_from_json) export(gargle_oauth_client_type) export(gargle_oauth_email) export(gargle_oauth_sitrep) export(gargle_oob_default) export(gargle_verbosity) export(gce_access_token) export(gce_instance_service_accounts) export(init_AuthState) export(local_cred_funs) export(local_gargle_verbosity) export(oauth_app_from_json) export(oauth_external_token) export(request_build) export(request_develop) export(request_make) export(request_retry) export(response_as_json) export(response_process) export(secret_decrypt_json) export(secret_encrypt_json) export(secret_has_key) export(secret_make_key) export(secret_read_rds) export(secret_write_rds) export(tidyverse_api_key) export(tidyverse_app) export(tidyverse_client) export(token_email) export(token_fetch) export(token_tokeninfo) export(token_userinfo) export(with_cred_funs) export(with_gargle_verbosity) import(fs) import(rlang) importFrom(glue,glue) importFrom(glue,glue_collapse) importFrom(glue,glue_data) importFrom(lifecycle,deprecated) gargle/LICENSE0000644000176200001440000000005414431310014012506 0ustar liggesusersYEAR: 2023 COPYRIGHT HOLDER: gargle authors gargle/README.md0000644000176200001440000001072214456265371013011 0ustar liggesusers # gargle [![CRAN status](https://www.r-pkg.org/badges/version/gargle)](https://cran.r-project.org/package=gargle) [![Codecov test coverage](https://codecov.io/gh/r-lib/gargle/branch/main/graph/badge.svg)](https://app.codecov.io/gh/r-lib/gargle?branch=main) [![R-CMD-check](https://github.com/r-lib/gargle/actions/workflows/R-CMD-check.yaml/badge.svg)](https://github.com/r-lib/gargle/actions/workflows/R-CMD-check.yaml) The goal of gargle is to take some of the agonizing pain out of working with Google APIs. This includes functions and classes for handling common credential types and for preparing, executing, and processing HTTP requests. The target user of gargle is an *R package author* who is wrapping one of the ~250 Google APIs listed in the [APIs Explorer](https://developers.google.com/apis-explorer). gargle aims to play roughly the same role as [Google’s official client libraries](https://developers.google.com/api-client-library/), but for R. gargle may also be useful to useRs making direct calls to Google APIs, who are prepared to navigate the details of low-level API access. gargle’s functionality falls into two main domains: - **Auth.** The `token_fetch()` function calls a series of concrete credential-fetching functions to obtain a valid access token (or it quietly dies trying). - This covers explicit service accounts, application default credentials, Google Compute Engine, (experimentally) workload identity federation, and the standard OAuth2 browser flow. - gargle offers the `Gargle2.0` class, which extends `httr::Token2.0`. It is the default class for user OAuth 2.0 credentials. There are two main differences from `httr::Token2.0`: greater emphasis on the user’s email (e.g. Google identity) and default token caching is at the user level. - **Requests and responses**. A family of functions helps to prepare HTTP requests, (possibly with reference to an API spec derived from a Discovery Document), make requests, and process the response. See the [articles](https://gargle.r-lib.org/articles/) for holistic advice on how to use gargle. ## Installation You can install the released version of gargle from [CRAN](https://CRAN.R-project.org) with: ``` r install.packages("gargle") ``` And the development version from [GitHub](https://github.com/) with: ``` r # install.packages("pak") pak::pak("r-lib/gargle") ``` ## Basic usage gargle is a low-level package and does not do anything visibly exciting on its own. But here’s a bit of usage in an interactive scenario where a user confirms they want to use a specific Google identity and loads an OAuth2 token. ``` r library(gargle) token <- token_fetch() #> The gargle package is requesting access to your Google account. #> Enter '1' to start a new auth process or select a pre-authorized account. #> 1: Send me to the browser for a new auth process. #> 2: janedoe_personal@gmail.com #> 3: janedoe@example.com #> Selection: 2 token #> ── ───────────────────────────────────────────────────── #> oauth_endpoint: google #> app: gargle-clio #> email: janedoe_personal@gmail.com #> scopes: ...userinfo.email #> credentials: access_token, expires_in, refresh_token, scope, token_type, id_token ``` Here’s an example of using request and response helpers to make a one-off request to the [Web Fonts Developer API](https://developers.google.com/fonts/docs/developer_api). We show the most popular web font families served by Google Fonts. ``` r library(gargle) req <- request_build( method = "GET", path = "webfonts/v1/webfonts", params = list( sort = "popularity" ), key = gargle_api_key(), base_url = "https://www.googleapis.com" ) resp <- request_make(req) out <- response_process(resp) out <- out[["items"]][1:8] sort(vapply(out, function(x) x[["family"]], character(1))) #> [1] "Lato" "Material Icons" "Montserrat" "Noto Sans JP" #> [5] "Open Sans" "Poppins" "Roboto" "Roboto Condensed" ``` Please note that the ‘gargle’ project is released with a [Contributor Code of Conduct](https://gargle.r-lib.org/CODE_OF_CONDUCT.html). By contributing to this project, you agree to abide by its terms. [Privacy policy](https://www.tidyverse.org/google_privacy_policy) gargle/man/0000755000176200001440000000000014444241216012270 5ustar liggesusersgargle/man/credentials_service_account.Rd0000644000176200001440000000511314440363243020311 0ustar liggesusers% Generated by roxygen2: do not edit by hand % Please edit documentation in R/credentials_service_account.R \name{credentials_service_account} \alias{credentials_service_account} \title{Load a service account token} \usage{ credentials_service_account(scopes = NULL, path = "", ..., subject = NULL) } \arguments{ \item{scopes}{A character vector of scopes to request. Pick from those listed at \url{https://developers.google.com/identity/protocols/oauth2/scopes}. For certain token flows, the \code{"https://www.googleapis.com/auth/userinfo.email"} scope is unconditionally included. This grants permission to retrieve the email address associated with a token; gargle uses this to index cached OAuth tokens. This grants no permission to view or send email and is generally considered a low-value scope.} \item{path}{JSON identifying the service account, in one of the forms supported for the \code{txt} argument of \code{\link[jsonlite:fromJSON]{jsonlite::fromJSON()}} (typically, a file path or JSON string).} \item{...}{Additional arguments passed to all credential functions.} \item{subject}{An optional subject claim. Specify this if you wish to use the service account represented by \code{path} to impersonate the \code{subject}, who is a normal user. Before this can work, an administrator must grant the service account domain-wide authority. Identify the user to impersonate via their email, e.g. \code{subject = "user@example.com"}. Note that gargle automatically adds the non-sensitive \code{"https://www.googleapis.com/auth/userinfo.email"} scope, so this scope must be enabled for the service account, along with any other \code{scopes} being requested.} } \value{ An \code{\link[httr:Token-class]{httr::TokenServiceAccount}} or \code{NULL}. } \description{ Load a service account token } \details{ Note that fetching a token for a service account requires a reasonably accurate system clock. For more information, see the \code{vignette("how-gargle-gets-tokens")}. } \examples{ \dontrun{ token <- credentials_service_account( scopes = "https://www.googleapis.com/auth/userinfo.email", path = "/path/to/your/service-account.json" ) } } \seealso{ Additional reading on delegation of domain-wide authority: \itemize{ \item \url{https://developers.google.com/identity/protocols/oauth2/service-account#delegatingauthority} } Other credential functions: \code{\link{credentials_app_default}()}, \code{\link{credentials_byo_oauth2}()}, \code{\link{credentials_external_account}()}, \code{\link{credentials_gce}()}, \code{\link{credentials_user_oauth2}()}, \code{\link{token_fetch}()} } \concept{credential functions} gargle/man/gargle_map_cli.Rd0000644000176200001440000000050114321544762015506 0ustar liggesusers% Generated by roxygen2: do not edit by hand % Please edit documentation in R/utils-ui.R \name{gargle_map_cli} \alias{gargle_map_cli} \title{Map a cli-styled template over an object} \usage{ gargle_map_cli(x, ...) } \description{ For internal use in gargle, googledrive, and googlesheets4 (for now). } \keyword{internal} gargle/man/credentials_app_default.Rd0000644000176200001440000000665114440363243017431 0ustar liggesusers% Generated by roxygen2: do not edit by hand % Please edit documentation in R/credentials_app_default.R \name{credentials_app_default} \alias{credentials_app_default} \title{Load Application Default Credentials} \usage{ credentials_app_default(scopes = NULL, ..., subject = NULL) } \arguments{ \item{scopes}{A character vector of scopes to request. Pick from those listed at \url{https://developers.google.com/identity/protocols/oauth2/scopes}. For certain token flows, the \code{"https://www.googleapis.com/auth/userinfo.email"} scope is unconditionally included. This grants permission to retrieve the email address associated with a token; gargle uses this to index cached OAuth tokens. This grants no permission to view or send email and is generally considered a low-value scope.} \item{...}{Additional arguments passed to all credential functions.} \item{subject}{An optional subject claim. Specify this if you wish to use the service account represented by \code{path} to impersonate the \code{subject}, who is a normal user. Before this can work, an administrator must grant the service account domain-wide authority. Identify the user to impersonate via their email, e.g. \code{subject = "user@example.com"}. Note that gargle automatically adds the non-sensitive \code{"https://www.googleapis.com/auth/userinfo.email"} scope, so this scope must be enabled for the service account, along with any other \code{scopes} being requested.} } \value{ An \code{\link[httr:Token-class]{httr::TokenServiceAccount}}, a \code{\link{WifToken}}, an \code{\link[httr:Token-class]{httr::Token2.0}} or \code{NULL}. } \description{ Loads credentials from a file identified via a search strategy known as Application Default Credentials (ADC). The hope is to make auth "just work" for someone working on Google-provided infrastructure or who has used Google tooling to get started, such as the \href{https://cloud.google.com/sdk/gcloud}{\code{gcloud} command line tool}. A sequence of paths is consulted, which we describe here, with some abuse of notation. ALL_CAPS represents the value of an environment variable and \verb{\%||\%} is used in the spirit of a \href{https://en.wikipedia.org/wiki/Null_coalescing_operator}{null coalescing operator}. \if{html}{\out{
}}\preformatted{GOOGLE_APPLICATION_CREDENTIALS CLOUDSDK_CONFIG/application_default_credentials.json # on Windows: (APPDATA \%||\% SystemDrive \%||\% C:)\\gcloud\\application_default_credentials.json # on not-Windows: ~/.config/gcloud/application_default_credentials.json }\if{html}{\out{
}} If the above search successfully identifies a JSON file, it is parsed and ingested as a service account, an external account ("workload identity federation"), or a user account. Literally, if the JSON describes a service account, we call \code{\link[=credentials_service_account]{credentials_service_account()}} and if it describes an external account, we call \code{\link[=credentials_external_account]{credentials_external_account()}}. } \examples{ \dontrun{ credentials_app_default() } } \seealso{ \itemize{ \item \url{https://cloud.google.com/docs/authentication#adc} \item \url{https://cloud.google.com/sdk/docs/} } Other credential functions: \code{\link{credentials_byo_oauth2}()}, \code{\link{credentials_external_account}()}, \code{\link{credentials_gce}()}, \code{\link{credentials_service_account}()}, \code{\link{credentials_user_oauth2}()}, \code{\link{token_fetch}()} } \concept{credential functions} gargle/man/bulletize.Rd0000644000176200001440000000051314321544762014563 0ustar liggesusers% Generated by roxygen2: do not edit by hand % Please edit documentation in R/utils-ui.R \name{bulletize} \alias{bulletize} \title{Abbreviate a bullet list neatly} \usage{ bulletize(x, bullet = "*", n_show = 5, n_fudge = 2) } \description{ For internal use in gargle, googledrive, and googlesheets4 (for now). } \keyword{internal} gargle/man/field_mask.Rd0000644000176200001440000000321514431310014014643 0ustar liggesusers% Generated by roxygen2: do not edit by hand % Please edit documentation in R/field_mask.R \name{field_mask} \alias{field_mask} \title{Generate a field mask} \usage{ field_mask(x) } \arguments{ \item{x}{A named R list, where the requirement for names applies at all levels, i.e. recursively.} } \value{ A Google API field mask, as a string. } \description{ Many Google API requests take a field mask, via a \code{fields} parameter, in the URL and/or in the body. \code{field_mask()} generates such a field mask from an R list, typically a list that is destined to be part of the body of a request that writes or updates a resource. \code{field_mask()} is designed to help in the common case where the attributes you wish to modify are exactly the ones represented in the object. It is possible to use a "larger" field mask, that is either less specific or that explicitly includes other attributes, in which case the attributes covered by the mask but absent from the object are reset to default values. This is not exactly the use case \code{field_mask()} is designed for, but its output could still be useful as a first step in constructing such a mask. } \examples{ x <- list(sheetId = 1234, title = "my_favorite_worksheet") field_mask(x) x <- list( userEnteredFormat = list( backgroundColor = list( red = 159 / 255, green = 183 / 255, blue = 196 / 255 ) ) ) field_mask(x) x <- list( sheetId = 1234, gridProperties = list(rowCount = 5, columnCount = 3) ) field_mask(x) } \seealso{ The documentation for the \href{https://protobuf.dev/reference/protobuf/google.protobuf/#json-encoding-of-field-masks}{JSON encoding of a Protocol Buffers FieldMask}. } gargle/man/credentials_gce.Rd0000644000176200001440000001015614431310014015662 0ustar liggesusers% Generated by roxygen2: do not edit by hand % Please edit documentation in R/credentials_gce.R \name{credentials_gce} \alias{credentials_gce} \title{Get a token from the Google metadata server} \usage{ credentials_gce( scopes = "https://www.googleapis.com/auth/cloud-platform", service_account = "default", ... ) } \arguments{ \item{scopes}{A character vector of scopes to request. Pick from those listed at \url{https://developers.google.com/identity/protocols/oauth2/scopes}. For certain token flows, the \code{"https://www.googleapis.com/auth/userinfo.email"} scope is unconditionally included. This grants permission to retrieve the email address associated with a token; gargle uses this to index cached OAuth tokens. This grants no permission to view or send email and is generally considered a low-value scope.} \item{service_account}{Name of the GCE service account to use.} \item{...}{Additional arguments passed to all credential functions.} } \value{ A \code{\link[=GceToken]{GceToken()}} or \code{NULL}. } \description{ If your code is running on Google Cloud, we can often obtain a token for an attached service account directly from a metadata server. This is more secure than working with an explicit a service account key, as \code{\link[=credentials_service_account]{credentials_service_account()}} does, and is the preferred method of auth for workloads running on Google Cloud. The most straightforward scenario is when you are working in a VM on Google Compute Engine and it's OK to use the default service account. This should "just work" automatically. \code{credentials_gce()} supports other use cases (such as GKE Workload Identity), but may require some explicit setup, such as: \itemize{ \item Create a service account, grant it appropriate scopes(s) and IAM roles, attach it to the target resource. This prep work happens outside of R, e.g., in the Google Cloud Console. On the R side, provide the email address of this appropriately configured service account via \code{service_account}. \item Specify details for constructing the root URL of the metadata service: \itemize{ \item The logical option \code{"gargle.gce.use_ip"}. If undefined, this defaults to \code{FALSE}. \item The environment variable \code{GCE_METADATA_URL} is consulted when \code{"gargle.gce.use_ip"} is \code{FALSE}. If undefined, the default is \code{metadata.google.internal}. \item The environment variable \code{GCE_METADATA_IP} is consulted when \code{"gargle.gce.use_ip"} is \code{TRUE}. If undefined, the default is \verb{169.254.169.254}. } \item Change (presumably increase) the timeout for requests to the metadata server via the \code{"gargle.gce.timeout"} global option. This timeout is given in seconds and is set to a value (strategy, really) that often works well in practice. However, in some cases it may be necessary to increase the timeout with code such as: } \if{html}{\out{
}}\preformatted{options(gargle.gce.timeout = 3) }\if{html}{\out{
}} For details on specific use cases, such as Google Kubernetes Engine (GKE), see \code{vignette("non-interactive-auth")}. } \examples{ \dontrun{ credentials_gce() } } \seealso{ A related auth flow that can be used on certain non-Google cloud providers is workload identity federation, which is implemented in \code{\link[=credentials_external_account]{credentials_external_account()}}. \url{https://cloud.google.com/compute/docs/access/service-accounts} \url{https://cloud.google.com/iam/docs/best-practices-service-accounts} How to attach a service account to a resource: \url{https://cloud.google.com/iam/docs/impersonating-service-accounts#attaching-to-resources} \url{https://cloud.google.com/kubernetes-engine/docs/concepts/workload-identity} \url{https://cloud.google.com/kubernetes-engine/docs/how-to/workload-identity} \url{https://cloud.google.com/compute/docs/metadata/overview} Other credential functions: \code{\link{credentials_app_default}()}, \code{\link{credentials_byo_oauth2}()}, \code{\link{credentials_external_account}()}, \code{\link{credentials_service_account}()}, \code{\link{credentials_user_oauth2}()}, \code{\link{token_fetch}()} } \concept{credential functions} gargle/man/GceToken.Rd0000644000176200001440000001424014431310014014244 0ustar liggesusers% Generated by roxygen2: do not edit by hand % Please edit documentation in R/credentials_gce.R \name{GceToken} \alias{GceToken} \title{Token for use on Google Compute Engine instances} \description{ Token for use on Google Compute Engine instances Token for use on Google Compute Engine instances } \details{ This class uses the metadata service available on GCE VMs to fetch access tokens. Not intended for direct use. See \code{\link[=credentials_gce]{credentials_gce()}} instead. } \keyword{internal} \section{Super classes}{ \code{\link[httr:Token]{httr::Token}} -> \code{\link[httr:Token2.0]{httr::Token2.0}} -> \code{GceToken} } \section{Methods}{ \subsection{Public methods}{ \itemize{ \item \href{#method-GceToken-new}{\code{GceToken$new()}} \item \href{#method-GceToken-init_credentials}{\code{GceToken$init_credentials()}} \item \href{#method-GceToken-refresh}{\code{GceToken$refresh()}} \item \href{#method-GceToken-can_refresh}{\code{GceToken$can_refresh()}} \item \href{#method-GceToken-format}{\code{GceToken$format()}} \item \href{#method-GceToken-print}{\code{GceToken$print()}} \item \href{#method-GceToken-cache}{\code{GceToken$cache()}} \item \href{#method-GceToken-load_from_cache}{\code{GceToken$load_from_cache()}} \item \href{#method-GceToken-revoke}{\code{GceToken$revoke()}} \item \href{#method-GceToken-validate}{\code{GceToken$validate()}} \item \href{#method-GceToken-clone}{\code{GceToken$clone()}} } } \if{html}{\out{
Inherited methods
}} \if{html}{\out{
}} \if{html}{\out{}} \if{latex}{\out{\hypertarget{method-GceToken-new}{}}} \subsection{Method \code{new()}}{ Get an access for a GCE service account. \subsection{Usage}{ \if{html}{\out{
}}\preformatted{GceToken$new(params)}\if{html}{\out{
}} } \subsection{Arguments}{ \if{html}{\out{
}} \describe{ \item{\code{params}}{A list of parameters for \code{fetch_gce_access_token()}.} } \if{html}{\out{
}} } \subsection{Returns}{ A GceToken. } } \if{html}{\out{
}} \if{html}{\out{}} \if{latex}{\out{\hypertarget{method-GceToken-init_credentials}{}}} \subsection{Method \code{init_credentials()}}{ Request an access token. \subsection{Usage}{ \if{html}{\out{
}}\preformatted{GceToken$init_credentials()}\if{html}{\out{
}} } } \if{html}{\out{
}} \if{html}{\out{}} \if{latex}{\out{\hypertarget{method-GceToken-refresh}{}}} \subsection{Method \code{refresh()}}{ Refreshes the token. In this case, that just means "ask again for an access token". \subsection{Usage}{ \if{html}{\out{
}}\preformatted{GceToken$refresh()}\if{html}{\out{
}} } } \if{html}{\out{
}} \if{html}{\out{}} \if{latex}{\out{\hypertarget{method-GceToken-can_refresh}{}}} \subsection{Method \code{can_refresh()}}{ Placeholder implementation of required method. Returns \code{TRUE}. \subsection{Usage}{ \if{html}{\out{
}}\preformatted{GceToken$can_refresh()}\if{html}{\out{
}} } } \if{html}{\out{
}} \if{html}{\out{}} \if{latex}{\out{\hypertarget{method-GceToken-format}{}}} \subsection{Method \code{format()}}{ Format a \code{\link[=GceToken]{GceToken()}}. \subsection{Usage}{ \if{html}{\out{
}}\preformatted{GceToken$format(...)}\if{html}{\out{
}} } \subsection{Arguments}{ \if{html}{\out{
}} \describe{ \item{\code{...}}{Not used.} } \if{html}{\out{
}} } } \if{html}{\out{
}} \if{html}{\out{}} \if{latex}{\out{\hypertarget{method-GceToken-print}{}}} \subsection{Method \code{print()}}{ Print a \code{\link[=GceToken]{GceToken()}}. \subsection{Usage}{ \if{html}{\out{
}}\preformatted{GceToken$print(...)}\if{html}{\out{
}} } \subsection{Arguments}{ \if{html}{\out{
}} \describe{ \item{\code{...}}{Not used.} } \if{html}{\out{
}} } } \if{html}{\out{
}} \if{html}{\out{}} \if{latex}{\out{\hypertarget{method-GceToken-cache}{}}} \subsection{Method \code{cache()}}{ Placeholder implementation of required method. \subsection{Usage}{ \if{html}{\out{
}}\preformatted{GceToken$cache()}\if{html}{\out{
}} } } \if{html}{\out{
}} \if{html}{\out{}} \if{latex}{\out{\hypertarget{method-GceToken-load_from_cache}{}}} \subsection{Method \code{load_from_cache()}}{ Placeholder implementation of required method. \subsection{Usage}{ \if{html}{\out{
}}\preformatted{GceToken$load_from_cache()}\if{html}{\out{
}} } } \if{html}{\out{
}} \if{html}{\out{}} \if{latex}{\out{\hypertarget{method-GceToken-revoke}{}}} \subsection{Method \code{revoke()}}{ Placeholder implementation of required method. \subsection{Usage}{ \if{html}{\out{
}}\preformatted{GceToken$revoke()}\if{html}{\out{
}} } } \if{html}{\out{
}} \if{html}{\out{}} \if{latex}{\out{\hypertarget{method-GceToken-validate}{}}} \subsection{Method \code{validate()}}{ Placeholder implementation of required method \subsection{Usage}{ \if{html}{\out{
}}\preformatted{GceToken$validate()}\if{html}{\out{
}} } } \if{html}{\out{
}} \if{html}{\out{}} \if{latex}{\out{\hypertarget{method-GceToken-clone}{}}} \subsection{Method \code{clone()}}{ The objects of this class are cloneable with this method. \subsection{Usage}{ \if{html}{\out{
}}\preformatted{GceToken$clone(deep = FALSE)}\if{html}{\out{
}} } \subsection{Arguments}{ \if{html}{\out{
}} \describe{ \item{\code{deep}}{Whether to make a deep clone.} } \if{html}{\out{
}} } } } gargle/man/gargle_api_key.Rd0000644000176200001440000000161714431310014015513 0ustar liggesusers% Generated by roxygen2: do not edit by hand % Please edit documentation in R/gargle_api_key.R \name{gargle_api_key} \alias{gargle_api_key} \title{API key for demonstration purposes} \usage{ gargle_api_key() } \description{ Some APIs accept requests for public resources, in which case the request must be sent with an API key in lieu of a token. This function provides an API key for limited use in prototyping and for testing and documentation of gargle itself. This key may be deleted or rotated at any time. There are no guarantees about which APIs are enabled. DO NOT USE THIS IN A PACKAGE or for anything other than interactive, small-scale experimentation. You can get your own API key, without these limitations. See the \href{https://gargle.r-lib.org/articles/get-api-credentials.html}{How to get your own API credentials} vignette for more details. } \examples{ gargle_api_key() } \keyword{internal} gargle/man/token_fetch.Rd0000644000176200001440000000342314431310014015037 0ustar liggesusers% Generated by roxygen2: do not edit by hand % Please edit documentation in R/token_fetch.R \name{token_fetch} \alias{token_fetch} \title{Fetch a token for the given scopes} \usage{ token_fetch(scopes = NULL, ...) } \arguments{ \item{scopes}{A character vector of scopes to request. Pick from those listed at \url{https://developers.google.com/identity/protocols/oauth2/scopes}. For certain token flows, the \code{"https://www.googleapis.com/auth/userinfo.email"} scope is unconditionally included. This grants permission to retrieve the email address associated with a token; gargle uses this to index cached OAuth tokens. This grants no permission to view or send email and is generally considered a low-value scope.} \item{...}{Additional arguments passed to all credential functions.} } \value{ An \code{\link[httr:Token-class]{httr::Token}} (often an instance of something that inherits from \code{httr::Token}) or \code{NULL}. } \description{ This is a rather magical function that calls a series of concrete credential-fetching functions, each wrapped in a \code{tryCatch()}. \code{token_fetch()} keeps trying until it succeeds or there are no more functions to try. See the \code{vignette("how-gargle-gets-tokens")} for a full description of \code{token_fetch()}. } \examples{ \dontrun{ token_fetch(scopes = "https://www.googleapis.com/auth/userinfo.email") } } \seealso{ \code{\link[=cred_funs_list]{cred_funs_list()}} reveals the current registry of credential-fetching functions, in order. Other credential functions: \code{\link{credentials_app_default}()}, \code{\link{credentials_byo_oauth2}()}, \code{\link{credentials_external_account}()}, \code{\link{credentials_gce}()}, \code{\link{credentials_service_account}()}, \code{\link{credentials_user_oauth2}()} } \concept{credential functions} gargle/man/internal-assets.Rd0000644000176200001440000000071314431310014015661 0ustar liggesusers% Generated by roxygen2: do not edit by hand % Please edit documentation in R/gargle_api_key.R, R/gargle_oauth_client.R \name{internal-assets} \alias{internal-assets} \alias{tidyverse_api_key} \alias{tidyverse_client} \alias{tidyverse_app} \title{Assets for internal use} \usage{ tidyverse_api_key() tidyverse_client(type = NULL) tidyverse_app() } \description{ Assets for use inside specific packages maintained by the tidyverse team. } \keyword{internal} gargle/man/gargle2.0_token.Rd0000644000176200001440000000717614433520365015456 0ustar liggesusers% Generated by roxygen2: do not edit by hand % Please edit documentation in R/Gargle-class.R \name{gargle2.0_token} \alias{gargle2.0_token} \title{Generate a gargle token} \usage{ gargle2.0_token( email = gargle_oauth_email(), client = gargle_client(), package = "gargle", scope = NULL, use_oob = gargle_oob_default(), credentials = NULL, cache = if (is.null(credentials)) gargle_oauth_cache() else FALSE, ..., app = deprecated() ) } \arguments{ \item{email}{Optional. If specified, \code{email} can take several different forms: \itemize{ \item \code{"jane@gmail.com"}, i.e. an actual email address. This allows the user to target a specific Google identity. If specified, this is used for token lookup, i.e. to determine if a suitable token is already available in the cache. If no such token is found, \code{email} is used to pre-select the targeted Google identity in the OAuth chooser. (Note, however, that the email associated with a token when it's cached is always determined from the token itself, never from this argument). \item \code{"*@example.com"}, i.e. a domain-only glob pattern. This can be helpful if you need code that "just works" for both \code{alice@example.com} and \code{bob@example.com}. \item \code{TRUE} means that you are approving email auto-discovery. If exactly one matching token is found in the cache, it will be used. \item \code{FALSE} or \code{NA} mean that you want to ignore the token cache and force a new OAuth dance in the browser. } Defaults to the option named \code{"gargle_oauth_email"}, retrieved by \code{\link[=gargle_oauth_email]{gargle_oauth_email()}} (unless a wrapper package implements different default behavior).} \item{client}{A Google OAuth client, preferably constructed via \code{\link[=gargle_oauth_client_from_json]{gargle_oauth_client_from_json()}}, which returns an instance of \code{gargle_oauth_client}. For backwards compatibility, for a limited time, gargle will still accept an "OAuth app" created with \code{\link[httr:oauth_app]{httr::oauth_app()}}.} \item{package}{Name of the package requesting a token. Used in messages.} \item{scope}{A character vector of scopes to request.} \item{use_oob}{Whether to use out-of-band authentication (or, perhaps, a variant implemented by gargle and known as "pseudo-OOB") when first acquiring the token. Defaults to the value returned by \code{\link[=gargle_oob_default]{gargle_oob_default()}}. Note that (pseudo-)OOB auth only affects the initial OAuth dance. If we retrieve (and possibly refresh) a cached token, \code{use_oob} has no effect. If the OAuth client is provided implicitly by a wrapper package, its type probably defaults to the value returned by \code{\link[=gargle_oauth_client_type]{gargle_oauth_client_type()}}. You can take control of the client type by setting \code{options(gargle_oauth_client_type = "web")} or \code{options(gargle_oauth_client_type = "installed")}.} \item{credentials}{Advanced use only: allows you to completely customise token generation.} \item{cache}{Specifies the OAuth token cache. Defaults to the option named \code{"gargle_oauth_cache"}, retrieved via \code{\link[=gargle_oauth_cache]{gargle_oauth_cache()}}.} \item{...}{Absorbs arguments intended for use by other credential functions. Not used.} \item{app}{\ifelse{html}{\href{https://lifecycle.r-lib.org/articles/stages.html#deprecated}{\figure{lifecycle-deprecated.svg}{options: alt='[Deprecated]'}}}{\strong{[Deprecated]}} Replaced by the \code{client} argument.} } \value{ An object of class \link{Gargle2.0}, either new or loaded from the cache. } \description{ Constructor function for objects of class \link{Gargle2.0}. } \examples{ \dontrun{ gargle2.0_token() } } gargle/man/gce_instance_service_accounts.Rd0000644000176200001440000000161514431310014020610 0ustar liggesusers% Generated by roxygen2: do not edit by hand % Please edit documentation in R/credentials_gce.R \name{gce_instance_service_accounts} \alias{gce_instance_service_accounts} \title{List all service accounts available on this GCE instance} \usage{ gce_instance_service_accounts() } \value{ A data frame, where each row is a service account. Due to aliasing, there is no guarantee that each row represents a distinct service account. } \description{ List all service accounts available on this GCE instance } \examples{ \dontshow{if (gargle:::is_gce()) (if (getRversion() >= "3.4") withAutoprint else force)(\{ # examplesIf} credentials_gce() \dontshow{\}) # examplesIf} } \seealso{ The return value is built from a recursive query of the so-called "directory" of the instance's service accounts as documented in \url{https://cloud.google.com/compute/docs/metadata/default-metadata-values#vm_instance_metadata}. } gargle/man/gargle-package.Rd0000644000176200001440000000212714431310014015400 0ustar liggesusers% Generated by roxygen2: do not edit by hand % Please edit documentation in R/gargle-package.R \docType{package} \name{gargle-package} \alias{gargle} \alias{gargle-package} \title{gargle: Utilities for Working with Google APIs} \description{ Provides utilities for working with Google APIs \url{https://developers.google.com/apis-explorer}. This includes functions and classes for handling common credential types and for preparing, executing, and processing HTTP requests. } \seealso{ Useful links: \itemize{ \item \url{https://gargle.r-lib.org} \item \url{https://github.com/r-lib/gargle} \item Report bugs at \url{https://github.com/r-lib/gargle/issues} } } \author{ \strong{Maintainer}: Jennifer Bryan \email{jenny@posit.co} (\href{https://orcid.org/0000-0002-6983-2759}{ORCID}) Authors: \itemize{ \item Craig Citro \email{craigcitro@google.com} \item Hadley Wickham \email{hadley@posit.co} (\href{https://orcid.org/0000-0003-4757-117X}{ORCID}) } Other contributors: \itemize{ \item Google Inc [copyright holder] \item Posit Software, PBC [copyright holder, funder] } } \keyword{internal} gargle/man/gargle_oauth_client_from_json.Rd0000644000176200001440000000670014433520365020640 0ustar liggesusers% Generated by roxygen2: do not edit by hand % Please edit documentation in R/gargle_oauth_client.R \name{gargle_oauth_client_from_json} \alias{gargle_oauth_client_from_json} \alias{gargle_oauth_client} \title{Create an OAuth client for Google} \usage{ gargle_oauth_client_from_json(path, name = NULL) gargle_oauth_client( id, secret, redirect_uris = NULL, type = c("installed", "web"), name = hash(id) ) } \arguments{ \item{path}{JSON downloaded from \href{https://console.cloud.google.com}{Google Cloud Console}, containing a client id and secret, in one of the forms supported for the \code{txt} argument of \code{\link[jsonlite:fromJSON]{jsonlite::fromJSON()}} (typically, a file path or JSON string).} \item{name}{A label for this specific client, presumably the same name used to label it in Google Cloud Console. Unfortunately there is no way to make that true programmatically, i.e. the JSON representation does not contain this information.} \item{id}{Client ID} \item{secret}{Client secret} \item{redirect_uris}{Where your application listens for the response from Google's authorization server. If you didn't configure this specifically when creating the client (which is only possible for clients of the "web" type), you can leave this unspecified.} \item{type}{Specifies the type of OAuth client. The valid values are a subset of possible Google client types and reflect the key used to describe the client in its JSON representation: \itemize{ \item \code{"installed"} is associated with a "Desktop app" \item \code{"web"} is associated with a "Web application" }} } \value{ An OAuth client: An S3 list with class \code{gargle_oauth_client}. For backwards compatibility reasons, this currently also inherits from the httr S3 class \code{oauth_app}, but that is a temporary measure. An instance of \code{gargle_oauth_client} stores more information than httr's \code{oauth_app}, such as the OAuth client's type ("web" or "installed"). There are some redundant fields in this object during the httr-to-httr2 transition period. The legacy fields \code{appname} and \code{key} repeat the information in the future-facing fields \code{name} and (client) \code{id}. Prefer \code{name} and \code{id} to \code{appname} and \code{key} in downstream code. Prefer the constructors \code{gargle_oauth_client_from_json()} and \code{gargle_oauth_client()} to \code{\link[httr:oauth_app]{httr::oauth_app()}} and \code{\link[=oauth_app_from_json]{oauth_app_from_json()}}. } \description{ A \code{gargle_oauth_client} consists of: \itemize{ \item A type. gargle only supports the "Desktop app" and "Web application" client types. Different types are associated with different OAuth flows. \item A client ID and secret. \item Optionally, one or more redirect URIs. \item A name. This is really a human-facing label. Or, rather, it can be used that way, but the default is just a hash. We recommend using the same name here as the name used to label the client ID in the \href{https://console.cloud.google.com}{Google Cloud Platform Console}. } A \code{gargle_oauth_client} is an adaptation of httr's \code{\link[=oauth_app]{oauth_app()}} (currently) and httr2's \code{oauth_client()} (which gargle will migrate to in the future). } \examples{ \dontrun{ gargle_oauth_client_from_json( path = "/path/to/the/JSON/you/downloaded/from/gcp/console.json", name = "my-nifty-oauth-client" ) } gargle_oauth_client( id = "some_long_id", secret = "ssshhhhh_its_a_secret", name = "my-nifty-oauth-client" ) } gargle/man/gargle_oauth_sitrep.Rd0000644000176200001440000000204714431310014016576 0ustar liggesusers% Generated by roxygen2: do not edit by hand % Please edit documentation in R/oauth-cache.R \name{gargle_oauth_sitrep} \alias{gargle_oauth_sitrep} \title{OAuth token situation report} \usage{ gargle_oauth_sitrep(cache = NULL) } \arguments{ \item{cache}{Specifies the OAuth token cache. Defaults to the option named \code{"gargle_oauth_cache"}, retrieved via \code{\link[=gargle_oauth_cache]{gargle_oauth_cache()}}.} } \value{ A data frame with one row per cached token, invisibly. Note this data frame may contain more columns than it seems, e.g. the \code{filepath} column isn't printed by default. } \description{ Get a human-oriented overview of the existing gargle OAuth tokens: \itemize{ \item Filepath of the current cache \item Number of tokens found there \item Compact summary of the associated \itemize{ \item Email = Google identity \item OAuth client (actually, just its nickname) \item Scopes \item Hash (actually, just the first 7 characters) Mostly useful for the development of gargle and client packages. } } } \examples{ gargle_oauth_sitrep() } gargle/man/WifToken.Rd0000644000176200001440000001430414321544762014315 0ustar liggesusers% Generated by roxygen2: do not edit by hand % Please edit documentation in R/credentials_external_account.R \name{WifToken} \alias{WifToken} \title{Token for use with workload identity federation} \description{ Token for use with workload identity federation Token for use with workload identity federation } \details{ Not intended for direct use. See \code{\link[=credentials_external_account]{credentials_external_account()}} instead. } \keyword{internal} \section{Super classes}{ \code{\link[httr:Token]{httr::Token}} -> \code{\link[httr:Token2.0]{httr::Token2.0}} -> \code{WifToken} } \section{Methods}{ \subsection{Public methods}{ \itemize{ \item \href{#method-WifToken-new}{\code{WifToken$new()}} \item \href{#method-WifToken-init_credentials}{\code{WifToken$init_credentials()}} \item \href{#method-WifToken-refresh}{\code{WifToken$refresh()}} \item \href{#method-WifToken-format}{\code{WifToken$format()}} \item \href{#method-WifToken-print}{\code{WifToken$print()}} \item \href{#method-WifToken-can_refresh}{\code{WifToken$can_refresh()}} \item \href{#method-WifToken-cache}{\code{WifToken$cache()}} \item \href{#method-WifToken-load_from_cache}{\code{WifToken$load_from_cache()}} \item \href{#method-WifToken-validate}{\code{WifToken$validate()}} \item \href{#method-WifToken-revoke}{\code{WifToken$revoke()}} \item \href{#method-WifToken-clone}{\code{WifToken$clone()}} } } \if{html}{\out{
Inherited methods
}} \if{html}{\out{
}} \if{html}{\out{}} \if{latex}{\out{\hypertarget{method-WifToken-new}{}}} \subsection{Method \code{new()}}{ Get a token via workload identity federation \subsection{Usage}{ \if{html}{\out{
}}\preformatted{WifToken$new(params = list())}\if{html}{\out{
}} } \subsection{Arguments}{ \if{html}{\out{
}} \describe{ \item{\code{params}}{A list of parameters for \code{init_oauth_external_account()}.} } \if{html}{\out{
}} } \subsection{Returns}{ A WifToken. } } \if{html}{\out{
}} \if{html}{\out{}} \if{latex}{\out{\hypertarget{method-WifToken-init_credentials}{}}} \subsection{Method \code{init_credentials()}}{ Enact the actual token exchange for workload identity federation. \subsection{Usage}{ \if{html}{\out{
}}\preformatted{WifToken$init_credentials()}\if{html}{\out{
}} } } \if{html}{\out{
}} \if{html}{\out{}} \if{latex}{\out{\hypertarget{method-WifToken-refresh}{}}} \subsection{Method \code{refresh()}}{ Refreshes the token, which means re-doing the entire token flow in this case. \subsection{Usage}{ \if{html}{\out{
}}\preformatted{WifToken$refresh()}\if{html}{\out{
}} } } \if{html}{\out{
}} \if{html}{\out{}} \if{latex}{\out{\hypertarget{method-WifToken-format}{}}} \subsection{Method \code{format()}}{ Format a \code{\link[=WifToken]{WifToken()}}. \subsection{Usage}{ \if{html}{\out{
}}\preformatted{WifToken$format(...)}\if{html}{\out{
}} } \subsection{Arguments}{ \if{html}{\out{
}} \describe{ \item{\code{...}}{Not used.} } \if{html}{\out{
}} } } \if{html}{\out{
}} \if{html}{\out{}} \if{latex}{\out{\hypertarget{method-WifToken-print}{}}} \subsection{Method \code{print()}}{ Print a \code{\link[=WifToken]{WifToken()}}. \subsection{Usage}{ \if{html}{\out{
}}\preformatted{WifToken$print(...)}\if{html}{\out{
}} } \subsection{Arguments}{ \if{html}{\out{
}} \describe{ \item{\code{...}}{Not used.} } \if{html}{\out{
}} } } \if{html}{\out{
}} \if{html}{\out{}} \if{latex}{\out{\hypertarget{method-WifToken-can_refresh}{}}} \subsection{Method \code{can_refresh()}}{ Placeholder implementation of required method. Returns \code{TRUE}. \subsection{Usage}{ \if{html}{\out{
}}\preformatted{WifToken$can_refresh()}\if{html}{\out{
}} } } \if{html}{\out{
}} \if{html}{\out{}} \if{latex}{\out{\hypertarget{method-WifToken-cache}{}}} \subsection{Method \code{cache()}}{ Placeholder implementation of required method. Returns self. \subsection{Usage}{ \if{html}{\out{
}}\preformatted{WifToken$cache()}\if{html}{\out{
}} } } \if{html}{\out{
}} \if{html}{\out{}} \if{latex}{\out{\hypertarget{method-WifToken-load_from_cache}{}}} \subsection{Method \code{load_from_cache()}}{ Placeholder implementation of required method. Returns self. \subsection{Usage}{ \if{html}{\out{
}}\preformatted{WifToken$load_from_cache()}\if{html}{\out{
}} } } \if{html}{\out{
}} \if{html}{\out{}} \if{latex}{\out{\hypertarget{method-WifToken-validate}{}}} \subsection{Method \code{validate()}}{ Placeholder implementation of required method. \subsection{Usage}{ \if{html}{\out{
}}\preformatted{WifToken$validate()}\if{html}{\out{
}} } } \if{html}{\out{
}} \if{html}{\out{}} \if{latex}{\out{\hypertarget{method-WifToken-revoke}{}}} \subsection{Method \code{revoke()}}{ Placeholder implementation of required method. \subsection{Usage}{ \if{html}{\out{
}}\preformatted{WifToken$revoke()}\if{html}{\out{
}} } } \if{html}{\out{
}} \if{html}{\out{}} \if{latex}{\out{\hypertarget{method-WifToken-clone}{}}} \subsection{Method \code{clone()}}{ The objects of this class are cloneable with this method. \subsection{Usage}{ \if{html}{\out{
}}\preformatted{WifToken$clone(deep = FALSE)}\if{html}{\out{
}} } \subsection{Arguments}{ \if{html}{\out{
}} \describe{ \item{\code{deep}}{Whether to make a deep clone.} } \if{html}{\out{
}} } } } gargle/man/oauth_app_from_json.Rd0000644000176200001440000000256414431310014016607 0ustar liggesusers% Generated by roxygen2: do not edit by hand % Please edit documentation in R/gargle_oauth_client.R \name{oauth_app_from_json} \alias{oauth_app_from_json} \alias{gargle_app} \title{Create an OAuth app from JSON} \usage{ oauth_app_from_json(path, appname = NULL) gargle_app() } \arguments{ \item{path}{JSON downloaded from \href{https://console.cloud.google.com}{Google Cloud Console}, containing a client id and secret, in one of the forms supported for the \code{txt} argument of \code{\link[jsonlite:fromJSON]{jsonlite::fromJSON()}} (typically, a file path or JSON string).} \item{appname}{name of the application. This is not used for OAuth, but is used to make it easier to identify different applications.} } \description{ \ifelse{html}{\href{https://lifecycle.r-lib.org/articles/stages.html#deprecated}{\figure{lifecycle-deprecated.svg}{options: alt='[Deprecated]'}}}{\strong{[Deprecated]}} \code{oauth_app_from_json()} is being replaced with \code{\link[=gargle_oauth_client_from_json]{gargle_oauth_client_from_json()}}, in light of the new \code{gargle_oauth_client} class. Now \code{oauth_app_from_json()} potentially warns about this deprecation and immediately passes its inputs through to \code{\link[=gargle_oauth_client_from_json]{gargle_oauth_client_from_json()}}. \code{gargle_app()} is being replaced with \code{\link[=gargle_client]{gargle_client()}}. } \keyword{internal} gargle/man/AuthState-class.Rd0000644000176200001440000002200514433520365015566 0ustar liggesusers% Generated by roxygen2: do not edit by hand % Please edit documentation in R/AuthState-class.R \name{AuthState-class} \alias{AuthState-class} \alias{AuthState} \title{Authorization state} \description{ An \code{AuthState} object manages an authorization state, typically on behalf of a wrapper package that makes requests to a Google API. The \verb{vignette("gargle-auth-in-client-package)} describes a design for wrapper packages that relies on an \code{AuthState} object. This state can then be incorporated into the package's requests for tokens and can control the inclusion of tokens in requests to the target API. \itemize{ \item \code{api_key} is the simplest way to associate a request with a specific Google Cloud Platform \href{https://cloud.google.com/resource-manager/docs/cloud-platform-resource-hierarchy#projects}{project}. A few calls to certain APIs, e.g. reading a public Sheet, can succeed with an API key, but this is the exception. \item \code{client} is an OAuth client ID (and secret) associated with a specific Google Cloud Platform \href{https://cloud.google.com/resource-manager/docs/cloud-platform-resource-hierarchy#projects}{project}. This is used in the OAuth flow, in which an authenticated user authorizes the client to access or manipulate data on their behalf. \item \code{auth_active} reflects whether outgoing requests will be authorized by an authenticated user or are unauthorized requests for public resources. These two states correspond to sending a request with a token versus an API key, respectively. \item \code{cred} is where the current token is cached within a session, once one has been fetched. It is generally assumed to be an instance of \code{\link[httr:Token-class]{httr::TokenServiceAccount}} or \code{\link[httr:Token-class]{httr::Token2.0}} (or a subclass thereof), probably obtained via \code{\link[=token_fetch]{token_fetch()}} (or one of its constituent credential fetching functions). } An \code{AuthState} should be created through the constructor function \code{\link[=init_AuthState]{init_AuthState()}}, which has more details on the arguments. } \section{Public fields}{ \if{html}{\out{
}} \describe{ \item{\code{package}}{Package name.} \item{\code{client}}{An OAuth client.} \item{\code{app}}{\ifelse{html}{\href{https://lifecycle.r-lib.org/articles/stages.html#deprecated}{\figure{lifecycle-deprecated.svg}{options: alt='[Deprecated]'}}}{\strong{[Deprecated]}} Use \code{client} instead.} \item{\code{api_key}}{An API key.} \item{\code{auth_active}}{Logical, indicating whether auth is active.} \item{\code{cred}}{Credentials.} } \if{html}{\out{
}} } \section{Methods}{ \subsection{Public methods}{ \itemize{ \item \href{#method-AuthState-new}{\code{AuthState$new()}} \item \href{#method-AuthState-format}{\code{AuthState$format()}} \item \href{#method-AuthState-set_client}{\code{AuthState$set_client()}} \item \href{#method-AuthState-set_app}{\code{AuthState$set_app()}} \item \href{#method-AuthState-set_api_key}{\code{AuthState$set_api_key()}} \item \href{#method-AuthState-set_auth_active}{\code{AuthState$set_auth_active()}} \item \href{#method-AuthState-set_cred}{\code{AuthState$set_cred()}} \item \href{#method-AuthState-clear_cred}{\code{AuthState$clear_cred()}} \item \href{#method-AuthState-get_cred}{\code{AuthState$get_cred()}} \item \href{#method-AuthState-has_cred}{\code{AuthState$has_cred()}} \item \href{#method-AuthState-clone}{\code{AuthState$clone()}} } } \if{html}{\out{
}} \if{html}{\out{}} \if{latex}{\out{\hypertarget{method-AuthState-new}{}}} \subsection{Method \code{new()}}{ Create a new AuthState \subsection{Usage}{ \if{html}{\out{
}}\preformatted{AuthState$new( package = NA_character_, client = NULL, api_key = NULL, auth_active = TRUE, cred = NULL, app = deprecated() )}\if{html}{\out{
}} } \subsection{Arguments}{ \if{html}{\out{
}} \describe{ \item{\code{package}}{Package name.} \item{\code{client}}{An OAuth client.} \item{\code{api_key}}{An API key.} \item{\code{auth_active}}{Logical, indicating whether auth is active.} \item{\code{cred}}{Credentials.} \item{\code{app}}{\ifelse{html}{\href{https://lifecycle.r-lib.org/articles/stages.html#deprecated}{\figure{lifecycle-deprecated.svg}{options: alt='[Deprecated]'}}}{\strong{[Deprecated]}} Use \code{client} instead.} } \if{html}{\out{
}} } \subsection{Details}{ For more details on the parameters, see \code{\link[=init_AuthState]{init_AuthState()}} } } \if{html}{\out{
}} \if{html}{\out{}} \if{latex}{\out{\hypertarget{method-AuthState-format}{}}} \subsection{Method \code{format()}}{ Format an AuthState \subsection{Usage}{ \if{html}{\out{
}}\preformatted{AuthState$format(...)}\if{html}{\out{
}} } \subsection{Arguments}{ \if{html}{\out{
}} \describe{ \item{\code{...}}{Not used.} } \if{html}{\out{
}} } } \if{html}{\out{
}} \if{html}{\out{}} \if{latex}{\out{\hypertarget{method-AuthState-set_client}{}}} \subsection{Method \code{set_client()}}{ Set the OAuth client \subsection{Usage}{ \if{html}{\out{
}}\preformatted{AuthState$set_client(client)}\if{html}{\out{
}} } \subsection{Arguments}{ \if{html}{\out{
}} \describe{ \item{\code{client}}{An OAuth client.} } \if{html}{\out{
}} } } \if{html}{\out{
}} \if{html}{\out{}} \if{latex}{\out{\hypertarget{method-AuthState-set_app}{}}} \subsection{Method \code{set_app()}}{ \ifelse{html}{\href{https://lifecycle.r-lib.org/articles/stages.html#deprecated}{\figure{lifecycle-deprecated.svg}{options: alt='[Deprecated]'}}}{\strong{[Deprecated]}} Deprecated method to set the OAuth client \subsection{Usage}{ \if{html}{\out{
}}\preformatted{AuthState$set_app(app)}\if{html}{\out{
}} } \subsection{Arguments}{ \if{html}{\out{
}} \describe{ \item{\code{app}}{\ifelse{html}{\href{https://lifecycle.r-lib.org/articles/stages.html#deprecated}{\figure{lifecycle-deprecated.svg}{options: alt='[Deprecated]'}}}{\strong{[Deprecated]}} Use \code{client} instead.} } \if{html}{\out{
}} } } \if{html}{\out{
}} \if{html}{\out{}} \if{latex}{\out{\hypertarget{method-AuthState-set_api_key}{}}} \subsection{Method \code{set_api_key()}}{ Set the API key \subsection{Usage}{ \if{html}{\out{
}}\preformatted{AuthState$set_api_key(value)}\if{html}{\out{
}} } \subsection{Arguments}{ \if{html}{\out{
}} \describe{ \item{\code{value}}{An API key.} } \if{html}{\out{
}} } } \if{html}{\out{
}} \if{html}{\out{}} \if{latex}{\out{\hypertarget{method-AuthState-set_auth_active}{}}} \subsection{Method \code{set_auth_active()}}{ Set whether auth is (in)active \subsection{Usage}{ \if{html}{\out{
}}\preformatted{AuthState$set_auth_active(value)}\if{html}{\out{
}} } \subsection{Arguments}{ \if{html}{\out{
}} \describe{ \item{\code{value}}{Logical, indicating whether to send requests authorized with user credentials.} } \if{html}{\out{
}} } } \if{html}{\out{
}} \if{html}{\out{}} \if{latex}{\out{\hypertarget{method-AuthState-set_cred}{}}} \subsection{Method \code{set_cred()}}{ Set credentials \subsection{Usage}{ \if{html}{\out{
}}\preformatted{AuthState$set_cred(cred)}\if{html}{\out{
}} } \subsection{Arguments}{ \if{html}{\out{
}} \describe{ \item{\code{cred}}{User credentials.} } \if{html}{\out{
}} } } \if{html}{\out{
}} \if{html}{\out{}} \if{latex}{\out{\hypertarget{method-AuthState-clear_cred}{}}} \subsection{Method \code{clear_cred()}}{ Clear credentials \subsection{Usage}{ \if{html}{\out{
}}\preformatted{AuthState$clear_cred()}\if{html}{\out{
}} } } \if{html}{\out{
}} \if{html}{\out{}} \if{latex}{\out{\hypertarget{method-AuthState-get_cred}{}}} \subsection{Method \code{get_cred()}}{ Get credentials \subsection{Usage}{ \if{html}{\out{
}}\preformatted{AuthState$get_cred()}\if{html}{\out{
}} } } \if{html}{\out{
}} \if{html}{\out{}} \if{latex}{\out{\hypertarget{method-AuthState-has_cred}{}}} \subsection{Method \code{has_cred()}}{ Report if we have credentials \subsection{Usage}{ \if{html}{\out{
}}\preformatted{AuthState$has_cred()}\if{html}{\out{
}} } } \if{html}{\out{
}} \if{html}{\out{}} \if{latex}{\out{\hypertarget{method-AuthState-clone}{}}} \subsection{Method \code{clone()}}{ The objects of this class are cloneable with this method. \subsection{Usage}{ \if{html}{\out{
}}\preformatted{AuthState$clone(deep = FALSE)}\if{html}{\out{
}} } \subsection{Arguments}{ \if{html}{\out{
}} \describe{ \item{\code{deep}}{Whether to make a deep clone.} } \if{html}{\out{
}} } } } gargle/man/gargle_client.Rd0000644000176200001440000000236614431310014015352 0ustar liggesusers% Generated by roxygen2: do not edit by hand % Please edit documentation in R/gargle_oauth_client.R \name{gargle_client} \alias{gargle_client} \title{OAuth client for demonstration purposes} \usage{ gargle_client(type = NULL) } \arguments{ \item{type}{Specifies the type of OAuth client. The valid values are a subset of possible Google client types and reflect the key used to describe the client in its JSON representation: \itemize{ \item \code{"installed"} is associated with a "Desktop app" \item \code{"web"} is associated with a "Web application" }} } \value{ An OAuth client, produced by \code{\link[=gargle_oauth_client]{gargle_oauth_client()}}, invisibly. } \description{ Invisibly returns an instance of \code{\link[=gargle_oauth_client]{gargle_oauth_client}} that can be used to test drive gargle before obtaining your own client ID and secret. This OAuth client may be deleted or rotated at any time. There are no guarantees about which APIs are enabled. DO NOT USE THIS IN A PACKAGE or for anything other than interactive, small-scale experimentation. You can get your own OAuth client ID and secret, without these limitations. See the \code{vignette("get-api-credentials")} for more details. } \examples{ \dontrun{ gargle_client() } } \keyword{internal} gargle/man/gargle_secret.Rd0000644000176200001440000000755014444241216015374 0ustar liggesusers% Generated by roxygen2: do not edit by hand % Please edit documentation in R/secret.R \name{gargle_secret} \alias{gargle_secret} \alias{secret_encrypt_json} \alias{secret_decrypt_json} \alias{secret_make_key} \alias{secret_write_rds} \alias{secret_read_rds} \alias{secret_has_key} \title{Encrypt/decrypt JSON or an R object} \usage{ secret_encrypt_json(json, path = NULL, key) secret_decrypt_json(path, key) secret_make_key() secret_write_rds(x, path, key) secret_read_rds(path, key) secret_has_key(key) } \arguments{ \item{json}{A JSON file (or string).} \item{path}{The path to write to (\code{secret_encrypt_json()}, \code{secret_write_rds()}) or to read from (\code{secret_decrypt_json()}, \code{secret_read_rds()}).} \item{key}{Encryption key, as implemented by httr2's \href{https://httr2.r-lib.org/reference/secrets.html}{secret functions}. This should almost always be the name of an environment variable whose value was generated with \code{secret_make_key()} (which is an inlined copy of \code{httr2::secret_make_key()}).} \item{x}{An R object.} } \value{ \itemize{ \item \code{secret_encrypt_json()}: The encrypted JSON string, invisibly. In typical use, this function is mainly called for its side effect, which is to write an encrypted file. \item \code{secret_decrypt_json()}: The decrypted JSON string, invisibly. \item \code{secret_write_rds()}: \code{x}, invisibly \item \code{secret_read_rds()}: the decrypted object. \item \code{secret_make_key()}: a random string to use as an encryption key. \item \code{secret_has_key()} returns \code{TRUE} if the key is available and \code{FALSE} otherwise. } } \description{ These functions help to encrypt and decrypt confidential information that you might need when deploying gargle-using projects or in CI/CD. They basically rely on inlined copies of the \href{https://httr2.r-lib.org/reference/secrets.html}{secret functions in the httr2 package}. The awkwardness of inlining code from httr2 can be removed if/when gargle starts to depend on httr2. \itemize{ \item The \code{secret_encrypt_json()} + \code{secret_decrypt_json()} pair is unique to gargle, given how frequently Google auth relies on JSON files, e.g., service account tokens and OAuth clients. \item The \code{secret_write_rds()} + \code{secret_read_rds()} pair is just a copy of functions from httr2. They are handy if you need to secure a user token. \item \code{secret_make_key()} and \code{secret_has_key()} are also copies of functions from httr2. Use \code{secret_make_key} to generate a key. Use \code{secret_has_key()} to condition on key availability in, e.g., examples, tests, or apps. } } \examples{ \dontshow{if (secret_has_key("GARGLE_KEY")) (if (getRversion() >= "3.4") withAutoprint else force)(\{ # examplesIf} # gargle ships with JSON for a fake service account # here we put the encrypted JSON into a new file tmp <- tempfile() secret_encrypt_json( fs::path_package("gargle", "extdata", "fake_service_account.json"), tmp, key = "GARGLE_KEY" ) # complete the round trip by providing the decrypted JSON to a credential # function credentials_service_account( scopes = "https://www.googleapis.com/auth/userinfo.email", path = secret_decrypt_json( fs::path_package("gargle", "secret", "gargle-testing.json"), key = "GARGLE_KEY" ) ) file.remove(tmp) # make an artificial Gargle2.0 token fauxen <- gargle2.0_token( email = "jane@example.org", client = gargle_oauth_client( id = "CLIENT_ID", secret = "SECRET", name = "CLIENT" ), credentials = list(token = "fauxen"), cache = FALSE ) fauxen # store the fake token in an encrypted file tmp2 <- tempfile() secret_write_rds(fauxen, path = tmp2, key = "GARGLE_KEY") # complete the round trip by providing the decrypted token to the "BYO token" # credential function rt_fauxen <- credentials_byo_oauth2( token = secret_read_rds(tmp2, key = "GARGLE_KEY") ) rt_fauxen file.remove(tmp2) \dontshow{\}) # examplesIf} } gargle/man/request_develop.Rd0000644000176200001440000001377014440166714016002 0ustar liggesusers% Generated by roxygen2: do not edit by hand % Please edit documentation in R/request_develop.R \name{request_develop} \alias{request_develop} \alias{request_build} \title{Build a Google API request} \usage{ request_develop( endpoint, params = list(), base_url = "https://www.googleapis.com" ) request_build( method = "GET", path = "", params = list(), body = list(), token = NULL, key = NULL, base_url = "https://www.googleapis.com" ) } \arguments{ \item{endpoint}{List of information about the target endpoint or, in Google's vocabulary, the target "method". Presumably prepared from the \href{https://developers.google.com/discovery/v1/getting_started#background-resources}{Discovery Document} for the target API.} \item{params}{Named list. Values destined for URL substitution, the query, or, for \code{request_develop()} only, the body. For \code{request_build()}, body parameters must be passed via the \code{body} argument.} \item{base_url}{Character.} \item{method}{Character. An HTTP verb, such as \code{GET} or \code{POST}.} \item{path}{Character. Path to the resource, not including the API's \code{base_url}. Examples: \code{drive/v3/about} or \code{drive/v3/files/{fileId}}. The \code{path} can be a template, i.e. it can include variables inside curly brackets, such as \code{{fileId}} in the example. Such variables are substituted by \code{request_build()}, using named parameters found in \code{params}.} \item{body}{List. Values to send in the API request body.} \item{token}{Token, ready for inclusion in a request, i.e. prepared with \code{\link[httr:config]{httr::config()}}.} \item{key}{API key. Needed for requests that don't contain a token. For more, see Google's document Credentials, access, security, and identity (\verb{https://support.google.com/googleapi/answer/6158857?hl=en&ref_topic=7013279}). A key can be passed as a named component of \code{params}, but note that the formal argument \code{key} will clobber it, if non-\code{NULL}.} } \value{ \code{request_develop()}: \code{list()} with components \code{method}, \code{path}, \code{params}, \code{body}, and \code{base_url}. \code{request_build()}: \code{list()} with components \code{method}, \code{path} (post-substitution), \code{query} (the input \code{params} not used in URL substitution), \code{body}, \code{token}, \code{url} (the full URL, post-substitution, including the query). } \description{ Intended primarily for internal use in client packages that provide high-level wrappers for users. The \code{vignette("request-helper-functions")} describes how one might use these functions inside a wrapper package. } \section{\code{request_develop()}}{ Combines user input (\code{params}) with information about an API endpoint. \code{endpoint} should contain these components: \itemize{ \item \code{path}: See documentation for argument. \item \code{method}: See documentation for argument. \item \code{parameters}: Compared with \code{params} supplied by user. An error is thrown if user-supplied \code{params} aren't named in \code{endpoint$parameters} or if user fails to supply all required parameters. In the return value, body parameters are separated from those destined for path substitution or the query. } The return value is typically used as input to \code{request_build()}. } \section{\code{request_build()}}{ Builds a request, in a purely mechanical sense. This function does nothing specific to any particular Google API or endpoint. \itemize{ \item Use with the output of \code{request_develop()} or with hand-crafted input. \item \code{params} are used for variable substitution in \code{path}. Leftover \code{params} that are not bound by the \code{path} template automatically become HTTP query parameters. \item Adds an API key to the query iff \code{token = NULL} and removes the API key otherwise. Client packages should generally pass their own API key in, but note that \code{\link[=gargle_api_key]{gargle_api_key()}} is available for small-scale experimentation. } See \code{googledrive::generate_request()} for an example of usage in a client package. googledrive has an internal list of selected endpoints, derived from the Drive API Discovery Document (\verb{https://www.googleapis.com/discovery/v1/apis/drive/v3/rest}), exposed via \code{googledrive::drive_endpoints()}. An element from such a list is the expected input for \code{endpoint}. \code{googledrive::generate_request()} is a wrapper around \code{request_develop()} and \code{request_build()} that inserts a googledrive-managed API key and some logic about Team Drives. All user-facing functions use \code{googledrive::generate_request()} under the hood. } \examples{ \dontrun{ ## Example with a prepared endpoint ept <- googledrive::drive_endpoints("drive.files.update")[[1]] req <- request_develop( ept, params = list( fileId = "abc", addParents = "123", description = "Exciting File" ) ) req req <- request_build( method = req$method, path = req$path, params = req$params, body = req$body, token = "PRETEND_I_AM_A_TOKEN" ) req ## Example with no previous knowledge of the endpoint ## List a file's comments ## https://developers.google.com/drive/v3/reference/comments/list req <- request_build( method = "GET", path = "drive/v3/files/{fileId}/comments", params = list( fileId = "your-file-id-goes-here", fields = "*" ), token = "PRETEND_I_AM_A_TOKEN" ) req # Example with no previous knowledge of the endpoint and no token # use an API key for which the Places API is enabled! API_KEY <- "1234567890" # get restaurants close to a location in Vancouver, BC req <- request_build( method = "GET", path = "maps/api/place/nearbysearch/json", params = list( location = "49.268682,-123.167117", radius = 100, type = "restaurant" ), key = API_KEY, base_url = "https://maps.googleapis.com" ) resp <- request_make(req) out <- response_process(resp) vapply(out$results, function(x) x$name, character(1)) } } \seealso{ Other requests and responses: \code{\link{request_make}()}, \code{\link{response_process}()} } \concept{requests and responses} gargle/man/check_is_service_account.Rd0000644000176200001440000000342014431310014017547 0ustar liggesusers% Generated by roxygen2: do not edit by hand % Please edit documentation in R/credentials_service_account.R \name{check_is_service_account} \alias{check_is_service_account} \title{Check for a service account} \usage{ check_is_service_account(path, hint, call = caller_env()) } \arguments{ \item{path}{JSON identifying the service account, in one of the forms supported for the \code{txt} argument of \code{\link[jsonlite:fromJSON]{jsonlite::fromJSON()}} (typically, a file path or JSON string).} \item{hint}{The relevant function to call for configuring an OAuth client.} \item{call}{The execution environment of a currently running function, e.g. \code{call = caller_env()}. The corresponding function call is retrieved and mentioned in error messages as the source of the error. You only need to supply \code{call} when throwing a condition from a helper function which wouldn't be relevant to mention in the message. Can also be \code{NULL} or a \link[rlang:topic-defuse]{defused function call} to respectively not display any call or hard-code a code to display. For more information about error calls, see \ifelse{html}{\link[rlang:topic-error-call]{Including function calls in error messages}}{\link[rlang:topic-error-call]{Including function calls in error messages}}.} } \value{ Nothing. Exists purely to throw an error. } \description{ This pre-checks information provided to a high-level, user-facing auth function, such as \code{googledrive::drive_auth()}, before passing the user's input along to \code{\link[=token_fetch]{token_fetch()}}, which is designed to silently swallow errors. Some users are confused about the difference between an OAuth client and a service account and they provide the (path to the) JSON for one, when the other is what's actually expected. } \keyword{internal} gargle/man/credentials_external_account.Rd0000644000176200001440000001043314321544762020501 0ustar liggesusers% Generated by roxygen2: do not edit by hand % Please edit documentation in R/credentials_external_account.R \name{credentials_external_account} \alias{credentials_external_account} \title{Get a token for an external account} \usage{ credentials_external_account( scopes = "https://www.googleapis.com/auth/cloud-platform", path = "", ... ) } \arguments{ \item{scopes}{A character vector of scopes to request. Pick from those listed at \url{https://developers.google.com/identity/protocols/oauth2/scopes}. For certain token flows, the \code{"https://www.googleapis.com/auth/userinfo.email"} scope is unconditionally included. This grants permission to retrieve the email address associated with a token; gargle uses this to index cached OAuth tokens. This grants no permission to view or send email and is generally considered a low-value scope.} \item{path}{JSON containing the workload identity configuration for the external account, in one of the forms supported for the \code{txt} argument of \code{\link[jsonlite:fromJSON]{jsonlite::fromJSON()}} (probably, a file path, although it could be a JSON string). The instructions for generating this configuration are given at \href{https://cloud.google.com/iam/docs/configuring-workload-identity-federation}{Configuring workload identity federation}. Note that external account tokens are a natural fit for use as Application Default Credentials, so consider storing the configuration file in one of the standard locations consulted for ADC, instead of providing \code{path} explicitly. See \code{\link[=credentials_app_default]{credentials_app_default()}} for more.} \item{...}{Additional arguments passed to all credential functions.} } \value{ A \code{\link[=WifToken]{WifToken()}} or \code{NULL}. } \description{ \ifelse{html}{\href{https://lifecycle.r-lib.org/articles/stages.html#experimental}{\figure{lifecycle-experimental.svg}{options: alt='[Experimental]'}}}{\strong{[Experimental]}} Workload identity federation is a new (as of April 2021) keyless authentication mechanism that allows applications running on a non-Google Cloud platform, such as AWS, to access Google Cloud resources without using a conventional service account token. This eliminates the dilemma of how to safely manage service account credential files. Unlike service accounts, the configuration file for workload identity federation contains no secrets. Instead, it holds non-sensitive metadata. The external application obtains the needed sensitive data "on-the-fly" from the running instance. The combined data is then used to obtain a so-called subject token from the external identity provider, such as AWS. This is then sent to Google's Security Token Service API, in exchange for a very short-lived federated access token. Finally, the federated access token is sent to Google's Service Account Credentials API, in exchange for a short-lived GCP access token. This access token allows the external application to impersonate a service account and inherit the permissions of the service account to access GCP resources. This feature is still experimental in gargle and \strong{currently only supports AWS}. It also requires installation of the suggested packages \pkg{aws.signature} and \pkg{aws.ec2metadata}. Workload identity federation \strong{can} be used with other platforms, such as Microsoft Azure or any identity provider that supports OpenID Connect. If you would like gargle to support this token flow for additional platforms, please \href{https://github.com/r-lib/gargle/issues}{open an issue on GitHub} and describe your use case. } \examples{ \dontrun{ credentials_external_account() } } \seealso{ There is substantial setup necessary, both on the GCP and AWS side, to use this authentication method. These two links provide, respectively, a high-level overview and step-by-step instructions. \itemize{ \item \url{https://cloud.google.com/blog/products/identity-security/enable-keyless-access-to-gcp-with-workload-identity-federation/} \item \url{https://cloud.google.com/iam/docs/configuring-workload-identity-federation} } Other credential functions: \code{\link{credentials_app_default}()}, \code{\link{credentials_byo_oauth2}()}, \code{\link{credentials_gce}()}, \code{\link{credentials_service_account}()}, \code{\link{credentials_user_oauth2}()}, \code{\link{token_fetch}()} } \concept{credential functions} gargle/man/request_retry.Rd0000644000176200001440000001205214431310014015461 0ustar liggesusers% Generated by roxygen2: do not edit by hand % Please edit documentation in R/request_retry.R \name{request_retry} \alias{request_retry} \title{Make a Google API request, repeatedly} \usage{ request_retry(..., max_tries_total = 5, max_total_wait_time_in_seconds = 100) } \arguments{ \item{...}{Passed along to \code{\link[=request_make]{request_make()}}.} \item{max_tries_total}{Maximum number of tries.} \item{max_total_wait_time_in_seconds}{Total seconds we are willing to dedicate to waiting, summed across all tries. This is a technical upper bound and actual cumulative waiting will be less.} } \value{ Object of class \code{response} from \link{httr}. } \description{ Intended primarily for internal use in client packages that provide high-level wrappers for users. It is a drop-in substitute for \code{\link[=request_make]{request_make()}} that also has the ability to retry the request. Codes that are considered retryable: 408, 429, 500, 502, 503. } \details{ Consider an example where we are willing to make a request up to 5 times. \if{html}{\out{
}}\preformatted{try 1 2 3 4 5 |--|----|--------|----------------| wait 1 2 3 4 }\if{html}{\out{
}} There will be up to 5 - 1 = 4 waits and we generally want the waiting period to get longer, in an exponential way. Such schemes are called exponential backoff. \code{request_retry()} implements exponential backoff with "full jitter", where each waiting time is generated from a uniform distribution, where the interval of support grows exponentially. A common alternative is "equal jitter", which adds some noise to fixed, exponentially increasing waiting times. Either way our waiting times are based on a geometric series, which, by convention, is usually written in terms of powers of 2: \if{html}{\out{
}}\preformatted{b, 2b, 4b, 8b, ... = b * 2^0, b * 2^1, b * 2^2, b * 2^3, ... }\if{html}{\out{
}} The terms in this series require knowledge of \code{b}, the so-called exponential base, and many retry functions and libraries require the user to specify this. But most users find it easier to declare the total amount of waiting time they can tolerate for one request. Therefore \code{request_retry()} asks for that instead and solves for \code{b} internally. This is inspired by the Opnieuw Python library for retries. Opnieuw's interface is designed to eliminate uncertainty around: \itemize{ \item Units: Is this thing given in seconds? minutes? milliseconds? \item Ambiguity around how things are counted: Are we starting at 0 or 1? Are we counting tries or just the retries? \item Non-intuitive required inputs, e.g., the exponential base. } Let \emph{n} be the total number of tries we're willing to make (the argument \code{max_tries_total}) and let \emph{W} be the total amount of seconds we're willing to dedicate to making and retrying this request (the argument \code{max_total_wait_time_in_seconds}). Here's how we determine \emph{b}: \if{html}{\out{
}}\preformatted{sum_\{i=0\}^(n - 1) b * 2^i = W b * sum_\{i=0\}^(n - 1) 2^i = W b * ( (2 ^ n) - 1) = W b = W / ( (2 ^ n) - 1) }\if{html}{\out{
}} } \section{Special cases}{ \code{request_retry()} departs from exponential backoff in three special cases: \itemize{ \item It actually implements \emph{truncated} exponential backoff. There is a floor and a ceiling on random wait times. \item \code{Retry-After} header: If the response has a header named \code{Retry-After} (case-insensitive), it is assumed to provide a non-negative integer indicating the number of seconds to wait. If present, we wait this many seconds and do not generate a random waiting time. (In theory, this header can alternatively provide a datetime after which to retry, but we have no first-hand experience with this variant for a Google API.) \item Sheets API quota exhaustion: In the course of googlesheets4 development, we've grown very familiar with the \verb{429 RESOURCE_EXHAUSTED} error. As of 2023-04-15, the Sheets API v4 has a limit of 300 requests per minute per project and 60 requests per minute per user per project. Limits for reads and writes are tracked separately. In our experience, the "60 (read or write) requests per minute per user" limit is the one you hit most often. If we detect this specific failure, the first wait time is a bit more than one minute, then we revert to exponential backoff. } } \examples{ \dontrun{ req <- gargle::request_build( method = "GET", path = "path/to/the/resource", token = "PRETEND_I_AM_TOKEN" ) gargle::request_retry(req) } } \seealso{ \itemize{ \item \url{https://aws.amazon.com/blogs/architecture/exponential-backoff-and-jitter/} \item \url{https://tech.channable.com/posts/2020-02-05-opnieuw.html} \item \url{https://github.com/channable/opnieuw} \item \url{https://cloud.google.com/storage/docs/retry-strategy} \item \url{https://www.rfc-editor.org/rfc/rfc7231#section-7.1.3} \item \url{https://developers.google.com/sheets/api/limits} \item \url{https://googleapis.dev/python/google-api-core/latest/retry.html} } } gargle/man/figures/0000755000176200001440000000000014431310014013721 5ustar liggesusersgargle/man/figures/lifecycle-defunct.svg0000644000176200001440000000242414431310014020031 0ustar liggesusers lifecycle: defunct lifecycle defunct gargle/man/figures/lifecycle-maturing.svg0000644000176200001440000000243014431310014020224 0ustar liggesusers lifecycle: maturing lifecycle maturing gargle/man/figures/lifecycle-archived.svg0000644000176200001440000000243014431310014020163 0ustar liggesusers lifecycle: archived lifecycle archived gargle/man/figures/lifecycle-soft-deprecated.svg0000644000176200001440000000246614431310014021460 0ustar liggesusers lifecycle: soft-deprecated lifecycle soft-deprecated gargle/man/figures/lifecycle-questioning.svg0000644000176200001440000000244414431310014020750 0ustar liggesusers lifecycle: questioning lifecycle questioning gargle/man/figures/lifecycle-superseded.svg0000644000176200001440000000244014431310014020542 0ustar liggesusers lifecycle: superseded lifecycle superseded gargle/man/figures/lifecycle-stable.svg0000644000176200001440000000247214431310014017656 0ustar liggesusers lifecycle: stable lifecycle stable gargle/man/figures/lifecycle-experimental.svg0000644000176200001440000000245014431310014021075 0ustar liggesusers lifecycle: experimental lifecycle experimental gargle/man/figures/lifecycle-deprecated.svg0000644000176200001440000000244014431310014020477 0ustar liggesusers lifecycle: deprecated lifecycle deprecated gargle/man/oauth_external_token.Rd0000644000176200001440000000322714431310014016772 0ustar liggesusers% Generated by roxygen2: do not edit by hand % Please edit documentation in R/credentials_external_account.R \name{oauth_external_token} \alias{oauth_external_token} \title{Generate OAuth token for an external account} \usage{ oauth_external_token( path = "", scopes = "https://www.googleapis.com/auth/cloud-platform" ) } \arguments{ \item{path}{JSON containing the workload identity configuration for the external account, in one of the forms supported for the \code{txt} argument of \code{\link[jsonlite:fromJSON]{jsonlite::fromJSON()}} (probably, a file path, although it could be a JSON string). The instructions for generating this configuration are given at \href{https://cloud.google.com/iam/docs/configuring-workload-identity-federation}{Configuring workload identity federation}. Note that external account tokens are a natural fit for use as Application Default Credentials, so consider storing the configuration file in one of the standard locations consulted for ADC, instead of providing \code{path} explicitly. See \code{\link[=credentials_app_default]{credentials_app_default()}} for more.} \item{scopes}{A character vector of scopes to request. Pick from those listed at \url{https://developers.google.com/identity/protocols/oauth2/scopes}. For certain token flows, the \code{"https://www.googleapis.com/auth/userinfo.email"} scope is unconditionally included. This grants permission to retrieve the email address associated with a token; gargle uses this to index cached OAuth tokens. This grants no permission to view or send email and is generally considered a low-value scope.} } \description{ Generate OAuth token for an external account } \keyword{internal} gargle/man/credentials_byo_oauth2.Rd0000644000176200001440000000626014431310014017200 0ustar liggesusers% Generated by roxygen2: do not edit by hand % Please edit documentation in R/credentials_byo_oauth2.R \name{credentials_byo_oauth2} \alias{credentials_byo_oauth2} \title{Load a user-provided token} \usage{ credentials_byo_oauth2(scopes = NULL, token, ...) } \arguments{ \item{scopes}{A character vector of scopes to request. Pick from those listed at \url{https://developers.google.com/identity/protocols/oauth2/scopes}. For certain token flows, the \code{"https://www.googleapis.com/auth/userinfo.email"} scope is unconditionally included. This grants permission to retrieve the email address associated with a token; gargle uses this to index cached OAuth tokens. This grants no permission to view or send email and is generally considered a low-value scope.} \item{token}{A token with class \link[httr:Token-class]{Token2.0} or an object of httr's class \code{request}, i.e. a token that has been prepared with \code{\link[httr:config]{httr::config()}} and has a \link[httr:Token-class]{Token2.0} in the \code{auth_token} component.} \item{...}{Additional arguments passed to all credential functions.} } \value{ An \link[httr:Token-class]{Token2.0}. } \description{ This function is designed to pass its \code{token} input through, after doing a few checks and some light processing: \itemize{ \item If \code{token} has class \code{request}, i.e. it is a token that has been prepared with \code{\link[httr:config]{httr::config()}}, the \code{auth_token} component is extracted. For example, such input could be returned by \code{googledrive::drive_token()} or \code{bigrquery::bq_token()}. \item If \code{token} is an instance of \code{Gargle2.0} (so: a gargle-obtained user token), checks that it appears to be a Google OAuth token, based on its embedded \code{oauth_endpoint}. Refreshes the token, if it's refreshable. \item Returns the \code{token}. } There is no point in providing \code{scopes}. They are ignored because the \code{scopes} associated with the token have already been baked in to the token itself and gargle does not support incremental authorization. The main point of \code{credentials_byo_oauth2()} is to allow \code{token_fetch()} (and packages that wrap it) to accommodate a "bring your own token" workflow. This also makes it possible to obtain a token with one package and then register it for use with another package. For example, the default scope requested by googledrive is also sufficient for operations available in googlesheets4. You could use a shared token like so: \if{html}{\out{
}}\preformatted{library(googledrive) library(googlesheets4) drive_auth(email = "jane_doe@example.com") gs4_auth(token = drive_token()) # work with both packages freely now, with the same identity }\if{html}{\out{
}} } \examples{ \dontrun{ # assume `my_token` is a Token2.0 object returned by a function such as # credentials_user_oauth2() credentials_byo_oauth2(token = my_token) } } \seealso{ Other credential functions: \code{\link{credentials_app_default}()}, \code{\link{credentials_external_account}()}, \code{\link{credentials_gce}()}, \code{\link{credentials_service_account}()}, \code{\link{credentials_user_oauth2}()}, \code{\link{token_fetch}()} } \concept{credential functions} gargle/man/response_process.Rd0000644000176200001440000001042714431310014016144 0ustar liggesusers% Generated by roxygen2: do not edit by hand % Please edit documentation in R/response_process.R \name{response_process} \alias{response_process} \alias{response_as_json} \alias{gargle_error_message} \title{Process a Google API response} \usage{ response_process( resp, error_message = gargle_error_message, remember = TRUE, call = caller_env() ) response_as_json(resp, call = caller_env()) gargle_error_message(resp, call = caller_env()) } \arguments{ \item{resp}{Object of class \code{response} from \link{httr}.} \item{error_message}{Function that produces an informative error message from the primary input, \code{resp}. It must return a character vector.} \item{remember}{Whether to remember the most recently processed response.} \item{call}{The execution environment of a currently running function, e.g. \code{call = caller_env()}. The corresponding function call is retrieved and mentioned in error messages as the source of the error. You only need to supply \code{call} when throwing a condition from a helper function which wouldn't be relevant to mention in the message. Can also be \code{NULL} or a \link[rlang:topic-defuse]{defused function call} to respectively not display any call or hard-code a code to display. For more information about error calls, see \ifelse{html}{\link[rlang:topic-error-call]{Including function calls in error messages}}{\link[rlang:topic-error-call]{Including function calls in error messages}}.} } \value{ The content of the request, as a list. An HTTP status code of 204 (No content) is a special case returning \code{TRUE}. } \description{ \code{response_process()} is intended primarily for internal use in client packages that provide high-level wrappers for users. Typically applied as the final step in this sequence of calls: \itemize{ \item Request prepared with \code{\link[=request_build]{request_build()}}. \item Request made with \code{\link[=request_make]{request_make()}}. \item Response processed with \code{response_process()}. } All that's needed for a successful request is to parse the JSON extracted via \code{httr::content()}. Therefore, the main point of \code{response_process()} is to handle less happy outcomes: \itemize{ \item Status codes in the 400s (client error) and 500s (server error). The structure of the error payload varies across Google APIs and we try to create a useful message for all variants we know about. \item Non-JSON content type, such as HTML. \item Status code in the 100s (information) or 300s (redirection). These are unexpected. } If \code{process_response()} results in an error, a redacted version of the \code{resp} input is returned in the condition (auth tokens are removed). } \details{ When \code{remember = TRUE} (the default), gargle stores the most recently seen response internally, for \emph{post hoc} examination. The stored response is literally just the most recent \code{resp} input, but with auth tokens redacted. It can be accessed via the unexported function \code{gargle:::gargle_last_response()}. A companion function \code{gargle:::gargle_last_content()} returns the content of the last response, which is probably the most useful form for \emph{post mortem} analysis. The \code{response_as_json()} helper is exported only as an aid to maintainers who wish to use their own \code{error_message} function, instead of gargle's built-in \code{gargle_error_message()}. When implementing a custom \code{error_message} function, call \code{response_as_json()} immediately on the input in order to inherit gargle's handling of non-JSON input. } \examples{ \dontrun{ # get an OAuth2 token with 'userinfo.email' scope token <- token_fetch(scopes = "https://www.googleapis.com/auth/userinfo.email") # see the email associated with this token req <- gargle::request_build( method = "GET", path = "v1/userinfo", token = token, base_url = "https://openidconnect.googleapis.com" ) resp <- gargle::request_make(req) response_process(resp) # make a bad request (this token has incorrect scope) req <- gargle::request_build( method = "GET", path = "fitness/v1/users/{userId}/dataSources", token = token, params = list(userId = 12345) ) resp <- gargle::request_make(req) response_process(resp) } } \seealso{ Other requests and responses: \code{\link{request_develop}()}, \code{\link{request_make}()} } \concept{requests and responses} gargle/man/token-info.Rd0000644000176200001440000000356414431310014014625 0ustar liggesusers% Generated by roxygen2: do not edit by hand % Please edit documentation in R/token-info.R \name{token-info} \alias{token-info} \alias{token_userinfo} \alias{token_email} \alias{token_tokeninfo} \title{Get info from a token} \usage{ token_userinfo(token) token_email(token) token_tokeninfo(token) } \arguments{ \item{token}{A token with class \link[httr:Token-class]{Token2.0} or an object of httr's class \code{request}, i.e. a token that has been prepared with \code{\link[httr:config]{httr::config()}} and has a \link[httr:Token-class]{Token2.0} in the \code{auth_token} component.} } \value{ A list containing: \itemize{ \item \code{token_userinfo()}: user info \item \code{token_email()}: user's email (obtained from a call to \code{token_userinfo()}) \item \code{token_tokeninfo()}: token info } } \description{ These functions send the \code{token} to Google endpoints that return info about a token or a user. } \details{ It's hard to say exactly what info will be returned by the "userinfo" endpoint targetted by \code{token_userinfo()}. It depends on the token's scopes. Where possible, OAuth2 tokens obtained via the gargle package include the \verb{https://www.googleapis.com/auth/userinfo.email} scope, which guarantees we can learn the email associated with the token. If the token has the \verb{https://www.googleapis.com/auth/userinfo.profile} scope, there will be even more information available. But for a token with unknown or arbitrary scopes, we can't make any promises about what information will be returned. } \examples{ \dontrun{ # with service account token t <- token_fetch( scopes = "https://www.googleapis.com/auth/drive", path = "path/to/service/account/token/blah-blah-blah.json" ) # or with an OAuth token t <- token_fetch( scopes = "https://www.googleapis.com/auth/drive", email = "janedoe@example.com" ) token_userinfo(t) token_email(t) tokens_tokeninfo(t) } } gargle/man/gargle_options.Rd0000644000176200001440000001102214431310014015554 0ustar liggesusers% Generated by roxygen2: do not edit by hand % Please edit documentation in R/gargle-package.R, R/utils-ui.R \name{gargle_options} \alias{gargle_options} \alias{gargle_oauth_email} \alias{gargle_oob_default} \alias{gargle_oauth_cache} \alias{gargle_oauth_client_type} \alias{gargle_verbosity} \alias{local_gargle_verbosity} \alias{with_gargle_verbosity} \title{Options consulted by gargle} \usage{ gargle_oauth_email() gargle_oob_default() gargle_oauth_cache() gargle_oauth_client_type() gargle_verbosity() local_gargle_verbosity(level, env = caller_env()) with_gargle_verbosity(level, code) } \arguments{ \item{level}{Verbosity level: "debug" > "info" > "silent"} \item{env}{The environment to use for scoping} \item{code}{Code to execute with specified verbosity level} } \description{ Wrapper functions around options consulted by gargle, which provide: \itemize{ \item A place to hang documentation. \item The mechanism for setting a default. } If the built-in defaults don't suit you, set one or more of these options. Typically, this is done in the \code{.Rprofile} startup file, with code along these lines: \if{html}{\out{
}}\preformatted{options( gargle_oauth_email = "jane@example.com", gargle_oauth_cache = "/path/to/folder/that/does/not/sync/to/cloud" ) }\if{html}{\out{
}} } \section{\code{gargle_oauth_email}}{ \code{gargle_oauth_email()} returns the option named "gargle_oauth_email", which is undefined by default. If set, this option should be one of: \itemize{ \item An actual email address corresponding to your preferred Google identity. Example:\code{janedoe@gmail.com}. \item A glob pattern that indicates your preferred Google domain. Example:\verb{*@example.com}. \item \code{TRUE} to allow email and OAuth token auto-discovery, if exactly one suitable token is found in the cache. \item \code{FALSE} or \code{NA} to force the OAuth dance in the browser. } } \section{\code{gargle_oob_default}}{ \code{gargle_oob_default()} returns \code{TRUE} unconditionally on RStudio Server, Posit Workbench, Posit Cloud, or Google Colaboratory, since it is not possible to launch a local web server in these contexts. In this case, for the final step of the OAuth dance, the user is redirected to a specific URL where they must copy a code and paste it back into the R session. In all other contexts, \code{gargle_oob_default()} consults the option named \code{"gargle_oob_default"}, then the option named \code{"httr_oob_default"}, and eventually defaults to \code{FALSE}. "oob" stands for out-of-band. Read more about out-of-band authentication in the vignette \code{vignette("auth-from-web")}. } \section{\code{gargle_oauth_cache}}{ \code{gargle_oauth_cache()} returns the option named "gargle_oauth_cache", defaulting to \code{NA}. If defined, the option must be set to a logical value or a string. \code{TRUE} means to cache using the default user-level cache file, \verb{~/.R/gargle/gargle-oauth}, \code{FALSE} means don't cache, and \code{NA} means to guess using some sensible heuristics. } \section{\code{gargle_oauth_client_type}}{ \code{gargle_oauth_client_type()} returns the option named "gargle_oauth_client_type", if defined. If defined, the option must be either "installed" or "web". If the option is not defined, the function returns: \itemize{ \item "web" on RStudio Server, Posit Workbench, Posit Cloud, or Google Colaboratory \item "installed" otherwise } Primarily intended to help infer the most suitable OAuth client type when a user is relying on a built-in client, such as the tidyverse client used by packages like bigrquery, googledrive, and googlesheets4. } \section{\code{gargle_verbosity}}{ \code{gargle_verbosity()} returns the option named "gargle_verbosity", which determines gargle's verbosity. There are three possible values, inspired by the logging levels of log4j: \itemize{ \item "debug": Fine-grained information helpful when debugging, e.g. figuring out how \code{token_fetch()} is working through the registry of credential functions. Previously, this was activated by setting an option named "gargle_quiet" to \code{FALSE}. \item "info" (default): High-level information that a typical user needs to see. Since typical gargle usage is always indirect, i.e. gargle is called by another package, gargle itself is very quiet. There are very few messages emitted when \code{gargle_verbosity = "info"}. \item "silent": No messages at all. However, warnings or errors are still thrown normally. } } \examples{ gargle_oauth_email() gargle_oob_default() gargle_oauth_cache() gargle_oauth_client_type() gargle_verbosity() } gargle/man/gce_access_token.Rd0000644000176200001440000000172114431310014016024 0ustar liggesusers% Generated by roxygen2: do not edit by hand % Please edit documentation in R/credentials_gce.R \name{gce_access_token} \alias{gce_access_token} \title{Fetch access token for a service account on GCE} \usage{ gce_access_token( scopes = "https://www.googleapis.com/auth/cloud-platform", service_account = "default" ) } \arguments{ \item{scopes}{A character vector of scopes to request. Pick from those listed at \url{https://developers.google.com/identity/protocols/oauth2/scopes}. For certain token flows, the \code{"https://www.googleapis.com/auth/userinfo.email"} scope is unconditionally included. This grants permission to retrieve the email address associated with a token; gargle uses this to index cached OAuth tokens. This grants no permission to view or send email and is generally considered a low-value scope.} \item{service_account}{Name of the GCE service account to use.} } \description{ Fetch access token for a service account on GCE } \keyword{internal} gargle/man/request_make.Rd0000644000176200001440000000510114431310014015226 0ustar liggesusers% Generated by roxygen2: do not edit by hand % Please edit documentation in R/request_make.R \name{request_make} \alias{request_make} \title{Make a Google API request} \usage{ request_make(x, ..., encode = "json", user_agent = gargle_user_agent()) } \arguments{ \item{x}{List. Holds the components for an HTTP request, presumably created with \code{\link[=request_develop]{request_develop()}} or \code{\link[=request_build]{request_build()}}. Must contain a \code{method} and \code{url}. If present, \code{body} and \code{token} are used.} \item{...}{Optional arguments passed through to the HTTP method. Currently neither gargle nor httr checks that all are used, so be aware that unused arguments may be silently ignored.} \item{encode}{If the body is a named list, how should it be encoded? Can be one of form (application/x-www-form-urlencoded), multipart, (multipart/form-data), or json (application/json). For "multipart", list elements can be strings or objects created by \code{\link[httr:upload_file]{upload_file()}}. For "form", elements are coerced to strings and escaped, use \code{I()} to prevent double-escaping. For "json", parameters are automatically "unboxed" (i.e. length 1 vectors are converted to scalars). To preserve a length 1 vector as a vector, wrap in \code{I()}. For "raw", either a character or raw vector. You'll need to make sure to set the \code{\link[httr:content_type]{content_type()}} yourself.} \item{user_agent}{A user agent string, prepared by \code{\link[httr:user_agent]{httr::user_agent()}}. When in doubt, a client package should have an internal function that extends \code{gargle_user_agent()} by prepending its return value with the client package's name and version.} } \value{ Object of class \code{response} from \link{httr}. } \description{ Intended primarily for internal use in client packages that provide high-level wrappers for users. \code{request_make()} does relatively little: \itemize{ \item Calls an HTTP method. \item Adds a user agent. \item Enforces \code{"json"} as the default for \code{encode}. This differs from httr's default behaviour, but aligns better with Google APIs. } Typically the input is created with \code{\link[=request_build]{request_build()}} and the output is processed with \code{\link[=response_process]{response_process()}}. } \examples{ \dontrun{ req <- gargle::request_build( method = "GET", path = "path/to/the/resource", token = "PRETEND_I_AM_TOKEN" ) gargle::request_make(req) } } \seealso{ Other requests and responses: \code{\link{request_develop}()}, \code{\link{response_process}()} } \concept{requests and responses} gargle/man/init_AuthState.Rd0000644000176200001440000000363014433520365015511 0ustar liggesusers% Generated by roxygen2: do not edit by hand % Please edit documentation in R/AuthState-class.R \name{init_AuthState} \alias{init_AuthState} \title{Create an AuthState} \usage{ init_AuthState( package = NA_character_, client = NULL, api_key = NULL, auth_active = TRUE, cred = NULL, app = deprecated() ) } \arguments{ \item{package}{Package name, an optional string. It is recommended to record the name of the package whose auth state is being managed. Ultimately, this may be used in some downstream messaging.} \item{client}{A Google OAuth client, preferably constructed via \code{\link[=gargle_oauth_client_from_json]{gargle_oauth_client_from_json()}}, which returns an instance of \code{gargle_oauth_client}. For backwards compatibility, for a limited time, gargle will still accept an "OAuth app" created with \code{\link[httr:oauth_app]{httr::oauth_app()}}.} \item{api_key}{Optional. API key (a string). Some APIs accept unauthorized, "token-free" requests for public resources, but only if the request includes an API key.} \item{auth_active}{Logical. \code{TRUE} means requests should include a token (and probably not an API key). \code{FALSE} means requests should include an API key (and probably not a token).} \item{cred}{Credentials. Typically populated indirectly via \code{\link[=token_fetch]{token_fetch()}}.} \item{app}{\ifelse{html}{\href{https://lifecycle.r-lib.org/articles/stages.html#deprecated}{\figure{lifecycle-deprecated.svg}{options: alt='[Deprecated]'}}}{\strong{[Deprecated]}} Replaced by the \code{client} argument.} } \value{ An object of class \link{AuthState}. } \description{ Constructor function for objects of class \link{AuthState}. } \examples{ my_client <- gargle_oauth_client( id = "some_long_client_id", secret = "ssshhhhh_its_a_secret", name = "my-nifty-oauth-client" ) init_AuthState( package = "my_package", client = my_client, api_key = "api_key_api_key_api_key", ) } gargle/man/credentials_user_oauth2.Rd0000644000176200001440000001277214433520365017410 0ustar liggesusers% Generated by roxygen2: do not edit by hand % Please edit documentation in R/credentials_user_oauth2.R \name{credentials_user_oauth2} \alias{credentials_user_oauth2} \title{Get an OAuth token for a user} \usage{ credentials_user_oauth2( scopes = NULL, client = gargle_client(), package = "gargle", ..., app = deprecated() ) } \arguments{ \item{scopes}{A character vector of scopes to request. Pick from those listed at \url{https://developers.google.com/identity/protocols/oauth2/scopes}. For certain token flows, the \code{"https://www.googleapis.com/auth/userinfo.email"} scope is unconditionally included. This grants permission to retrieve the email address associated with a token; gargle uses this to index cached OAuth tokens. This grants no permission to view or send email and is generally considered a low-value scope.} \item{client}{A Google OAuth client, preferably constructed via \code{\link[=gargle_oauth_client_from_json]{gargle_oauth_client_from_json()}}, which returns an instance of \code{gargle_oauth_client}. For backwards compatibility, for a limited time, gargle will still accept an "OAuth app" created with \code{\link[httr:oauth_app]{httr::oauth_app()}}.} \item{package}{Name of the package requesting a token. Used in messages.} \item{...}{ Arguments passed on to \code{\link[=gargle2.0_token]{gargle2.0_token}} \describe{ \item{\code{email}}{Optional. If specified, \code{email} can take several different forms: \itemize{ \item \code{"jane@gmail.com"}, i.e. an actual email address. This allows the user to target a specific Google identity. If specified, this is used for token lookup, i.e. to determine if a suitable token is already available in the cache. If no such token is found, \code{email} is used to pre-select the targeted Google identity in the OAuth chooser. (Note, however, that the email associated with a token when it's cached is always determined from the token itself, never from this argument). \item \code{"*@example.com"}, i.e. a domain-only glob pattern. This can be helpful if you need code that "just works" for both \code{alice@example.com} and \code{bob@example.com}. \item \code{TRUE} means that you are approving email auto-discovery. If exactly one matching token is found in the cache, it will be used. \item \code{FALSE} or \code{NA} mean that you want to ignore the token cache and force a new OAuth dance in the browser. } Defaults to the option named \code{"gargle_oauth_email"}, retrieved by \code{\link[=gargle_oauth_email]{gargle_oauth_email()}} (unless a wrapper package implements different default behavior).} \item{\code{use_oob}}{Whether to use out-of-band authentication (or, perhaps, a variant implemented by gargle and known as "pseudo-OOB") when first acquiring the token. Defaults to the value returned by \code{\link[=gargle_oob_default]{gargle_oob_default()}}. Note that (pseudo-)OOB auth only affects the initial OAuth dance. If we retrieve (and possibly refresh) a cached token, \code{use_oob} has no effect. If the OAuth client is provided implicitly by a wrapper package, its type probably defaults to the value returned by \code{\link[=gargle_oauth_client_type]{gargle_oauth_client_type()}}. You can take control of the client type by setting \code{options(gargle_oauth_client_type = "web")} or \code{options(gargle_oauth_client_type = "installed")}.} \item{\code{cache}}{Specifies the OAuth token cache. Defaults to the option named \code{"gargle_oauth_cache"}, retrieved via \code{\link[=gargle_oauth_cache]{gargle_oauth_cache()}}.} \item{\code{credentials}}{Advanced use only: allows you to completely customise token generation.} }} \item{app}{\ifelse{html}{\href{https://lifecycle.r-lib.org/articles/stages.html#deprecated}{\figure{lifecycle-deprecated.svg}{options: alt='[Deprecated]'}}}{\strong{[Deprecated]}} Replaced by the \code{client} argument.} } \value{ A \link{Gargle2.0} token. } \description{ Consults the token cache for a suitable OAuth token and, if unsuccessful, gets a token via the browser flow. A cached token is suitable if it's compatible with the user's request in this sense: \itemize{ \item OAuth client must be same. \item Scopes must be same. \item Email, if provided, must be same. If specified email is a glob pattern like \code{"*@example.com"}, email matching is done at the domain level. } gargle is very conservative about using OAuth tokens discovered in the user's cache and will generally seek interactive confirmation. Therefore, in a non-interactive setting, it's important to explicitly specify the \code{"email"} of the target account or to explicitly authorize automatic discovery. See \code{\link[=gargle2.0_token]{gargle2.0_token()}}, which this function wraps, for more. Non-interactive use also suggests it might be time to use a \link[=credentials_service_account]{service account token} or \link[=credentials_external_account]{workload identity federation}. } \examples{ \dontrun{ # Drive scope, built-in gargle demo client scopes <- "https://www.googleapis.com/auth/drive" credentials_user_oauth2(scopes, client = gargle_client()) # bring your own client client <- gargle_oauth_client_from_json( path = "/path/to/the/JSON/you/downloaded/from/gcp/console.json", name = "my-nifty-oauth-client" ) credentials_user_oauth2(scopes, client) } } \seealso{ Other credential functions: \code{\link{credentials_app_default}()}, \code{\link{credentials_byo_oauth2}()}, \code{\link{credentials_external_account}()}, \code{\link{credentials_gce}()}, \code{\link{credentials_service_account}()}, \code{\link{token_fetch}()} } \concept{credential functions} gargle/man/cred_funs.Rd0000644000176200001440000001011214431310014014507 0ustar liggesusers% Generated by roxygen2: do not edit by hand % Please edit documentation in R/cred_funs.R \name{cred_funs} \alias{cred_funs} \alias{cred_funs_list} \alias{cred_funs_add} \alias{cred_funs_set} \alias{cred_funs_clear} \alias{cred_funs_list_default} \alias{cred_funs_set_default} \alias{local_cred_funs} \alias{with_cred_funs} \title{Credential function registry} \usage{ cred_funs_list() cred_funs_add(...) cred_funs_set(funs, ls = deprecated()) cred_funs_clear() cred_funs_list_default() cred_funs_set_default() local_cred_funs( funs = cred_funs_list_default(), action = c("replace", "modify"), .local_envir = caller_env() ) with_cred_funs( funs = cred_funs_list_default(), code, action = c("replace", "modify") ) } \arguments{ \item{...}{<\code{\link[rlang:dyn-dots]{dynamic-dots}}> One or more credential functions, in \code{name = value} form. Each credential function is subject to a superficial check that it at least "smells like" a credential function: its first argument must be named \code{scopes}, and its signature must include \code{...}. To remove a credential function, you can use a specification like \code{name = NULL}.} \item{funs}{A named list of credential functions.} \item{ls}{\ifelse{html}{\href{https://lifecycle.r-lib.org/articles/stages.html#deprecated}{\figure{lifecycle-deprecated.svg}{options: alt='[Deprecated]'}}}{\strong{[Deprecated]}} This argument has been renamed to \code{funs}.} \item{action}{Whether to use \code{funs} to replace or modify the registry with funs: \itemize{ \item \code{"replace"} does \code{cred_funs_set(funs)} \item \code{"modify"} does \code{cred_funs_add(!!!funs)} }} \item{.local_envir}{The environment to use for scoping. Defaults to current execution environment.} \item{code}{Code to run with temporary credential function registry.} } \value{ A list of credential functions or \code{NULL}. } \description{ Functions to query or manipulate the registry of credential functions consulted by \code{\link[=token_fetch]{token_fetch()}}. } \section{Functions}{ \itemize{ \item \code{cred_funs_list()}: Get the list of registered credential functions. \item \code{cred_funs_add()}: Register one or more new credential fetching functions. Function(s) are added to the \emph{front} of the list. So: \itemize{ \item "First registered, last tried." \item "Last registered, first tried." } Can also be used to \emph{remove} a function from the registry. \item \code{cred_funs_set()}: Register a list of credential fetching functions. \item \code{cred_funs_clear()}: Clear the credential function registry. \item \code{cred_funs_list_default()}: Return the default list of credential functions. \item \code{cred_funs_set_default()}: Reset the registry to the gargle default. \item \code{local_cred_funs()}: Modify the credential function registry in the current scope. It is an example of the \verb{local_*()} functions in \pkg{withr}. \item \code{with_cred_funs()}: Evaluate \code{code} with a temporarily modified credential function registry. It is an example of the \verb{with_*()} functions in \pkg{withr}. }} \examples{ names(cred_funs_list()) creds_one <- function(scopes, ...) {} cred_funs_add(one = creds_one) cred_funs_add(two = creds_one, three = creds_one) names(cred_funs_list()) cred_funs_add(two = NULL) names(cred_funs_list()) # restore the default list cred_funs_set_default() # remove one specific credential fetcher cred_funs_add(credentials_gce = NULL) names(cred_funs_list()) # force the use of one specific credential fetcher cred_funs_set(list(credentials_user_oauth2 = credentials_user_oauth2)) names(cred_funs_list()) # restore the default list cred_funs_set_default() # run some code with a temporary change to the registry # creds_one ONLY with_cred_funs( list(one = creds_one), names(cred_funs_list()) ) # add creds_one to the list with_cred_funs( list(one = creds_one), names(cred_funs_list()), action = "modify" ) # remove credentials_gce with_cred_funs( list(credentials_gce = NULL), names(cred_funs_list()), action = "modify" ) } \seealso{ \code{\link[=token_fetch]{token_fetch()}}, which is where the registry is actually used. } gargle/man/Gargle-class.Rd0000644000176200001440000002020214433520365015062 0ustar liggesusers% Generated by roxygen2: do not edit by hand % Please edit documentation in R/Gargle-class.R \name{Gargle-class} \alias{Gargle-class} \alias{Gargle2.0} \title{OAuth2 token objects specific to Google APIs} \description{ \code{Gargle2.0} is based on the \code{\link[httr:Token-class]{Token2.0}} class provided in httr. The preferred way to create a \code{Gargle2.0} token is through the constructor function \code{\link[=gargle2.0_token]{gargle2.0_token()}}. Key differences with \code{Token2.0}: \itemize{ \item The key for a cached \code{Token2.0} comes from hashing the endpoint, client, and scopes. For the \code{Gargle2.0} subclass, the identifier or key is expanded to include the email address associated with the token. This makes it easier to work with Google APIs with multiple identities. \item \code{Gargle2.0} tokens are cached, by default, at the user level, following the XDG spec for storing user-specific data and cache files. In contrast, the default location for \code{Token2.0} is \code{./.httr-oauth}, i.e. in current working directory. \code{Gargle2.0} behaviour makes it easier to reuse tokens across projects and makes it less likely that tokens are accidentally synced to a remote location like GitHub or DropBox. \item Each \code{Gargle2.0} token is cached in its own file. The token cache is a directory of such files. In contrast, \code{Token2.0} tokens are cached as components of a list, which is typically serialized to \code{./.httr-oauth}. } } \keyword{internal} \section{Super classes}{ \code{\link[httr:Token]{httr::Token}} -> \code{\link[httr:Token2.0]{httr::Token2.0}} -> \code{Gargle2.0} } \section{Public fields}{ \if{html}{\out{
}} \describe{ \item{\code{email}}{Email associated with the token.} \item{\code{package}}{Name of the package requesting a token. Used in messages.} \item{\code{client}}{An OAuth client.} } \if{html}{\out{
}} } \section{Methods}{ \subsection{Public methods}{ \itemize{ \item \href{#method-Gargle2.0-new}{\code{Gargle2.0$new()}} \item \href{#method-Gargle2.0-format}{\code{Gargle2.0$format()}} \item \href{#method-Gargle2.0-print}{\code{Gargle2.0$print()}} \item \href{#method-Gargle2.0-hash}{\code{Gargle2.0$hash()}} \item \href{#method-Gargle2.0-cache}{\code{Gargle2.0$cache()}} \item \href{#method-Gargle2.0-load_from_cache}{\code{Gargle2.0$load_from_cache()}} \item \href{#method-Gargle2.0-refresh}{\code{Gargle2.0$refresh()}} \item \href{#method-Gargle2.0-init_credentials}{\code{Gargle2.0$init_credentials()}} \item \href{#method-Gargle2.0-clone}{\code{Gargle2.0$clone()}} } } \if{html}{\out{
Inherited methods
}} \if{html}{\out{
}} \if{html}{\out{}} \if{latex}{\out{\hypertarget{method-Gargle2.0-new}{}}} \subsection{Method \code{new()}}{ Create a Gargle2.0 token \subsection{Usage}{ \if{html}{\out{
}}\preformatted{Gargle2.0$new( email = gargle_oauth_email(), client = gargle_client(), package = "gargle", credentials = NULL, params = list(), cache_path = gargle_oauth_cache(), app = deprecated() )}\if{html}{\out{
}} } \subsection{Arguments}{ \if{html}{\out{
}} \describe{ \item{\code{email}}{Optional email address. See \code{\link[=gargle2.0_token]{gargle2.0_token()}} for full details.} \item{\code{client}}{An OAuth consumer application.} \item{\code{package}}{Name of the package requesting a token. Used in messages.} \item{\code{credentials}}{Exists largely for testing purposes.} \item{\code{params}}{A list of parameters for the internal function \code{init_oauth2.0()}, which is a modified version of \code{\link[httr:init_oauth2.0]{httr::init_oauth2.0()}}. gargle actively uses \code{scope} and \code{use_oob}, but does not use \code{user_params}, \code{type}, \code{as_header} (hard-wired to \code{TRUE}), \code{use_basic_auth} (accept default of \code{use_basic_auth = FALSE}), \code{config_init}, or \code{client_credentials}.} \item{\code{cache_path}}{Specifies the OAuth token cache. Read more in \code{\link[=gargle_oauth_cache]{gargle_oauth_cache()}}.} \item{\code{app}}{\ifelse{html}{\href{https://lifecycle.r-lib.org/articles/stages.html#deprecated}{\figure{lifecycle-deprecated.svg}{options: alt='[Deprecated]'}}}{\strong{[Deprecated]}} Use \code{client} instead.} } \if{html}{\out{
}} } \subsection{Returns}{ A Gargle2.0 token. } } \if{html}{\out{
}} \if{html}{\out{}} \if{latex}{\out{\hypertarget{method-Gargle2.0-format}{}}} \subsection{Method \code{format()}}{ Format a Gargle2.0 token \subsection{Usage}{ \if{html}{\out{
}}\preformatted{Gargle2.0$format(...)}\if{html}{\out{
}} } \subsection{Arguments}{ \if{html}{\out{
}} \describe{ \item{\code{...}}{Not used.} } \if{html}{\out{
}} } } \if{html}{\out{
}} \if{html}{\out{}} \if{latex}{\out{\hypertarget{method-Gargle2.0-print}{}}} \subsection{Method \code{print()}}{ Print a Gargle2.0 token \subsection{Usage}{ \if{html}{\out{
}}\preformatted{Gargle2.0$print(...)}\if{html}{\out{
}} } \subsection{Arguments}{ \if{html}{\out{
}} \describe{ \item{\code{...}}{Not used.} } \if{html}{\out{
}} } } \if{html}{\out{
}} \if{html}{\out{}} \if{latex}{\out{\hypertarget{method-Gargle2.0-hash}{}}} \subsection{Method \code{hash()}}{ Generate the email-augmented hash of a Gargle2.0 token \subsection{Usage}{ \if{html}{\out{
}}\preformatted{Gargle2.0$hash()}\if{html}{\out{
}} } } \if{html}{\out{
}} \if{html}{\out{}} \if{latex}{\out{\hypertarget{method-Gargle2.0-cache}{}}} \subsection{Method \code{cache()}}{ Put a Gargle2.0 token into the cache \subsection{Usage}{ \if{html}{\out{
}}\preformatted{Gargle2.0$cache()}\if{html}{\out{
}} } } \if{html}{\out{
}} \if{html}{\out{}} \if{latex}{\out{\hypertarget{method-Gargle2.0-load_from_cache}{}}} \subsection{Method \code{load_from_cache()}}{ (Attempt to) get a Gargle2.0 token from the cache \subsection{Usage}{ \if{html}{\out{
}}\preformatted{Gargle2.0$load_from_cache()}\if{html}{\out{
}} } } \if{html}{\out{
}} \if{html}{\out{}} \if{latex}{\out{\hypertarget{method-Gargle2.0-refresh}{}}} \subsection{Method \code{refresh()}}{ (Attempt to) refresh a Gargle2.0 token \subsection{Usage}{ \if{html}{\out{
}}\preformatted{Gargle2.0$refresh()}\if{html}{\out{
}} } } \if{html}{\out{
}} \if{html}{\out{}} \if{latex}{\out{\hypertarget{method-Gargle2.0-init_credentials}{}}} \subsection{Method \code{init_credentials()}}{ Initiate a new Gargle2.0 token \subsection{Usage}{ \if{html}{\out{
}}\preformatted{Gargle2.0$init_credentials()}\if{html}{\out{
}} } } \if{html}{\out{
}} \if{html}{\out{}} \if{latex}{\out{\hypertarget{method-Gargle2.0-clone}{}}} \subsection{Method \code{clone()}}{ The objects of this class are cloneable with this method. \subsection{Usage}{ \if{html}{\out{
}}\preformatted{Gargle2.0$clone(deep = FALSE)}\if{html}{\out{
}} } \subsection{Arguments}{ \if{html}{\out{
}} \describe{ \item{\code{deep}}{Whether to make a deep clone.} } \if{html}{\out{
}} } } } gargle/DESCRIPTION0000644000176200001440000000332714456300740013231 0ustar liggesusersPackage: gargle Title: Utilities for Working with Google APIs Version: 1.5.2 Authors@R: c( person("Jennifer", "Bryan", , "jenny@posit.co", role = c("aut", "cre"), comment = c(ORCID = "0000-0002-6983-2759")), person("Craig", "Citro", , "craigcitro@google.com", role = "aut"), person("Hadley", "Wickham", , "hadley@posit.co", role = "aut", comment = c(ORCID = "0000-0003-4757-117X")), person("Google Inc", role = "cph"), person("Posit Software, PBC", role = c("cph", "fnd")) ) Description: Provides utilities for working with Google APIs . This includes functions and classes for handling common credential types and for preparing, executing, and processing HTTP requests. License: MIT + file LICENSE URL: https://gargle.r-lib.org, https://github.com/r-lib/gargle BugReports: https://github.com/r-lib/gargle/issues Depends: R (>= 3.6) Imports: cli (>= 3.0.1), fs (>= 1.3.1), glue (>= 1.3.0), httr (>= 1.4.5), jsonlite, lifecycle, openssl, rappdirs, rlang (>= 1.1.0), stats, utils, withr Suggests: aws.ec2metadata, aws.signature, covr, httpuv, knitr, rmarkdown, sodium, spelling, testthat (>= 3.1.7) VignetteBuilder: knitr Config/Needs/website: tidyverse/tidytemplate Config/testthat/edition: 3 Encoding: UTF-8 Language: en-US RoxygenNote: 7.2.3 NeedsCompilation: no Packaged: 2023-07-20 18:17:52 UTC; jenny Author: Jennifer Bryan [aut, cre] (), Craig Citro [aut], Hadley Wickham [aut] (), Google Inc [cph], Posit Software, PBC [cph, fnd] Maintainer: Jennifer Bryan Repository: CRAN Date/Publication: 2023-07-20 18:50:08 UTC gargle/build/0000755000176200001440000000000014456275120012620 5ustar liggesusersgargle/build/vignette.rds0000644000176200001440000000101714456275120015156 0ustar liggesusersTMo0~-P=qJUBj\jC?$+g-8؉=3O Ia2 ~G4xJc/'}+V+V|2]-<>:+,-JJ #qRT(&_f.bf ʘq"5OBƸjs HD+#r,JlYIGubZy&W PW%T(0F9\ڠ'UUDCpRih!KVԙ?E"4Zgtԗ hN3}Ǚ҈|U[ քv ]w'܉YWV+n+vUiyL~"ʫv nmtQ-UɆX^+VM~Kn7Tx5٩X[S gxlpN av hˣ|P*`!-Y_ޥx V{PQ5Jey)"J GQlgy]/BJ ېY .6b*Z?GۿlFnYaݥPd(? '8J-Թ"/O[T5s8)ca-C~~]$j۲+('z\Bx/C, aplB>5Og# h0~/bQ:l!/3~& gargle/tests/testthat/fixtures/tokeninfo_40X.R0000644000176200001440000000170714022166555021153 0ustar liggesusers# intent is to specify an existing, valid, but STALE token in the cache googledrive::drive_auth("jenny.f.bryan@gmail.com") token <- googledrive::drive_token() token <- token$auth_token req <- gargle::request_build( method = "GET", path = "oauth2/v3/tokeninfo", token = token ) resp <- gargle::request_make(req) # if this is not 400, it's not what we want # perhaps the token isn't actually stale? stopifnot(httr::status_code(resp) == 400) saveRDS( gargle:::redact_response(resp), testthat::test_path("fixtures", "tokeninfo-stale_400.rds"), version = 2 ) gargle::response_process(resp) # specify a bad path req <- gargle::request_build( method = "GET", path = "oauth2/v3/tokeninf", # <-- typo here token = token ) resp <- gargle::request_make(req) stopifnot(httr::status_code(resp) == 404) saveRDS( gargle:::redact_response(resp), testthat::test_path("fixtures", "tokeninfo-bad-path_404.rds"), version = 2 ) gargle::response_process(resp) gargle/tests/testthat/fixtures/sheets-spreadsheets-get-nonexistent-sheet-id_404.R0000644000176200001440000000077514022166555027733 0ustar liggesusers# ---- sheet ID that does not exist googlesheets4::gs4_deauth() req <- googlesheets4::request_generate( endpoint = "sheets.spreadsheets.get", params = list( spreadsheetId = "DOES_NOT_EXIST", fields = "spreadsheetId" ) ) resp <- googlesheets4::request_make(req) stopifnot(httr::status_code(resp) == 404) saveRDS( gargle:::redact_response(resp), testthat::test_path( "fixtures", "sheets-spreadsheets-get-nonexistent-sheet-id_404.rds" ), version = 2 ) gargle::response_process(resp) gargle/tests/testthat/fixtures/drive-files-get-nonexistent-file-id_404.R0000644000176200001440000000067514022166555025767 0ustar liggesusers# ---- file ID that does not exist googledrive::drive_deauth() req <- googledrive::request_generate( endpoint = "drive.files.get", params = list( fileId = "NOPE_NOT_A_GOOD_ID" ) ) resp <- googledrive::request_make(req) stopifnot(httr::status_code(resp) == 404) saveRDS( gargle:::redact_response(resp), testthat::test_path("fixtures", "drive-files-get-nonexistent-file-id_404.rds"), version = 2 ) gargle::response_process(resp) gargle/tests/testthat/fixtures/sheets-spreadsheets-get-api-key-not-enabled_403.rds0000644000176200001440000000276714067372466030004 0ustar liggesusersWMoEv|NR*ԑVJMdZ4 dMv4̬zpWʭ'~+;;zqć8DycyD21 O2Gɮ30~֑녂"EX&إ"onW*l˅?h4*QlQb[S:!rz-_z[~T?{D[v>6/B bpڦNLz(7Q>W#- ob׵%eN`2; "4vrbۧԍެ{N-/=r^[/A{Q߉krÒSŘ ]nu?Ug%VάJ+ յbFqu钥KJw._ZtQs|ʙҝ,㆛9!g%"ܻa>982'. e׵8&,xHDF0[4!9u3[_s;"r&b%rkqP7\%2l˜Uɱ# FZL6m,P8OAA%iMm*$/GRtOaa) i4ː9Im_K1`RC%`7ׁ༟8sK&.Sׂqs)Ec b5nx'5irR#ON/N0hI 8{1 y4B9u&0+@ V̮,P'rXE,$:&A z-Y^]- bQ@&VzQQNiR1*MF Tz7Cc*(͉<ЀyXD91!A1hQQ:$EtA)]܆Sq$B@رOeB\Vj^=onUc[oV{>(t@Uu\qCk /9^9~m]arg{#)>BxV}Of"߽]d9xJbQ(Qe4ʻ @K5~}tORs~\AuJLh(Y:j;5}{&7G E:,S^,$yΜxkO OR}h6,gG~woq!ix?b7;l2:]S1vcAJ"SN`3vG!"Zω@.;ɢkgargle/tests/testthat/fixtures/sheets-spreadsheets-get-quota-exceeded-readgroup_429.rds0000644000176200001440000000267714431310014031122 0ustar liggesusersW_oEwlǦNSZUxA]q պI SR!dm־mn{oB߃'KTؽ=^鲻32L6f3ٜ殊vY| |::36!db"`VӈU֙CzOAfƓ`a >'ٌ57wZ:یŕo*)`;A}㨚J/QDqLA]ʈ} h {~r+F-F{s7̡x 9J Biww&%؄x߻۽#r /+Ʌ!GprP|)^$΄QU& to<1>Z vGF,#R< +Q= )HM-%6vmmF@[*nNv-ڌwZ%(>)S ϩ )|-|(JRE-(ҟ [ H.ASC%E^ۙ.rlWtTώ? gargle/tests/testthat/fixtures/drive-automated-queries_429.rds0000644000176200001440000000312214431310014024230 0ustar liggesusersWOoEwMR#Gmz/MbBSR$JCAXƻݝl||$>8#q#^9I쭋z fgޛ}b.f\~WTr,B?,-!B] I} b")V0m.{EccV\z@_Yf"2;--ˡ'*(NRw#ۤyq4˥Ko*knk:#V$ڥRIj oU[?G4O S& = #l(KjcOSLJ7P=#sPGhtP25vq8=Zgʓ?=$ssPN5ZT1vZ崫r"9D%<%-C2CLb/ӘF ۳MѨpz* VQ>(/&2oigYww2Ւmwۑ3NϒNDJ|O;Uv0)@d;:1@Naٗ~hwN$#PͭNV׻Eea梷xnlhhG=xAHBd ƾm/ E`oh_X\?{s, z?VsKʟjЉC`IL9IJ\,wv>ȡE=@R]&sɤ5<YPH{t網 l /¨N%9 P ͍"u'>s4jEΪ鹤D5J.BIvL<8" mO[d)l-a;' ch#(G>_Źs&*+B;N.Agargle/tests/testthat/fixtures/sheets-spreadsheets-get-nonexistent-sheet-id_404.rds0000644000176200001440000000225114067372466030322 0ustar liggesusersVoEG)E U,c8Q`8BprGc4̬c΁c%_!g8 zD[?iߛ}͞%#H,#ŮÈ +Cyn`,xn@&e/$*ÛmkH3pǨ)gǜgmFZ>wvQ.#CO_ YoE dD\-bqlq"Hzf. =b3. ^{/ ѳFѧBdYϜH8C|^€{ oq~ƯyPNLөAK'M4ˉ1ω0IBmkJ!1d"'5x;$Y'A%4~/`'%,]V L!wf%8FD@\PԱ\e~e5`J^%}⣵_^#ÿNKjE$)s_'+R??)[z/>>|z^}> $tC,:axQeݤܱgM-k` >dGȦ^ djw9zPRb}QxhfE%|]? p.Oިi%5GKC*~pNoGGH)vrjnZyYnwz p r`^z#TRnjzBYbXh,5"+zX" F .kw,mNR@ 1s9WSW-Ҹ|$'9nPG؎XB _/EOy2#-Mg K7qHɿ)Rն8 G.A [ }Fp'D͑ gargle/tests/testthat/fixtures/drive-automated-queries_429.R0000644000176200001440000000074514431310014023651 0ustar liggesusers# I got a response object from a user # see discussion here: # https://github.com/r-lib/gargle/issues/254 resp <- readRDS("~/Downloads/gargle-last-response.rds") # this response object was captured before I started to omit the handle from # the stored response, so I do it retroactively resp$handle <- NULL gargle::response_process(resp) stopifnot(httr::status_code(resp) == 429) saveRDS( resp, testthat::test_path("fixtures", "drive-automated-queries_429.rds"), version = 2 ) gargle/tests/testthat/fixtures/sheets-spreadsheets-get-nonexistent-range_400.rds0000644000176200001440000000233714067372466027715 0ustar liggesusersVoE#󠨀T,.k$zwffֵ pā3āK/\zf;~,k7i2|3=4P(#?W?G|Jg̢t2`1 Mi"t~dL?;{N똢.8e%S9~W9-`v -T+5=neoAI^Svf0ؿ]7No TPPKπe uC_j$X ! ,@`3 I[u\Y>.6$EvYHrق[ G+[~,zA}@Pݭת=z6Jb.MlH]PRE5Q[yyVlօ|Cky[ܭ x]H=e aA%vE`p qxK$ IgZрäs`C\O)MZ39/mMbA ,I{|?\j]4pLh.@BùF11l"} 09^aC-0%8x%Q%La=+㓏㒔`(J΂/y _x~%LÒ04|<%/C;9S;VwM}[٫'?_b%6tuq>-T~|W|Jj_~^~vrz'A# mkT4Km&jCwa>pa _OTe l3f3*F EOrQ`G5K^̖Lը% ۹ ~ #8. FsqŮMLea'6ER5Z^:##6 qX{X[Fa^ݸ%o̧[vXi%/#*(_+ ioo_%qF꠾Z,a!q꨼_ի{ՃNQR12:æ~=dߒ:Då&QOBQ%$*lBa5 KQiemsEQ (Zʚr~j5Ju1- Sێu5k|Lq [2lD3XA0ŷq6(|&@\EjBJLm.]}dUw(iS2aLYLttd, aJ] `~!11YؓnqgQeOyQ_q?+ `s}|`#m4OG1䏑.&kGc{S;&N Cx8h_O_v`.29JzYm#4o*R!8UPIJJcJ M%x& f? -Ef:)F"q,Mh!nmNE.+Vuxo@Km1 JXҮqMҐwʂRF vz RcK?SdSb3t6!~h6ḨW~zg{^ݑ]Ǐ~t_P͊Ed1(IȉcًX`ަXԬ`3CN82C5yJ.h)ᮝ8\cfq'!@|^-4*o`;ϖUŲۄ~t;o(|V泛;<(֏dKzSj! fZ(Ma^Ȯ4?dƦ9ɡsP;Ň F-,8oK1YAoa/\ec#9jM4d56ast)irrM8v9DE3J\̙Q(O6f,GL=Լ.Es*xl 7:#;qgQW z b|gargle/tests/testthat/fixtures/sheets-spreadsheets-get-api-key-not-enabled_403.R0000644000176200001440000000133314025711277027370 0ustar liggesusers# ---- API key that does not have Sheets enabled req <- gargle::request_develop( endpoint = googlesheets4::gs4_endpoints("sheets.spreadsheets.get")[[1]], params = list( spreadsheetId = "DOES_NOT_MATTER", fields = "spreadsheetId" ), base_url = "https://sheets.googleapis.com/" ) req <- gargle::request_build( path = req$path, method = req$method, params = req$params, key = gargle::gargle_api_key(), base_url = req$base_url ) resp <- gargle::request_make(req) stopifnot(httr::status_code(resp) == 403) saveRDS( gargle:::redact_response(resp), testthat::test_path( "fixtures", "sheets-spreadsheets-get-api-key-not-enabled_403.rds" ), version = 2 ) gargle::response_process(resp) gargle/tests/testthat/fixtures/sheets-spreadsheets-get-quota-exceeded-readgroup_429.R0000644000176200001440000000154214431310014030521 0ustar liggesusers# ---- make too many read requests in a time interval googlesheets4::gs4_deauth() deaths_id <- as.character(googlesheets4::gs4_example("deaths")) f <- function(ssid = deaths_id, i = 0) { req <- googlesheets4::request_generate( "sheets.spreadsheets.get", params = list(spreadsheetId = ssid, fields = "spreadsheetId") ) raw_resp <- gargle::request_make(req) code <- httr::status_code(raw_resp) cat(i, "code:", code, "\n") if (code >= 200 && code < 300) { invisible(TRUE) } else { invisible(raw_resp) } } n <- 300 i <- 0 resp <- TRUE while (isTRUE(resp) && i < n) { i <- i + 1 resp <- f(i = i) } resp stopifnot(httr::status_code(resp) == 429) saveRDS( gargle:::redact_response(resp), testthat::test_path("fixtures", "sheets-spreadsheets-get-quota-exceeded-readgroup_429.rds"), version = 2 ) gargle::response_process(resp) gargle/tests/testthat/fixtures/tokeninfo-bad-path_404.rds0000644000176200001440000000304714431310014023134 0ustar liggesusersWoDlHҔJ 4UEk76ޏ6i6jP4kϮ=f<$? 7. Z!.?pΙ[yw#B@$o޼g˅BX+ Yx?K\g0'5W0tu=Cl 5^E4Uy0]&p>' jv1hBaVaZ;1JcڪՐiƦ[̝)8vz>>2AdZ¹=à-DĎ 2c1l.i*U=$WKKV8!=ቼԋKzGwZ_dA 4DamǢō%qhR=-GMŏO6ң6ǚ*Hssі28p6._.t?Qе. ,bǡA2N?6m搓eÁnamF8pڑ'mlvyȺtj698m3H^E02j>^X#_% Aɨ2u 57;"/.tD%9k3Q *U.!B)ҸTnP\.KIj Z㱾R!)ItZ%sMsrS=i3!bٜA@Te$`" Z㱓{WBKBuvHd Y滍NM3REXxrhzȢG=fֲނVI@=$GΞ:F.ܑ|h֣:D9kܗLM `)'lMxn:4cb7G\$[zr D8Q=v)賮GN H.@%WyOaK71gv[ߜMC箨TL-dlօ?rEWxAa8Т;Ce2\a%D@E~2*.ryrcҡC:q`b1"rR-N3%3D,Ld;E3Pb6ص%+)>HtV&I쐒OyAвCSF!-?$sgargle/tests/testthat/fixtures/sheets-spreadsheets-get-bad-field-mask_400.rds0000644000176200001440000000254614067372466027007 0ustar liggesusersVoEwu۷Vo?jڇ]+omu̯(mQ*__*8(E>gqJ|ۼwWWD#mWw*^uY%|ZEf̚IQv,>S% bs%en`:Z "Kk׳+1 b+;]DB G[;prp=iK'>(T[ۻo[Kj)V,.eQ ˫4af.OYz\6O]*^:}ii}Zcu)en|<6v|++;1Z̦nSﳕǣDw 3ټɪ}9n+DLKJ?68eB ;2+:V4}NBH1- mr2g++C |H15Z.qVe4OC$XF+/bL " pfakM>ΆXg,c(NHG9rW;$FFg8{`hcP D1%X4B95&0!CNLH 'vuA3j#̛~D˜mۻ[nMKNq{"+*n5Belb;R+:6n)s_1z@X]514tn8 jAm(!vU֣e<- 7ULum i`([%ZaaD̞( uqN9TMGՐ2o܍_.|׍[/6~_x4GsG*fZw1NlI_Jkc's݁,<5hV2NI&#Q*|'l"+sݶbjZjrXe"|ٗ-IH\!<zx%7Jb4 X]Hߦl@/% DEqDm s*ѾqƇ~#&b ߵN7/Z֙s["0mhcNcI]cMf|yJs!F5b"|N 3 ̩2_ [D]vb1+A qc{vfqzo@₄ĉC'Nzq@}1v-EVy3s{[2L6sl^sWKۀg a쌄fzvvVR:40 hԬ0qpxhU ԧ0kWY1avR4X:=}ŒS$wl kưS #G3 M1POuƈvǠTWQie}6w=ސ7b ǑN2L E{ l4ΝNA5$) )SkSG'Cl܍s84{: ی[X0(Iҙ tJ}OM1W+F}ᤜH^#l ~T?RbG7oǶ_#vj߱JJ!4jRooͶrk89ߌ[f32:j1B=8%X[d05ˍf,n)&47Z ǵv}Z6wu}R3iC2[K@{ll!ӻ7'n@݄=E,i_Ҩ%,mɷ!iҨNӧW.!ٚ.|R̤< "V@qR0pLmI9jKl5Vc-^ Yryjc,P T6 'nJ ;flu}x||XW @D @D @D @ 4eel ̄3&K>5>e P>?1Aw ޛJ\4cp^rDDZxa`l$F'6k pp V^d"1ZCaYU3x(E7wS8; VY K+BAs"D%`yH4p7WUq齨CLcxlhZԴ^ rG{~wv{]~~!Ȗ?^/fK@ܣV u~p;7Xۯ=߾}7*/ƲpE^3 Т*&O;lcԲVd2}D,@Ff/o6ZF,)ODub%%Us฻n,fRʎa[dm2r/KtE>ʼx/z\"^§Y ߀DyQ_8:[[.`ު4?]Au]tc¦!j Dm^&_jbҺO86Er31M0#-vۥUS,]]1ߛ?gŬBC6t'XxکUnLs:KG5p/ bNI'H;]6 DUgargle/tests/testthat/test-roxygen-templates.R0000644000176200001440000000307114433502265021311 0ustar liggesuserstest_that("PREFIX_auth_description()", { expect_snapshot(writeLines(PREFIX_auth_description())) }) test_that("PREFIX_auth_details()", { expect_snapshot(writeLines(PREFIX_auth_details())) }) test_that("PREFIX_auth_params()", { expect_snapshot(writeLines(PREFIX_auth_params())) }) test_that("PREFIX_deauth_description_with_api_key()", { expect_snapshot(writeLines(PREFIX_deauth_description_with_api_key())) }) test_that("PREFIX_deauth_description_no_api_key()", { expect_snapshot(writeLines(PREFIX_deauth_description_no_api_key())) }) test_that("PREFIX_token_description()", { expect_snapshot(writeLines(PREFIX_token_description())) }) test_that("PREFIX_token_return()", { expect_snapshot(writeLines(PREFIX_token_return())) }) test_that("PREFIX_has_token_description()", { expect_snapshot(writeLines(PREFIX_has_token_description())) }) test_that("PREFIX_has_token_return()", { expect_snapshot(writeLines(PREFIX_has_token_return())) }) test_that("PREFIX_auth_configure_description()", { expect_snapshot(writeLines(PREFIX_auth_configure_description())) }) test_that("PREFIX_auth_configure_params()", { expect_snapshot(writeLines(PREFIX_auth_configure_params())) }) test_that("PREFIX_auth_configure_return()", { expect_snapshot(writeLines(PREFIX_auth_configure_return())) }) test_that("PREFIX_user_description()", { expect_snapshot(writeLines(PREFIX_user_description())) }) test_that("PREFIX_user_seealso()", { expect_snapshot(writeLines(PREFIX_user_seealso())) }) test_that("PREFIX_user_return()", { expect_snapshot(writeLines(PREFIX_user_return())) }) gargle/tests/testthat/test-Gargle-class.R0000644000176200001440000001017414433473312020130 0ustar liggesuserstest_that("email is ingested correctly", { fauxen_email <- function(email = NULL) { gargle2.0_token(email = email, credentials = list(a = 1))$email } expect_null(fauxen_email()) expect_null(fauxen_email(NULL)) expect_equal(fauxen_email(NA), NA_character_) expect_equal(fauxen_email(FALSE), NA_character_) expect_equal(fauxen_email(TRUE), "*") expect_equal(fauxen_email("a@example.org"), "a@example.org") }) test_that("email can be set in option", { fauxen_email <- function(email = NULL) { withr::with_options( list(gargle_oauth_email = email), gargle2.0_token(credentials = list(a = 1))$email ) } expect_null(fauxen_email(NULL)) expect_equal(fauxen_email(NA), NA_character_) expect_equal(fauxen_email(FALSE), NA_character_) expect_equal(fauxen_email(TRUE), "*") expect_equal(fauxen_email("a@example.org"), "a@example.org") }) test_that("Attempt to initiate OAuth2 flow fails if non-interactive", { local_interactive(FALSE) expect_snapshot(gargle2.0_token(cache = FALSE), error = TRUE) }) test_that("`email = NA`, `email = FALSE` means we don't consult the cache", { cache_folder <- path_temp("email-na-test") withr::defer(dir_delete(cache_folder)) local_interactive(FALSE) # make sure there's one token in the cache and that, by default, we use it fauxen_in <- gargle2.0_token( email = "a@example.org", credentials = list(a = 1), cache = cache_folder ) # can't use with_gargle_verbosity() here, because we use # local_gargle_verbosity("info") in token_match(), to force the user to # see messaging about auto-discovery suppressMessages( fauxen_out <- gargle2.0_token(cache = cache_folder) ) expect_gargle2.0_token(fauxen_in, fauxen_out) # `email = NA` and `email = FALSE` prevent the cache from being consulted expect_snapshot( gargle2.0_token(email = NA, cache = cache_folder), error = TRUE ) expect_snapshot( gargle2.0_token(email = FALSE, cache = cache_folder), error = TRUE ) }) test_that("Gargle2.0 prints nicely", { fauxen <- gargle2.0_token( email = "a@example.org", client = gargle_oauth_client(id = "CLIENT_ID", secret = "SECRET", name = "CLIENT"), credentials = list(a = 1), cache = FALSE ) expect_snapshot(print(fauxen)) }) test_that("we reject redirect URIs from conventional OOB for pseudo-OOB flow", { expect_snapshot( error = TRUE, select_pseudo_oob_value("urn:ietf:wg:oauth:2.0:oob") ) expect_error( select_pseudo_oob_value("urn:ietf:wg:oauth:2.0:oob:auto"), class = "gargle_error" ) expect_error( select_pseudo_oob_value("oob"), class = "gargle_error" ) }) test_that("we reject local web server redirect URIs for pseudo-OOB flow", { expect_snapshot( error = TRUE, select_pseudo_oob_value("http://localhost") ) expect_error( select_pseudo_oob_value("http://localhost:4000"), class = "gargle_error" ) expect_error( select_pseudo_oob_value("http://127.0.0.1:1410"), class = "gargle_error" ) }) test_that("we reject non-https redirect URIs for pseudo-OOB flow", { expect_error( select_pseudo_oob_value("http://example.com/google-callback/blah.html"), class = "gargle_error" ) }) test_that("we insist on finding exactly one redirect URI for pseudo-OOB flow", { redirect_uris <- c( "https://example.com/google-callback/one.html", "https://example.com/google-callback/two.html" ) expect_snapshot(error = TRUE, select_pseudo_oob_value(redirect_uris)) }) test_that("we can identify the redirect URI suitable for pseudo-OOB flow", { redirect_uris <- c( "http://localhost:8111/", "http://localhost:8111", "http://127.0.0.1:8100/", "https://codepen.io/USER/full/abcdef123456" ) expect_equal(select_pseudo_oob_value(redirect_uris), redirect_uris[4]) }) test_that("gargle2.0_token(app) is deprecated but still works", { withr::local_options(lifecycle_verbosity = "warning") client <- gargle_oauth_client(id = "CLIENT_ID", secret = "SECRET", name = "ABC") expect_snapshot( t <- gargle2.0_token(email = NA, credentials = list(a = 1), app = client) ) expect_equal(t$client$name, "ABC") expect_equal(t$app$appname, "ABC") }) gargle/tests/testthat/test-inside-the-house.R0000644000176200001440000000416314431310014020762 0ustar liggesuserstest_that("gargle is 'inside the house'", { expect_true(from_permitted_package()) expect_no_error(check_permitted_package()) }) test_that("it is possible to be 'outside the house'", { expect_false(local(gargle:::from_permitted_package(), envir = globalenv())) expect_snapshot( local(gargle:::check_permitted_package(), envir = globalenv()), error = TRUE ) }) test_that("gargle API key", { key <- gargle_api_key() expect_true(is_string(key)) }) test_that("tidyverse API key", { key <- tidyverse_api_key() expect_true(is_string(key)) expect_snapshot( local(tidyverse_api_key(), envir = globalenv()), error = TRUE ) }) test_that("gargle oauth app (deprecated)", { expect_snapshot( oa <- gargle_app() ) expect_s3_class(oa, "oauth_app") expect_match(oa$appname, "^gargle") }) test_that("gargle oauth installed client", { oc <- gargle_client() expect_s3_class(oc, "gargle_oauth_client") expect_s3_class(oc, "oauth_app") expect_match(oc$name, "^gargle") expect_equal(oc$type, "installed") expect_equal(gargle_client("installed"), oc) }) test_that("gargle oauth web client", { oc <- gargle_client("web") expect_s3_class(oc, "gargle_oauth_client") expect_s3_class(oc, "oauth_app") expect_match(oc$name, "^gargle") expect_equal(oc$type, "web") expect_equal(oc$redirect_uris, "https://www.tidyverse.org/google-callback/") }) test_that("tidyverse oauth app (deprecated)", { expect_snapshot( oa <- tidyverse_app() ) expect_s3_class(oa, "oauth_app") expect_match(oa$appname, "^tidyverse") }) test_that("tidyverse oauth installed client", { oc <- tidyverse_client() expect_s3_class(oc, "gargle_oauth_client") expect_s3_class(oc, "oauth_app") expect_match(oc$name, "^tidyverse") expect_equal(oc$type, "installed") expect_equal(tidyverse_client("installed"), oc) }) test_that("tidyverse oauth web client", { oc <- tidyverse_client("web") expect_s3_class(oc, "gargle_oauth_client") expect_s3_class(oc, "oauth_app") expect_match(oc$name, "^tidyverse") expect_equal(oc$type, "web") expect_equal(oc$redirect_uris, "https://www.tidyverse.org/google-callback/") }) gargle/tests/testthat/test-credentials_gce.R0000644000176200001440000000307214431310014020721 0ustar liggesuserstest_that("GCE metadata server hostname is correct w.r.t. option and env var", { withr::local_options(list(gargle.gce.use_ip = NULL)) withr::local_envvar(c(GCE_METADATA_URL = NA)) expect_equal(gce_metadata_hostname(), "metadata.google.internal") withr::local_options(list(gargle.gce.use_ip = FALSE)) expect_equal(gce_metadata_hostname(), "metadata.google.internal") withr::local_envvar(GCE_METADATA_URL = "some.fake.hostname") expect_equal(gce_metadata_hostname(), "some.fake.hostname") }) test_that("GCE metadata server IP address is correct w.r.t. option and env var", { withr::local_options(list(gargle.gce.use_ip = TRUE)) withr::local_envvar(c(GCE_METADATA_IP = NA)) expect_equal(gce_metadata_hostname(), "169.254.169.254") withr::local_envvar(c(GCE_METADATA_IP = "1.2.3.4")) expect_equal(gce_metadata_hostname(), "1.2.3.4") }) test_that("GCE metadata detection fails not on GCE", { withr::local_envvar(GCE_METADATA_URL = "some.fake.hostname") expect_false(is_gce()) }) test_that("Can list service accounts", { skip_if_not(is_gce(), "Not on GCE") service_accounts <- gce_instance_service_accounts() expect_s3_class(service_accounts, class = "data.frame") }) test_that("gce_timeout() works", { withr::with_options( list(gargle.gce.timeout = NULL), { expect_equal(gce_timeout(), 0.8) expect_equal(gce_timeout(), 2) } ) withr::with_options( new = list(gargle.gce.timeout = 100), { expect_equal(gce_timeout(), 100) expect_equal(gce_timeout(200), 100) expect_equal(gce_timeout(), 200) } ) }) gargle/tests/testthat/test-secret.R0000644000176200001440000000303714436207160017110 0ustar liggesusers# testing just the secret_* bits that are unique to gargle ---- test_that("secret_encrypt_json()/secret_decrypt_json() round-trip", { key <- openssl::rand_bytes(32) pth <- withr::local_tempfile() in_pth <- fs::path_package("gargle", "extdata", "fake_service_account.json") # path in secret_encrypt_json(in_pth, pth, key = key) res <- secret_decrypt_json(pth, key = key) expect_equal( jsonlite::fromJSON(in_pth, simplifyVector = FALSE), jsonlite::fromJSON(res, simplifyVector = FALSE) ) # string in in_str <- readChar(in_pth, nchars = fs::file_size(in_pth)) secret_encrypt_json(in_str, pth, key = key) res <- secret_decrypt_json(pth, key = key) expect_equal( jsonlite::fromJSON(in_str, simplifyVector = FALSE), jsonlite::fromJSON(res, simplifyVector = FALSE) ) }) test_that("secret_get_key() error", { withr::local_envvar(TESTTHAT = NA) expect_snapshot( error = TRUE, secret_get_key("HA_HA_HA_NO") ) }) test_that("as_key() error", { expect_snapshot( error = TRUE, as_key(pi) ) }) # gargle's older deprecated secret_ functions ---- test_that("older secret functions are deprecated", { withr::local_options(lifecycle_verbosity = "warning") withr::local_envvar(FAKEPKG_PASSWORD = "fake_password") expect_snapshot(secret_pw_name("pkg")) expect_snapshot(absorb_it <- secret_pw_gen()) expect_snapshot(secret_pw_exists("fakePKG")) expect_snapshot(absorb_it <- secret_pw_get("fakePKG")) expect_snapshot(secret_can_decrypt("fakePKG")) # leaving secret_write, secret_read untested }) gargle/tests/testthat/test-credentials_byo_oauth2.R0000644000176200001440000000251014433520365022250 0ustar liggesuserstest_that("credentials_byo_oauth2() demands a Token2.0", { expect_snapshot( credentials_byo_oauth2(token = "a_naked_access_token"), error = TRUE ) }) test_that("credentials_byo_oauth2() rejects a token that obviously not Google", { fauxen <- gargle2.0_token( email = "a@example.org", client = gargle_oauth_client(id = "CLIENT_ID", secret = "SECRET", name = "CLIENT"), credentials = list(access_token = "ACCESS_TOKEN_1"), cache = FALSE ) fauxen$endpoint <- httr::oauth_endpoints("github") expect_snapshot(credentials_byo_oauth2(token = fauxen), error = TRUE) }) test_that("credentials_byo_oauth2() just passes valid input through", { token <- httr::Token2.0$new( app = httr::oauth_app("x", "y", "z"), endpoint = httr::oauth_endpoints("google"), credentials = list(access_token = "ACCESS_TOKEN"), cache_path = FALSE ) expect_equal(credentials_byo_oauth2(token = token), token) }) test_that("credentials_byo_oauth2() extracts a token from a request", { token <- httr::Token2.0$new( app = httr::oauth_app("x", "y", "z"), endpoint = httr::oauth_endpoints("google"), credentials = list(access_token = "ACCESS_TOKEN"), cache_path = FALSE ) configured_token <- httr::config(token = token) expect_equal( credentials_byo_oauth2(token = configured_token), token ) }) gargle/tests/testthat/test-oauth-refresh.R0000644000176200001440000000167014321544762020405 0ustar liggesusers test_that("'deleted_client' causes extra special feedback", { err <- list( error = "deleted_client", error_description = "The OAuth client was deleted." ) expect_snapshot( gargle_refresh_failure( err, httr::oauth_app(appname = NULL, key = "KEY", secret = "SECRET") ) ) expect_snapshot( gargle_refresh_failure( err, httr::oauth_app(appname = "APPNAME", key = "KEY", secret = "SECRET") ) ) expect_snapshot( gargle_refresh_failure( err, httr::oauth_app(appname = "APPNAME", key = "KEY", secret = "SECRET"), package = "PACKAGE" ) ) expect_snapshot( gargle_refresh_failure( err, httr::oauth_app(appname = "fake-calliope", key = "KEY", secret = "SECRET") ) ) expect_snapshot( gargle_refresh_failure( err, httr::oauth_app(appname = "fake-calliope", key = "KEY", secret = "SECRET"), package = "PACKAGE" ) ) }) gargle/tests/testthat/test-oauth-cache.R0000644000176200001440000002507414433723755020023 0ustar liggesusers# cache_establish ------------------------------------------------------------ test_that("cache_establish() insists on sensible input", { expect_snapshot(cache_establish(letters[1:2]), error = TRUE) expect_snapshot(cache_establish(1), error = TRUE) expect_snapshot(cache_establish(list(1)), error = TRUE) }) test_that("`cache = TRUE` uses default cache path", { local_mocked_bindings( ## we don't want to actually initialize a cache cache_create = function(path) NULL ) expect_equal(cache_establish(TRUE), gargle_default_oauth_cache_path()) }) test_that("`cache = FALSE` does nothing", { expect_null(cache_establish(FALSE)) }) test_that("`cache = NA` is like `cache = FALSE` if cache not available", { local_mocked_bindings( # we want no existing cache to be found, be it current or legacy gargle_default_oauth_cache_path = function() file_temp(), cache_allowed = function(path) FALSE ) expect_equal(cache_establish(NA), cache_establish(FALSE)) }) test_that("`cache = ` creates cache folder, recursively", { tmpfolder <- path_temp("foo", "bar") withr::defer(dir_delete(tmpfolder)) cache_establish(tmpfolder) expect_true(dir_exists(tmpfolder)) }) test_that("`cache = ` adds new cache folder to relevant 'ignores'", { tmpproj <- file_temp() withr::defer(dir_delete(tmpproj)) local_gargle_verbosity("silent") dir_create(tmpproj) writeLines("", path(tmpproj, "DESCRIPTION")) writeLines("", path(tmpproj, ".gitignore")) cache_establish(path(tmpproj, "oauth-cache")) expect_match(readLines(path(tmpproj, ".gitignore")), "oauth-cache$") expect_match( readLines(path(tmpproj, ".Rbuildignore")), "oauth-cache$", fixed = TRUE ) }) test_that("default is to consult and set the oauth cache option", { withr::local_options(list(gargle_oauth_cache = NA)) local_mocked_bindings( # we want no existing cache to be found, be it current or legacy gargle_default_oauth_cache_path = function() file_temp(), cache_allowed = function(path) FALSE ) expect_equal(getOption("gargle_oauth_cache"), NA) cache_establish() expect_false(getOption("gargle_oauth_cache")) }) # cache_allowed() -------------------------------------------------------------- test_that("cache_allowed() returns false when non-interactive (or testing)", { local_interactive(FALSE) expect_false(cache_allowed(getwd())) }) # cache_clean() -------------------------------------------------------------- test_that("cache_clean() works", { cache_folder <- path_temp("cache_clean-test") withr::defer(dir_delete(cache_folder)) fauxen_a <- gargle2.0_token( email = "a@example.org", client = gargle_oauth_client(id = "CLIENT_ID", secret = "SECRET", name = "apple"), credentials = list(a = 1), cache = cache_folder ) fauxen_b <- gargle2.0_token( email = "b@example.org", client = gargle_oauth_client(id = "CLIENT_ID", secret = "SECRET", name = "banana"), credentials = list(b = 1), cache = cache_folder ) dat <- gargle_oauth_dat(cache_folder) expect_equal(nrow(dat), 2) expect_snapshot( cache_clean(cache_folder, "apple") ) dat <- gargle_oauth_dat(cache_folder) expect_equal(dat$client, "banana") local_gargle_verbosity("silent") cache_clean(cache_folder, "banana") dat <- gargle_oauth_dat(cache_folder) expect_equal(nrow(dat), 0) }) # validate_token_list() ------------------------------------------------------ test_that("cache_load() repairs tokens stored with names != their hash", { cache_folder <- file_temp() withr::defer(dir_delete(cache_folder)) fauxen_a <- gargle2.0_token( email = "a@example.org", credentials = list(a = 1), cache = cache_folder ) fauxen_b <- gargle2.0_token( email = "b@example.org", credentials = list(b = 1), cache = cache_folder ) file_move( dir_ls(cache_folder), path(cache_folder, c("abc123_c@example.org", "def456_d@example.org")) ) local_gargle_verbosity("debug") # bit of fiddliness to deal with hashes that can vary by OS out <- capture.output( tokens <- cache_load(cache_folder), type = "message" ) out <- sub("[[:xdigit:]]+(?=.+\\(hash\\)$)", "{TOKEN_HASH}", out, perl = TRUE) expect_snapshot( writeLines(out) ) expect_gargle2.0_token(tokens[[1]], fauxen_a) expect_gargle2.0_token(tokens[[2]], fauxen_b) }) # token into and out of cache --------------------------------------------- test_that("token_from_cache() returns NULL when caching turned off", { fauxen <- gargle2.0_token( email = "a@example.org", credentials = list(a = 1), cache = FALSE ) expect_null(token_from_cache(fauxen)) }) test_that("token_into_cache(), token_from_cache() roundtrip", { cache_folder <- file_temp() withr::defer(dir_delete(cache_folder)) ## this calls token_into_cache() token_in <- gargle2.0_token( email = "a@example.org", credentials = list(a = 1), cache = cache_folder ) ## this calls token_from_cache() token_out <- gargle2.0_token( email = "a@example.org", cache = cache_folder ) expect_gargle2.0_token(token_in, token_out) expect_equal(token_out$credentials, list(a = 1)) }) # token_match() ---------------------------------------- test_that("token_match() returns NULL if nothing to match against", { expect_null(token_match("whatever", character())) }) test_that("token_match() returns the full match", { one_existing <- "abc_a@example.com" two_existing <- c(one_existing, "def_b@example.com") expect_equal( one_existing, token_match(one_existing, one_existing) ) expect_equal( one_existing, token_match(one_existing, two_existing) ) }) test_that("token_match() returns NULL if email given, but no full match", { candidate <- "abc_a@example.org" expect_null(token_match(candidate, "def_a@example.org")) expect_null(token_match(candidate, "abc_b@example.org")) expect_null(token_match(candidate, "a@example.org")) expect_null(token_match(candidate, "abc")) expect_null(token_match(candidate, "abc_")) }) test_that("token_match() returns NULL if no email and no short hash match", { expect_null(token_match("abc_", "def_a@example.org")) expect_null(token_match("abc_*", "def_a@example.org")) }) test_that("token_match() finds a match based on domain", { one_match_of_two <- c("abc_jane@example.org", "abc_jane@gmail.com") expect_snapshot( m <- token_match("abc_*@example.org", one_match_of_two) ) expect_equal(m, one_match_of_two[[1]]) }) test_that("token_match() scolds but returns short hash match when non-interactive", { local_interactive(FALSE) one_existing <- "abc_a@example.com" two_existing <- c(one_existing, "abc_b@example.com") expect_snapshot( m <- token_match("abc_", one_existing) ) expect_equal(m, one_existing) expect_snapshot( m <- token_match("abc_*", one_existing) ) expect_equal(m, one_existing) expect_snapshot( m <- token_match("abc_", two_existing) ) expect_equal(m, one_existing) expect_snapshot( m <- token_match("abc_*", two_existing) ) expect_equal(m, one_existing) }) # situation report ---------------------------------------------------------- test_that("gargle_oauth_dat() reports on specified cache", { tmp_cache <- file_temp() withr::defer(dir_delete(tmp_cache)) gargle2.0_token( email = "a@example.org", credentials = list(a = 1), cache = tmp_cache ) gargle2.0_token( email = "b@example.org", credentials = list(b = 2), cache = tmp_cache ) dat <- gargle_oauth_dat(tmp_cache) expect_s3_class(dat, "gargle_oauth_dat") expect_equal(nrow(dat), 2) expect_equal(dat$email, c("a@example.org", "b@example.org")) }) test_that("gargle_oauth_dat() is OK with nonexistent or empty cache", { tmp_cache <- file_temp() withr::defer(dir_delete(tmp_cache)) columns <- c("email", "client", "scopes", "hash", "filepath") dat <- gargle_oauth_dat(tmp_cache) expect_s3_class(dat, "gargle_oauth_dat") expect_equal(nrow(dat), 0) expect_setequal(names(dat), columns) gargle2.0_token( email = "a@example.org", credentials = list(a = 1), cache = tmp_cache ) file_delete(dir_ls(tmp_cache)) dat <- gargle_oauth_dat(tmp_cache) expect_s3_class(dat, "gargle_oauth_dat") expect_equal(nrow(dat), 0) expect_setequal(names(dat), columns) }) test_that("gargle_oauth_sitrep() works with a cache", { tmp_cache <- file_temp() withr::defer(dir_delete(tmp_cache)) gargle2.0_token( email = "a@example.org", credentials = list(a = 1), cache = tmp_cache ) gargle2.0_token( email = "b@example.org", credentials = list(b = 2), cache = tmp_cache ) # bit of fiddliness to remove the volatile path and the hashes that can vary # by OS out <- capture.output( gargle_oauth_sitrep(tmp_cache), type = "message" ) out <- sub(tmp_cache, "{path to gargle oauth cache}", out, fixed = TRUE) out <- sub("[[:xdigit:]]{7}[.]{3}", "{hash...}", out) expect_snapshot( writeLines(out) ) }) test_that("gargle_oauth_sitrep() consults the option for cache location", { tmp_cache <- file_temp() withr::defer(dir_delete(tmp_cache)) gargle2.0_token( email = "a@example.org", credentials = list(a = 1), cache = tmp_cache ) withr::local_options(list(gargle_oauth_cache = tmp_cache)) local_gargle_verbosity("debug") out <- capture.output( gargle_oauth_sitrep(), type = "message" ) # bit of fiddliness to remove the volatile path and the hashes that can vary # by OS out <- sub(tmp_cache, "{path to gargle oauth cache}", out, fixed = TRUE) out <- sub("[[:xdigit:]]{7}[.]{3}", "{hash...}", out) expect_snapshot( writeLines(out) ) }) # helpers ----------------------------------------------------------- test_that("match2() works", { expect_equal(match2("a", c("a", "b", "a")), c(1L, 3L)) expect_equal(match2("b", c("a", "b", "a")), 2L) expect_true(is.na(match2("c", c("a", "b", "a")))) }) test_that("mask_email() works", { hash <- "2a46e6750476326f7085ebdab4ad103d" expect_equal( mask_email(c( "2a46e6750476326f7085ebdab4ad103d_jenny@example.com", "2a46e6750476326f7085ebdab4ad103d_NA", "2a46e6750476326f7085ebdab4ad103d_", "2a46e6750476326f7085ebdab4ad103d_FIRST_LAST@example.com" )), rep_len(hash, 4) ) }) test_that("extract_email() works", { expect_equal(extract_email("abc123_a"), "a") expect_equal(extract_email("abc123_b@example.com"), "b@example.com") expect_equal(extract_email("abc123_"), "") expect_equal(extract_email("abc123_FIRST_LAST@a.com"), "FIRST_LAST@a.com") }) test_that("keep_hash_paths() works", { x <- c("aa_bb_cc", "a.md", "b.rds", "c.txt", "dd123_e@example.org") expect_equal(keep_hash_paths(x), x[c(1, 5)]) }) gargle/tests/testthat/test-token-info.R0000644000176200001440000000124114436207160017667 0ustar liggesuserstest_that("token_*() functions work", { skip_if_offline() token <- credentials_service_account( scopes = "https://www.googleapis.com/auth/userinfo.email", path = secret_decrypt_json( fs::path_package("gargle", "secret", "gargle-testing.json"), key = "GARGLE_KEY" ) ) expect_no_error( # this implies a call to token_userinfo() email <- token_email(token) ) expect_no_error( tokeninfo <- token_tokeninfo(token) ) expect_match(email, "^gargle-testing@.*[.]iam[.]gserviceaccount[.]com") expect_equal(email, tokeninfo$email) expect_true( "https://www.googleapis.com/auth/userinfo.email" %in% tokeninfo$scope ) }) gargle/tests/testthat/test-AuthState-class.R0000644000176200001440000001034614433520365020633 0ustar liggesuserstest_that("inputs are checked when creating AuthState", { client <- gargle_oauth_client(id = "CLIENT_ID", secret = "SECRET") expect_snapshot( init_AuthState( package = NULL, client = client, api_key = "API_KEY", auth_active = TRUE ), error = TRUE ) expect_snapshot(init_AuthState(client = "not_an_oauth_client"), error = TRUE) expect_snapshot(init_AuthState(client = client, api_key = 1234), error = TRUE) expect_snapshot( init_AuthState(client = client, api_key = "API_KEY", auth_active = NULL), error = TRUE ) a <- init_AuthState( package = "PACKAGE", client = client, api_key = "API_KEY", auth_active = TRUE ) expect_s3_class(a, "AuthState") }) test_that("AuthState client can be modified and cleared", { client <- gargle_oauth_client(id = "CLIENT_ID", secret = "SECRET", name = "AAA") a <- init_AuthState(client = client, api_key = "API_KEY", auth_active = TRUE) expect_equal(a$client$name, "AAA") client2 <- gargle_oauth_client(id = "CLIENT_ID", secret = "SECRET", name = "BBB") a$set_client(client2) expect_equal(a$client$name, "BBB") a$set_client(NULL) expect_null(a$client) }) test_that("AuthState api_key can be modified and cleared", { client <- gargle_oauth_client(id = "CLIENT_ID", secret = "SECRET", name = "AAA") a <- init_AuthState(client = client, api_key = "AAA", auth_active = TRUE) expect_equal(a$api_key, "AAA") a$set_api_key("BBB") expect_equal(a$api_key, "BBB") a$set_api_key(NULL) expect_null(a$api_key) }) test_that("AuthState auth_active can be toggled", { client <- gargle_oauth_client(id = "CLIENT_ID", secret = "SECRET", name = "AAA") a <- init_AuthState(client = client, api_key = "AAA", auth_active = TRUE) expect_true(a$auth_active) a$set_auth_active(FALSE) expect_false(a$auth_active) }) test_that("AuthState supports basic handling of cred", { client <- gargle_oauth_client(id = "CLIENT_ID", secret = "SECRET", name = "AAA") a <- init_AuthState(client = client, api_key = "AAA", auth_active = TRUE) a$set_cred("hi") expect_true(a$has_cred()) expect_equal(a$get_cred(), "hi") a$clear_cred() expect_false(a$has_cred()) a$set_cred("bye") expect_equal(a$get_cred(), "bye") }) test_that("AuthState prints nicely", { client <- gargle_oauth_client(id = "CLIENT_ID", secret = "SECRET", name = "AAA") a <- init_AuthState( package = "PKG", client = client, api_key = "API_KEY", auth_active = TRUE ) a$set_cred(structure("TOKEN", class = "some_sort_of_token")) expect_snapshot(print(a)) }) test_that("init_Authstate(app) argument is deprecated, but still works", { withr::local_options(lifecycle_verbosity = "warning") client <- gargle_oauth_client(id = "CLIENT_ID", secret = "SECRET") expect_snapshot( a <- init_AuthState( package = "PACKAGE", app = client, api_key = "API_KEY", auth_active = TRUE ) ) expect_s3_class(a, "AuthState") expect_s3_class(a$client, "gargle_oauth_client") }) test_that("AuthState$new(app) is deprecated, but still works", { withr::local_options(lifecycle_verbosity = "warning") client <- gargle_oauth_client(id = "CLIENT_ID", secret = "SECRET") expect_snapshot( a <- AuthState$new( package = "PACKAGE", app = client, api_key = "API_KEY", auth_active = TRUE ) ) expect_s3_class(a, "AuthState") expect_s3_class(a$client, "gargle_oauth_client") }) test_that("$set_app is deprecated, but still works", { withr::local_options(lifecycle_verbosity = "warning") client <- gargle_oauth_client(id = "CLIENT_ID", secret = "SECRET", name = "AAA") a <- init_AuthState( client = client, # this just needs to be some package that is guaranteed to be installed, in # order to fully exercise the deprecation warning package = "rlang", api_key = "API_KEY", auth_active = TRUE ) client2 <- gargle_oauth_client(id = "CLIENT_ID", secret = "SECRET", name = "BBB") expect_snapshot( a$set_app(client2) ) expect_equal(a$client$name, "BBB") }) test_that("$app still returns the client", { client <- gargle_oauth_client(id = "CLIENT_ID", secret = "SECRET", name = "AAA") a <- init_AuthState(client = client, api_key = "API_KEY", auth_active = TRUE) expect_equal(a$app, client) }) gargle/tests/testthat/test-oauth-init.R0000644000176200001440000000111214431310014017660 0ustar liggesuserstest_that("use_oob must be TRUE or FALSE", { expect_snapshot(error = TRUE, check_oob("a")) expect_snapshot(error = TRUE, check_oob(c(FALSE, FALSE))) }) test_that("OOB requires an interactive session", { local_interactive(FALSE) expect_snapshot(error = TRUE, check_oob(TRUE)) }) test_that("makes no sense to pass oob_value if not OOB", { skip_if_not_installed("httpuv") local_interactive(TRUE) expect_snapshot(error = TRUE, check_oob(FALSE, "custom_value")) }) test_that("oob_value has to be a string", { expect_snapshot(error = TRUE, check_oob(TRUE, c("a", "b"))) }) gargle/tests/testthat/helper.R0000644000176200001440000000051414436207160016122 0ustar liggesusersexpect_gargle2.0_token <- function(object, expected) { expect_equal(object$cache_path, expected$cache_path) expect_equal( object$endpoint, expected$endpoint) expect_equal( object$email, expected$email) expect_equal( object$app, expected$app) expect_equal( object$params, expected$params) } gargle/tests/testthat/_snaps/0000755000176200001440000000000014456265227016015 5ustar liggesusersgargle/tests/testthat/_snaps/oauth-refresh.md0000644000176200001440000000352214452076502021105 0ustar liggesusers# 'deleted_client' causes extra special feedback Code gargle_refresh_failure(err, httr::oauth_app(appname = NULL, key = "KEY", secret = "SECRET")) Condition Warning: Unable to refresh token, because the associated OAuth client has been deleted. --- Code gargle_refresh_failure(err, httr::oauth_app(appname = "APPNAME", key = "KEY", secret = "SECRET")) Condition Warning: Unable to refresh token, because the associated OAuth client has been deleted. * Client name: 'APPNAME' --- Code gargle_refresh_failure(err, httr::oauth_app(appname = "APPNAME", key = "KEY", secret = "SECRET"), package = "PACKAGE") Condition Warning: Unable to refresh token, because the associated OAuth client has been deleted. * Client name: 'APPNAME' i If you did not configure this OAuth client, it may be built into the PACKAGE package. If so, consider re-installing PACKAGE to get an updated client. --- Code gargle_refresh_failure(err, httr::oauth_app(appname = "fake-calliope", key = "KEY", secret = "SECRET")) Condition Warning: Unable to refresh token, because the associated OAuth client has been deleted. i You appear to be relying on the default client used by the gargle package. Consider re-installing gargle, in case the default client has been updated. --- Code gargle_refresh_failure(err, httr::oauth_app(appname = "fake-calliope", key = "KEY", secret = "SECRET"), package = "PACKAGE") Condition Warning: Unable to refresh token, because the associated OAuth client has been deleted. i You appear to be relying on the default client used by the PACKAGE package. Consider re-installing PACKAGE and gargle, in case the default client has been updated. gargle/tests/testthat/_snaps/AuthState-class.md0000644000176200001440000000431714452076500021337 0ustar liggesusers# inputs are checked when creating AuthState Code init_AuthState(package = NULL, client = client, api_key = "API_KEY", auth_active = TRUE) Condition Error in `initialize()`: ! is_scalar_character(package) is not TRUE --- Code init_AuthState(client = "not_an_oauth_client") Condition Error in `initialize()`: ! is.null(client) || is.oauth_app(client) is not TRUE --- Code init_AuthState(client = client, api_key = 1234) Condition Error in `initialize()`: ! is.null(api_key) || is_string(api_key) is not TRUE --- Code init_AuthState(client = client, api_key = "API_KEY", auth_active = NULL) Condition Error in `initialize()`: ! is_bool(auth_active) is not TRUE # AuthState prints nicely Code print(a) Output -- ---------------------------------------------------- package: PKG client: AAA api_key: API_KEY auth_active: TRUE credentials: # init_Authstate(app) argument is deprecated, but still works Code a <- init_AuthState(package = "PACKAGE", app = client, api_key = "API_KEY", auth_active = TRUE) Condition Warning: The `app` argument of `init_AuthState()` is deprecated as of gargle 1.5.0. i Please use the `client` argument instead. # AuthState$new(app) is deprecated, but still works Code a <- AuthState$new(package = "PACKAGE", app = client, api_key = "API_KEY", auth_active = TRUE) Condition Warning: The `app` argument of `AuthState$initialize()` is deprecated as of gargle 1.5.0. i Please use the `client` argument instead. i The deprecated feature was likely used in the R6 package. Please report the issue at . # $set_app is deprecated, but still works Code a$set_app(client2) Condition Warning: `AuthState$set_app()` was deprecated in gargle 1.5.0. i Please use `AuthState$set_client()` instead. i This probably needs to be addressed in the rlang package. i Please report the issue at . gargle/tests/testthat/_snaps/credentials_byo_oauth2.md0000644000176200001440000000071014452076500022753 0ustar liggesusers# credentials_byo_oauth2() demands a Token2.0 Code credentials_byo_oauth2(token = "a_naked_access_token") Condition Error in `credentials_byo_oauth2()`: ! inherits(token, "Token2.0") is not TRUE # credentials_byo_oauth2() rejects a token that obviously not Google Code credentials_byo_oauth2(token = fauxen) Condition Error in `credentials_byo_oauth2()`: ! Token doesn't use Google's OAuth endpoint. gargle/tests/testthat/_snaps/Gargle-class.md0000644000176200001440000000420014452076501020626 0ustar liggesusers# Attempt to initiate OAuth2 flow fails if non-interactive Code gargle2.0_token(cache = FALSE) Condition Error in `self$init_credentials()`: ! OAuth2 flow requires an interactive session. # `email = NA`, `email = FALSE` means we don't consult the cache Code gargle2.0_token(email = NA, cache = cache_folder) Condition Error in `self$init_credentials()`: ! OAuth2 flow requires an interactive session. --- Code gargle2.0_token(email = FALSE, cache = cache_folder) Condition Error in `self$init_credentials()`: ! OAuth2 flow requires an interactive session. # Gargle2.0 prints nicely Code print(fauxen) Output -- -------------------------------------------------------- oauth_endpoint: google client: CLIENT email: 'a@example.org' scopes: ...userinfo.email credentials: a # we reject redirect URIs from conventional OOB for pseudo-OOB flow Code select_pseudo_oob_value("urn:ietf:wg:oauth:2.0:oob") Condition Error in `select_pseudo_oob_value()`: ! OAuth client does not have a redirect URI suitable for the pseudo-OOB flow. # we reject local web server redirect URIs for pseudo-OOB flow Code select_pseudo_oob_value("http://localhost") Condition Error in `select_pseudo_oob_value()`: ! OAuth client does not have a redirect URI suitable for the pseudo-OOB flow. # we insist on finding exactly one redirect URI for pseudo-OOB flow Code select_pseudo_oob_value(redirect_uris) Condition Error in `select_pseudo_oob_value()`: ! Can't determine which redirect URI to use for the pseudo-OOB flow: * https://example.com/google-callback/one.html * https://example.com/google-callback/two.html # gargle2.0_token(app) is deprecated but still works Code t <- gargle2.0_token(email = NA, credentials = list(a = 1), app = client) Condition Warning: The `app` argument of `gargle2.0_token()` is deprecated as of gargle 1.5.0. i Please use the `client` argument instead. gargle/tests/testthat/_snaps/gargle_oauth_client.md0000644000176200001440000000367514452076500022337 0ustar liggesusers# gargle_oauth_client() rejects bad input Code gargle_oauth_client() Condition Error in `gargle_oauth_client()`: ! `id` must be a single string, not absent. --- Code gargle_oauth_client(1234) Condition Error in `gargle_oauth_client()`: ! `id` must be a single string, not the number 1234. --- Code gargle_oauth_client(id = "ID") Condition Error in `gargle_oauth_client()`: ! `secret` must be a single string, not absent. --- Code gargle_oauth_client(id = "ID", secret = 1234) Condition Error in `gargle_oauth_client()`: ! `secret` must be a single string, not the number 1234. --- Code gargle_oauth_client("ID", "SECRET", type = "nope") Condition Error in `gargle_oauth_client()`: ! `type` must be one of "installed" or "web", not "nope". # gargle_oauth_client() has special handling for web clients Code gargle_oauth_client("ID", "SECRET", type = "web") Condition Error in `gargle_oauth_client()`: ! A "web" type OAuth client must have one or more 'redirect_uris'. --- Code gargle_oauth_client("ID", "SECRET", type = "web", redirect_uris = c( "http://localhost:8111/", "http://127.0.0.1:8100/", "https://example.com/aaa/bbb/v")) Message name: 7f82e05dfbeb26a264621f1482a14e25 id: ID secret: type: web redirect_uris: http://localhost:8111/, http://127.0.0.1:8100/, https://example.com/aaa/bbb/v # service account JSON throws an informative error Code gargle_oauth_client_from_json(test_path("fixtures", "service-account-token.json")) Condition Error in `gargle_oauth_client_from_json()`: ! JSON has an unexpected form i Are you sure this is the JSON downloaded for an OAuth client? i It is easy to confuse the JSON for an OAuth client and a service account. gargle/tests/testthat/_snaps/oauth-cache.md0000644000176200001440000001044414452076502020513 0ustar liggesusers# cache_establish() insists on sensible input Code cache_establish(letters[1:2]) Condition Error in `cache_establish()`: ! `cache` must have length 1, not 2. --- Code cache_establish(1) Condition Error in `cache_establish()`: ! `cache` must be logical or character, not the number 1. --- Code cache_establish(list(1)) Condition Error in `cache_establish()`: ! `cache` must be logical or character, not a list. # cache_clean() works Code cache_clean(cache_folder, "apple") Message v Deleting 1 token obtained with an old tidyverse OAuth client. i Expect interactive prompts to re-auth with the new client. ! Is this rolling of credentials highly disruptive to your workflow? That means you should rely on your own OAuth client (or switch to a service account token). Learn more these in these articles: Output [1] TRUE # cache_load() repairs tokens stored with names != their hash Code writeLines(out) Output ! Cache contains tokens with names that do not match their hash: * "abc123_c@example.org" (name) * '{TOKEN_HASH}_a@example.org' (hash) * "def456_d@example.org" (name) * '{TOKEN_HASH}_b@example.org' (hash) Will attempt to repair by renaming # token_match() finds a match based on domain Code m <- token_match("abc_*@example.org", one_match_of_two) Message i The gargle package is using a cached token for 'jane@example.org'. # token_match() scolds but returns short hash match when non-interactive Code m <- token_match("abc_", one_existing) Message ! Using an auto-discovered, cached token. To suppress this message, modify your code or options to clearly consent to the use of a cached token. See gargle's "Non-interactive auth" vignette for more details: i The gargle package is using a cached token for 'a@example.com'. --- Code m <- token_match("abc_*", one_existing) Message i The gargle package is using a cached token for 'a@example.com'. --- Code m <- token_match("abc_", two_existing) Message i Suitable tokens found in the cache, associated with these emails: * 'a@example.com' * 'b@example.com' Defaulting to the first email. ! Using an auto-discovered, cached token. To suppress this message, modify your code or options to clearly consent to the use of a cached token. See gargle's "Non-interactive auth" vignette for more details: i The gargle package is using a cached token for 'a@example.com'. --- Code m <- token_match("abc_*", two_existing) Message i Suitable tokens found in the cache, associated with these emails: * 'a@example.com' * 'b@example.com' Defaulting to the first email. ! Using an auto-discovered, cached token. To suppress this message, modify your code or options to clearly consent to the use of a cached token. See gargle's "Non-interactive auth" vignette for more details: i The gargle package is using a cached token for 'a@example.com'. # gargle_oauth_sitrep() works with a cache Code writeLines(out) Output 2 tokens found in this gargle OAuth cache: '{path to gargle oauth cache}' email client scopes hash... _____________ ___________ ______ __________ a@example.org gargle-clio {hash...} b@example.org gargle-clio {hash...} # gargle_oauth_sitrep() consults the option for cache location Code writeLines(out) Output i Taking cache location from the `"gargle_oauth_cache"` option. 1 token found in this gargle OAuth cache: '{path to gargle oauth cache}' email client scopes hash... _____________ ___________ ______ __________ a@example.org gargle-clio {hash...} gargle/tests/testthat/_snaps/cred_funs.md0000644000176200001440000000257214452076500020303 0ustar liggesusers# We insist on valid credential function (or NULL) Code cred_funs_add(a = mean) Condition Error in `cred_funs_check()`: ! Not a valid credential function: x Element 1 --- Code cred_funs_set(list(a = NULL)) Condition Error in `cred_funs_check()`: ! Not a valid credential function: x Element 1 # We insist on uniquely named credential functions Code cred_funs_add(creds_one) Condition Error in `cred_funs_check()`: ! Each credential function must have a unique name --- Code cred_funs_add(a = creds_one) Condition Error in `cred_funs_add()`: ! This name already appears in the credential function registry: x 'a' --- Code cred_funs_set(list(creds_one, a = function(scopes, ...) { })) Condition Error in `cred_funs_check()`: ! Each credential function must have a unique name --- Code cred_funs_set(list(a = creds_one, a = function(scopes, ...) { })) Condition Error in `cred_funs_check()`: ! Each credential function must have a unique name # cred_funs_set() warns for use of `ls` Code out <- cred_funs_set(ls = list(a = function(scopes, ...) { })) Condition Warning: The `ls` argument of `cred_funs_set()` is deprecated as of gargle 1.3.0. i Please use the `funs` argument instead. gargle/tests/testthat/_snaps/credentials_service_account.md0000644000176200001440000000136514456265227024075 0ustar liggesusers# check_is_service_account() errors for OAuth client Code PKG_auth(fs::path_package("gargle", "extdata", "client_secret_installed.googleusercontent.com.json")) Condition Error in `PKG_auth()`: ! `path` does not represent a service account. Did you provide the JSON for an OAuth client instead of for a service account? Use `PKG_auth_configure()` to configure the OAuth client. # check_is_service_account() errors for invalid input Code PKG_auth("wut") Condition Error in `PKG_auth()`: ! `path` does not represent a service account. Did you provide the JSON for an OAuth client instead of for a service account? Use `PKG_auth_configure()` to configure the OAuth client. gargle/tests/testthat/_snaps/response_process.md0000644000176200001440000001562614452076513021737 0ustar liggesusers# Resource exhausted (Sheets, ReadGroup) Code response_process(resp) Condition Error in `expect_recorded_error()`: ! Client error: (429) RESOURCE_EXHAUSTED * Either out of resource quota or reaching rate limiting. The client should look for google.rpc.QuotaFailure error detail for more information. * Quota exceeded for quota metric 'Read requests' and limit 'Read requests per minute per user' of service 'sheets.googleapis.com' for consumer 'project_number:603366585132'. Error details: * reason: RATE_LIMIT_EXCEEDED * domain: googleapis.com * metadata.quota_location: global * metadata.quota_metric: sheets.googleapis.com/read_requests * metadata.quota_limit: ReadRequestsPerMinutePerUser * metadata.quota_limit_value: 60 * metadata.consumer: projects/603366585132 * metadata.service: sheets.googleapis.com Links * Description: Request a higher quota limit. URL: https://cloud.google.com/docs/quota#requesting_higher_quota # Request for non-existent resource (Drive) Code response_process(resp) Condition Error in `expect_recorded_error()`: ! Client error: (404) Not Found File not found: NOPE_NOT_A_GOOD_ID. * domain: global * reason: notFound * message: File not found: NOPE_NOT_A_GOOD_ID. * locationType: parameter * location: fileId # Too many requests (Drive, HTML content) Code response_process(resp) Condition Error in `expect_recorded_error()`: ! Client error: (429) Too Many Requests (RFC 6585) x Expected content type 'application/json', not 'text/html'. i See 'VOLATILE_FILE_PATH' for the html error content. i Or execute `browseURL("VOLATILE_FILE_PATH")` to view it in your browser. # HTML error is offered as a file Code strwrap(readLines(path_to_html_error), width = 60) Output [1] "Sorry...
Sorry...
Google

We're sorry...

... but" [16] "your computer or network may be sending automated queries." [17] "To protect our users, we can't process your request right" [18] "now.

See Google" [20] "Help for more information.

Google" [23] "Home" # Request for which we don't have scope (Fitness) Code response_process(resp) Condition Error in `expect_recorded_error()`: ! Client error: (403) Forbidden Request had insufficient authentication scopes. PERMISSION_DENIED * message: Insufficient Permission * domain: global * reason: insufficientPermissions # Use key that's not enabled for the API (Sheets) Code response_process(resp) Condition Error in `expect_recorded_error()`: ! Client error: (403) PERMISSION_DENIED * Client does not have sufficient permission. This can happen because the OAuth token does not have the right scopes, the client doesn't have permission, or the API has not been enabled for the client project. * Google Sheets API has not been used in project 977449744253 before or it is disabled. Enable it by visiting https://console.developers.google.com/apis/api/sheets.googleapis.com/overview?project=977449744253 then retry. If you enabled this API recently, wait a few minutes for the action to propagate to our systems and retry. Error details: Links * Description: Google developers console API activation URL: https://console.developers.google.com/apis/api/sheets.googleapis.com/overview?project=977449744253 * reason: SERVICE_DISABLED * domain: googleapis.com * metadata.consumer: projects/977449744253 * metadata.service: sheets.googleapis.com # Request with invalid argument (Sheets, bad range) Code response_process(resp) Condition Error in `expect_recorded_error()`: ! Client error: (400) INVALID_ARGUMENT * Client specified an invalid argument. Check error message and error details for more information. * Unable to parse range: NOPE!A5:F15 # Request with bad field mask (Sheets) Code response_process(resp) Condition Error in `expect_recorded_error()`: ! Client error: (400) INVALID_ARGUMENT * Client specified an invalid argument. Check error message and error details for more information. * Request contains an invalid argument. Error details: Field violations * Field: sheets.sheetProperties Description: Error expanding 'fields' parameter. Cannot find matching fields for path 'sheets.sheetProperties'. # Request for nonexistent resource (Sheets) Code response_process(resp) Condition Error in `expect_recorded_error()`: ! Client error: (404) NOT_FOUND * A specified resource is not found, or the request is rejected by undisclosed reasons, such as whitelisting. * Requested entity was not found. # Request with invalid value (tokeninfo, stale token) Code response_process(resp) Condition Error in `expect_recorded_error()`: ! Client error: (400) Bad Request * Invalid Value # Request to bad URL (tokeninfo, HTML content) Code response_process(resp) Condition Error in `expect_recorded_error()`: ! Client error: (404) Not Found x Expected content type 'application/json', not 'text/html'. i See 'VOLATILE_FILE_PATH' for the html error content. i Or execute `browseURL("VOLATILE_FILE_PATH")` to view it in your browser. gargle/tests/testthat/_snaps/request_retry.md0000644000176200001440000000162414452076512021250 0ustar liggesusers# request_retry() logic works as advertised Code fail_then_succeed <- request_retry(max_total_wait_time_in_seconds = 5) Message x Request failed [429] oops i Retry 1 happens in {WAIT_TIME} seconds ... (strategy: exponential backoff, full jitter) --- Code fail_then_succeed <- request_retry() Message x Request failed [429] oops i Retry 1 happens in {WAIT_TIME} seconds ... (strategy: 'Retry-After' header) --- Code fail_max_tries <- request_retry(max_tries_total = 3, max_total_wait_time_in_seconds = 6) Message x Request failed [429] oops i Retry 1 happens in {WAIT_TIME} seconds ... (strategy: exponential backoff, full jitter) x Request failed [429] oops i Retry 2 happens in {WAIT_TIME} seconds ... (strategy: exponential backoff, full jitter) gargle/tests/testthat/_snaps/oauth-init.md0000644000176200001440000000161314452076502020411 0ustar liggesusers# use_oob must be TRUE or FALSE Code check_oob("a") Condition Error in `check_oob()`: ! `use_oob` must be `TRUE` or `FALSE`, not the string "a". --- Code check_oob(c(FALSE, FALSE)) Condition Error in `check_oob()`: ! `use_oob` must be `TRUE` or `FALSE`, not a logical vector. # OOB requires an interactive session Code check_oob(TRUE) Condition Error in `check_oob()`: ! Out-of-band auth only works in an interactive session. # makes no sense to pass oob_value if not OOB Code check_oob(FALSE, "custom_value") Condition Error in `check_oob()`: ! The `oob_value` argument can only be used when `use_oob = TRUE`. # oob_value has to be a string Code check_oob(TRUE, c("a", "b")) Condition Error in `check_oob()`: ! Out-of-band auth only works in an interactive session. gargle/tests/testthat/_snaps/inside-the-house.md0000644000176200001440000000331314452076501021500 0ustar liggesusers# it is possible to be 'outside the house' Code local(gargle:::check_permitted_package(), envir = globalenv()) Condition Error: ! Attempt to directly access a credential that can only be used within tidyverse packages. This error may mean that you need to: * Create a new project on Google Cloud Platform. * Enable relevant APIs for your project. * Create an API key and/or an OAuth client ID. * Configure your requests to use your API key and OAuth client ID. i See gargle's "How to get your own API credentials" vignette for more details: i # tidyverse API key Code local(tidyverse_api_key(), envir = globalenv()) Condition Error in `tidyverse_api_key()`: ! Attempt to directly access a credential that can only be used within tidyverse packages. This error may mean that you need to: * Create a new project on Google Cloud Platform. * Enable relevant APIs for your project. * Create an API key and/or an OAuth client ID. * Configure your requests to use your API key and OAuth client ID. i See gargle's "How to get your own API credentials" vignette for more details: i # gargle oauth app (deprecated) Code oa <- gargle_app() Condition Warning: `gargle_app()` was deprecated in gargle 1.3.0. i Please use `gargle_client()` instead. # tidyverse oauth app (deprecated) Code oa <- tidyverse_app() Condition Warning: `tidyverse_app()` was deprecated in gargle 1.3.0. i Please use `tidyverse_client()` instead. gargle/tests/testthat/_snaps/request_develop.md0000644000176200001440000000045114452076502021535 0ustar liggesusers# request_develop() errors for unrecognized parameters These parameters are unknown: x 'b' x 'c' i API endpoint: 'some.api.endpoint' # request_develop() errors if required parameter is missing These parameters are missing: x 'a' i API endpoint: 'some.api.endpoint' gargle/tests/testthat/_snaps/gargle_oauth_endpoint.md0000644000176200001440000000052414452076500022667 0ustar liggesusers# gargle_oauth_endpoint() snapshot Code gargle_oauth_endpoint() Output authorize: https://accounts.google.com/o/oauth2/v2/auth access: https://oauth2.googleapis.com/token validate: https://oauth2.googleapis.com/tokeninfo revoke: https://oauth2.googleapis.com/revoke gargle/tests/testthat/_snaps/secret.md0000644000176200001440000000376214452076513017626 0ustar liggesusers# secret_get_key() error Code secret_get_key("HA_HA_HA_NO") Condition Error: ! Env var `HA_HA_HA_NO` not defined. # as_key() error Code as_key(pi) Condition Error in `as_key()`: ! `key` must be one of the following: * a string giving the name of an env var * a raw vector containing the key * a string wrapped in `I()` that contains the base64url encoded key # older secret functions are deprecated Code secret_pw_name("pkg") Condition Warning: `secret_pw_name()` was deprecated in gargle 1.5.0. i Use the new secret functions instead: i Output [1] "PKG_PASSWORD" --- Code absorb_it <- secret_pw_gen() Condition Warning: `secret_pw_gen()` was deprecated in gargle 1.5.0. i Use the new secret functions instead: i --- Code secret_pw_exists("fakePKG") Condition Warning: `secret_pw_name()` was deprecated in gargle 1.5.0. i Use the new secret functions instead: i Output [1] TRUE --- Code absorb_it <- secret_pw_get("fakePKG") Condition Warning: `secret_pw_name()` was deprecated in gargle 1.5.0. i Use the new secret functions instead: i --- Code secret_can_decrypt("fakePKG") Condition Warning: `secret_can_decrypt()` was deprecated in gargle 1.5.0. i Use the new secret functions instead: i Warning: `secret_pw_name()` was deprecated in gargle 1.5.0. i Use the new secret functions instead: i Output [1] TRUE gargle/tests/testthat/_snaps/roxygen-templates.md0000644000176200001440000001773614453545575022050 0ustar liggesusers# PREFIX_auth_description() Code writeLines(PREFIX_auth_description()) Output @description Authorize PACKAGE to view and manage YOUR STUFF. This function is a wrapper around [gargle::token_fetch()]. By default, you are directed to a web browser, asked to sign in to your Google account, and to grant PACKAGE permission to operate on your behalf with A GOOGLE PRODUCT. By default, with your permission, these user credentials are cached in a folder below your home directory, from where they can be automatically refreshed, as necessary. Storage at the user level means the same token can be used across multiple projects and tokens are less likely to be synced to the cloud by accident. # PREFIX_auth_details() Code writeLines(PREFIX_auth_details()) Output @details Most users, most of the time, do not need to call `PREFIX_auth()` explicitly -- it is triggered by the first action that requires authorization. Even when called, the default arguments often suffice. However, when necessary, `PREFIX_auth()` allows the user to explicitly: * Declare which Google identity to use, via an `email` specification. * Use a service account token or workload identity federation via `path`. * Bring your own `token`. * Customize `scopes`. * Use a non-default `cache` folder or turn caching off. * Explicitly request out-of-band (OOB) auth via `use_oob`. If you are interacting with R within a browser (applies to RStudio Server, Posit Workbench, Posit Cloud, and Google Colaboratory), you need OOB auth or the pseudo-OOB variant. If this does not happen automatically, you can request it explicitly with `use_oob = TRUE` or, more persistently, by setting an option via `options(gargle_oob_default = TRUE)`. The choice between conventional OOB or pseudo-OOB auth is determined by the type of OAuth client. If the client is of the "installed" type, `use_oob = TRUE` results in conventional OOB auth. If the client is of the "web" type, `use_oob = TRUE` results in pseudo-OOB auth. Packages that provide a built-in OAuth client can usually detect which type of client to use. But if you need to set this explicitly, use the `"gargle_oauth_client_type"` option: ```r options(gargle_oauth_client_type = "web") # pseudo-OOB # or, alternatively options(gargle_oauth_client_type = "installed") # conventional OOB ``` For details on the many ways to find a token, see [gargle::token_fetch()]. For deeper control over auth, use [PREFIX_auth_configure()] to bring your own OAuth client or API key. To learn more about gargle options, see [gargle::gargle_options]. # PREFIX_auth_params() Code writeLines(PREFIX_auth_params()) Output @inheritParams gargle::credentials_service_account @inheritParams gargle::credentials_external_account @inheritParams gargle::credentials_app_default @inheritParams gargle::credentials_gce @inheritParams gargle::credentials_byo_oauth2 @inheritParams gargle::credentials_user_oauth2 @inheritParams gargle::gargle2.0_token # PREFIX_deauth_description_with_api_key() Code writeLines(PREFIX_deauth_description_with_api_key()) Output @description Put PACKAGE into a de-authorized state. Instead of sending a token, PACKAGE will send an API key. This can be used to access public resources for which no Google sign-in is required. This is handy for using PACKAGE in a non-interactive setting to make requests that do not require a token. It will prevent the attempt to obtain a token interactively in the browser. The user can configure their own API key via [PREFIX_auth_configure()] and retrieve that key via [PREFIX_api_key()]. In the absence of a user-configured key, a built-in default key is used. # PREFIX_deauth_description_no_api_key() Code writeLines(PREFIX_deauth_description_no_api_key()) Output @description Clears any currently stored token. The next time PACKAGE needs a token, the token acquisition process starts over, with a fresh call to [PREFIX_auth()] and, therefore, internally, a call to [gargle::token_fetch()]. Unlike some other packages that use gargle, PACKAGE is not usable in a de-authorized state. Therefore, calling `PREFIX_deauth()` only clears the token, i.e. it does NOT imply that subsequent requests are made with an API key in lieu of a token. # PREFIX_token_description() Code writeLines(PREFIX_token_description()) Output @description For internal use or for those programming around the GOOGLE API. Returns a token pre-processed with [httr::config()]. Most users do not need to handle tokens "by hand" or, even if they need some control, [PREFIX_auth()] is what they need. If there is no current token, [PREFIX_auth()] is called to either load from cache or initiate OAuth2.0 flow. If auth has been deactivated via [PREFIX_deauth()], `PREFIX_token()` returns `NULL`. # PREFIX_token_return() Code writeLines(PREFIX_token_return()) Output @return A `request` object (an S3 class provided by [httr][httr::httr]). # PREFIX_has_token_description() Code writeLines(PREFIX_has_token_description()) Output @description Reports whether PACKAGE has stored a token, ready for use in downstream requests. # PREFIX_has_token_return() Code writeLines(PREFIX_has_token_return()) Output @return Logical. # PREFIX_auth_configure_description() Code writeLines(PREFIX_auth_configure_description()) Output @description These functions give more control over and visibility into the auth configuration than [PREFIX_auth()] does. `PREFIX_auth_configure()` lets the user specify their own: * OAuth client, which is used when obtaining a user token. * API key. If PACKAGE is de-authorized via [PREFIX_deauth()], all requests are sent with an API key in lieu of a token. See the `vignette("get-api-credentials", package = "gargle")` for more. If the user does not configure these settings, internal defaults are used. `PREFIX_oauth_client()` and `PREFIX_api_key()` retrieve the currently configured OAuth client and API key, respectively. # PREFIX_auth_configure_params() Code writeLines(PREFIX_auth_configure_params()) Output @param client A Google OAuth client, presumably constructed via [gargle::gargle_oauth_client_from_json()]. Note, however, that it is preferred to specify the client with JSON, using the `path` argument. @inheritParams gargle::gargle_oauth_client_from_json @param api_key API key. @param app `r lifecycle::badge('deprecated')` Replaced by the `client` argument. # PREFIX_auth_configure_return() Code writeLines(PREFIX_auth_configure_return()) Output @return * `PREFIX_auth_configure()`: An object of R6 class [gargle::AuthState], invisibly. * `PREFIX_oauth_client()`: the current user-configured OAuth client. * `PREFIX_api_key()`: the current user-configured API key. # PREFIX_user_description() Code writeLines(PREFIX_user_description()) Output @description Reveals the email address of the user associated with the current token. If no token has been loaded yet, this function does not initiate auth. # PREFIX_user_seealso() Code writeLines(PREFIX_user_seealso()) Output @seealso [gargle::token_userinfo()], [gargle::token_email()], [gargle::token_tokeninfo()] # PREFIX_user_return() Code writeLines(PREFIX_user_return()) Output @return An email address or, if no token has been loaded, `NULL`. gargle/tests/testthat/_snaps/request_make.md0000644000176200001440000000055414452076502021020 0ustar liggesusers# request_make() errors for invalid HTTP methods Code request_make(list(method = httr::GET)) Condition Error in `request_make()`: ! `x$method` must be a single string, not a function. --- Code request_make(list(method = "PETCH")) Condition Error in `request_make()`: ! Not a recognized HTTP method: `PETCH`. gargle/tests/testthat/_snaps/utils-ui.md0000644000176200001440000001102014452076514020077 0ustar liggesusers# gargle_verbosity() validates the value it finds Option "gargle_verbosity" must be one of: 'debug', 'info', or 'silent'. # gargle_verbosity() accomodates people using the old option Code out <- gargle_verbosity() Condition Warning: The "gargle_quiet" option was deprecated in gargle 1.1.0. i Please use the "gargle_verbosity" option instead. x Don't do this: `options(gargle_quiet = FALSE)` v Do this instead: `options(gargle_verbosity = "debug")` # gargle_info() works Code gargle_info(c("aa {.field {blah}} bb", "cc {.emph xyz} dd")) Message aa 'BLAH' bb cc xyz dd --- Code gargle_info(c("ee {.field {blah}} ff", "gg {.emph xyz} hh")) Message ee 'BLAH' ff gg xyz hh --- Code gargle_info(c("ii {.field {blah}} jj", "kk {.emph xyz} ll")) # gargle_debug() works Code gargle_debug(c("11 {.field {foo}} 22", "33 {.file a/b/c} 44")) Message 11 'FOO' 22 33 'a/b/c' 44 --- Code gargle_debug(c("55 {.field {foo}} 66", "77 {.file a/b/c} 88")) --- Code gargle_debug(c("99 {.field {foo}} 00", "11 {.file a/b/c} 22")) # bulletize() works Code cli::cli_bullets(bulletize(letters)) Message * a * b * c * d * e ... and 21 more --- Code cli::cli_bullets(bulletize(letters, bullet = "x")) Message x a x b x c x d x e ... and 21 more --- Code cli::cli_bullets(bulletize(letters, n_show = 2)) Message * a * b ... and 24 more --- Code cli::cli_bullets(bulletize(letters[1:6])) Message * a * b * c * d * e * f --- Code cli::cli_bullets(bulletize(letters[1:7])) Message * a * b * c * d * e * f * g --- Code cli::cli_bullets(bulletize(letters[1:8])) Message * a * b * c * d * e ... and 3 more --- Code cli::cli_bullets(bulletize(letters[1:6], n_fudge = 0)) Message * a * b * c * d * e ... and 1 more --- Code cli::cli_bullets(bulletize(letters[1:8], n_fudge = 3)) Message * a * b * c * d * e * f * g * h # cli_menu() basic usage Code cli_menu_with_mock(1) Message Found multiple thingies. Which one do you want to use? 1: label a 2: label b 3: label c Selection: 1 Output [1] 1 # cli_menu() does not infinite loop with invalid mocked input Code cli_menu_with_mock("nope") Message Found multiple thingies. Which one do you want to use? 1: label a 2: label b 3: label c Selection: nope Enter a number between 1 and 3, or enter 0 to exit. Selection: 0 Condition Error: ! Exiting... # cli_menu() can work through multiple valid mocked inputs Code out <- cli_menu_with_mock(c(1, 3)) Message Found multiple thingies. Which one do you want to use? 1: label 1 2: label 2 3: label 3 Selection: 1 Found multiple thingies. Which one do you want to use? 1: label 1 2: label 2 3: label 3 Selection: 3 # cli_menu(), request exit via 0 Code cli_menu_with_mock(0) Message Found multiple thingies. Which one do you want to use? 1: label a 2: label b 3: label c Selection: 0 Condition Error: ! Exiting... # cli_menu(exit =) works Code cli_menu_with_mock(1) Message Hey we need to talk. What do you want to do? 1: Give up 2: Some other thing Selection: 1 Condition Error: ! Exiting... --- Code cli_menu_with_mock(2) Message Hey we need to talk. What do you want to do? 1: Give up 2: Some other thing Selection: 2 Output [1] 2 # cli_menu() inline markup and environment passing Code cli_menu_with_mock(1) Message Hey we need to "talk". What do you want to "do"? 1: Send email to 'jane@example.com' 2: Install the nifty package Selection: 1 Output [1] 1 # cli_menu() not_interactive, many strings, chained error Code wrapper_fun() Condition Error in `wrapper_fun()`: ! Multiple things found. i Use `thingy` to specify one of "thing 1", "thing 2", and "thing 3". gargle/tests/testthat/test-gargle_oauth_endpoint.R0000644000176200001440000000013614431310014022145 0ustar liggesuserstest_that("gargle_oauth_endpoint() snapshot", { expect_snapshot(gargle_oauth_endpoint()) }) gargle/tests/testthat/test-utils-ui.R0000644000176200001440000001123514431310014017361 0ustar liggesuserstest_that("gargle_verbosity() defaults to 'info'", { withr::local_options(list( gargle_verbosity = NULL, gargle_quiet = NULL )) expect_equal(gargle_verbosity(), "info") }) test_that("gargle_verbosity() validates the value it finds", { withr::local_options(list(gargle_verbosity = TRUE)) expect_snapshot_error(gargle_verbosity()) }) test_that("gargle_verbosity() accomodates people using the old option", { withr::local_options(list( gargle_verbosity = NULL, gargle_quiet = FALSE )) expect_snapshot( out <- gargle_verbosity() ) expect_equal(out, "debug") }) test_that("gargle_info() works", { blah <- "BLAH" local_gargle_verbosity("debug") expect_snapshot(gargle_info(c("aa {.field {blah}} bb", "cc {.emph xyz} dd"))) local_gargle_verbosity("info") expect_snapshot(gargle_info(c("ee {.field {blah}} ff", "gg {.emph xyz} hh"))) local_gargle_verbosity("silent") expect_snapshot(gargle_info(c("ii {.field {blah}} jj", "kk {.emph xyz} ll"))) }) test_that("gargle_debug() works", { foo <- "FOO" local_gargle_verbosity("debug") expect_snapshot(gargle_debug(c("11 {.field {foo}} 22", "33 {.file a/b/c} 44"))) local_gargle_verbosity("info") expect_snapshot(gargle_debug(c("55 {.field {foo}} 66", "77 {.file a/b/c} 88"))) local_gargle_verbosity("silent") expect_snapshot(gargle_debug(c("99 {.field {foo}} 00", "11 {.file a/b/c} 22"))) }) test_that("bulletize() works", { expect_snapshot(cli::cli_bullets(bulletize(letters))) expect_snapshot(cli::cli_bullets(bulletize(letters, bullet = "x"))) expect_snapshot(cli::cli_bullets(bulletize(letters, n_show = 2))) expect_snapshot(cli::cli_bullets(bulletize(letters[1:6]))) expect_snapshot(cli::cli_bullets(bulletize(letters[1:7]))) expect_snapshot(cli::cli_bullets(bulletize(letters[1:8]))) expect_snapshot(cli::cli_bullets(bulletize(letters[1:6], n_fudge = 0))) expect_snapshot(cli::cli_bullets(bulletize(letters[1:8], n_fudge = 3))) }) # menu(), but based on readline() + cli and mockable --------------------------- test_that("cli_menu() basic usage", { cli_menu_with_mock <- function(x) { local_user_input(x) cli_menu( "Found multiple thingies.", "Which one do you want to use?", glue("label {head(letters, 3)}") ) } expect_snapshot(cli_menu_with_mock(1)) }) test_that("cli_menu() does not infinite loop with invalid mocked input", { cli_menu_with_mock <- function(x) { local_user_input(x) cli_menu( "Found multiple thingies.", "Which one do you want to use?", glue("label {head(letters, 3)}") ) } expect_snapshot(cli_menu_with_mock("nope"), error = TRUE) }) test_that("cli_menu() can work through multiple valid mocked inputs", { cli_menu_with_mock <- function(x) { local_user_input(x) header <- "Found multiple thingies." prompt <- "Which one do you want to use?" choices <- glue("label {1:3}") first <- cli_menu(header, prompt, choices) second <- cli_menu(header, prompt, choices) c(first, second) } expect_snapshot( out <- cli_menu_with_mock(c(1, 3)) ) expect_equal(out, c(1, 3)) }) test_that("cli_menu(), request exit via 0", { cli_menu_with_mock <- function(x) { local_user_input(x) cli_menu( "Found multiple thingies.", "Which one do you want to use?", glue("label {head(letters, 3)}") ) } expect_snapshot(error = TRUE, cli_menu_with_mock(0)) }) test_that("cli_menu(exit =) works", { cli_menu_with_mock <- function(x) { local_user_input(x) cli_menu( header = "Hey we need to talk.", prompt = "What do you want to do?", choices = c( "Give up", "Some other thing" ), exit = 1 ) } expect_snapshot(error = TRUE, cli_menu_with_mock(1)) expect_snapshot(cli_menu_with_mock(2)) }) test_that("cli_menu() inline markup and environment passing", { cli_menu_with_mock <- function(x) { local_user_input(x) verb <- "talk" action <- "do" pkg_name <- "nifty" cli_menu( header = "Hey we need to {.str {verb}}.", prompt = "What do you want to {.str {action}}?", choices = c( "Send email to {.email jane@example.com}", "Install the {.pkg {pkg_name}} package" ) ) } expect_snapshot(cli_menu_with_mock(1)) }) test_that("cli_menu() not_interactive, many strings, chained error", { wrapper_fun <- function() { local_interactive(FALSE) things <- glue("thing {1:3}") cli_menu( header = "Multiple things found.", prompt = "Which one do you want to use?", choices = things, not_interactive = c(i = "Use {.arg thingy} to specify one of {.str {things}}.") ) } expect_snapshot(wrapper_fun(), error = TRUE) }) gargle/tests/testthat/test-request_retry.R0000644000176200001440000001025114431310014020520 0ustar liggesuserstest_that("request_retry() logic works as advertised", { faux_response <- function(status_code = 200, h = NULL) { structure( list( status_code = status_code, headers = if (!is.null(h)) httr::insensitive(h) ), class = "response" ) } # allows us to replay a fixed set of responses faux_request_make <- function(responses = list(faux_response())) { i <- 0 force(responses) function(...) { i <<- i + 1 responses[[i]] } } # turn this: Retry 1 happens in 1.7 seconds # Retry 1 happens in 1 seconds # into this: Retry 1 happens in {WAIT_TIME} seconds scrub_wait_time <- function(x) { sub( "(?<=in )[[:digit:]]+([.][[:digit:]]+)?(?= seconds)", "{WAIT_TIME}", x, perl = TRUE ) } # turn this: (strategy: exponential backoff, full jitter, clipped to floor of 1 seconds) # (strategy: exponential backoff, full jitter, clipped to ceiling of 45 seconds) # into this: (strategy: exponential backoff, full jitter) scrub_strategy <- function(x) { sub( ", clipped to (floor|ceiling) of [[:digit:]]+([.][[:digit:]]+)? seconds", "", x, perl = TRUE ) } scrub <- function(x) scrub_strategy(scrub_wait_time(x)) local_gargle_verbosity("debug") local_mocked_bindings(gargle_error_message = function(...) "oops") # succeed on first try local_mocked_bindings(request_make = faux_request_make()) out <- request_retry() expect_equal(httr::status_code(out), 200) # fail, then succeed (exponential backoff) r <- list(faux_response(429), faux_response()) local_mocked_bindings(request_make = faux_request_make(r)) expect_snapshot( fail_then_succeed <- request_retry(max_total_wait_time_in_seconds = 5), transform = scrub ) expect_equal(httr::status_code(fail_then_succeed), 200) # fail, then succeed (Retry-After header) r <- list( faux_response(429, h = list(`Retry-After` = 1.4)), faux_response() ) local_mocked_bindings(request_make = faux_request_make(r)) expect_snapshot( fail_then_succeed <- request_retry(), transform = scrub ) expect_equal(httr::status_code(fail_then_succeed), 200) # make sure max_tries_total is adjustable) r <- list( faux_response(429), faux_response(429), faux_response(429), faux_response() ) local_mocked_bindings(request_make = faux_request_make(r[1:3])) expect_snapshot( fail_max_tries <- request_retry( max_tries_total = 3, max_total_wait_time_in_seconds = 6 ), transform = scrub ) expect_equal(httr::status_code(fail_max_tries), 429) }) test_that("backoff() obeys obvious bounds from min_wait and max_wait", { faux_error <- function() { structure(list(status_code = 429), class = "response") } # raw wait_times in U[0,1], therefore all become min_wait + U[0,1] local_mocked_bindings(gargle_error_message = function(...) "oops") suppressMessages( wait_times <- vapply( rep.int(1, 100), backoff, FUN.VALUE = numeric(1), resp = faux_error(), min_wait = 3 ) ) expect_true(all(wait_times > 3)) expect_true(all(wait_times < 4)) # raw wait_times in U[0,6], those that are < 1 become min_wait + U[0,1] and # those > 3 become max_wait + U[0,1] suppressMessages( wait_times <- vapply( rep.int(1, 100), backoff, FUN.VALUE = numeric(1), resp = faux_error(), base = 6, max_wait = 3 ) ) expect_true(all(wait_times > 1)) expect_true(all(wait_times < 3 + 1)) }) test_that("backoff() honors Retry-After header", { faux_429 <- function(h) { structure( list( status_code = 429, headers = httr::insensitive(h) ), class = "response" ) } local_mocked_bindings(gargle_error_message = function(...) "oops") # play with capitalization and character vs numeric suppressMessages( out <- backoff(1, faux_429(list(`Retry-After` = "1.2"))) ) expect_equal(out, 1.2) suppressMessages( out <- backoff(1, faux_429(list(`retry-after` = 2.4))) ) expect_equal(out, 2.4) # should work even when tries_made > 1 suppressMessages( out <- backoff(3, faux_429(list(`reTry-aFteR` = 3.6))) ) expect_equal(out, 3.6) }) gargle/tests/testthat/test-gargle_oauth_client.R0000644000176200001440000000334314431310014021606 0ustar liggesuserstest_that("gargle_oauth_client() rejects bad input", { expect_snapshot(error = TRUE, gargle_oauth_client()) expect_snapshot(error = TRUE, gargle_oauth_client(1234)) expect_snapshot(error = TRUE, gargle_oauth_client(id = "ID")) expect_snapshot(error = TRUE, gargle_oauth_client(id = "ID", secret = 1234)) expect_snapshot( error = TRUE, gargle_oauth_client("ID", "SECRET", type = "nope") ) }) test_that("gargle_oauth_client() has special handling for web clients", { expect_snapshot( error = TRUE, gargle_oauth_client("ID", "SECRET", type = "web") ) expect_snapshot( gargle_oauth_client( "ID", "SECRET", type = "web", redirect_uris = c( "http://localhost:8111/", "http://127.0.0.1:8100/", "https://example.com/aaa/bbb/v" ) ) ) }) test_that("service account JSON throws an informative error", { expect_snapshot( error = TRUE, gargle_oauth_client_from_json( test_path("fixtures", "service-account-token.json") ) ) }) # deprecated functions ---- test_that("oauth app from JSON", { withr::local_options(lifecycle_verbosity = "quiet") oa <- oauth_app_from_json( fs::path_package("gargle", "extdata", "client_secret_installed.googleusercontent.com.json") ) expect_s3_class(oa, "oauth_app") expect_match(oa$appname, "^a_project") expect_equal(oa$secret, "ssshh-i-am-a-secret") expect_equal(oa$key, "abc.apps.googleusercontent.com") oa <- oauth_app_from_json( fs::path_package("gargle", "extdata", "client_secret_web.googleusercontent.com.json") ) expect_s3_class(oa, "oauth_app") expect_match(oa$appname, "^a_project") expect_equal(oa$secret, "ssshh-i-am-a-secret") expect_equal(oa$key, "abc.apps.googleusercontent.com") }) gargle/tests/testthat/test-utils.R0000644000176200001440000000250614067372466016777 0ustar liggesuserstest_that("add_email_scope() works", { email_url <- add_email_scope() expect_length(email_url, 1) expect_equal(add_email_scope(email_url), email_url) expect_equal( add_email_scope("whatever"), c("whatever", email_url) ) expect_equal( add_email_scope(c("whatever" = "whatever")), c("whatever", email_url) ) }) test_that("base_scope() extracts the last scope part", { scopes <- c( "https://www.googleapis.com/auth/userinfo.email", "https://www.googleapis.com/auth/drive", "https://www.googleapis.com/auth/gmail.readonly", "https://www.googleapis.com/auth/gmail.modify", "https://www.googleapis.com/auth/gmail.compose", "https://mail.google.com/" ) out <- base_scope(scopes) expect_equal( out, c( "...userinfo.email", "...drive", "...gmail.readonly", "...gmail.modify", "...gmail.compose", "...mail.google.com" ) ) }) test_that("obfuscate() works", { x <- c("123", "12345", "123456789") expect_equal( obfuscate(x, first = 1, last = 1), c("1...3", "1...5", "1...9") ) expect_equal( obfuscate(x, first = 2, last = 1), c("123", "12...5", "12...9") ) expect_equal( obfuscate(x, first = 4, last = 1), c("123", "12345", "1234...9") ) expect_equal( obfuscate(x, first = 3, last = 3), c("123", "12345", "123...789") ) }) gargle/tests/testthat/test-request_develop.R0000644000176200001440000000511714431310014021016 0ustar liggesuserstest_that("request_develop() errors for unrecognized parameters", { expect_snapshot_error( request_develop( endpoint = list(parameters = list(a = list()), id = "some.api.endpoint"), params = list(b = list(), c = list()) ) ) }) test_that("request_develop() errors if required parameter is missing", { expect_snapshot_error( request_develop( endpoint = list( parameters = list(a = list(required = TRUE)), id = "some.api.endpoint" ), params = list(b = list()) ) ) }) test_that("request_develop() separates body params from query", { req <- request_develop( endpoint = list( parameters = list( a = list(location = "body", required = FALSE), b = list(location = "query", required = FALSE) ) ), params = list(a = list(), b = list()) ) expect_equal(req$body, list(a = list())) expect_equal(req$params, list(b = list())) }) # https://github.com/r-lib/gargle/issues/122 test_that("request_develop() copes with a param that goes to path and body", { req <- request_develop( endpoint = list( parameters = list( two_places = list(location = "path", required = FALSE), two_places = list(location = "body", required = FALSE), just_path = list(location = "path", required = FALSE), just_body = list(location = "body", required = FALSE), elsewhere = list(location = "????", required = FALSE) ) ), params = list(two_places = list(), just_path = list(), just_body = list()) ) expect_equal(req$params, list(two_places = list(), just_path = list())) expect_equal(req$body, list(two_places = list(), just_body = list())) }) test_that("request_build() does substitution and puts remainder in query", { req <- request_build( path = "/{a}/xx/{b}", params = list(a = "A", b = "B", c = "C") ) expect_equal(req$url, "https://www.googleapis.com/A/xx/B?c=C") }) test_that("request_build() suppresses API key if token is non-NULL", { req <- request_build( params = list(key = "key in params"), key = "explicit key", token = httr::config(token = "token!") ) expect_false(grepl("key", req$url)) }) test_that("request_build() adds key, if available when token = NULL", { req <- request_build(key = "abc", token = NULL) expect_match(req$url, "key=abc") req <- request_build(params = list(key = "abc"), token = NULL) expect_match(req$url, "key=abc") }) test_that("request_build(): explicit API key > key in params", { req <- request_build(key = "abc", params = list(key = "def"), token = NULL) expect_match(req$url, "key=abc") }) gargle/tests/testthat/test-assets.R0000644000176200001440000000243114431310014017106 0ustar liggesuserstest_that("default options", { withr::local_options(list( gargle_oauth_cache = NULL, gargle_oob_default = NULL, httr_oob_default = NULL, gargle_oauth_client_type = NULL, gargle_oauth_email = NULL, gargle_verbosity = NULL, gargle_quiet = NULL )) expect_equal(gargle_oauth_cache(), NA) if (is_hosted_session()) { expect_true(gargle_oob_default()) expect_equal(gargle_oauth_client_type(), "web") } else { expect_false(gargle_oob_default()) expect_equal(gargle_oauth_client_type(), "installed") } expect_null(gargle_oauth_email()) expect_equal(gargle_verbosity(), "info") }) test_that("gargle_oob_default() consults gargle's option before httr's", { withr::local_options(list( gargle_oob_default = TRUE, httr_oob_default = FALSE )) expect_true(gargle_oob_default()) }) test_that("gargle_oob_default() consults httr's option", { withr::local_options(list( gargle_oob_default = NULL, httr_oob_default = TRUE )) expect_true(gargle_oob_default()) }) test_that("gargle_oauth_client_type() consults the option", { withr::local_options(list(gargle_oauth_client_type = "web")) expect_equal(gargle_oauth_client_type(), "web") }) test_that("gargle API key", { key <- gargle_api_key() expect_true(is_string(key)) }) gargle/tests/testthat/test-token_fetch.R0000644000176200001440000000276414431310014020106 0ustar liggesusers# These are used in several tests below. creds_always <- function(scopes, ...) { 1 } creds_never <- function(scopes, ...) { NULL } creds_failure <- function(scopes, ...) { stop("no creds") } creds_maybe <- function(scopes, arg1 = "", ...) { if (arg1 != "") { 2 } } test_that("Basic token fetching works", { withr::defer(cred_funs_set_default()) cred_funs_clear() cred_funs_add(always = creds_always) expect_equal(1, token_fetch(c())) cred_funs_add(never = creds_never) expect_equal(1, token_fetch(c())) }) test_that("We fetch tokens in order", { withr::defer(cred_funs_set_default()) cred_funs_clear() cred_funs_add(always = creds_always) cred_funs_add(maybe = creds_maybe) expect_equal(1, token_fetch(c())) expect_equal(2, token_fetch(c(), arg1 = "abc")) cred_funs_set(list(always = creds_always, maybe = creds_maybe)) expect_equal(1, token_fetch(c())) expect_equal(1, token_fetch(c(), arg1 = "abc")) }) test_that("We sometimes return no token", { withr::defer(cred_funs_set_default()) cred_funs_clear() cred_funs_add(never = creds_never) expect_null(token_fetch(c())) }) test_that("We don't need any registered functions", { withr::defer(cred_funs_set_default()) cred_funs_clear() expect_null(token_fetch(c())) }) test_that("We keep looking for credentials on error", { withr::defer(cred_funs_set_default()) cred_funs_clear() cred_funs_add(always = creds_always) cred_funs_add(failure = creds_failure) expect_equal(1, token_fetch(c())) }) gargle/tests/testthat/test-response_process.R0000644000176200001440000000574514431310014021213 0ustar liggesusersexpect_recorded_error <- function(filename, status_code) { rds_file <- test_path("fixtures", fs::path_ext_set(filename, "rds")) resp <- readRDS(rds_file) expect_error(response_process(resp), class = "gargle_error_request_failed") expect_error(response_process(resp), class = glue("http_error_{status_code}")) # HTML errors (as opposed to JSON) need this scrub_filepath <- function(x) { gsub( "([\"\'])\\S+gargle-unexpected-html-error-\\S+[.]html([\"\'])", "\\1VOLATILE_FILE_PATH\\2", x, perl = TRUE ) } expect_snapshot(response_process(resp), error = TRUE, transform = scrub_filepath) } test_that("Resource exhausted (Sheets, ReadGroup)", { expect_recorded_error( "sheets-spreadsheets-get-quota-exceeded-readgroup_429", 429 ) }) test_that("Request for non-existent resource (Drive)", { expect_recorded_error( "drive-files-get-nonexistent-file-id_404", 404 ) }) # https://github.com/r-lib/gargle/issues/254 test_that("Too many requests (Drive, HTML content)", { expect_recorded_error( "drive-automated-queries_429", 429 ) }) # https://github.com/r-lib/gargle/issues/254 test_that("HTML error is offered as a file", { rds_file <- test_path("fixtures", "drive-automated-queries_429.rds") resp <- readRDS(rds_file) err <- tryCatch( response_process(resp), gargle_error_request_failed = function(e) e ) regex <- "[^'\" \\t\\n\\r]+gargle-unexpected-html-error-\\S+[.]html" m <- gregexpr(regex, err$body, perl = TRUE) path_to_html_error <- unique(unlist(regmatches(err$body, m))) # the strwrap() result is a bit goofy, but seems least of all evils # this is mostly about making sure we excavate the HTML expect_snapshot(strwrap(readLines(path_to_html_error), width = 60)) unlink(path_to_html_error) }) test_that("Request for which we don't have scope (Fitness)", { expect_recorded_error( "fitness-get-wrong-scope_403", 403 ) }) test_that("Use key that's not enabled for the API (Sheets)", { expect_recorded_error( "sheets-spreadsheets-get-api-key-not-enabled_403", 403 ) }) test_that("Request with invalid argument (Sheets, bad range)", { expect_recorded_error( "sheets-spreadsheets-get-nonexistent-range_400", 400 ) }) test_that("Request with bad field mask (Sheets)", { expect_recorded_error( "sheets-spreadsheets-get-bad-field-mask_400", 400 ) }) test_that("Request for nonexistent resource (Sheets)", { expect_recorded_error( "sheets-spreadsheets-get-nonexistent-sheet-id_404", 404 ) }) test_that("Request with invalid value (tokeninfo, stale token)", { expect_recorded_error( "tokeninfo-stale_400", 400 ) }) test_that("Request to bad URL (tokeninfo, HTML content)", { expect_recorded_error( "tokeninfo-bad-path_404", 404 ) }) # helpers ---- test_that("RPC codes can be looked up (or not)", { expect_match( rpc_description("ALREADY_EXISTS"), "resource .* already exists" ) expect_null(rpc_description("MATCHES_NOTHING")) }) gargle/tests/testthat/test-request_make.R0000644000176200001440000000034714431310014020275 0ustar liggesuserstest_that("request_make() errors for invalid HTTP methods", { expect_snapshot( request_make(list(method = httr::GET)), error = TRUE ) expect_snapshot( request_make(list(method = "PETCH")), error = TRUE ) }) gargle/tests/testthat/test-field_mask.R0000644000176200001440000000103614431310014017702 0ustar liggesuserstest_that("field_mask works", { x <- list(a = "A") expect_equal(field_mask(x), "a") x <- list(a = "A", b = "B") expect_equal(field_mask(x), "a,b") x <- list(a = list(b = "B", c = "C")) expect_equal(field_mask(x), "a(b,c)") x <- list(a = "A", b = list(c = "C")) expect_equal(field_mask(x), "a,b.c") x <- list(a = "A", b = list(c = "C", d = list(e = "E"))) expect_equal(field_mask(x), "a,b.c,b.d.e") x <- list(a = "A", b = list(c = "C", d = "D", e = list(f = "F"))) expect_equal(field_mask(x), "a,b(c,d),b.e.f") }) gargle/tests/testthat.R0000644000176200001440000000007013423717161014642 0ustar liggesuserslibrary(testthat) library(gargle) test_check("gargle") gargle/vignettes/0000755000176200001440000000000014456275120013531 5ustar liggesusersgargle/vignettes/request-helper-functions.Rmd0000644000176200001440000002601614017730626021155 0ustar liggesusers--- title: "Request helper functions" output: rmarkdown::html_vignette vignette: > %\VignetteIndexEntry{Request helper functions} %\VignetteEngine{knitr::rmarkdown} %\VignetteEncoding{UTF-8} --- ```{r, include = FALSE} knitr::opts_chunk$set( collapse = TRUE, comment = "#>" ) ``` This vignette explains the purpose and usage of: * `request_develop(endpoint, params, base_url)` * `request_build(method, path, params, body, token, key, base_url)` * `request_make(x, ..., user_agent)` The target audience is someone writing an R package to wrap a Google API. ```{r setup} library(gargle) ``` ## Why use gargle's request helpers? Why would the developer of a Google-API-wrapping package care about the request helpers in gargle? You can write less code and safer code, in return for a modest investment in studying your target API. That is done by ingesting the API's so-called Discovery Document. Hundreds of Google APIs -- the ones addressed by the [API Discovery Service](https://developers.google.com/discovery/) -- share a great deal of behaviour. By ingesting the metadata provided by this service, you can use gargle's request helpers to exploit this shared data and logic, while also decreasing the chance that you and your users will submit ill-formed requests. The request helpers in gargle check the combined inputs from user and developer against suitably prepared API metadata: * If required parameters are missing, an error is thrown. * If unrecognized parameters are submitted, an error is thrown. * Parameters are automatically placed in their correct location: URL substitution, query, or body. * *Is there something else you care about? It is possible to do more, but it would help to have concrete requests.* Google provides [API libraries for several languages](https://developers.google.com/api-client-library/), including Java, Go, Python, JavaScript, Ruby and more (but not R). All of these libraries are machine-generated from the metadata provided by the API Discovery Service. It is the [official recommendation](https://developers.google.com/discovery/v1/using#build) to use the Discovery Document when building client libraries. The gargle package aims to implement key parts of this strategy, in a way that is also idiomatic for R and its developers. ## High-level design pattern gargle facilitates this design for API-wrapping packages: * A machine-assisted low-level interface driven by the Discovery Document: - Your package exports thin wrapper functions around gargle's helpers to form and make HTTP requests, that inject package-specific logic and data, such as an API key and user agent. This is for power users and yourself. * High-level, task-oriented, user-facing functions that constitute the main interface of your package. - These functions convert user input into the form required by the API and pass it along to your low-level interface functions. Later, specific examples are given, using the googledrive package. ## gargle's HTTP request helpers gargle provides support for creating and sending HTTP requests via these functions: `request_develop(endpoint, params, base_url)`: a.k.a. The Smart One. * Processes the info in `params` relative to detailed knowledge about the `endpoint`, derived from an API Discovery Document. * Checks for required and unrecognized parameters. * Peels off `params` destined for the body into their own part. * Returns request data in a form that anticipates the `httr::VERB()` call that is on the horizon. `request_build(method, path, params, body, token, key, base_url)`: a.k.a. The Dumb One. * Typically consumes the output of `request_develop()`, although that is not required. It can be called directly to enjoy a few luxuries even when making one-off API calls in the absence of an ingested Discovery Document. * Integrates `params` into a URL via substitution and the query string. * Sends either an API key or an OAuth token, but it provides no default values or logic for either. `request_make(x, ..., user_agent)`: actually makes the HTTP request. * Typically consumes the output of `request_build()`, although that is not required. However, if you have enough info to form a `request_make()` request, you would probably just make the `httr::VERB()` call yourself. * Consults `x$method` to determine which `httr::VERB()` to call, then calls it with the rest of `x`, `...`, and `user_agent` passed as arguments. They are usually called in the above order, though they don't have to be used that way. It is also fine to ignore this part of gargle and use it only for help with auth. They are separate parts of the package. ## Discovery Documents Google's [API Discovery Service](https://developers.google.com/discovery/) "provides a lightweight, JSON-based API that exposes machine-readable metadata about Google APIs". We recommend ingesting this metadata into an R list, stored as internal data in an API-wrapping client package. Then, HTTP requests inside high-level functions can be made concisely and safely, by referring to this metadata. The combined use of this data structure and gargle's request helpers can eliminate a lot of boilerplate data and logic that are shared across Google APIs and across endpoints within an API. The gargle package ships with some functions and scripts to facilitate the ingest of a Discovery Document. You can find these files in the gargle installation like so: ```{r} ddi_dir <- system.file("discovery-doc-ingest", package = "gargle") list.files(ddi_dir) ``` Main files of interest to the developer of a client package: * `ingest-functions.R` is a collection of functions for downloading and ingesting a Discovery Document. * `drive-example.R` uses those functions to ingest metadata on the Drive v3 API and store it as an internal data object for use in [googledrive](https://googledrive.tidyverse.org). The remaining files present an analysis of the Discovery Document for the Discovery API itself (very meta!) and write files that are useful for reference. Several are included at the end of this vignette. Why aren't the ingest functions exported by gargle? First, we regard this as functionality that is needed at development time, not install or run time. This is something you'll do every few months, probably associated with preparing a release of a wrapper package. Second, the packages that are useful for wrangling JSON and lists are not existing dependencies of gargle, so putting these function in gargle would require some unappealing compromises. ## Method (or endpoint) data Our Discovery Document ingest process leaves you with an R list. Let's assume it's available in your package's namespace as an internal object named `.endpoints`. Each item represents one method of the API (Google's vocabulary) or an endpoint (gargle's vocabulary). Each endpoint has an `id`. These `id`s are also used as names for the list. Examples of some `id`s from the Drive and Sheets APIs: ``` drive.about.get drive.files.create drive.teamdrives.list sheets.spreadsheets.create sheets.spreadsheets.values.clear sheets.spreadsheets.sheets.copyTo ``` Retrieve the metadata for one endpoint by name, e.g.: ```{r, eval = FALSE} .endpoints[["drive.files.create"]] ``` That info can be passed along to `request_develop(endpoint, params, base_url)`, which conducts sanity checks and combines this external knowledge with the data coming from the user and developer via `params`. ## Design suggestion: forming requests Here's the model used in googledrive. There is a low-level request helper, `googledrive::request_generate()`, that is used to form every request in the package. It is exported as part of a low-level API for expert use, but most users will never know it exists. ```{r eval = FALSE} # googledrive:: request_generate <- function(endpoint = character(), params = list(), key = NULL, token = drive_token()) { ept <- .endpoints[[endpoint]] if (is.null(ept)) { stop_glue("\nEndpoint not recognized:\n * {endpoint}") } ## modifications specific to googledrive package params$key <- key %||% params$key %||% drive_api_key() if (!is.null(ept$parameters$supportsTeamDrives)) { params$supportsTeamDrives <- TRUE } req <- gargle::request_develop(endpoint = ept, params = params) gargle::request_build( path = req$path, method = req$method, params = req$params, body = req$body, token = token ) } ``` The `endpoint` argument specifies an endpoint by its name, a.k.a. its `id`. `params` is where the processed user input goes. `key` and `token` refer to an API key and OAuth2 token, respectively. Both can be populated by default, but it is possible to pass them explicitly. If your package ships with a default API key, you should append it above as the final fallback value for `params$key`. Do not "borrow" an API key from gargle or another package; always send a key associated with your package or provided by your user. Per the Google User Data Policy , your application must accurately represent itself when authenticating to Google API services. After `googledrive::request_generate()` takes care of everything specific to the Drive API and the user's input and task, we call `gargle::request_develop()`. We finish preparing the request with `gargle::request_build()`, which enforces the rule that we always send exactly **one** of `key` and `token`. ## Design suggestion: making requests The output of `gargle::request_build()` specifies an HTTP request. `gargle::request_make()` can be used to actually execute it. ```{r, eval = FALSE} # gargle:: request_make <- function(x, ..., user_agent = gargle_user_agent()) { stopifnot(is.character(x$method)) method <- switch( x$method, GET = httr::GET, POST = httr::POST, PATCH = httr::PATCH, PUT = httr::PUT, DELETE = httr::DELETE, abort(glue("Not a recognized HTTP method: {bt(x$method)}")) ) method( url = x$url, body = x$body, x$token, user_agent, ... ) } ``` `request_make()` consults `x$method` to identify the `httr::VERB()` and then calls it with the remainder of `x`, `...` and the `user_agent`. In googledrive we have a thin wrapper around this that injects the googledrive user agent: ```{r, eval = FALSE} # googledrive:: request_make <- function(x, ...) { gargle::request_make(x, ..., user_agent = drive_ua()) } ``` ## Reference *derived from the Discovery Document for the Discovery Service* Properties of an endpoint ```{r asis = TRUE, echo = FALSE, comment = NA} cat(readLines(fs::path(ddi_dir, "method-properties-humane.txt")), sep = "\n") ``` API-wide endpoint parameters (taken from Discovery API but, empirically, are shared with other APIs): ```{r asis = TRUE, echo = FALSE, comment = NA} cat(readLines(fs::path(ddi_dir, "api-wide-parameters-humane.txt")), sep = "\n") ``` Properties of an endpoint parameters: ```{r asis = TRUE, echo = FALSE, comment = NA} cat(readLines(fs::path(ddi_dir, "parameter-properties-humane.txt")), sep = "\n") ``` gargle/vignettes/deleted_client.png0000644000176200001440000007250114067372466017221 0ustar liggesusersPNG  IHDR,qA diCCPICC ProfileHW\Gwda{ #A@ *! $-UnQѪE:΢qR*.T h7~{<{;T|HZ,OgMd (6_q@ew5? E H9BV2L^ 1X l5jmR7@<tۡU"ȃ<! %R BS YT4UC egsakP?,BGhbyL:XSc՘q4'>A]kHRʘT=j!Ppab!?"b qZ}N$1\- I1/E;wH(0s\vn_>Wm߮,HhoE!t`IZ<ĺ) c56m?d#W&㷇-Fk\yT^V+KxZ\],N-o qHI)& "EDjr:DTm]Yqxvn0QkEj-抒d\|L1\~ՕTx{t{׎bьbN͔K, X<`H'o59@g?½,8Im6W}9]>>RhtAo=̀0#/@cAH`2g9fT`-m`M08~Ep܂ <GbX#ⅰ$C $C,D*Ud+RB#gN&rFBޡJCPKQŁHI)3)))- .J?ՀD P Im [:::tNyK3Ҹ,JI{AaLz1}~~F;J+ԝ[ۨ{IEA7YTJ}>W?WFu>AARg   # 0|v .CX82"9*705641N3a\c|XĘLy҄c"2Yb`r0Si^ӫXfff+͚ͧo6?i3hDGjZZ$Y̲fqޢ2Rfe**jQnkuz1Y,UjgX(mt:٦ږcGcڭk뵷g?۾WAkG'tǯ8:Jn;ӝC9:_q!] \6\tE]}]Ů5P7?7&Α##kG^wsKbU6iԳ3G}jG_B< =zyx%MG/w׾m~ ~؉y3= hwГ1NcDcyl ad| ֆ {qsp{rp[#舊H wlz}gEbbcV\Y:^Xsƶbc7ޏsǵCǍzxxi|SH%N8-k?JLt*<%yw)RSmiziYiui#W&0g¹ IFs&)3-sGfȉk'vefg]4iƤ3'N>2Eo ʁlBvz~-/1W< EUǹr=dy~L ;  I VSgL픹eiNw($Esm~tqAFqfcoI՜yh졶?ia5G,?J=c}֞yMiub‰+;Nƞs~a\|௥/^|/W+ޘԻw'cE2?x`Cssk' a7(:š{;q >CnG0z{7(r4\4x!xa M @4]S-Dx7V}7WO=:e/-heXIfMM*>F(iNx,ASCIIScreenshot+ pHYs%%IR$iTXtXML:com.adobe.xmp 680 Screenshot 300 iDOT(1PX1IDATx|OzЫ*] `";>A"UAQz/4 4Aқz'B}sfֽ%7w33e?    :!K    T    "+s1    x@@@@tEUW@c@@@@ Ṕƀ@T]@;    +2    w@@@@@W Pue4@@@@@h *]@Օ9T    "+s1    x@@@@tEUW@c@@@@ PdxB'S jbh<3x0@@8xCcR]a}2@^`( pTz/!2RO^h4\@"h@r Ѻ=t?i%Ph((/bVߥޫ?zfU-TE TYz'&3W2ipi{@!PA: @Bh+.EǥZ@֛ }65!nŲT|PB6t$< 7@W @n,J.#pT,ao[c$9_+S @T@տBYM*;o/Nr7/FQt)hq)A_coJԸ d& /*>3uh | /9dǿf3GjJNHCWp>X9xw8kW=sQ4B ttoVMAyZw\/BU:YSmJVuD&qTKEAE^!9b`'oX{?I7`Ǯ+F\8*HTSJ ݺQ0GwnU)Fl5*R@>SYHN d96_fDXE7&Sݕ)Neғm'8g#R@ $GhcCZh0;DI)i)>y+U)Y&ߺI'G,j}Is*Ыۙ!@( PbItD[&* ‘ߊAZ!PU gߩҴ\QS(t9ٓ.'ya0WrUӉ4n=6 HtEܣ{SҕK6j;gP@6_ܢ4x vjYSSk !"v& #yRXjB|{tI?~j5`a*1iXG>N5%jJ/0\V&BԦ$PݓrQb]/87e{bl5qO+CXk~cˉxٿ?5\>I1~W>NZg*؄?VS''=Hq).л?z,@@  PU X ꥃhDҧrJs}; x'qbO^zY1M:KWC Τ\/D!6ctI ,АBAoIB?sXUJd]:pV Z _'B9d$\eU@ݷ.HՆAX沋 1/JM["F3i!0TÙ't'6mdBEpsJK(97f[++oS7:R޶]Tiʋ8K^gYģ.(;}x[ *ԔB+ R& ]J9J=",!8t.Fcaz .63?'XsFyxYC/f da]VV(_4\ P=% V,ӦhIZ۝Vv8ΈDs(#bcԙn)M-It2ƞˆrQ ۸o~A([E^ gx2qHm3 *\.'GNy>`V_ sGNeߛ&дSZjb9:ZГ\|ET!0w^*5{ѵ@*X%Q"kCy7wn k]##jNy9Ok a*Sq_).HO[#BJw:@)YxWzYOS/˞B-L#uZ*^5Yn 7-p--nPܑq!gY/# ?%+ҷa޴'   @jF Z hd oj_,k+(6 u|+6c.h'ݻ.~:TU\*ŵ9SOۦޏӔG+KDoi[G P= sФn9),XZT{ifGӕiSrӌ>_Tg']g1q\g {UҭFPs#:wTykL܏]$F.sjMtQo#[fԽ_"P-GP94ծ~9"   P*?(ꠅtr&&' x-< ꑾfu@LK7dz @n,MVLuشx ^DAHSTUoC!e{TT;;v5x_xox󢭜k\gԠ>tr~!!TiRfa~ăUKBToPD  KW,.YX֫B5w^ӒuA1L#4.'աyPT8|to \#Ov!Q˓F~&ͨ`rVSJ<ՉL J~C@+ )#QS=r 8ט sbY{,7͖6:&OFd {'ŤoHVdx )UV;xt/r5*w,ޭ @$&8N_XTe-(i{-FM%iZ :Pg 3kұiƂ =BU)%Zw/GgHA:CEuaE( O-q~}y;>=)nQoа uCRCIk Rpdt"īoK@̇9)_`)RU %;Mq ^ iL'GQG@<&1BT z4s,'VZח溮͚"\kBFI+,pz.%Fh(nUAZޑyż'*:4:3y'‹;G/@"LT -128v(A$:gUnXT=ΥznZB7/,&ٔѠ#)leE$ $`HUO[d/MQ(0_}9Vd`3SRgr W+@y9Ic(>ggY ]2D@I՛4Qfoohݳ3U/M;ԍ%)ᅯժa7)˭0={g<>j,ҺԠ"(d{ c?eup_s^j0jdu")(FGbcE5S66A !S7ΥWUe?)Ƣ_< '}@챈כ^NwJ|b=j~B{ܤ7<b%P)Nݝ>RSbPjYJ=#NbQ|{7riy-d:s5.JGPyl@PU^ xә;"Fc/]3u^'p\PBW0u,ʀD%\  "J]2L욓JԸBYq T& |q,Q-8)MbS ŦPq~y(PEi= z"'k- Y~3N ʫ;?.xͭbw_ *P۫  'z e (=$NB7<1bhI&rJkT-@|/Y m04>`Tޭ_NlZ"H "i"$Ѝi# Ɋ+Ij,B5 Svjȕ͟잓r9V @2j&ǣA@ݚ'Niv!:/b~\/@+ YIQ,f*oR=x*x@B(D?@@@@  P bHt@@@B(D?@@@@  P bHt@@@B(D?@@@@  P bHt@@@B(D?@@@@  P bHt@@@B(D?@@@@  P bHt@@@B(D?@@@@  P bHt@@@B(D?@@@@  P bHt@@@B(D?@@@@  P bHt@@@B(D?@@@@  P bHt@@@B(D?@@@@  P bHt@@@B(D?@@@@  P bHt@@@B(D?@@@@  P bHt@@@B(D?@@@@  P bHt@@@B(D?@@@@  P bHt@@@B(D?@@@@  P bHt@@@B(D?@@@@  P bHt@@@B(D?@@@@  P bHt@@@B(D?@@@@  P bHt@@@B(D?Tn߾MIrZxxEDD׾>EGߕ7_^ 9~m۾<$UZըݴۯh?.-7 ,%N fx@||<=Q>ȕVYJHh˖rlDK0a"M>CZǎQ#kGzRjⶩ. O5L`uVOuF*[U/%tڝoMn-T) Ըx[Iw簰0ڳ{'eϞ]N3j$횐@*VtI@a% Ю]GڹkՓzNCJϬ}w+VʏܩU\Ij͛TVg;NEHihw8PC}oC@A…d2秝;͚ͱcќvΚ94yNjٛ6o"W.}t|Kwlnc+K3gNz]J1]* K5s^&0yW4eTΙ=}3;b%+ ׯ%KO7lH;B e,wlnR+W6P@@TxԔGOyڵkKK.3_7ӿ33XɊ53mgcso@5 Zt@םSs(_\j?QOJi_RzXyw苉iժ!{.٭֨QIC=Hy[)駟 _׮F5[cuBOшl0͝Vv4slZ$ޏk׮jګ4r0ʑ#ZKzNzRVc6!z!KXl9}4t\E Kͫcѐ86e-޶?ӗ8hhkq7_SխiMH/;IYsN3 'z6^oM?J&OD-[,g9ڱC{}[_%JS㑶o3@mf/{4FԬ8كʔ)CM~5u*]zMM:gh+L1>\=]_nތZKRRRӴb*i*ӬFR;W»#"=*֣"HS :wjnK%PrNz>g_; z6q?Y۴yƏ+_#͚cǎ/iW^s,Gu--V^ fU׎vC,7vڕ@岟}6Ns<=,'碌e3NZY56MH:y39\j.k>=FNnذ-\v|aWjݺuhvPϞ=K/\[xb.F2Pfe˖k6nY5oޒbbbIdyt]dlE4[,>8HڐeNuUmB3c_#F6mZKs鶾yB7$8āQk^5mg@s9B:GY;zȩ߶򾳸?DEe}1ңBL˛UC.fw}O,|wz'gETŔ|OBLԬ2M>S[i޽&~J**į~fY3v7D[n̢Z VUQK=[wҝm'W4l A]秩l WL_pX݆F smYnȏUoQ-ժsUJl.:p&=~rjvudGŤ{O1LGU-EQӦ/Zjʣ@h}bKv2ZD`*lh?Uɓwҝm'WDd86y;V=+/IIb"UQkV}a_?W,Y#G ΝӎѳH3AZ͕}^i|rJkJݝC Zu L7E9r/?ƌTbf^C鰁6n` ,1şѶěwjN)r Uleu)͓wҝm'WA @fh<m4np4ު_[lܴjE3kT~IM#u%l۾]!C.,`\;;ծ?r{x5dɒr夹[#] cs-NO&w](|U5;W;|z #x<E,Zݽ)`ɪNwYն\jnXҥwʲ k֬U!\rJTkwņ-U[ʊi?wc/"6;%P1pS*S;w2129ůW۹Ayl+iի\ A5@1% \Yw*NUc+f:ZF[pw~GCڼeV[Mv): 9 \Ljwer[E, ֩[$:pTDg2#^mm Q^d9@@ P`%bM5U"du&PrCciՆ4Q=,G-Bw~zT}+}͛Em7W@(6JW_W]{ME-G3Rv߶`EN# @UV@\"~G,V>$*꼕9w2Rth":e8u,+ TR=ZҥKΚ#]~WV/w\Vz1SUUԹo~Vçs¹H+7VZ;^m~sj'u+q![% tE%"PKj/2kР!|Dciue˖EzO9̝;_u~m7d}gT͂%ߪ7ӑ`w}opyE Q5Ps_B*VL|kxdK}a VgOry{;?I@U-zu+ 7Szp6h& 6щ|;AuY4 QÆ ׯSQl\6h2-Z'8Oc!eػwѦ*zǨeNwr{8;m37SJgwh^3Ŵb*Z ;f'Ԯ_ktfj$V(@ժU:uP9#Oٞ Qu T+v8NN:kf;z]z>Ӹ̞ۚ;IYI #:2@#!###»?4͛7_MGĉ_H8^*8c>[ЗqcM7a*PXBAwJĞOYwv&lrrU&(ISk,Ο$Z=""ݻ''Pݱ7ۥ@5Y7aqIUvOION*(@Ց1,GGQBtpw9XٽXdrԡcڷoQ۽̝'gsȑz72rE$*?:Yn1=K OR4*b}Խzќѓtg͛+͛mI57+Nbf`O}Wmgl:';[+T-qB`„4m 9דӒ% kw"CQGL>rӿo*_\!8#,>m>~qȐAԸQ#i򔯤l~~~k*XbRx49#f^2'Q}Hi.]k\ru(u4ov)𒇗^|ATJ-Uvs%zG޽8*z֯[MUV?L&MLK~Kb F{W<敏S+6w[ЃxًqԤs0TT\x#܇*xNzRV=IA@O Pd ElcǎӿG~i+\0լ8ծ]ŋ5QXOeʔ{\DFE? .R wD)LŊu|$l[Nѭ( 0K+VF&9$ͳ?ʆ  Oڶmm?T~Ȝ9s@IҥE۰E˝=zw}7n$n@-PWzKfhpǩL2 ]v^X`Ũ#?hw أKG$@$8P -g$OS;=FodcVyW"D9eIQ 2LM5Gܛԏ={tydCIΝo8F~\׫kR|)z;Yl 2B%P?9#xj| фXu'dĈmzo)ܾo-y]gEzvMljբHCSLe?Ѳ/';o2պ uԛfm?BJ/1 d8d*^9|g_nI?ʫ?eYߥf2jciG|Pvcf\J९.~r}årJsB];w}Ns)Wu7i$tdf+LÏ?SB1H>>jo&]d ݻw%u˞=SO=U֬^ď?(u5/R]r>B/P!&MR\Y5$1L,sbW%JJI/jxUmgX1/㎓:Sխ-bKDRi۶mZPkҤmBF]>U<'ˢEKO?U/_>)U4oL=%1ES:XF4(pzEZ ޳@?;MVR|l% oVWgc֮]O|jQ&\Roؠ~$#  ɍ[ ϟ/`D{  `@uy畓 #c[_޿1b<ȣKE3G՗N}a[ SϊHl"2ǸټZYO w q$PlTp!yq 9sIɸsg[m}`eۧh\[Tc/PCcL1zw<^msʥѯ2b/d=sVaU]z--0#*,ziѲrɗ\߶nJkt=8qǝscfiܥdYw?vW2[/-E{xr̭w2nDh diښ=wj`hlXC$@!&pX A5(۶},M]-=AiM8濺mEN WBy]/YMSV2eϷak@t!raݺbex[[a]50$PgLQ8lC'Yti"m@.m9  ~7H[P)!ZR>&PS-;YIkZ69ۭ][AL:XF ,zD`dqPLF攙[/8Cdrm(1H#*XGsorkXN7!spU;n念/+\@~W֪MٯoBPUW7/$+/U0Lwy5`%6=\+ܒm-(a,tŅ-ܹn*CS0ʝW8 Êk֬Qªvҷoo{˃=.ץqmݪTS~ikj5t@}Q2򁬭~޽zoE]wk)RX^zq}9){&JM6]x0>5ĸR%K ,N;MU+)^eq1cFHH >X2)PQV,~TQSCQ+Ql^n_pf3F@Թ[OZzƹ_oE[g2uMwn"޽ GYmjUYyox3@)34­f %jT $ B5߿__;{ Cw?e=_EYVmzD:_0`Qp7Æ's`JFՏkq? vѿa7U L/z3ix% &*FHdeӳǝIU;vHZɓ&H͚5߃qq֮V:O@Mm&m.:d4k4"Em*ҪUǿF>f~ ڪQ!P!8!<VV׹6vjgzs8rj:F*3֬"'0;[e2iݺ˗?_}ʍN~f`KuE:(P ^IH = ,eǹ+TAXkkV*8XN?xm 7Aop޻CWul 7Wo\x=/,6 rZ8F>Tus wcTien~,jٲ 3- .mf)\FA}Qg<[<+,`!mk,}E'|֍/{#&ag6hT:zMp@uiO$@@ 8?5B& J+s+록[֭v7tiQqɶ_̣L9S,[N"ŽZl榃ƎmZgB w >([?yA{mUVh7nP~W vSI0e"WOv̾½]9US[lEܬ\ <\t4u${ݸim;[oXg5k{;t̶DP%Ps ״dݳpE? @vX AXÂDGȗRb8P_弥K__5ꔤm *꩓klɶfy ¶n0ڳԶ[d!Z+ɋ؊Atrڲ'%P!MyYd:wC;if]}oT$T `.lKrl d򢍋,uÜJNqUCZB(`5w8| )Kf5,# LSGW^ȶi6>3KYPaOksa4߆peO$@$E TsPv*ɫ#TF"L^l.M]wI'jON駟lbx)ٶR[$e1j̈Ӈ0U?UɮkZ6tk~Sxq} 1תMp㏪_pc" + a(t/eÆv!0G]$baNF}]ju@mw{~cSN͓GVYqbϘ1Mn LKSVWAumV#"QVcOau8]>AsPMa9eT)xsP$yټQrO$@$#@bn\CuHsxҪMD4$*PQ+}1tG:l-5r}}bXoa,*VPfϚin^׮[@t3^NTRl+P}v-R!&b:k-F sR'YM:ګ+P8dp7n[6σ-&M/+Wň:%-,`vh~(`7X. P6YP'PQ@Nk<*yz^u԰QHȨ@3w+{B`\} [;aXrDe3f<4|B[ [nI7n䏊XH۲DZe7hҢEъ5M![g)]nIU)2m^P.+h˄SB%W;bPL-[\8Xm~v[ݻ߮,ykr׿"PeA˲Y+aq5Z DޯT6`N Ⱀb+06hcԾj~^˔)#={tU0I϶mAE۶[";U_˗kg/[ߦ]U9G7w2 -_MUP,TUb)7_A擄if F/HH dT7+<1ئi:|Ν?*q|((oVo ,<R 5RB!O5TݦM#%r~A %KRn}ZxG)Kg!ʖO# $Liּ7}X@+*&q$@$@$jL<$.bDIQ[y͟LəHH@͆$@ @HH S(P3E@ ` U2MŎ F)bLK$@$'@'{    @(~VN$@$@$@$'@'{    @(~VN$@$@$@$'@'{    @(~VN$@$@$@$'@'{    @(~VN$@$@$@$'@'{    @(~VN$@$@$@$'@'{    @(~VN$@$@$@$'@'{    @(~VN$@$@$@$'@'{    @(~VN$@$@$@$'@'{    @(~VN$@$@$@$'@'{    @(~VN$@$@$@$'@'{    @(~VN$@$@$@$'@'{    @(~VN$@$@$@$'@'{    @(~VN$@$@$@$'@'{    8$e?"E gQKdw{dӦMeJɱ{`=lc׮Oe۶mr1HʕR~C exH$@1T˗^zY1cʝ[7k*GuT#<&>f ))x3Al2" C@F? Ϋ4A-F~ҳW-N;w.iӺqmrG#!lר{˼OEħ~EEPQ>5g[yRZtHX.0L.qnz0+] ׃D$@9&or]cG!e˖SNR%k׭mˏ?ۙ')Rd)zn裏DG=S>Ï}՗P>Xy]DsEFˤM裏 ύh󁾉@%)PvAm# Pq}9w~ b i7nT/լYGPbބ "/ hi#ݛ˫VMCsſv:[Ǻuog+3L_yU۶wDmݞGxV;XQT9j^O_bs2= `X94.'/>?Hy~'-[R?'Lj)Q矽5ktj:{s2[VvŠ|F M&4J:HY?ߣ&ɉHQ]v龇?1KtE{¤w_mٲC>ʉOo$"D>k2HRa/$@$@8qU\ՊJz ~רy]EV6=,f͎>Z*/T%_#ZQ^n4\mMk^eZ5Ít|N7xE.Rܖ ?4h-ô3dzw>2|\[h婹NJ )K,D[<݈<jU[MK*㵻dMtY?~g*WnyߙDػw";}L׵k7w=M8YǕ.s q_ExQe^X[^{L++ji65xPoϞ=i̝;ϫ^VV9w 6\E&7A}oڕ5Jm 00yy% H@FgZzjYvuP2;}̢o,ݻ]qmԨ|:U&ЦM+k@<,W"Bi׮ן+){ر/"JH=CrŕKĘQq3dp7nU+gU͚5kIQg40[=wAfjLMyU&Yh"1)붾ǜٳf C G%eIxS~޼yeS"欚;S?I??ٸqԫWWƌ~&Q?BW?a͚Ur+ oŋ*Ȝ9tMj:W_}ﱥ'x,xa^TcrKW`mLҺ;1c<3MUM>NL%6nܴI?:`0j,vSLGnݑ--lC4iC~0xpq]O12uwG ޣ>{ӦP u\j5t +ZP8*qa:,c/`"McQ7桎64/hòM~xv,Xx0d՝bW7Np^bV5( s;wսRj,LSXQ߂lq*xlRe-IHe-4.YHvzė'~/^+5A$"OW7իQQ_+fxe-qr`Dph Sph߯߀lIF~۴i)! Wwq!=yxǔ^ׯ`oÆ 6 }Ͻ6NY\mܻĻK 6L A9eܷ~g˹ށ&X_1ڈDMwٲS20ϻU:M>F3RTږ !$@$@9 pH ha52BUnr 6k:"1ϔN5_2")M9=gKխ "7Z&4[T+>lI"s=5؇ )Q6gμlt6Ig-*_nZcvt.ÂlرtR'OXwxV@Cx5;h~[N+\wnG[$JDcT5VҶTX@ !=5IRjQ\J=əGg©,j䛢Ŋ0;v7|#v~kwZ|劆Go.o]u$RNm0~V VDY$W\fJɭN1շ=KjTKXַOϛ#W3G3}3yTQ;=/IyŸ~%J?>RfN0; A*YD ڇvVREO"3q㦒/_>YUQbUϝђ%KHvdҥL5t $={WecH>sz~)^O|ɲaZG p2dǼҥJIjU~zR\Y.| 7k+ɖ<M>Fe|^cW*mKA6P  H@m3';y;+ip`,hX!a5)ձ/ӫlRldl_Pi Y.hL6b E wpFAW5ΤIAX9Sn+v @}{Ǿ7v=b c>ci;b i~ *8a0yq׬qme&fAMoqXlRe2HH u *jj!ZAzR$q{ժUh% D JCe=}DYQ5kQNoxtI֪Rv رEzxǼhGyBF_c0a\Jx?˂ JgiAU ¤3fLUȻDMmA%}yңG/m=+Bk%M]rYgɠH-DlTa!jJdʕz&MGzةӍҳǝn͂JDT?,-@1HH 5k,(3j,QjK!+*3: .Q6*d,QV@S Ve D=>Yт Ӧd/E-;qfÍ;+h5b1YHf P6G묶+55k}QoNtf\4 j*}|^cW*mKA4V # HaH*h8ëY)kTn:- x u^zYCW_Yqa T LP 9jdL)R`>5b(|~`=3uAPh85ʔ 9WSg2YMH:q &`B?_1s^Ɉ'lOeܓOβyZ͚>w벰Nhs]*T&ZT>+D? @1NX /[l d *t#0đΜ1@,N\]+8?4u]:ǻ}²j|H. 7NŽ&=W<AgΑvBMPy_ɶ-Ux&pǜ$@$@YsPOQؓu0/uN;eRp!%,zipRАIkl|6)ZІG ur~Xvaެ9I lѢ,_~oР<0>$9nٮ!`6{9)ᤰO .@GpХ~ziKĩ\8˸T/3SvU*_|7pq_~TR ?@tk&^oV'B-\;=0rN8w%Tg!PsfI8yu훠>F|^cWmCRa`w P7m|[ @j}f>frJ47,<7"89Ս[Ҫ- {GXV&zժ z>3Őv0PnZ }T۶s^XR'(~9in6$'Lm!V;XRjsFu{7d۟U\?(Հ=N2! /,N5*EIh_dۣeos }e/}&mRJԦ:ڙD[?qgB ۝iC&klےaI޼yA4$@$@Qcj "    t@$@$@$@$E5}$@$@$@$@! @N`HHHHPfHHHH (PC l @ , j:M    "@ł>    @ A' $@$@$@$@Y(PXG$@$@$@$!6HHHH j HHHHB@5& d@bA @P    ,Y,#    t@$@$@$@$E5}$@$@$@$@! @N`HHHHPfHHHH (PC l @ , j:M    "@ł>    @ A' $@$@$@$@Y(PXG$@$@$@$!6HHHH j HHHHB@5& d@bA @P    ,Y,#    t@$@$@$@$EyP"IENDB`gargle/vignettes/auth-from-web.Rmd0000644000176200001440000003367614433520365016670 0ustar liggesusers--- title: "Auth when using R from the browser" output: rmarkdown::html_vignette vignette: > %\VignetteIndexEntry{Auth when using R from the browser} %\VignetteEngine{knitr::rmarkdown} %\VignetteEncoding{UTF-8} --- ```{r, include = FALSE} knitr::opts_chunk$set( collapse = TRUE, comment = "#>" ) ``` If you are working with R in a web-based context, such as [RStudio Server](https://posit.co/download/rstudio-server/), [Posit Cloud](https://posit.cloud/), [Posit Workbench](https://posit.co/products/enterprise/workbench/), or [Google Colaboratory](https://colab.research.google.com/), your experience of browser-based auth flows will be different from those using R on their local machine. You need to use **out-of-band authentication**, sometimes denoted "oob" or "OOB". After the usual auth dance, instead of seeing "authentication successful, return to R!", you are presented with an authorization code to copy and paste back into your R session. For folks who are running R on their local machine, this final exchange can be done automagically, using a temporary local webserver, but that is not possible for those accessing a remote R session through the browser. On February 16, 2022, Google announced the (partial) deprecation of the OAuth out-of-band (OOB) flow, to be enacted no later than February 1, 2023. The deprecation applies to Google Cloud Platform (GCP) projects that are in production mode. OOB still works for projects that are in testing mode. The built-in tidyverse client (used by googledrive, googlesheets4, and bigrquery) is associated with a GCP project that is in production mode. Therefore, conventional OOB auth stopped working for the built-in client in February 2023. In anticipation of this, gargle gained a new auth flow in version 1.3.0 that we call "pseudo-OOB", which should allow casual users to continue to enjoy a low-friction auth experience, even from RStudio Server, Posit Cloud, Posit Workbench, and Google Colaboratory. If you attempt to do conventional OOB auth with a client that no longer supports it, you'll see something like this: ```{r, echo = FALSE, out.width = "400px"} #| fig-cap: > #| Access blocked: Tidyverse API Packages's request is invalid. #| Error 400: invalid_request #| fig-alt: > #| Screenshot with the following text: "Access blocked: Tidyverse API #| Packages's request is invalid", "You can't sign in because Tidyverse API #| Packages sent an invalid request. You can try again later, or contact the #| developer about this issue. Learn more about this error", "If you are a #| developer of Tidyverse API Packages, see error details.", "Error 400: #| invalid_request". knitr::include_graphics("invalid_request.png") ``` If you work on any of the affected platforms and are experiencing new auth problems, your first move should be to update all packages involved (gargle and one or more of googledrive, googlesheets4, bigrquery). **Restart R.** Re-execute your code in an interactive context that will allow you to re-auth. This vignette documents various matters around OOB auth, both conventional and pseudo-OOB, for users who want to understand this more deeply. Some of the packages that use gargle for auth and for which this article applies: * [bigrquery](https://bigrquery.r-dbi.org) * [googledrive](https://googledrive.tidyverse.org) * [googlesheets4](https://googlesheets4.tidyverse.org) * [gmailr](https://gmailr.r-lib.org) *note: gmailr does not use the built-in tidyverse OAuth client* ## Consider using a service account token (or no token!) If you have concerns about using OOB auth, consider whether your task truly requires auth as a specific, normal user. Can the task be completed with _no auth_, i.e. you are accessing something that is world readable or readable for "anyone with a link"? In that case, the wrapper package probably provides a function to go into a de-authorized state, such as `googledrive::drive_deauth()` or `googlesheets4::gs4_deauth()`. If the task requires auth, consider whether it really must be as a specific user. You may be able to accomplish the task with a service account, which you create for this specific purpose. A service account token is much easier to work with on a server and in non-interactive contexts than a user token. A service account can also be given much more selective permissions than a user account and can be more easily deleted, once it is no longer needed. Remember that the service account will need to be explicitly given permission to access any necessary resources (e.g. permission to read or write a specific Drive file or Sheet). A service account doesn't somehow inherit permissions indirectly from the user who owns the GCP project in which it lives. To learn more about using a service account, see `vignette("non-interactive-auth")`. ## When and how to use OOB In the absence of any user instructions, the function `gargle::gargle_oob_default()` is used to decide whether to use OOB auth. By default, OOB auth is used on RStudio Server, Posit Cloud, Posit Workbench, and Google Colaboratory, or if the option `"gargle_oob_default"` is set to `TRUE`. (Note that we use the term "OOB auth" here to include both the existing, conventional form of OOB and gargle's new pseudo-OOB.) Wrapper packages generally also allow the user to opt-in to OOB auth when making a direct call to an auth function. For example, the functions `googledrive::drive_auth()`, `googlesheets4::gs4_auth()`, `bigrquery::bq_auth()`, and `gmailr::gm_auth()` all have a `use_oob` argument. Notably, all of these `use_oob` arguments default to `gargle::gargle_oob_default()`. gargle usually automatically detects when it should use OOB auth, but here is what it could look like if we are not using OOB, but should be. During auth, you are redirected to localhost on port 1410 and receive an error along these lines: ``` Chrome: This site can't be reached; localhost refused to connect. Firefox: Unable to connect; can't establish a connection. ``` If this happens you might need to explicitly request OOB. Below we review two different methods. ## Request OOB auth in the `PKG_auth()` call Packages like googledrive and bigrquery aim to make auth "just work" for most users, i.e. it's automatically triggered upon first need. However, it is always possible to initiate auth yourself, which gives you the opportunity to specify non-default values of certain parameters. Here's how you could request OOB auth, using googledrive as an example: ```{r eval = FALSE} library(googledrive) drive_auth(use_oob = TRUE) # now carry on with your work drive_find(n_max = 5) ``` ## Set the `"gargle_oob_default"` option If you know that you *always* want to use OOB, as a user or within a project, the best way to express this is to set the `"gargle_oob_default"` option. ```{r eval = FALSE} options(gargle_oob_default = TRUE) ``` This code could appear at the top of a script, in a setup chunk for `.Rmd`, or in a Shiny app. But it probably makes even more sense in a `.Rprofile` startup file, at the user or project level. Once the `"gargle_oob_default"` option has been set, it is honored by downstream calls to `PKG_auth()`, explicit or implicit, because the default value of `use_oob` is `gargle::gargle_oob_default()`, which consults the option. ## Conventional vs. pseudo-OOB auth gargle now supports two OOB flows, which we call "conventional OOB" (the existing, legacy OOB flow) and "pseudo-OOB" (the new flow introduced in response to the partial deprecation of conventional OOB). If we are using OOB auth, the decision between conventional or pseudo-OOB is made based on the currently configured OAuth client. * If the OAuth client is of type `"installed"` (shows as "Desktop" in Google Cloud Console) or is of unknown type, gargle uses conventional OOB. Note that this will not necessarily succeed, due to the deprecation process described above. * If the OAuth client is of type `"web"` (shows as "Web application" in Google Cloud Console), gargle uses the new pseudo-OOB flow. ```{=html}
use_oob
FALSE TRUE
client type installed use httpuv to spin up
a temporary web server
conventional OOB
web --not possible-- pseudo-OOB
``` Packages that use a built-in tidyverse OAuth client (googledrive, googlesheets4, and bigrquery) should automatically select a "web" client on RStudio Server, Posit Cloud, Posit Workbench, and Google Colaboratory and an "installed" client otherwise. If you need to explicitly request a "web" client in some other setting, you can use the global option `"gargle_oauth_client_type"`: ```{r eval = FALSE} options(gargle_oauth_client_type = "web") ``` Users who configure their own OAuth client will need to be intentional when choosing the client type, depending on where the code is running. On the R side, it is recommended to setup an OAuth client using `gargle_oauth_client_from_json()`, which allows the client type (`"installed"` vs. `"web"`) to be detected programmatically from the downloaded JSON. The less-preferred approach is to use `gargle_oauth_client()` and provide the information yourself. ## How pseudo-OOB works Pseudo-OOB works just like non-OOB and conventional OOB in terms of the user's interactions with Google authorization server. This is where the user authenticates themselves with Google and consents to the type of access being requested by the R code. These flows differ in how they handle a successful response from the authorization server. Specifically, the flows use different redirect URIs. * A (temporary) local webserver is used to listen for this response at, e.g., `http://localhost:1410/` if R is running locally and the httpuv package is available (i.e. a non-OOB flow). * In conventional OOB, a special redirect value is used, typically `urn:ietf:wg:oauth:2.0:oob`, and the authorization code is provided to the user via a browser window for manual copy/paste. This page is served by Google. Google has deprecated conventional OOB for projects in production mode (but it is still allowed for projects in testing mode). * In gargle's pseudo-OOB, a redirect URI from the configured OAuth client is used to receive the response. This page is responsible for exposing a code that the user can copy/paste, similar to conventional OOB (except the page is *not* served by Google). Unlike conventional OOB, this is not the authorization code itself, but is something from which the code can be extracted, along with a state token to mitigate cross-site request forgery. This is actually implemented using an [OAuth flow for web server applications](https://developers.google.com/identity/protocols/oauth2/web-server). Note that we (gargle) call this pseudo-OOB, but it is not technically OOB from Google's point-of-view. The built-in OAuth client used for pseudo-OOB by tidyverse packages redirects to . This is a static landing page that does not collect any data and exists solely to give the interactive R user a way to convey the authorization token back to the waiting R process and thereby complete the auth process. ### More details about the deprecation of conventional OOB Key links: * Blog post: [Making Google OAuth interactions safer by using more secure OAuth flows](https://developers.googleblog.com/2022/02/making-oauth-flows-safer.html) * [Out-Of-Band (OOB) flow Migration Guide](https://developers.google.com/identity/protocols/oauth2/resources/oob-migration#web-application) * [Using OAuth 2.0 to Access Google APIs](https://developers.google.com/identity/protocols/oauth2) ## But I didn't need OOB yesterday! Sometimes the usual OAuth web flow suddenly stops working for people working directly with R (so NOT via the browser) and they use OOB auth to get unstuck again. What's going on in this case? The initial error looks something like this: ``` createTcpServer: address already in use Error in httpuv::startServer(use$host, use$port, list(call = listen)) : Failed to create server ``` It's characteristic of some other process sitting on port 1410, which is what gargle is trying to use for auth. It's true that using OOB auth is a workaround. But OOB auth is, frankly, more clunky, so why use it if you don't have to? Here are ways to fix this. * Restart your system. This will almost certainly kill the offending process, which is usually a zombie process. * Hunt down the offending process, verify it looks expendable, and kill it. On *nix-y systems, use `lsof` to get the process ID: ``` sudo lsof -i :1410 ``` The output will look something like this: ``` COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME R 16664 jenny 20u IPv4 0x63761a50856c65f 0t0 TCP localhost:hiq (LISTEN) ``` In this case, as is typical, this is a zombie R process and I feel confident killing it. The process ID is listed there as PID. Note that and kill the process, like so, filling in the PID you found: ``` kill -9 ``` So, to be clear, in this example, the command would be: ``` kill -9 16664 ``` The normal, non-OOB auth web flow should work again now. ## Further reading If you're working on a data product that will be deployed (for example on [shinyapps.io](https://www.shinyapps.io) or [Posit Connect](https://posit.co/products/enterprise/connect/)), you will also need to consider how the deployed content will authenticate non-interactively, which is covered in `vignette("non-interactive-auth")`. gargle/vignettes/oauth-client-not-app.Rmd0000644000176200001440000002367714433520365020163 0ustar liggesusers--- title: "Transition from OAuth app to OAuth client" output: rmarkdown::html_vignette vignette: > %\VignetteIndexEntry{Transition from OAuth app to OAuth client} %\VignetteEngine{knitr::rmarkdown} %\VignetteEncoding{UTF-8} --- ```{r, include = FALSE} knitr::opts_chunk$set( collapse = TRUE, comment = "#>" ) ``` ```{r setup} library(gargle) ``` Over the course of several releases (v1.3.0, v1.4.0, and v1.5.0), gargle has shifted to using an OAuth **client** in the user flow facilitated by `gargle::credentials_user_oauth2()`, instead the previous OAuth "app". This is a more than just a vocabulary change (but it is also a vocabulary change). This vignette explains what actually changed and how wrapper packages should adjust. ## Why change was needed In 2022, Google partially deprecated the out-of-band (OOB) OAuth flow. The OOB flow is used by R users who are working with Google APIs and who use R in the browser, such as via RStudio Server, Posit Workbench, Posit Cloud, or Google Colaboratory. Conventional OOB auth **still works** under certain conditions, for example, if the OAuth client is associated with a GCP project that is in testing mode or that is internal to a Google Workspace. But conventional OOB is no longer supported for projects that serve external users that are in production mode. In particular, this means that conventional OOB is no longer supported for the GCP project that has historically made auth "just work" for casual users of packages such as googledrive, googlesheets4, and bigrquery. The default OAuth client used by these package no longer works with conventional OOB. In response, as of v1.3.0, gargle implements a new variant of OOB, called **pseudo-OOB**, to continue to provide a user-friendly auth flow for googledrive/googlesheets4/bigrquery on RStudio Server/Posit Workbench/Posit Cloud/Google Colaboratory. The pseudo-OOB flow is also available for other developers to use. This flow is triggered when `use_oob = TRUE` (an existing convention in gargle and gargle-using packages) **and** the configured OAuth client is of the *web* type (when creating an OAuth client, this is called the "Web application" type). ```{=html}
use_oob
FALSE TRUE
client type installed use httpuv to spin up
a temporary web server
conventional OOB
web --not possible-- pseudo-OOB
``` In the past, gargle basically assumed that every OAuth client was of the *installed* type (when creating an OAuth client, this is called the "Desktop app" type). Therefore, the introduction of pseudo-OOB meant that gargle had to learn about different OAuth client types (web vs. installed). And that didn't play well with `httr::oauth_app()`, which gargle had been using to store the client ID and secret. That's why there is a new S3 class, `"gargle_oauth_client"`, with a constructor of the same name. Since more information is now necessary to instantiate a client (e.g. its type and, potentially, redirect URIs), the recommended way to create a client is to provide JSON downloaded from the GCP console to `gargle_oauth_client_from_json()`. Since we had to introduce a new S3 class and supporting functions, we also took this chance to make the vocabulary pivot from "OAuth app" to "OAuth client". Google's documentation has always talked about the "OAuth client", so this is more natural. This vocabulary is also more future-facing, anticipating the day when gargle might shift from httr to httr2, which uses `httr2:oauth_client()`. As a bridging measure, the `"gargle_oauth_client"` class currently inherits from httr's `"oauth_app"`, but this probably won't be true in the long-term. ### How to instantiate an OAuth client in R If you do auth via gargle, here are some recommended changes: 1. Stop using `httr::oauth_app()` or `gargle::oauth_app_from_json()` to instantiate an OAuth client. 2. Start using `gargle_oauth_client_from_json()` (strongly recommended) or `gargle_oauth_client()` instead. This advice applies to anything you do inside your package and also to what you encourage and document for your users. gargle ships with JSON files for two non-functional OAuth clients, just to make this all more concrete: ```{r} (path_to_installed_client <- system.file( "extdata", "client_secret_installed.googleusercontent.com.json", package = "gargle" )) jsonlite::prettify(scan(path_to_installed_client, what = character())) (client <- gargle_oauth_client_from_json(path_to_installed_client)) class(client) (path_to_web_client <- system.file( "extdata", "client_secret_web.googleusercontent.com.json", package = "gargle" )) jsonlite::prettify(scan(path_to_web_client, what = character())) (client <- gargle_oauth_client_from_json(path_to_web_client)) class(client) ``` Notice the difference in the JSON for the installed vs. web client. Note the class of the `client` object, the new `type` field, and the `redirect_uris`. ## `AuthState` class There are two gargle classes that are impacted by the OAuth-app-to-client switch: `AuthState` and `Gargle2.0`. We cover `AuthState` here and `Gargle2.0` in the next section. If a wrapper package follows the design laid out in `vignette("gargle-auth-in-client-package")`, it will use an instance of `AuthState` to manage the package's auth state. Let's assume that internal object is named `.auth`, which it usually is. Here are the changes you need to know about in `AuthState`: * The `app` field is deprecated, in favor of a new field `client`. If you request `.auth$app`, there will be a deprecation message and the `client` field is returned. * The `$set_app()` method is deprecated, in favor of a new `$set_client()` method. If you call `.auth$set_app()`, there will be a deprecation message and the input is used, instead, to set the `client` field. * The `app` argument of the `init_AuthState()` constructor is deprecated in favor of the new `client` argument. If you call `init_AuthState(app = x)`, there will be a deprecation message and the input `x` is used as the `client` argument instead. Here are the changes you probably need to make in your package: * The first argument of the user-facing function, `PKG_auth_configure()`, should become `client` (which is new). Move the existing `app` argument to the last position and deprecate it. * Deprecate `PKG_oauth_app()` (the function to reveal the user's configured OAuth client). * Introduce `PKG_oauth_client()` to replace `PKG_oauth_app()`. Here's how `googledrive::drive_auth_configure()` and `googledrive::drive_oauth_client()` looked before and after the transition: ```{r, eval = FALSE} # BEFORE drive_auth_configure <- function(app, path, api_key) { # not showing this code .auth$set_app(app) # more code we're not showing } drive_oauth_app <- function() .auth$app # AFTER drive_auth_configure <- function(client, path, api_key, app = deprecated()) { if (lifecycle::is_present(app)) { lifecycle::deprecate_warn( "2.1.0", "drive_auth_configure(app)", "drive_auth_configure(client)" ) drive_auth_configure(client = app, path = path, api_key = api_key) } # not showing this code .auth$set_client(client) # more code we're not showing } drive_oauth_client <- function() .auth$client drive_oauth_app <- function() { lifecycle::deprecate_warn( "2.1.0", "drive_oauth_app()", "drive_oauth_client()" ) drive_oauth_client() } ``` The approach above follows various conventions explained in `vignette("communicate", package = "lifecycle")`. If you also choose to use the lifecycle package to assist in this process, `usethis::use_lifecycle()` function does some helpful one-time setup in your package: ```{r eval = FALSE} usethis::use_lifecycle() ``` The roxygen documentation helpers in gargle assume `PKG_auth_configure()` is adapted as shown above: * `PREFIX_auth_configure_description()` crosslinks to `PREFIX_oauth_client()` now, not `PREFIX_oauth_app()`. * `PREFIX_auth_configure_params()` documents the `client` argument * `PREFIX_auth_configure_params()` uses a lifecycle badge and text to communicate that `app` is deprecated. * `PREFIX_auth_configure_params()` crosslinks to `gargle::gargle_oauth_client_from_json()` which requires gargle (>= 1.3.0) ## `Gargle2.0` class `Gargle2.0` is the second gargle class that is impacted by the OAuth-app-to-client switch. Here are the changes you probably need to make in your package: * Inside `PKG_auth()`, you presumably call `gargle::token_fetch()`. If you are passing `app = `, change that to `client = `. Neither `app` nor `client` are formal arguments of `gargle::token_fetch()`, instead, these are intended for eventual use by `gargle::credentials_user_oauth2()`. Here's a sketch of how this looks in `googledrive::drive_auth()`: ```{r eval = FALSE} drive_auth <- function(...) { # code not shown cred <- gargle::token_fetch( scopes = scopes, # app = drive_oauth_client() %||% , # BEFORE client = drive_oauth_client() %||% , # AFTER email = email, path = path, package = "googledrive", cache = cache, use_oob = use_oob, token = token ) # code not shown } ``` * If you ever call `gargle::credentials_user_oauth2()` directly, use the new `client` argument instead of the deprecated `app` argument. gargle/vignettes/non-interactive-auth.Rmd0000644000176200001440000005551614433520365020254 0ustar liggesusers--- title: "Non-interactive auth" output: rmarkdown::html_vignette vignette: > %\VignetteIndexEntry{Non-interactive auth} %\VignetteEngine{knitr::rmarkdown} %\VignetteEncoding{UTF-8} --- ```{r, include = FALSE} knitr::opts_chunk$set( collapse = TRUE, comment = "#>", eval = FALSE ) ``` Here we describe how to do auth with a package that uses gargle, without requiring any user interaction. This comes up in a wide array of contexts, ranging from simple rendering of a local R Markdown document to deploying a data product on a remote server. We assume the wrapper package uses the design described in `vignette("gargle-auth-in-client-package")`. Examples include: * [bigrquery](https://bigrquery.r-dbi.org) * [googledrive](https://googledrive.tidyverse.org) * [googlesheets4](https://googlesheets4.tidyverse.org) * [gmailr](https://gmailr.r-lib.org) *note: gmailr does not use the built-in tidyverse OAuth client* Full details on `gargle::token_fetch()`, which powers this strategy, are given in `vignette("how-gargle-gets-tokens")`. ## Provide a token or pre-authorize token discovery The main principle for auth that does not require user interaction: > Provide a token directly or take advance measures that indicate you want a token to be discovered. We present several ways to achieve this, basically in order of preference. ## Sidebar 1: Deployment First, a word about deployed environments. If this doesn't apply to you, skip this section. Let's identify a specific type of project: it is developed in one place, with interactivity -- such as your local computer -- and then deployed elsewhere, where it must run without further interaction -- such as on [Posit Connect](https://posit.co/products/enterprise/connect/) or [shinyapps.io](https://www.shinyapps.io). In this context, it may make sense to depart from gargle's default behaviour, which is to store tokens outside the project, and to embed them in the project instead. An example at the end of this vignette demonstrates the use of a project-level OAuth cache. A service account token could also be stored in the project. When you embed tokens in the project and deploy, remember, that, by default, they are no more secure or hidden than the other source files in the project. `vignette("managing-tokens-securely")` describes a method for embedding an encrypted token in the project, which is an extra level of care needed if you want to access credentials within, e.g., a continuous integration service, such as GitHub Actions. ## Sidebar 2: I just want my `.Rmd` to render TL;DR is that you need to successfully authenticate *once* in an interactive session and then, in your code, give gargle permission to use a token it finds in the cache. These sorts of commands achieve that: ```{r} # Approach #1: use an option. # Either specify the user: options(gargle_oauth_email = "jenny@example.com") # Or, if you only use one Google identity, you can be more vague: options(gargle_oauth_email = TRUE) # Or, you can specify the identity to use at the domain level: options(gargle_oauth_email = "*@example.com") # Approach #2: call PACKAGE_auth() proactively. library(googledrive) # Either specify the user: drive_auth(email = "jenny@example.com") # Or, if you only use one Google identity, you can be more vague: drive_auth(email = TRUE) # Or, you can specify the identity to use at the domain level: drive_auth(email = "*@example.com") ``` At the end of this article, this scenario is explained in detail, if you want to understand why this works. ## Embrace credentials available in certain cloud settings In certain cloud computing contexts, a service account token may be ambiently available (or you can arrange for that to be true). Think about it: if your workload is running on Google Compute Engine (GCE), it's already "inside the Google house". It seems like there should be a way to avoid another round of auth and that is indeed the case. Another advantage of these cloud auth workflows is that there is never any need to download and carefully manage a file that contains sensitive information. This is why they are often described as "keyless". If you *can* use one of these methods, you should seriously consider doing so. ### Google Compute Engine This section applies to code running on a GCE instance, either literally, or on another Google Cloud product built on top of GCE. You should consider Google's own documentation to be definitive, but we'll try to give a useful summary here and to explain how gargle works with GCE: A Google Cloud Platform (GCP) project generally has a GCE default service account and, by default, a new GCE instance runs as that service account. (If you wish, you can use a *different* service account by taking explicit steps when you create an instance or by modifying it while it's stopped.) The main point is that, for an application running on GCE, a service account identity is generally available. GCE allows applications to get an OAuth access token from its metadata server and this is what `gargle::credentials_gce()` does (which is one of functions tried by `gargle::token_fetch()`, which is called by wrapper packages). This token request can be made for specific scopes and, in general, most wrapper packages will indeed be asking for specific scopes relevant to the API they access. Consider the signature of `googledrive::drive_auth()`: ```{r} drive_auth <- function(email = gargle::gargle_oauth_email(), path = NULL, scopes = "https://www.googleapis.com/auth/drive", cache = gargle::gargle_oauth_cache(), use_oob = gargle::gargle_oob_default(), token = NULL) { ... } ``` The googledrive package asks for a token with `"drive"` scope, by default. This brings up one big gotcha when using packages like googledrive or googlesheets4 on GCE. By default, a GCE instance will be running as the default service account, with the `"cloud-platform"` scope and this will, generally speaking, allow the service account to work with various Cloud products. However, the `"cloud-platform"` scope does not permit operations with non-Cloud APIs, such as Drive and Sheets. If you want the service account identity for your GCE instance to be able to get an access token for use with Drive and Sheets, you will need to explicitly add, e.g., the `"drive"` scope when you create the instance (or stop the instance and add that scope). (Note that, in contrast, BigQuery is considered a Cloud product and therefore bigrquery can operate with the `"cloud-platform"` scope.) Be aware that you might also need to explicitly grant the service account an appropriate level of access (e.g. read or write) to any Drive files you intend to work on. Finally, if you want to opt-out of using the default service account and, instead, auth as a normal user, even though you are on GCE, that is also possible. One way to achieve that is to remove `credentials_gce()` from the set of auth functions tried by `gargle::token_fetch()` by executing this command before any explicit or implicit auth happens: ```{r} # removes `credentials_gce()` from gargle's registry gargle::cred_funs_add(credentials_gce = NULL) ``` You can make a similar change in more scoped way with the helpers `gargle::with_cred_funs()` or `gargle::local_cred_funs()`. ### Workload Identity on Google Kubernetes Engine (GKE) Here we discuss how gargle's GCE auth can work for a related service, Google Kubernetes Engine (GKE), using [Workload Identity](https://cloud.google.com/kubernetes-engine/docs/how-to/workload-identity). This is more complicated that direct usage of GCE and some extra configuration is needed to make a service account's metadata available for the GKE instance to discover. GKE is the underlying technology behind Google's managed Airflow service, [Cloud Composer](https://cloud.google.com/composer), so this also applies to R docker files being called in that environment. Workload Identity is the recommended way to do authentication on GKE and other places, if possible, since it eliminates the use of a file that holds the service key, which is a potential security risk. 1. Following the [Workload Identity docs](https://cloud.google.com/kubernetes-engine/docs/how-to/workload-identity), you create a service account as normal and give it permissions and scopes needed to, say, upload to BigQuery. Imagine that `my-service-key@my-project.iam.gserviceaccount.com` has the `https://www.googleapis.com/auth/bigquery` scope. 2. Instead of downloading a JSON key, you instead migrate that permission by adding a policy binding to another service account within Kubernetes. 3. Create the service account within Kubernetes, ideally within a new namespace: ```sh # create namespace kubectl create namespace my-namespace # Create Kubernetes service account kubectl create serviceaccount --namespace my-namespace bq-service-account ``` 4. Bind that Kubernetes service account to the service account outside of Kubernetes you created in step 1, and assign it an annotation: ```sh # Create IAM policy binding betwwen k8s SA and GSA gcloud iam service-accounts add-iam-policy-binding my-service-key@my-project.iam.gserviceaccount.com \ --role roles/iam.workloadIdentityUser \ --member "serviceAccount:my-project.svc.id.goog[my-namespace/bq-service-account]" # Annotate k8s SA kubectl annotate serviceaccount bq-service-account \ --namespace my-namespace \ iam.gke.io/gcp-service-account=my-service-key@my-project.iam.gserviceaccount.com ``` This key will now be available to add to pods within the cluster. For Airflow, you can pass them in using the Python code `GKEStartPodOperator(...., namespace='my-namespace', service_account_name='bq-service-account')`. Documentation around `GKEStartPodOperator()` within Cloud Composer can be found [here](https://cloud.google.com/composer/docs/composer-2/use-gke-operator). 5. In order for the R function `gargle::gce_credentials()` do the right thing, you need to do two things: - Set `"gargle.gce.use_ip"` option to `TRUE`, in order to use the metadata server that's relevant on GKE. - Specify the target service account, i.e. you can't just passively accept the default, which is to use the `"default"` service account. `gce_instance_service_accounts()` can be helpful, e.g., if you want to know which service accounts your Docker container can see. Here is example code that you might execute in your Docker container: ```{r} options(gargle.gce.use_ip = TRUE) t <- gargle::credentials_gce("my-service-key@my-project.iam.gserviceaccount.com") # ... do authenticated stuff with the token t ... ``` Let's assume that PKG is an R package that implements gargle auth in the standard way, such as bigrquery or googledrive. At the time of writing the `service_account` argument is not exposed in the usual, high-level `PKG_auth()` function (. So if you need to use a non-`default` service account, you need to call `credentials_gce()` directly and pass that token to `PKG_auth()`: Here's an example of how that might look: ```{r} library(PKG) options(gargle.gce.use_ip = TRUE) t <- gargle::credentials_gce( "my-service-key@my-project.iam.gserviceaccount.com", # use YOUR service account scopes = "https://www.googleapis.com/auth/PKG" # use REAL scopes ) PKG_auth(token = t) # ... do authenticated stuff... ``` ### AWS Keyless auth is even possible from non-Google cloud platforms, using [Workload identity federation](https://cloud.google.com/iam/docs/configuring-workload-identity-federation). This is implemented in the experimental function `credentials_external_account()`, which currently only supports AWS. ## Provide a service account token directly When two computers are talking to each other, possibly with no human involvement, the most appropriate type of token to use is a service account token. If you're not working in cloud context with automatic access to a service account (see previous section), you can still use a service account, but it will require more explicit effort. 1. Create a service account and then download its credentials as a JSON file. This is described in `vignette("get-api-credentials")`, specifically in the *Service account token* section. 1. Call the wrapper package's main auth function proactively and provide the path to this JSON file. Example using googledrive: ```{r} library(googledrive) drive_auth(path = "/path/to/your/service-account-token.json") ``` If this code is running on, e.g., a continuous integration service and you need to use an encrypted token, see `vignette("managing-tokens-securely")`. For certain APIs, service accounts are inherently awkward, because you often want to do things *on behalf of a specific user*. Gmail is a good example. If you are sending email programmatically, you probably want to send it as yourself (or from some other specific email account) instead of from `zestybus-geosyogl@fuffapster-654321.iam.gserviceaccount.com`. This is, in fact, possible but is described as "impersonation", which should tip you off that Google does not exactly encourage this workflow. Some details: * This requires "delegating domain-wide authority " to the service account. * It is only possible in the context of a G Suite domain and only an administrator of the domain can set this up. * The domain-wide authority is granted only for specific scopes, so those can be as narrow as possible. This may make a domain administrator more receptive to the idea. * This is documented in a few different places, such as: - [Delegating domain-wide authority to the service account](https://developers.google.com/identity/protocols/oauth2/service-account#delegatingauthority) from Google Identity Platform docs - [Perform G Suite Domain-Wide Delegation of Authority](https://developers.google.com/admin-sdk/directory/v1/guides/delegation) from G Suite Admin SDK docs * The `subject` argument of `credentials_service_account()` and `credentials_app_default()` is available to specify which user to impersonate, e.g. `subject = "user@example.com"`. This argument first appeared in gargle 0.5.0, so it may not necessarily be exposed yet in user-facing auth functions like `drive_auth()`. If you need `subject` in a client package, that is a reasonable feature request. It is also possible to get a token with an explicit call to, e.g., `credentials_service_account()` and then pass that token to the auth function: ```{r} t <- gargle::credentials_service_account( path = "/path/to/your/service-account-token.json", scopes = ..., subject = "user@example.com" ) googledrive::dive_auth(token = t) ``` If delegation of domain-wide authority is impossible or unappealing, you must use an OAuth user token, as described below. ## Rig a service or external account for use with Application Default Credentials Wrapper packages that use `gargle::token_fetch()` in the recommended way have access to the token search strategy known as **Application Default Credentials**. You need to put the JSON corresponding to your service or external account in a very specific location or, alternatively, record the location of this JSON file in a specific environment variable. Full details are in the `credentials_app_default()` section of `vignette("how-gargle-gets-tokens")`. If you have your token rigged properly, you **do not** need to do anything else, i.e. you do not need to call `PACKAGE_auth()` explicitly. Your token should just get discovered upon first need. For troubleshooting purposes, you can set a gargle option to see verbose output about the execution of `gargle::token_fetch()`: ```{r} options(gargle_verbosity = "debug") ``` withr-style convenience helpers also exist: `with_gargle_verbosity()` and `local_gargle_verbosity()`. ## Provide an OAuth token directly If you somehow have the OAuth token you want to use as an R object, you can provide it directly to the `token` argument of the main auth function. Example using googledrive: ```{r} library(googledrive) my_oauth_token <- # some process that results in the token you want to use drive_auth(token = my_oauth_token) ``` gargle caches each OAuth user token it obtains to an `.rds` file, by default. If you know the filepath to the token you want to use, you could use `readRDS()` to read it and provide as the `token` argument to the wrapper's auth function. Example using googledrive: ```{r} # googledrive drive_auth(token = readRDS("/path/to/your/oauth-token.rds")) ``` How would you know this filepath? That requires some attention to the location of gargle's OAuth token cache folder, which is described in the next section. Full details are in the `credentials_byo_oauth2()` section of `vignette("how-gargle-gets-tokens")`. ## Arrange for an OAuth user token to be re-discovered This is the least recommended strategy, but it appeals to many users, because it doesn't require creating a service account. Just remember that the perceived ease of using the token you already have (an OAuth user token) is quickly cancelled out by the greater difficulty of managing such tokens for non-interactive use. You might be forced to use this strategy with certain APIs, such as Gmail, that are difficult to use with a service account. Two main principles: 1. Take charge of -- or at least notice -- the folder where OAuth tokens are being cached. 2. Make sure exactly one cached token will be identified and pre-authorize its use. There are many ways to do this. We'll work several examples using that convey the range of what's possible. ### I just want my `.Rmd` to render **Step 1**: Get that first token. You must run your code at least once, interactively, do the auth dance, and allow gargle to store the token in its cache. ```{r} library(googledrive) # do anything that triggers auth drive_find(n_max) ``` **Step 2**: Revise your code to pre-authorize the use of that token next time. Now your `.Rmd` can be rendered or your `.R` script can run, without further interaction. You have two choices to make: * Set the `gargle_oauth_email` option or call `PACKAGE_auth(email = ...)`. - The option-based approach can be implemented in each `.Rmd` or `.R` or in a user-level or project level `.Rprofile` startup file. * Authorize the use of the "matching token": - `email = TRUE` works if we're only going to find, at most, 1 token, i.e. you always auth with the same identity - `email = "jane@example.com"` pre-authorizes use of a token associated with a specific identity - `email = "*@example.com"` pre-authorizes use of a token associated with an identity from a specific domain; good for code that might be executed on the machines of both `alice@example.com` and `bob@example.com` This sets an option that allows gargle to use cached tokens whenever there's a unique match: ```{r} options(gargle_oauth_email = TRUE) ``` This sets an option to use tokens associated with a specific email address: ```{r} options(gargle_oauth_email = "jenny@example.com") ``` This sets an option to use tokens associated with an email address with a specific domain: ```{r} options(gargle_oauth_email = "*@example.com") ``` This gets a token *right now* and allows the use of a matching token, using googledrive as an example: ```{r} drive_auth(email = TRUE) ``` This gets a token *right now*, for the user with a specific email address: ```{r} drive_auth(email = "jenny@example.com") ``` This gets a token *right now*, first checking the cache for a token associated with a specific domain: ```{r} drive_auth(email = "*@example.com") ``` ### Project-level OAuth cache This is like the previous example, but with an added twist: we use a project-level OAuth cache. This is good for deployed data products. **Step 1**: Obtain the token intended for non-interactive use and make sure it's cached in a (hidden) directory of the current project. Using googledrive as an example: ```{r} library(googledrive) # designate project-specific cache options(gargle_oauth_cache = ".secrets") # check the value of the option, if you like gargle::gargle_oauth_cache() # trigger auth on purpose --> store a token in the specified cache drive_auth() # see your token file in the cache, if you like list.files(".secrets/") ``` Do this setup once per project. Another way to accomplish the same setup is to specify the desired cache location directly in the call to the auth function: ```{r} library(googledrive) # trigger auth on purpose --> store a token in the specified cache drive_auth(cache = ".secrets") ``` **Step 2**: In all downstream use, announce the location of the cache and pre-authorize the use of a suitable token discovered there. Continuing the googledrive example: ```{r} library(googledrive) options( gargle_oauth_cache = ".secrets", gargle_oauth_email = TRUE ) # now use googledrive with no need for explicit auth drive_find(n_max = 5) ``` Setting the option `gargle_oauth_email = TRUE` says that googledrive is allowed to use a token that it finds in the cache, without interacting with a user, as long as it discovers EXACTLY one matching token. This option-setting code needs to appear in each script, `.Rmd`, or app that needs to use this token non-interactively. Depending on the context, it might be suitable to accomplish this in a startup file, e.g. project-level `.Rprofile`. Here's a variation where we say which token to use by explicitly specifying the associated email. This is handy if there's a reason to have more than one token in the cache. ```{r} library(googledrive) options( gargle_oauth_cache = ".secrets", gargle_oauth_email = "jenny@example.com" ) # now use googledrive with no need for explicit auth drive_find(n_max = 5) ``` Here's another variation where we specify the necessary info directly in an auth call, instead of in options: ```{r} library(googledrive) drive_auth(cache = ".secrets", email = TRUE) # now use googledrive with no need for explicit auth drive_find(n_max = 5) ``` Here's one last variation that's applicable when the local cache could contain multiple tokens: ```{r} library(googledrive) drive_auth(cache = ".secrets", email = "jenny@example.com") # now use googledrive with no need for explicit auth drive_find(n_max = 5) ``` Be very intentional about paths and working directory. Personally I would use `here::here(".secrets)"` everywhere above, to make things more robust. For troubleshooting purposes, you can set a gargle option to see verbose output about the execution of `gargle::token_fetch()`: ```{r} options(gargle_verbosity = "debug") ``` withr-style convenience helpers also exist: `with_gargle_verbosity()` and `local_gargle_verbosity()`. For a cached token to be considered a "match", it must match the current request with respect to user's email, scopes, and OAuth client (client ID or key and secret). By design, these settings have very low visibility, because we usually want to use the defaults. If your token is not being discovered, consider if any of these fields might explain the mismatch. gargle/vignettes/get-api-credentials.Rmd0000644000176200001440000003344214431310014020005 0ustar liggesusers--- title: "How to get your own API credentials" output: rmarkdown::html_vignette vignette: > %\VignetteIndexEntry{How to get your own API credentials} %\VignetteEngine{knitr::rmarkdown} %\VignetteEncoding{UTF-8} --- ```{r, include = FALSE} knitr::opts_chunk$set( collapse = TRUE, comment = "#>" ) ``` Here we describe how to obtain different types of credentials that can be important when working with a Google API: * API key (not relevant to all APIs) * OAuth 2.0 client ID and secret * Service account token * External account token ("workload identity federation") This can be important for both users and developers: * Package authors: If you are writing a package to wrap a Google API, you may provide some built-in auth assets so that things "just work" for your users. Regardless, you will need credentials to use during package development and in testing. * Package users: Wrapper packages may or may not provide default auth assets. If they don't, you are required to provide your own. Even if they do, you may prefer to bring your own, to have more control over your own destiny. With your own credentials, you avoid sharing quota with other users, which can reduce time-consuming errors and retries. If you use your own credentials, you are no longer at the mercy of someone else choosing to roll the credentials when you least expect it. * Everyone: The best method for auth in non-interactive settings is to use a service account token or workload identity federation, which require some advance setup. Note that most users of gargle-using packages do not need to read this and can just enjoy the automatic token flow. This article is for people who have a specific reason to be more proactive about auth. ## Get a Google Cloud Platform project You will need a Google Cloud Platform (GCP) project to hold your credentials. Go to the Google Cloud Platform Console: * * This may involve logging in or selecting your preferred Google identity. * This may involve selecting the relevant organization. This console is your general destination for inspecting and modifying your GCP projects. Create a new project here, if necessary. Otherwise, select the project of interest, if you have more than one. ## Enable API(s) Enable the relevant APIs(s) for your GCP project. In the left sidebar, navigate to *APIs & Services > Library*. Identify the API of interest. Click Enable. If you get this wrong, i.e. need to enable more APIs later, you can always come back and do this then. ## Think about billing For some APIs, you won't be able to do anything interesting with the credentials hosted in your project unless you have also linked a billing account. This is true, for example, for BigQuery and anything that has to do with Maps. This is NOT true, for example, for Drive or Sheets or Gmail. If your target API requires a billing account, that obviously raises the stakes for how you manage any API keys, OAuth clients, or service account tokens. Plan accordingly. If you're new to Google Cloud Platform, you'll get to enjoy [GCP Free Tier](https://cloud.google.com/free/). At the time of writing, this means you get a $300 credit and no additional billing will happen without your express consent. So there is a low-stress way to experiment with APIs, with a billing account enabled, without putting actual money on the line. ## API Key Some APIs accept requests to read public resources, in which case the request can be sent with an API key in lieu of a token. If this is possible, it's a good idea to expose this workflow in a wrapper package, because then your users can decide to go into a "de-authed" mode. When using the package in a non-interactive or indirect fashion (e.g. a scheduled job on a remote server or via Shiny), it is wonderful to NOT have to manage a token, if the work can be done with an API key instead. *Some APIs aren't really usable without a token, in which case an API key may not be relevant and you can ignore this section.* * From the GCP Console, in the target GCP Project, go to *APIs & Services > Credentials*. * Do *Create credentials > API key*. * You can capture the new API key via clipboard right away or close this pop-up and copy it later from the Credentials page. * In any case, I suggest you take the opportunity to edit the API key from the Credentials page and give it a nickname. Package maintainers might want to build an API key in as a fallback, possibly taking some measures to obfuscate the key and limit its use to your package. ### What does a user do with an API key? Package users could register an API key for use with a wrapper package. For example, in googlesheets4, one would use `googlesheets4::gs4_auth_configure()` to store a key for use in downstream requests, i.e. after a call to `googlesheets4::gs4_deauth()`: ```{r eval = FALSE} library(googlesheets4) gs4_auth_configure(api_key = "YOUR_API_KEY_GOES_HERE") gs4_deauth() # now you can read public resources, such as official example Sheets, # without any need for auth gs4_example("gapminder") %>% read_sheet() ``` ## OAuth client ID and secret Most APIs are used to create and modify resources on behalf of the user and these requests must include the user's token. A regular user will generally need to send an OAuth2 token, which is obtained under the auspices of an OAuth "app" or "client". This is called three-legged OAuth, where the 3 legs are the client, the user, and Google. The basic steps are described in the [Prerequisites section](https://developers.google.com/identity/protocols/oauth2/native-app) for doing Google OAuth 2.0 for Mobile & Desktop Apps: * From the GCP Console, in the target GCP Project, go to *APIs & Services > Credentials*. * Do *Create credentials > OAuth client ID*. * Select Application type, either "Desktop app" (the most common type used with gargle) or "Web application" (useful for the pseudo-OOB flow). * You can capture the client ID and secret via clipboard at this point or, as we recommend, download the full information as JSON. We recommend using the JSON, as this conveys the client type (desktop vs. web) and any redirect URIs (important for the web type). * At any time, you can navigate to a particular client ID and click "Download JSON". Two ways to package this info for use with gargle: 1. Use `gargle::gargle_oauth_client_from_json()`. This is the preferred workflow, because the JSON conveys the client type (desktop vs. web) and any redirect URIs (important for the web type). - Provide the path to the downloaded JSON file. 1. Use `gargle::gargle_oauth_client()`. In both cases, I suggest you devise a nickname for each OAuth credential and use it as the credential's name in GCP Console and as the `name` argument to `gargle::gargle_oauth_client_from_json()` or `gargle::gargle_oauth_client()`. Package maintainers might want to build this client in as a fallback, possibly taking some measures to obfuscate the client ID and secret and limit its use to your package. * Note that three-legged OAuth always requires the involvement of a user, so the word "secret" here can be somewhat confusing. It is not a secret in the same sense as a password or token. But you probably still want to store it in an opaque way, so that someone else cannot easily "borrow" it and present an OAuth consent screen that impersonates your package. ### What does a user do with an OAuth client? Package users could register their own client for use with a wrapper package. For example, in googledrive, one would use `googledrive::drive_auth_configure()` to do this: ```{r, eval = FALSE} library(googledrive) google_client <- gargle::gargle_oauth_client_from_json( path = "/path/to/the/JSON/that/was/downloaded/from/gcp/console.json", name = "acme-corp-google-client" ) drive_auth_configure(app = google_client) # now any new OAuth tokens are obtained with the configured client ``` ## Service account token For a long time, the recommended way to make authorized requests to an API in a non-interactive context was to use a service account token. As of April 2021, an alternative exists -- workload identity federation -- which is available to applications running on specific non-Google Cloud platforms, such as AWS. If you **can** use workload identity federation, you probably should (see the next section). But for those who can't, here we outline the use of a conventional service account. An official overview of service accounts is given in this [official documentation by Google](https://cloud.google.com/iam/docs/service-accounts?_ga=2.215917847.-1040593195.1558621244). But note that it's not necessary to understand all of that in order to use a service account token. * From the GCP Console, in the target GCP Project, go to *IAM & Admin > Service accounts*. * Give it a decent name and description. - For example, the service account used to create the googledrive docs has name "googledrive-docs" and description "Used when generating googledrive documentation". * Service account permissions. Whether you need to do anything here depends on the API(s) you are targetting. You can also modify roles later and iteratively sort this out. - For example, the service account used to create the googledrive docs does not have any explicit roles. - The service account used to test bigrquery has roles BigQuery Admin and Storage Admin. * Grant users access to this service account? So far, I have not done this, so feel free to do nothing here. Or if you know this is useful to you, then by all means do so. * Do *Create key* and download as JSON. This file is what we mean when we talk about a "service account token" in the documentation of gargle and packages that use gargle. `gargle::credentials_service_account()` expects the `path` to this file. * Appreciate that this JSON file holds sensitive information. Treat it like a username & password combo! This file holds credentials that potentially have a lot of power and that don't expire. * Consider storing this file in such a way that it will be automatically discovered by the Application Default Credentials search strategy. See `credentials_app_default()` for details. * You will notice the downloaded JSON file has an awful name, so sometimes I create a symlink that uses the service account's name, to make it easier to tell what this file is. * Remember to grant this service account the necessary permissions on any resources you plan to access, e.g., read or write permission on a specific Google Sheet. The service account has no formal relationship to you as a Google user and won't automatically inherit permissions. Authors of wrapper packages can use the symmetric encryption strategy described in [Managing tokens securely](https://gargle.r-lib.org/articles/articles/managing-tokens-securely.html) to use this token on remote servers, such as continuous integration services like GitHub Actions. ### What does a user do with a service account token? You could provide the token's filepath to a wrapper package's main auth function, e.g.: ```{r eval = FALSE} # googledrive drive_auth(path = "/path/to/your/service-account-token.json") ``` Alternatively, you could put the token somewhere (or store its location in an environment variable) so that it is auto-discovered by the [Application Default Credentials](https://gargle.r-lib.org/articles/how-gargle-gets-tokens.html#credentials_app_default) search strategy. ## Workload identity federation Workload identity federation is a new (as of April 2021) keyless authentication mechanism that allows applications running on a non-Google Cloud platform, such as AWS, to access Google Cloud resources without using a conventional service account token. This eliminates the dilemma of how to safely manage service account credential files. Unlike service accounts, the configuration file for workload identity federation contains no secrets. Instead, it holds non-sensitive metadata. The external application obtains the needed sensitive data "on-the-fly" from the running instance. The combined data is then used for a token exchange that ultimately yields a short-lived GCP access token. This access token allows the external application to impersonate a service account and inherit the permissions of the service account to access GCP resources. So what's not to love? Well, first, this auth flow is only available if your code is running on AWS, Azure, or another platform that supports the OpenID Connect protocol. Second, there's a non-trivial amount of pre-configuration necessary on both ends. But once that is done, you can download a configuration file that makes auth work automagically with gargle. This feature is still experimental in gargle and **currently only supports AWS**. For more, see the documentation for `credentials_external_account()`. Like conventional service account tokens, workload identity federation is a great fit for the Application Default Credentials strategy for discovering credentials. See `credentials_app_default()` for more about that. These two links provide, respectively, a high-level overview and step-by-step instructions for this flow: * Blog post: [Keyless API authentication — Better cloud security through workload identity federation, no service account keys necessary](https://cloud.google.com/blog/products/identity-security/enable-keyless-access-to-gcp-with-workload-identity-federation/) * Documentation: [Configuring workload identity federation](https://cloud.google.com/iam/docs/configuring-workload-identity-federation) ## Further reading Learn more in Google's documentation: * Credentials, access, security, and identity: `https://support.google.com/googleapi/answer/6158857?hl=en&ref_topic=7013279` * [Using OAuth 2.0 for Installed Applications](https://developers.google.com/identity/protocols/oauth2/native-app) gargle/vignettes/gargle-auth-in-client-package.Rmd0000644000176200001440000004371714433520365021661 0ustar liggesusers--- title: "How to use gargle for auth in a client package" output: rmarkdown::html_vignette vignette: > %\VignetteIndexEntry{How to use gargle for auth in a client package} %\VignetteEngine{knitr::rmarkdown} %\VignetteEncoding{UTF-8} --- ```{r, include = FALSE} knitr::opts_chunk$set( collapse = TRUE, comment = "#>" ) ``` gargle provides common infrastructure for use with Google APIs. This vignette describes one possible design for using gargle to deal with auth, in a client package that provides a high-level wrapper for a specific API. There are frequent references to [googledrive](https://googledrive.tidyverse.org), which uses the design described here, along with [bigrquery](https://bigrquery.r-dbi.org) (v1.2.0 and higher), [gmailr](https://gmailr.r-lib.org) (v1.0.0 and higher), and [googlesheets4](https://googlesheets4.tidyverse.org) (the successor to [googlesheets](https://github.com/jennybc/googlesheets)). ## Key choices Getting a token requires several pieces of information and there are stark differences in how much users (need to) know or control about this process. Let's review them, with an eye towards identifying the responsibilities of the package author versus the user. * Overall config: OAuth client and API key. Who provides? * Token-level properties: Google identity (email) and scopes. * Request-level: Who manages tokens and injects them into requests? ### User-facing auth In googledrive, the main user-facing auth function is `googledrive::drive_auth()`. Here is its definition (at least approximately, remember this is static code): ```{r, eval = FALSE} # googledrive:: drive_auth <- function(email = gargle::gargle_oauth_email(), path = NULL, scopes = "https://www.googleapis.com/auth/drive", cache = gargle::gargle_oauth_cache(), use_oob = gargle::gargle_oob_default(), token = NULL) { # this catches a common error, where the user passes JSON for an OAuth client # to the `path` argument, which only expects a service account token gargle::check_is_service_account(path, hint = "drive_auth_configure") cred <- gargle::token_fetch( scopes = scopes, client = drive_oauth_client() %||% , email = email, path = path, package = "googledrive", cache = cache, use_oob = use_oob, token = token ) if (!inherits(cred, "Token2.0")) { # throw an informative error here } .auth$set_cred(cred) .auth$set_auth_active(TRUE) invisible() } ``` `drive_auth()` is called automatically upon the first need of a token and that can lead to user interaction, but does not necessarily do so. `drive_auth()` can be called explicitly by the user, but usually that is not necessary. `token_fetch()` is described in the `vignette("how-gargle-gets-tokens")`. The internal `.auth` object maintains googledrive's auth state and is explained next. ### Auth state A client package can use an internal object of class `gargle::AuthState` to hold the auth state. In googledrive, the main auth file defines a placeholder `.auth` object: ```{r eval = FALSE} .auth <- NULL ``` The actual initialization happens in `.onLoad()`: ```{r} .onLoad <- function(libname, pkgname) { utils::assignInMyNamespace( ".auth", gargle::init_AuthState(package = "googledrive", auth_active = TRUE) ) # other stuff } ``` The initialization of `.auth` is done this way to ensure that we get an instance of the `AuthState` class using the current, installed version of gargle (vs. the ambient version from whenever gargle was built, perhaps by CRAN). An `AuthState` instance has other fields which, in this googledrive example, are not set at this point. The OAuth `client` and `api_key` are configurable by the user and, when `NULL`, downstream functions can fall back to internal credentials. The `cred` field is populated by the first call to `drive_auth()` (direct or indirectly via `drive_token()`). ### OAuth client Most users should present OAuth user credentials to Google APIs. However, most users would love to be spared the fiddly details surrounding this. The OAuth client is one example. (Historically, following the lead of the httr package, we have used the term OAuth *app*, but we now use the term OAuth *client*.) The client is a component that most users do not even know about and they are content to use the same client for all work through a wrapper package: possibly, the client built into the package. There is a field in the `.auth` auth state to hold the OAuth `client`. Exported auth helpers, `drive_oauth_client()` and `drive_auth_configure()`, retrieve and modify the current client to support users who want to (or must) take that level of control. ```{r, eval = FALSE} library(googledrive) # first: download the OAuth client as a JSON file drive_auth_configure( path = "/path/to/the/JSON/that/was/downloaded/from/gcp/console.json" ) drive_oauth_client() #> #> name: acme-corp-google-client #> id: 123456789.apps.googleusercontent.com #> secret: #> type: installed #> redirect_uris: http://localhost ``` Do not "borrow" an OAuth client ID and secret from gargle or any other package; always use credentials associated with your package or provided by your user. Per the Google User Data Policy , your application must accurately represent itself when authenticating to Google API services. Some APIs and scopes are considered so sensitive that is essentially impossible for a package to provide a built-in OAuth client. Users **must** get and configure their own client. Among the packages mentioned as examples, this is true of gmailr. ### API key Some Google APIs can be used in an unauthenticated state, if and only if requests include an API key. For example, this is a great way to read a Google Sheet that is world-readable or readable by "anyone with a link" from a Shiny app, thereby designing away the need to manage user credentials on the server. The user can provide their own API key via `drive_auth_configure(api_key =)` and retrieve that value with `drive_api_key()`, just as with the OAuth client. The API key is stored in the `api_key` field of the `.auth` auth state. ```{r, eval = FALSE} library(googledrive) drive_auth_configure(api_key = "123456789") drive_api_key() #> "123456789" ``` Many users aren't motivated to take this level of control and appreciate when a package provides a built-in default API key. As with the client, packages should obtain their own API key and not borrow the gargle or tidyverse key. Some APIs are not usable without a token, in which case a wrapper package may not even expose functionality for managing an API key. Among the packages mentioned as examples, this is true of bigrquery. ### Email or Google identity In contrast to the OAuth client and API key, every user must express which identity they wish to present to the API. This is a familiar concept and users expect to specify this. Since users may have more than one Google account, it's quite likely that they will want to switch between accounts, even within a single R session, or that they might want to explicitly declare the identity to be used in a specific script or app. That explains why `drive_auth()` has the optional `email` argument that lets users proactively specify their identity. `drive_auth()` is usually called indirectly upon first need, but a user can also call it proactively in order to specify their target `email`: ```{r eval = FALSE} # googledrive:: drive_auth(email = "janedoe_work@gmail.com") ``` If `email` is not given, gargle also checks for an option named `"gargle_oauth_email"`. The `email` is used to look up tokens in the cache and, if no suitable token is found, it is used to pre-configure the OAuth chooser in the browser. Read more in the help for `gargle::gargle_oauth_email()`. ### Scopes Most users have no concept of scopes. They just know they want to work with, e.g., Google Drive or Google Sheets. A client package can usually pick sensible default scopes, that will support what most users want to do. Here's a reminder of the signature of `googledrive::drive_auth()`: ```{r, eval = FALSE} # googledrive:: drive_auth <- function(email = gargle::gargle_oauth_email(), path = NULL, scopes = "https://www.googleapis.com/auth/drive", cache = gargle::gargle_oauth_cache(), use_oob = gargle::gargle_oob_default(), token = NULL) { ... } ``` googledrive ships with a default scope, but a motivated user could call `drive_auth()` preemptively at the start of the session and request different scopes. For example, if they intend to only read data and want to guard against inadvertent file modification, they might opt for the `drive.readonly` scope. ```{r, eval = FALSE} # googledrive:: drive_auth(scopes = "https://www.googleapis.com/auth/drive.readonly") ``` ### OAuth cache and Out-of-bound auth The location of the token cache and whether to prefer out-of-bound auth are two aspects of OAuth where most users are content to go along with sensible default behavior. For those who want to exert control, that can be done in direct calls to `drive_auth()` or by configuring an option. Read the help for `gargle::gargle_oauth_cache()` and `gargle::gargle_oob_default()` and `vignette("auth-from-web")` for more. ## Overview of mechanics Here's a concrete outline of how one could set up a client package to get its auth functionality from gargle. 1. Add gargle to your package's `Imports`. 1. Create a file `R/YOURPKG_auth.R`. 1. Create an internal `gargle::AuthClass` object to hold auth state. Follow the googledrive example above. 1. Define standard functions for the auth interface between gargle and your package; do this in `R/YOURPKG_auth.R`. Examples: [`tidyverse/googledrive/R/drive_auth.R`](https://github.com/tidyverse/googledrive/blob/main/R/drive_auth.R) and [`r-dbi/bigrquery/R/bq_auth.R`](https://github.com/r-dbi/bigrquery/blob/main/R/bq-auth.R). 1. Use gargle's roxygen helpers to create the docs for your auth functions. This relieves you from writing docs and you inherit standard wording. See previously cited examples for inspiration. 1. Use the functions `YOURPKG_token()` and `YOURPKG_api_key()` (defined in the standard auth interface) to insert a token or API key in your package's requests. ## Getting that first token I focus on early use, by the naive user, with the OAuth flow. When the user first calls a high-level googledrive function such as `drive_find()`, a Drive request is ultimately generated with a call to `googledrive::request_generate()`. Here is its definition, at least approximately: ```{r eval = FALSE} # googledrive:: request_generate <- function(endpoint = character(), params = list(), key = NULL, token = drive_token()) { ept <- drive_endpoint(endpoint) if (is.null(ept)) { # throw error about unrecognized endpoint } ## modifications specific to googledrive package params$key <- key %||% params$key %||% drive_api_key() %||% if (!is.null(ept$parameters$supportsAllDrives)) { params$supportsAllDrives <- TRUE } req <- gargle::request_develop(endpoint = ept, params = params) gargle::request_build( path = req$path, method = req$method, params = req$params, body = req$body, token = token ) } ``` `googledrive::request_generate()` is a thin wrapper around `gargle::request_develop()` and `gargle::request_build()` that only implements details specific to googledrive, before delegating to more general functions in gargle. The `vignette("request-helper-functions")` documents these gargle functions. `googledrive::request_generate()` gets a token with `drive_token()`, which is defined like so: ```{r eval = FALSE} # googledrive:: drive_token <- function() { if (isFALSE(.auth$auth_active)) { return(NULL) } if (!drive_has_token()) { drive_auth() } httr::config(token = .auth$cred) } ``` where `drive_has_token()` in a helper defined as: ```{r eval = FALSE} # googledrive:: drive_has_token <- function() { inherits(.auth$cred, "Token2.0") } ``` By default, auth is active, and, for a fresh start, we won't have a token stashed in `.auth` yet. So this will result in a call to `drive_auth()` to obtain a credential, which is then cached in `.auth$cred` for the remainder of the session. All subsequent calls to `drive_token()` will just spit back this token. Above, we discussed scenarios where an advanced user might call `drive_auth()` proactively, with non-default arguments, possibly even loading a service token or using alternative flows, like an external account. Any token loaded in that way is stashed in `.auth$cred` and will be returned by subsequent calls to `drive_token()`. Multiple gargle-using packages can use a shared token by obtaining a suitably scoped token with one package, then registering that token with the other packages. For example, the default scope requested by googledrive is also sufficient for operations available in googlesheets4. You could use a shared token like so: ```{r eval = FALSE} library(googledrive) library(googlesheets4) drive_auth(email = "jane_doe@example.com") # gets a suitably scoped token # and stashes for googledrive use gs4_auth(token = drive_token()) # registers token with googlesheets4 # now work with both packages freely ... ``` It is important to make sure that the token-requesting package (googledrive, above) is using an OAuth client for which all the necessary APIs and scopes are enabled. ## Auth interface The exported functions like `drive_auth()`, `drive_token()`, etc. constitute the auth interface between googledrive and gargle and are centralized in [`tidyverse/googledrive/R/drive_auth.R`](https://github.com/tidyverse/googledrive/blob/main/R/drive_auth.R). That is a good template for how to use gargle to manage auth in a client package. In addition, the docs for these gargle-backed functions are generated automatically from standard information maintained in the gargle package. * `drive_token()` retrieves the current credential, in a form that is ready for inclusion in HTTP requests. If `auth_active` is `TRUE` and `cred` is `NULL`, `drive_auth()` is called to obtain a credential. If `auth_active` is `FALSE`, `NULL` is returned; client packages should be designed to fall back to including an API key in affected HTTP requests, if sensible for the API. * `drive_auth()` ensures we are dealing with an authenticated user and have a credential on hand with which to place authorized requests. Sets `auth_active` to `TRUE`. Can be called directly, but `drive_token()` will also call it as needed. * `drive_deauth()` clears the current token. It might also toggle `auth_active`, depending on the features of the target API. See below. * `drive_oauth_client()` returns `.auth$client`. * `drive_api_key()` returns `.auth$api_key`. * `drive_auth_configure()` can be used to configure auth. This is how an advanced user would enter their own OAuth client and API key into the auth config, in order to affect all subsequent requests. * `drive_user()` reports some information about the user associated with the current token. The Drive API offers an actual endpoint for this, which is not true for most Google APIs. Therefore the analogous function in bigrquery, `bq_user()` is a better general reference. ## De-auth APIs split into two classes: those that can be used, at least partially, without a token and those that cannot. If an API is usable without a token -- which is true for the Drive API -- such requests must include an API key. Therefore, the auth design for a client package is different for these two types of APIs. For an API that can be used without a token: `drive_deauth()` can be used at any time to enter a de-authorized state. It sets `auth_active` to `FALSE` and `.auth$cred` to `NULL`. In this state, requests are sent out with an API key and no token. This is a great way to eliminate any friction re: auth if there's no need for it, i.e. if all requests are for resources that are world readable or available to anyone who knows how to ask for it, such as files shared via "Anyone with the link". The de-authorized state is especially useful in non-interactive settings or where user interaction is indirect, such as via Shiny. For an API that cannot be used without a token: BigQuery is an example of such an API. `bq_deauth()` just clears the current token, so that the auth flow starts over the next time a token is needed. ## Bring Your Own Client and Key Advanced users can use their own OAuth client and API key. `drive_auth_configure()` lives in `R/drive_auth.R` and it provides the ability to modify the current `client` and `api_key`. Recall that `drive_oauth_client()` and `drive_api_key()` also exist for targeted, read-only access. The `vignette("get-api-credentials")` describes how to get an API key and OAuth client. Packages that always send a token will omit the API key functionality here. ## Changing identities (and more) One reason for a user to call `drive_auth()` directly and proactively is to switch from one Google identity to another or to make sure they are presenting themselves with a specific identity. `drive_auth()` accepts an `email` argument, which is honored when gargle determines if there is already a suitable token on hand. Here is a sketch of how a user could switch identities during a session, possibly non-interactive: ```{r eval = FALSE} library(googledrive) drive_auth(email = "janedoe_work@gmail.com") # do stuff with Google Drive here, with Jane Doe's "work" account drive_auth(email = "janedoe_personal@gmail.com") # do other stuff with Google Drive here, with Jane Doe's "personal" account drive_auth(path = "/path/to/a/service-account.json") # do other stuff with Google Drive here, using a service account ``` gargle/vignettes/how-gargle-gets-tokens.Rmd0000644000176200001440000005021714431310014020460 0ustar liggesusers--- title: "How gargle gets tokens" output: rmarkdown::html_vignette vignette: > %\VignetteIndexEntry{How gargle gets tokens} %\VignetteEngine{knitr::rmarkdown} %\VignetteEncoding{UTF-8} --- ```{r, include = FALSE} knitr::opts_chunk$set( collapse = TRUE, comment = "#>" ) ``` This vignette explains the purpose and usage of `token_fetch()` and the functions it subsequently calls. The goal of `token_fetch()` is to secure a token for use in downstream requests. The target audience is someone who works directly with a Google API. These people roughly fall into two camps: * The author of an R package that wraps a Google API. * The useR who is writing a script or app, without using such a wrapper, either because the wrapper does not exist or there's a reason to avoid the dependency. This vignette might also be useful to the user of a wrapper package who needs to influence the operations of `token_fetch()`, e.g. by telling it to try auth methods in a non-default order or to not try certain methods at all. `token_fetch()` is aimed at whoever is going to manage the returned token, e.g., incorporate it into downstream requests. It can be very nice for users if wrapper packages assume this responsibility, as opposed to requiring users to explicitly acquire and manage their tokens. We give a few design suggestions here and cover this in more depth in `vignette("gargle-auth-in-client-package")`. ```{r setup} library(gargle) ``` ## `token_fetch()` `token_fetch()` is a rather magical function for getting a token. The goal is to make auth relatively painless for users, while allowing developers and power users to take control when and if they need to. Most users will presumably interact with `token_fetch()` only in an indirect way, mediated through an API wrapper package. That is not because the interface of `token_fetch()` is unfriendly -- it's very flexible! The objective of `token_fetch()` is to allow package developers to take responsibility for *managing* the user's token, without having to implement all the different ways of *obtaining* that token in the first place. The signature of `token_fetch()` is very simple and, therefore, not very informative: ```{r, eval = FALSE} token_fetch(scopes, ...) ``` Under the hood, `token_fetch()` calls a sequence of much more specific credential functions, each wrapped in a `tryCatch()` and returning `NULL` if unsuccessful. The only formal argument these functions have in common is `scopes`, with the rest being passed via `...`. This gives a sense of the credential functions and reflects the order in which they are called: ```{r} writeLines(names(cred_funs_list())) ``` It is possible to manipulate this registry of functions. The help for `cred_funs_list()` is a good place to learn more and we present a concrete example in the last section of this vignette. For now, however, we assume you're working with the default registry that ships with gargle. Note also that these credential functions are exported and can be called directly. ## Get verbose output To see more information about what gargle is up to, set the `"gargle_verbosity"` option to "debug". Read more in the docs for `gargle_verbosity()`. ## `credentials_byo_oauth2()` The first function tried is `credentials_byo_oauth2()`. Here's how a call to `token_fetch()` might work: ```{r, eval = FALSE} token_fetch(token = ) credentials_byo_oauth2( token = ) ``` `credentials_byo_oauth2()` provides a back door for a "bring your own token" workflow. This function accounts for the scenario where an OAuth token has been obtained through external means and it's convenient to be able to put it into force. `credentials_byo_oauth2()` checks that `token` is of class `httr::Token2.0` and that it appears to be associated with Google. A `token` of class `request` is also acceptable, in which case the `auth_token` component is extracted and treated as the input. This is how a `Token2.0` object would present, if processed with `httr::config()`, as functions like `googledrive::drive_token()` and `bigrquery::bq_token()` do. If `token` is not provided or if it doesn't satisfy these requirements, we fail and `token_fetch()`'s execution moves on to the next function in the registry. ## `credentials_service_account()` The next function tried is `credentials_service_account()`. Here's how a call to `token_fetch()` with service account inputs plays out: ```{r, eval = FALSE} token_fetch(scopes = , path = "/path/to/your/service-account.json") # credentials_byo_oauth2() fails because no `token`, # which leads to this call: credentials_service_account( scopes = , path = "/path/to/your/service-account.json" ) ``` The `scopes` are often provided by the API wrapper function that is mediating the calls to `token_fetch()` and `credential_service_account()`. The `path` argument is presumably coming from the user. It is treated as a JSON representation of service account credentials, in any form that is acceptable to `jsonlite::fromJSON()`. In the above example, that is a file path, but it could also be a JSON string. If there is no named `path` argument or if it can't be parsed as a service account credential, we fail and `token_fetch()`'s execution moves on to the next function in the registry. Here is some Google documentation about service accounts: * [Cloud Identity and Access Management > Understanding service accounts](https://cloud.google.com/iam/docs/understanding-service-accounts) For R users, a service account is a great option for credentials that will be used in a script or application running remotely or in an unattended fashion. In particular, this is a better approach than trying to move OAuth2 credentials from one machine to another. For example, a service account is the preferred method of auth when testing and documenting a package on a continuous integration service. The JSON key file must be managed securely. In particular, it should not be kept in, e.g., a GitHub repository (unless it is encrypted). The encryption strategy used by gargle and other packages is described in the article [Managing tokens securely](https://gargle.r-lib.org/articles/articles/managing-tokens-securely.html). Note that fetching a token for a service account requires a reasonably accurate system clock. This is of particular importance for users running gargle inside a Docker container, as Docker for Windows has [intermittently seen problems with clock drift](https://github.com/docker/for-win/issues/4526). If your service account token requests fail with "Bad Request" inside a container, but succeed locally, check that the container's system clock is accurate. ## `credentials_external_account()` The next function tried is `credentials_external_account()`. Here's how a call to `token_fetch()` with an external account inputs plays out: ```{r, eval = FALSE} token_fetch(scopes = , path = "/path/to/your/external-account.json") # credentials_byo_oauth2() fails because no `token`, # credentials_service_account() fails because the JSON provided via # `path` is not of type "service_account", # which leads to this call: credentials_external_account( scopes = , path = "/path/to/your/external-account.json" ) ``` `credentials_external_account()` implements something called *workload identity federation* and is available to applications running on specific non-Google Cloud platforms. At the time of writing, gargle only supports AWS, but this could be expanded to other providers, such as Azure, if there is a documented need. Similar to `credentials_service_account()`, the `path` is treated as a JSON representation of the account's configuration and it's probably a file path. However, in contrast to `credentials_service_account()`, this JSON only contains non-sensitive metadata, which is, indeed, the main point of this flow. The secrets needed to complete auth are obtained "on-the-fly" from, e.g., the running EC2 instance. `credentials_external_account()` will fail for many reasons: there is no named `path` argument, the JSON at `path` can't be parsed as configuration for an external AWS account, we don't appear to running on AWS, suggested packages for AWS functionality are not installed, or the workload identity pool is misconfigured. If any of that happens, we fail and `token_fetch()`'s execution moves on to the next function in the registry. Here is some Google documentation about workload identity federation and the specifics for AWS: * Blog post: [Keyless API authentication — Better cloud security through workload identity federation, no service account keys necessary](https://cloud.google.com/blog/products/identity-security/enable-keyless-access-to-gcp-with-workload-identity-federation/) * Documentation: [Configuring workload identity federation](https://cloud.google.com/iam/docs/configuring-workload-identity-federation) ## `credentials_app_default()` The next function tried is `credentials_app_default()`. Here's how a call to `token_fetch()` might work: ```{r, eval = FALSE} token_fetch(scopes = ) # credentials_byo_oauth2() fails because no `token`, # credentials_service_account() fails because no `path`, # credentials_external_account() fails because no `path`, # which leads to this call: credentials_app_default( scopes = ) ``` `credentials_app_default()` loads credentials from a file identified via a search strategy known as [Application Default Credentials (ADC)](https://cloud.google.com/docs/authentication#adc). The credentials themselves are conventional service account, external account, or user credentials that happen to be stored in a pre-ordained location and format. The hope is to make auth "just work" for someone working on Google-provided infrastructure or who has used Google tooling to get started, such as the [`gcloud` command line tool](https://cloud.google.com/sdk/gcloud). A sequence of paths is consulted, which we describe here, with some abuse of notation. ALL_CAPS represents the value of an environment variable. ```{r, eval = FALSE} ${GOOGLE_APPLICATION_CREDENTIALS} ${CLOUDSDK_CONFIG}/application_default_credentials.json # on Windows: %APPDATA%\gcloud\application_default_credentials.json %SystemDrive%\gcloud\application_default_credentials.json C:\gcloud\application_default_credentials.json # on not-Windows: ~/.config/gcloud/application_default_credentials.json ``` If the above search successfully identifies a JSON file, it is parsed and ingested either as a service account token, an external account configuration, or an OAuth2 user credential. In the case of an OAuth2 credential, the requested `scopes` must also meet certain criteria. Note that this will NOT work for OAuth2 credentials initiated by gargle, which are stored on disk in `.rds` files. The storage of OAuth2 user credentials as JSON is unique to certain Google tools -- possibly just the [`gcloud` CLI](https://cloud.google.com/sdk/gcloud/reference/auth/application-default/login) -- and should probably be regarded as deprecated. It is recommended to use ADC with a service account or workload identity federation. If this quest is unsuccessful, we fail and `token_fetch()`'s execution moves on to the next function in the registry. The main takeaway lesson: * You can make auth "just work" by storing the JSON for a service account or an external account at one of the filepaths listed above. It will be automagically discovered when `token_fetch()` is called with only the `scopes` argument specified. Again, remember that the JSON key file for a conventional service account must be managed securely and should NOT live in a directory that syncs to the cloud. The JSON configuration for an external account is not actually sensitive and this is one of the benefits of this flow, but it's only available in a very narrow set of circumstances. ## `credentials_gce()` The next function tried is `credentials_gce()`. Here's how a call to `token_fetch()` might work: ```{r, eval = FALSE} token_fetch(scopes = ) # or perhaps token_fetch(scopes = , service_account = ) # credentials_byo_oauth2() fails because no `token`, # credentials_service_account() fails because no `path`, # credentials_external_account() fails because no `path`, # credentials_app_default() fails because no ADC found, # which leads to one of these calls: credentials_gce( scopes = , service_account = "default" ) # or credentials_gce( scopes = , service_account = ) ``` `credentials_gce()` retrieves service account credentials from a metadata service that is specific to virtual machine instances running on Google Cloud Engine (GCE). Basically, if you have to ask what this is about, this is not the auth method for you. Let us move on. If this seems to happening to you and it's not what you want, see the last section for how to remove this auth method. ## `credentials_user_oauth2()` The next and final function tried is `credentials_user_oauth2()`. Here's how a call to `token_fetch()` might work: ```{r, eval = FALSE} token_fetch(scopes = ) # credentials_byo_oauth2() fails because no `token`, # credentials_service_account() fails because no `path`, # credentials_external_account() fails because no `path`, # credentials_app_default() fails because no ADC found, # credentials_gce() fails because not on GCE, # which leads to this call: credentials_user_oauth2( scopes = , app = , package = "" ) ``` `credentials_user_oauth2()` is where the vast majority of users will end up. This is the function that choreographs the traditional "OAuth dance" in the browser. User credentials are cached locally, at the user level, by default. Therefore, after first use, there are scenarios in which gargle can determine unequivocally that it already has a suitable token on hand and can load (and possibly refresh) it, without additional user intervention. The `scopes`, `app` (likely to be renamed `client` in a future version of gargle), and `package` are generally provided by the API wrapper function that is mediating the calls to `token_fetch()`. Do not "borrow" an OAuth client from gargle or any other package; always use credentials associated with your package or provided by your user. Per the Google User Data Policy , your application must accurately represent itself when authenticating to Google API services. The wrapper package would presumably also declare itself as the package requesting a token (this is used in messages). So here's how a call to `token_fetch()` and `credentials_user_oauth2()` might look when initiated from `THINGY_auth()`, a function in the fictional thingyr wrapper package: ```{r, eval = FALSE} # user initiates auth or does something that triggers it indirectly THINGY_auth() # which then calls gargle::token_fetch( scopes = , app = thingy_app(), package = "thingyr" ) # which leads to this call: credentials_user_oauth2( scopes = , app = thingy_app(), package = "thingyr" ) ``` See `vignette("gargle-auth-in-client-package")` for design ideas for a function like `THINGY_auth()`. What happens tomorrow or next week? Do we make this user go through the browser dance again? How do we get to that happy place where we don't bug them constantly about auth? First, we define "suitable", i.e. what it means to find a matching token in the cache. `credentials_user_oauth2()` is a thin wrapper around `gargle2.0_token()` which is the constructor for the `gargle::Gargle2.0` class used to hold an OAuth2 token. And that call might look something like this (simplified for communication purposes): ```{r, eval = FALSE} gargle2.0_token( email = gargle_oauth_email(), app = thingy_app(), package = "thingyr", scope = , cache = gargle_oauth_cache() ) ``` gargle looks in the cache specified by `gargle_oauth_cache()` for a token that has these scopes, this client, and the Google identity specified by `email`. By default `email` is `NA`, so we might find one or more tokens that have the necessary scopes and client. In that case, gargle reveals the `email` associated with the matching token(s) and asks the user for explicit instructions about how to proceed. That looks something like this: ```{r, eval = FALSE} The thingyr package is requesting access to your Google account. Enter '1' to start a new auth process or select a pre-authorized account. 1: Send me to the browser for a new auth process. 2: janedoe_personal@gmail.com 3: janedoe@example.com 4: janedoe_work@gmail.com Selection: ``` If none of the tokens has the right scopes and client (or if the user declines to use a pre-existing token), we head to the browser to initiate OAuth2 flow *de novo*. A user can reduce the need for interaction by passing the target `email` to `thingy_auth()`: ```{r, eval = FALSE} thingy_auth(email = "janedoe_work@gmail.com") ``` or by specifying same in the `"gargle_oauth_email"` option. A value of `email = TRUE`, passed directly or via the option, is an alternative strategy: `TRUE` means that gargle is allowed to use a matching token whenever there is exactly one match. The elevated status of `email` for `gargle::Gargle2.0` tokens is motivated by the fact that many of us have multiple Google identities and need them to be very prominent when working with Google APIs. This is one of the main motivations for `gargle::Gargle2.0`, which extends `httr::Token2.0`. The `gargle::Gargle2.0` class also defaults to a user-level token cache, as opposed to project-level. An overview of the current OAuth cache is available via `gargle_oauth_cache()` and the output looks something like this: ```{r, eval = FALSE} gargle_oauth_sitrep() #> 14 tokens found in this gargle OAuth cache: #> ~/Library/Caches/gargle #' #' email app scopes hash... #' ----------------------------- ----------- ------------------------------ ---------- #' abcdefghijklm@gmail.com thingy ...bigquery, ...cloud-platform 128f9cc... #' buzzy@example.org gargle-demo 15acf95... #' stella@example.org gargle-demo ...drive 4281945... #' abcdefghijklm@gmail.com gargle-demo ...drive 48e7e76... #' abcdefghijklm@gmail.com tidyverse 69a7353... #' nopqr@ABCDEFG.com tidyverse ...spreadsheets.readonly 86a70b9... #' abcdefghijklm@gmail.com tidyverse ...drive d9443db... #' nopqr@HIJKLMN.com tidyverse ...drive d9443db... #' nopqr@ABCDEFG.com tidyverse ...drive d9443db... #' stuvwzyzabcd@gmail.com tidyverse ...drive d9443db... #' efghijklmnopqrtsuvw@gmail.com tidyverse ...drive d9443db... #' abcdefghijklm@gmail.com tidyverse ...drive.readonly ecd11fa... #' abcdefghijklm@gmail.com tidyverse ...bigquery, ...cloud-platform ece63f4... #' nopqr@ABCDEFG.com tidyverse ...spreadsheets f178dd8... ``` ## Manipulate the credential function registry Recall that you can get an overview of the credential functions that `token_fetch()` works through like so: ```{r} writeLines(names(cred_funs_list())) ``` Sometimes more than one of these auth methods "work", but only one of them actually "works" and, sadly, it's not the first one. In this case, gargle successfully gets a token, but then you experience token-related failure in downstream work. The most common example of this is someone who is working on Google Compute Engine (GCE), but they prefer to auth as a normal user, not as the default service account. Let's say you want to prevent `token_fetch()` from even trying one specific auth method, clearing the way for it to automagically use the method you want. You can remove a specific credential function from the registry. Here's how to do this for the scenario described above, where you want to skip GCE-specific auth: ```{r eval = FALSE} gargle::cred_funs_add(credentials_gce = NULL) ``` Learn more in the docs for `cred_funs_list()`. You can even make narrowly scoped changes to the registry with `local_cred_funs()` and `with_cred_funs()`. gargle/vignettes/troubleshooting.Rmd0000644000176200001440000002525214436207160017427 0ustar liggesusers--- title: "Troubleshooting gargle auth" output: rmarkdown::html_vignette vignette: > %\VignetteIndexEntry{Troubleshooting gargle auth} %\VignetteEngine{knitr::rmarkdown} %\VignetteEncoding{UTF-8} --- ```{r, include = FALSE} knitr::opts_chunk$set( collapse = TRUE, comment = "#>" ) ``` ```{r setup} library(gargle) ``` ## `"gargle_verbosity"` option There is a package-wide option that controls gargle's verbosity: `"gargle_verbosity"`. The function `gargle_verbosity()` reveals the current value: ```{r} gargle_verbosity() ``` It defaults to `"info"`, which is fairly quiet. This is because gargle is designed to try a bunch of auth methods (many of which will fail) and persist doggedly until one succeeds. If none succeeds, gargle tries to guide the user through auth or, in a non-interactive session, it throws an error. If you need to see all those gory details, set the `"gargle_verbosity"` option to `"debug"` and you'll get much more output as gargle works through various auth approaches. It is **normal** to see lots of errors, as gargle tries various auth methods in succession, most of which will often fail. ```{r} # save current value op <- options(gargle_verbosity = "debug") gargle_verbosity() # restore original value options(op) ``` Note there are also withr-style helpers: `with_gargle_verbosity()` and `local_gargle_verbosity()`. ```{r} gargle_verbosity() with_gargle_verbosity( "debug", gargle_verbosity() ) gargle_verbosity() f <- function() { local_gargle_verbosity("debug") gargle_verbosity() } f() gargle_verbosity() ``` ## `gargle_oauth_sitrep()` `gargle_oauth_sitrep()` provides an OAuth2 "situation report". `gargle_oauth_sitrep()` is only relevant to OAuth2 user tokens. If you are using (or struggling to use) a service account token, workload identity federation, Application Default Credentials, or credentials from the GCE metadata service, `gargle_oauth_sitrep()` isn't going to help you figure out what's going on. Here is indicative output of `gargle_oauth_sitrep()`, for someone who has accepted the default OAuth cache location and has played with several APIs via gargle-using packages. ```{r, eval = FALSE} gargle_oauth_sitrep() #' > 14 tokens found in this gargle OAuth cache: #' '~/Library/Caches/gargle' #' #' email app scope hash... #' ----------------------------- ----------- ------------------------------ ---------- #' abcdefghijklm@gmail.com thingy ...bigquery, ...cloud-platform 128f9cc... #' buzzy@example.org gargle-demo 15acf95... #' stella@example.org gargle-demo ...drive 4281945... #' abcdefghijklm@gmail.com gargle-demo ...drive 48e7e76... #' abcdefghijklm@gmail.com tidyverse 69a7353... #' nopqr@ABCDEFG.com tidyverse ...spreadsheets.readonly 86a70b9... #' abcdefghijklm@gmail.com tidyverse ...drive d9443db... #' nopqr@HIJKLMN.com tidyverse ...drive d9443db... #' nopqr@ABCDEFG.com tidyverse ...drive d9443db... #' stuvwzyzabcd@gmail.com tidyverse ...drive d9443db... #' efghijklmnopqrtsuvw@gmail.com tidyverse ...drive d9443db... #' abcdefghijklm@gmail.com tidyverse ...drive.readonly ecd11fa... #' abcdefghijklm@gmail.com tidyverse ...bigquery, ...cloud-platform ece63f4... #' nopqr@ABCDEFG.com tidyverse ...spreadsheets f178dd8... ``` It is relatively harmless to delete the folder serving as the OAuth cache. Or, if you have reason to believe one specific cached token is causing you pain, you could delete a specific token (an `.rds` file) from the cache. OAuth user tokens are meant to be perishable and replaceable. If you choose to delete your cache (or a specific token), here is the fallout you can expect: * You will need to re-auth (usually, meaning the browser dance) in projects that have been using the deleted tokens. * If you have `.R` or `.Rmd` files that you execute or render non-interactively, presumably with code such as `PKG_auth(email = "janedoe@example.com")`, those won't run non-interactively until you've obtained and cached a token for the package and that identity (email) interactively once. ## Why do good tokens go bad? Sometimes it feels like auth was working and then suddenly it stops working. If you've cached a token and used it successfully, why would it stop working? ### Too many tokens An existing token can go bad if you've created too many Google tokens, causing your oldest tokens to "fall off the edge". A specific Google user (email) can only have a certain number of OAuth tokens at a time (something like 50 per OAuth client). So, whenever you get a new token (as opposed to refreshing an existing token), there is the potential for it to invalidate an older token. This is unlikely to be an issue for a casual user, but it can absolutely become noticeable for someone who is developing against a Google API or someone working from many different machines / caches. Another reason that an existing token stops working is if it was obtained with an OAuth client that is in "testing" mode. Refresh tokens obtained that way only last for one week, whereas it's more typical for refresh tokens to last almost indefinitely (or, at least, for several months). ### Credential rolling Many users of packages like googlesheets4 or googledrive tacitly rely on the default OAuth client used by those packages. Periodically the maintainer of such a package will need to roll the client, i.e. create a new OAuth client and disable the old one. This will make it impossible to refresh existing tokens, made with the old, disabled client Those tokens will stop working. *In gargle v1.0.0, in March 2021, we rolled the client used in googlesheets4, googledrive, and bigrquery. We reserve the right to disable the old client at any time. Anyone relying on the default client will have to upgrade.* The solution is to update the package in question, e.g. googlesheets4: ```{r eval = FALSE} install.packages("googlesheets4") ``` **Restart R!** Resume your work. Chances are you'll be prompted to re-auth with the new client and you'll be back in business. What does this problem look like in the wild? With gargle versions up to v1.0.0, you will probably see this: ``` Auto-refreshing stale OAuth token. Error in get("refresh_oauth2.0", asNamespace("httr"))(self$endpoint, self$app, : Unauthorized (HTTP 401). ``` If you're trying to create a token, instead of refreshing one, you might see this in the browser, while R is waiting to receive input: ``` Google Authorization Error Error 401: deleted_client The OAuth client was deleted. ``` It might look something like this: ```{r, echo = FALSE, out.width = "400px"} #| fig-alt: > #| Screenshot with the following text: "Google", #| "Authorization Error", "Error 401: deleted_client", #| "The OAuth client was deleted." knitr::include_graphics("deleted_client.png") ``` As of gargle version v1.1.0, we're trying harder to recognize this specific problem and to provide a more detailed and actionable error message: ``` Auto-refreshing stale OAuth token. Error: Client error: (401) UNAUTHENTICATED * Request not authenticated due to missing, invalid, or expired OAuth token. * Request had invalid authentication credentials. Expected OAuth 2 access token, login cookie or other valid authentication credential. See https://developers.google.com/identity/sign-in/web/devconsole-project. Run `rlang::last_error()` to see where the error occurred. In addition: Warning message: Unable to refresh token, because the associated OAuth app has been deleted * You appear to be relying on the default app used by the googlesheets4 package * Consider re-installing googlesheets4 and gargle, in case the default app has been updated ``` ## How to avoid auth pain If you have rigged some remote mission critical thing (e.g. a Shiny app or cron job) to use a cached user OAuth token, one day, one of the problems described above will happen and your mission critical token will stop working. Your thing (e.g. the Shiny app or cron job) will mysteriously fail because the OAuth token can't be refreshed and a new token can't be obtained in a non-interactive setting. This is why cached user tokens are a poor fit for such applications. If you choose to use a cached user token anyway, be prepared to deal with this headache periodically. (You may not have much of a choice if you are using, for example, the gmailr package to work with the Gmail API, which has limited support for service accounts.) Consider using your own OAuth client to eliminate your exposure to a third-party deciding to roll their client. Be prepared to generate a fresh token interactively and upload it to the token cache consulted by your remote mission critical thing. Better yet, upgrade to a more robust strategy for [non-interactive auth](https://gargle.r-lib.org/articles/non-interactive-auth.html), such as a service account token. ## How to inspect the last response By default, `gargle::response_process()` stores the most recently processed response in an internal environment. You can access this response with the nonexported helper `gargle:::gargle_last_response()`. Prior to storage, a few parts of the response are redacted or deleted, such as the access token and the handle. These are either sensitive (the token) or useless (the handle) and they have more downside than upside for downstream debugging use. Here's an example of accessing the most recent response and writing it to file, which could be shared with someone else for debugging. The response is in this example has HTTP status 200, i.e. it is not an error. But this process works the same even if in the case of an error, e.g. an HTTP status >= 400. ```{r include = FALSE} # only run the chunk below in settings that are known to be safe, i.e. where # occasional, incidental failure is OK can_decrypt <- gargle::secret_has_key("GARGLE_KEY") ``` ```{r eval = can_decrypt, purl = can_decrypt} library(gargle) req <- request_build( method = "GET", path = "webfonts/v1/webfonts", params = list( sort = "popularity" ), key = gargle_api_key(), base_url = "https://www.googleapis.com" ) resp <- request_make(req) out <- response_process(resp) lr <- gargle:::gargle_last_response() tmp <- tempfile("gargle-last-response-") saveRDS(lr, tmp) # you could share this .rds file with a colleague or the gargle maintainer # how it would look to complete the round trip, i.e. load this on the other end rt_lr <- readRDS(tmp) all.equal(lr, rt_lr) # clean up unlink("tmp") ``` gargle/vignettes/invalid_request.png0000644000176200001440000037710114431310014017427 0ustar liggesusersPNG  IHDR˴ iCCPICC ProfileHTCBH %J]Y\ "eAWE\ kłņ}Au],ذ8wy79̗ܙ9g@g dLT K+'pH}A 0G Lm@ XU8Ga,fa|[/2y.nf7+X?QG|"|0 y*sX/6RD1Y7nK.h*и7n3 (hlGmX `f`UnU0Y 9BZ n )8܂ !BBDcC⇄ H<"R$Y,GJRFj_#)ҁE~5 š4&d`4 ]V5^=^Bo]st8%qq>P\.'-q5z\ wׅ{'x GEUJn|# ?J WGH%! ; g wD"M4%:t|*b$C$H$K;)$ 咊HH{I'HId%َON K=>EbLqRDy5UJ/eJ5Sԥ j=,RDiR~JJij4 -G[ME;IK{CM^z.}5~AlW)/VRnTAa3xF9 * EDGEHJJʠ*SV5T5Ku OHj&j~j"Bjz8!Ӈ)d.g`e,S*accWSb&l>;}}i'\^cXXA'Mf:&͇Zx- p9Z[jȚ6Q8x⁉Qm ۵/kt6yM-=ۯГ{Q8 ΀~~~~A2TCaaaထTFuF)\4mMLMbMV4<50֙>0y61iN4go1fZ8ZYTY\D-,%[,;&&LNiEY[Yu[CY7Yl49am8d찹ofd̶Ю=~}+KV;Lǩ+[89;ɝ7;wrY0*yb.]\s]f)); ܫݻ<8I?{ty{ yDbwq/=4i{4ϴIUz`2ef6dHդ3ٺs;d"Ylfȃ;s9͹,l8gC^wG~U91sU+{yż ~/ߺ@ y !.6\\wIKK3^Yft[ u C]rsۊm?ؾ~妕_EKlJK>O?}[}Ӛkkko\TgeⲷfmPPm#ucƮMFn\Vyʻa敛omkk6m%>,Nu@ucIMvOvhKN%;Lsm=kк{\oU_n(?5?d|haFq^@SZSWs|sǑ#-n-mQUԏ9N=^xۉ'e'_J=:7τi?|9sxm'λ?z‘܋M.5^v|NW6_s1un8wҭi:nG߾әuGt̻AC叴n{CSױnˏ#<#ϽOOj==geχ^f/偸WW^zf[aez_A܏mb? L\Ke}&#[hJ ]x5љzD#Gq 5 Jl3 P{{GfaQ PmlY!swuSpTI[N6eXIfMM*>F(iNxASCIIScreenshotG*QU pHYs%%IR$iTXtXML:com.adobe.xmp 1028 920 Screenshot ,iDOT(a7h@IDATxxo775t^`^#hbWbAPAib"]kAEC(!'|n )V@@@^D %?@rsMӼ|w!cERή+`ɦ-VSX-@\lHhHäߝ}(C@@Y9meMg['WmN`4 0wDIܑ@@@ 4ۀrM{'h'5TYfu=& te.c degˢEZ_۶m#)ٻD@AYL5 \Αƍ=Cڷo+{v9x?$rHppp.eeQZZ*lݺM:DG:mo|AW].:c뻣L6oUW$ڔMVҮ][ILH?-lQGʠ.۲R0@@]lp/59λ;&[.tw㹌Vʲ6=?&R-v?B9x9 Z'hl]o1c}~0w3  h6QI'KOH:{XE rʃmŵ6s7qԦ$ſve\byxHk2ao};dB6]&y@hjf0Zy߶*XnjzϿW^{Ӻ{[ok~u}N8N\z_P 95PZY ?: rIJLj"##Lh-f@!]Z,]/1oO߮i "멛dqڶVq5-5y[zIHh&-33S.[.z^v u߬l),,v%&&,mDʌATU\:4?'AAA^v Ziyt:*11|5zZ3z3N73O8V.j/? OYdc=V:`PhHtܱN}7{ | ~˝;u*~;v:^%`%ׄfY.gբ@@f0ҿ'_2ɫpXoٯZG(`wyq;D2~ @\=I;P{S6s3ucO=Yv$Sg:h@hN"`#+]MJ헛}۷*. w iw[mj55Mrǭ7J=+`sy7ϾTzu3T}@}5=ϚA_Tf;`jpХrބxVy\Ǧ֦ ?W8ɜYY?׳hmkFXe+z5^ҦMkyd̽&πycz%kWmZy=~UÆ2>fc2l{Ԉk}|Yl߯ϺtSKゆ6Sp "dVGv@ EK\ JAUk}cN|5}GO Zs>}:4wP{{u55k=?>u=駞d 5?ګ 4\ػZ|5ubMqu5%vZC>7l(yڽGr7{zٷc/oI^Rk0u9[]0 gfk`1=.w8L@01ٵ9<@DkWZmMXwhmg[OOiRO>/K.:_w)5y =L93uIgѿ5C3~U~w6`j`ZyYtp1fK /ʀ~A  mtv[]sBm,w3YA Qi:Ϟ󳦰Ǫ]Gk &'t筭СAݧf̞Sezp֥v7ZaZ)ܵQr䍷y[G[ 뎹}`ԫWJWc.9 j&;Z>SkK=㎑+)G}{ݣo5ͥ;늎T#ꢾO{Y$њDmTn\ۀ9us~.iuݧ|ggYXG@vf5o[ѹ![Sh͠6k= "{Inٲ=k ݻ!wrcs]?+ V9|Cuevڼgz\sZ߁C6S\xk]ԺYCŗ_K/fm׾:(NԄX=pN6e2ҐS/yL+/oy4lmx7~G4dN ֲ__Qcko?uR9ot8e5Cü>'<5Ѝ{4k}퍷~z1c?\3GbM=߫elO\@h"`5'S.?|=?KuaMTӧ#j .asE*/hSֻ!>WWf=Q|}<Äσ7|'9㴓fG#V٪lkKzp!^2`r)}mLvx̳8U{)_Ͽ6/AJXkM.5?;'"кVM5NT<=G_˻>tUT/c  6`uU%rֶ?| 4tT7]Yh~𯿽¦%Zš?u= =_=h}-u M}]6_c  ;4/x~p_4\jȬoCޮZ;{0Sj9_McEGfT0?6뇻旴6xhNSp>}yUC W^Nwߗw}`먭Of:1N_+eSf AgMgR=lݲE ?5P}It}i\SG$aR]xAot]>tnl>u?vM'Ґŵ?Mvdmy#%&:ګ eU2y=jLMiuxT94ۋk RtcoL=XWfm&#Vv_ܨ.}CV@@PYL}~CfN༽$r"\ju=Jn){/‚BYb+?5j@{.ƚfzv;ҺU+sq?o|O!`zknE.1nkҥ3VsGSsG79LH ֥1^_f]jS=u5aE[L~wm-JllɚV71ak}tk/jz=Gzgɷ9];w2}l5k]gm}Mu4@mڴ-[_f$g{p-CĘ{mx݋/  *LFߤdttu,:Bqd-;o;)`A_}PÜ 0kGuÆȡ`{NQ; @7i쀩Ѐ4ek\^ӢAIfZ\20*S󳲳GM*_L=YDߝڥs'_ن 2`ِMgӛ4o6 Xn kK{)}Dj܄ϳuטtpհϢMM ^}F#w\s&wYr[wR; k7snz"?j*]h:j0sd_ԸmOʢZj;tIfKO[(`~~~3L?5к>֫˯gbs~sDʨ郏>ujyj+'9fuU}wv_: r9g1GioY57b7՛软z;Cg;kdee=ʜjM˵il6P@@4ۀiM d_oW~jMj&ϟ)NNnlذjܲ&vSí.Æ^ ":M-,4:uȮXlZ9;S ro0AD%!!NS֛{2h?5#nP&jSmҭ-[J||\mN@@`h~4E hesZGiʋSh_4]ziGL|g*Ƴw}zMW9   u `ͫYL3 ֳb(s?u^M{s4U qOX%H:Rcr7< 9^n>@@vRpi 3j_.]:Y_Wv5pXͬ@OG*3hvژfi^hI=fA@t |:\?وft%k@@`5#tE 2):r69%O  4o95p~ <=   <   Lx @@|fC@@ _   3!O  /^@@@_'@@@BB   /@ w   ~!@@!@@@ `; @@@ `k   @ 0   _05P@@@ y@@@/~(   <   Lx @@|fC@@ _   3!O  /^@@@_'@@@BB   /@ w   ~!@@!@@@ `; @@@ `k   @ 0   _05P@@@ y@@@/~(   <   Lx @@|fC@@ _   3!O  /^@@@_'@@@BB   /@ w   ~!@@!@@@ `; @@@ `k   @ 0   _05P@@@ y@@@/~(   <   Lx @@|fC@@ _   3!O  /^@@@_'@@@BB   /@ w   ~!@@!@@@ `; @@@ `k   @ 0   _05P@@@ y@@@/~(   <   Lx @@|fC@@ _   3!O  /^@@@_'@2Yn|VN>di߾8?)!@@q@lye޼yR\\l=upp}rWJtt4A. @s `6@^3w}'999>%G}\{6" ;0w3 4ڵkߖ[vJd$1QwILLxryIJJ{;+ .M .Ma7l |̙3G rKPp,c8erQSR*/(4CCdžY!3ΐmt-  @ 0 R~@&(,,I&ɇ~({jܔ%={'_|y##o^(,*0R$Enmzt:ECM u_@Uor#4-[d̙KVV%vRe|u]$?Ud*,KJ/>$QCĹ4GZ 靈GsРAT~ 3%Fh5kX#~G^K<ٶ5,x8s$?MO0m)]Z/oJDT0YtiN[qt˙g):t "@ E1@_@j;cfo˕ibZ\ =L0W,vPaŶkJ;Iin'isYo߾ҲeKh0|"_Dh < ,ƍZeHHU*EsDƉE)]Hn#:L[jkڰhE8$si0!3BZ 4##F0oë܋  AhRT={|sZKqqmʖ Dz}))m4M۴ ~#s)ݶ{)珎P|j=48)ᒜ#n/:w1#77 0P @ZIRG7oo},YsYT}H;)f˴q:hq,uZ)^)eůUZDJ|No[dd}V?{vb@](@܅@i4Xj-XΝ;WБ`n1},sL˚c%T0N*_c}nf*Hw^{8vpoeMq96=sMu7@Ox\@駟zXjTsCLBYl/9SS)rLJߙ"e830r}y 6S$Ɋb+$jԦn3Mg״Ԧ:@˂  `7@U`o_|5 eslߞW]pI.gќgZҊ^$..F26ٔa¦ID#e%ŒV8_~h9&HZ9dqȲy%2 w?g||w3)*ʓ-$̩i/111r 'ȅ^h춳7 @ 0"46?`},uʑ|- K>[6gG՝A&x>mHk'4+@Ny>aЖdV222B:tl'-RKib@[M 9W?yV-fh>ASGթMt.M kB{ @c 0K"4˂6m5xOGb}iZK굜~jcAe8V:$&H$LGrY9L:EltwƔ1Z)]Skh8>t̕ҥ?ZAӺӌ,fhxޤunm4?G-C {3 4h0 @S KV?K^)m954}bLLCK/!%Ҍ[K_P8;'eKYv!2&VoFs.;*TPs<ʡ!e)N&a N 35UFƴLJJIv䐄HILj:-]t\vevD@QE@R@k-[fRپ-OL23B% ڽ{WYxY*f 2ɱ}L,e[I'))H^0N-/, 1A/>Y֚J|eSr A刈u WEȷKR)C %Qs-a77e%>&{[_5I]VIiu& #G̣iԭ[71c @C 0Z!40Mު[͠>VeԞ1RU$*dm$t)L8Ō0f~v_%CÂ&@ `6-WFh"5R&L)[E,sQ~,@vSXayI#L -miVMxHŜj`V]!ȑ򱅪}ϕPis힒6sĖDħH^nVFiF@ `6+WEhB^hqd~8"D)JY[1ڵks͚ 2䪓ER<Իj2k]p< |ZKWx"Έ 3Tj )H`3bmo3xyg|e~1?b!溥45 c  ̦P *P9`dwȢNݥh}%7 f2\յTr!CBq$)1F ,Y.=_$dh&E=EE4]**jvtzwLqI̬@8LGJd)ݲ2iqn{no+3c{}ss% sAd#pSm]ER\wu<3Zh64SE e  M&@l2jn XfizdRVaw SHq"ݯ\=1LMS#$T, k&Z&EVpdR^&'7YǺk`9M̴*E 6A2*!SĘj0.(cYۄO@ `63wAhDsn4C\ 5, t\O 3毫\m^fj3 o/jRӁ˒ԪݒrۙrՌl3zl94y׬ZWP@X\@0dȿ7Wmg|BB#=fWКJAmӦLi^!_gA%`@'@l~'Fv;EJTWSN=^ڵk-|z8ybd2jk2!`z̘hk`k3Su@ `62@hT0mdL⬧$==SnncI4ųl۶9$nM>QiK͈:X{1&(%%%Vm{Y=t]b$!!^6oܜQi8=DѪU ̑|mڴߘu}f.f_y.{G0M ' @0 4w*v̔K$% efПbϿkGkO5k7,<m?okk;W_.?t}ya엧ԩdĩ2sTO<#瞔 ϿJw q\xѹfg2Z|r^ZHʱb*xc=IbRbduwʊ}|TjFuJndƌu @c 0K"4gj)Ҷ˚묁n""Mz̚[4J+M5Ǭ!o}!/2M>KyRRZ*C\&\{hBIO?,=K&6QzFc⛲bjV=2y})hRRi{ ?X nV@HD@*C;9$I96ƀ9yY4~]^D+8ӔvҬgˮj+7m$ך@g/W SOj2՗ɣc6 s֧֔&&`^c`{qZw9"`ڊ|"4 @ Tb^kVz[2Tn"{mUW]*sl&}_y0uXmꚟ'۷3)o.v?{v6Ar;/rgYҹssTӔ}?JYT@IDATYI>c{ɔrQumj1C|0@ @_rG| CA{O{I_=ȏ}5{)B~=̳N|3l t})zer˭#g p)BCC̨C4-u(zEv9_~knګsLU,CKMfҪu)5ֹz(+nڴ4ݍ>m)s$N O&3E@& `62@h\πnV qHzN| Iҥ2b 3Vf[QWKnL~N[F FTC1at 8B~aA;.^pYԓ3dժuU^yU?3HJtJ'4}37{1qC4+> G\bL4C6w`*7g5re[oγ<  @0 4w&%n[i2**Bf2EGC^& ɵ@j#@ gEVk L?rgB9MᒐoF}$kksMIRs#s;y MBM@<"@u{3o4sL~-w7[ܳ˗) SB]_o}j `Zd׋4q YnisIi^!$)ݺNbbeʔ2fYr[|p< lb0}0!PC"@wf-.Y~i\ ~˧~+')/ʛ:[[Y=sһЃ]}f"qZs[ssH:DJ޲*TZ`feHIn\|$ǙZT)*rM'~-)H.0Nڵ I3DݏYXJo %- '~̓R Bq06 4< @ xm2vY>r{gP{n<07OJ ;AfTPshV~ {t)KNK%h: ʇ{}a&q&hs%E^}r>L䯿]l{"/-&Pꔗ?RM6ltՠ$`|"4 @ x`g̘JY>"{>tԙHDC bۉk/uKk &P9`up3On,IXXdddf2^R;),ΒBZeר(݂ϵ%g:/L^+rX#Z ˛ [n5)& JF^$ˇAKM{$Jc"EKh(b:6l{xr^b1!ZJK7\&}^ []Mr[L~ϕɓ^Z zđ3WIBRinݺɌ3*  @lT. д^=˺e寧^2!@)ݸT\(̈zv"V=DsȺtW8[!emŌd]Zk75XVCZ]aԬVڮ?9ldd y&D:XZƛAJLpnK`a @ @_rv5iְ2ThR^c0Xp9"b̌*SzGxEdUbőYV.0ZaRejy^G,]t_W#M>Xʖ|kZʖHdX9> !n%4uS D&6 'P9`Md60L O{,AP4)ۼ#7kʰ(k?ӥ$  :fdDs)wdQҹe\>-!{f/ӏsrbe6ݷR]ofaFKAHj/ei+ T铅 (@lD\. 4^sV)ȯ胩MJ+G 3Gn_k̼ -CLMaz4=!}6BڎK'l$=Z;Eo /!W"?H.?wvcxxtDٝ<@: 0L  &`Lm~W(6f?*Fu\nɝ䮾e٦2g׈sJF|۹I6f3j/K%8.DZ_Z>5|KLzLs,w 饲9vC ٳ6*:TZSi6  "@lF. +tt9s,3Y5h5[7HjfI4_qzr L-V$v,:&%y?0r}fxP)ZP#&b*P}/=.mF-5l'ҵS8Z6{9QU 1"Lb_+""B;<}, 4!5 _vWs,4TSmʖlTX^j3ᇜӚ8$d<=3}l6sk.^T>+ZkWQbve|o}aF`£I;Z4ZmXlm>*vs9ұcGtGb?  )@lHM w˖-zK3PU>>[dK HZI)RSTj;epi#j3DZt"\Ko\g.etƵҼ 3QҢEDDŽY}wrJ׮]D@MN @v-['h绋s luA}pV""̠?f~̬kr"^ :mae2әZM lvqVn9r -|" #46ݶmK;Xɩ*gutܙ`: _05P@]%j*5kOM\}4Mˑ<3(P {˕O͓>UP .-%%e[CzxawKlXXH+VAHi2>UXqreYw 0P@]#)ƍY*jٵkҥ\-Mɑebf+ksʮ#ϞұsYpx~fZ$ٌ >;$$D?p[Lf{;+ _ Bhrm:i&={t*QfL3lE4Hm4s,Ak\\th+]22sdoPicl5KUV4݁+@G? Jwߕ\v훹̩az_!=*{~.YnL5sYMa>:*l>}y,u{ @ `[L ~#~z>}5Inn\YY-tKbX(wfZܩ\hfC$**ʚndȐ!Ҷm[vV@4f1ʋ Krrrd7XӚ؅>ۥLsRy9g.l*Yf6}P{r7[!ӽ@T/b#4\f̙3G>c`@t674{͒ӄcǶrtCV>\"!&:x駟.}X6 $@l$X. h\|_5%4^`VF3Xt#Z"h>ZcyH.]܁s@q@A䩧 fh#IHHB#믿q_k׮ܷzL@^=vm?\(\=6NN=dm˨Q7p?vOpoW_#?cc\ں7_!%LN88>}5ʮ0mgzK஻sz}G^}e׶~Yb}N:,;v:I'^s#{ޙG7m|i)/ߪ/Pe;e_)}2L_U#߽V4`v麧'yk .zħ gu׶eР!ŗ_;))I~'X~/6ƥkҢE y'd3SgGyU7Ԇ]aPe:O 33S?`чK}.SN96V{׏YqW^;똏?z_wg_JPk*u>k0z!}8oz`/MwEjY3[+.G!>wiCk4OxSn}χ2xPZ4D v1eܣcUd{SOff٬_}ǟ(O?=B?]sj_j0wFn0}{0TSGU|GUPviC4?οP~%/EBBB|Ɔ zΝ;;sޔ8&`di ulݺڂ xuU{;؁s@uW!}?Ps3-[ȋ/*G!GחHOe5U9d{se}zVPviC4/KɧVCO6 YA0f-_lÆ ע5͜_&ӗXFl~>W_}-W \cw ~YBCCk<EY9_ƍȣ*, kێ2IRTT$rD3Qc.jژ̵S`ܸdi5^{yvFTw_s̘e@K;ڮdQ>+x|{nk04 fwq|^#12eri / `֛ `&0PS7V5mܧ&P\\,8+uT z=~A[zm͗ -[.=_rrrM֧Ur0@Auc?O?Uem14`A %Y0}[k"`~_lEths:t ],wy 4k[m7`=,Y"+9oiU GBlMҬg*('bnD n$nAK iiQIEA$Ux ;;;yovvݚw JUAXRmˢ?nnF fL=L0[1 eL\CMvqf`0MԔ;ϋtپm%A8&щ 1rUN&qZǚ+_N:e)pba9ѝZpהabt]xM0!MV4SS" }!zV;p`Qp";n޸IɅ4iPԩ@%Kfu<OЇ.Е+W6RLIiR%KPJ4t!:p0,-}Rk᜹;I$V ݷ._F]6O҉駳+95?e߾tJ&_tٸ?3x2'ܻ 2TZ8P &ɀٵC:# V\xΟ@|`"Db_o{Q$-_;G~e<_x*R ݾ}>ϔ5[Vz2EptY[Gv0?G/P4C ,L ҿ˒66t>ڸ,?;9q_߇ g.JE7X"\Tq;u N:+܏]zf˚={ [ wõDRjTgsQB}}0H` 0nKVYme n_S"ǏUkÛ-]`i#0@t8y3XQYB|%yعcR@s3.XHWќ'jՂ6OO>KJ4FNN| H*U_VyL&֯@ 2(bҪe ze賒8siumiVڴiM5ki_6hޜUJ%r ! V\iw߹oaϟSl"qB0Tnا[~UWS n5jT7`"Y;3gщ'`5Adۊ{AzƤ+/atɨ˵ O`)x-\0o"5+Isϫ4}Tj٪%B4i\8謭Uĕ+Wi H\/kܸaͷY.gܖ: DuԢ:KC#|GX/E=3S^}8Vn?X5?-KQẊ|D恁@B&~/*ڵk!$bi#0@t݅NU^Tc; ,R!]a6`FS^˶iU+W.~/^Bz8vh#2"zҤI-G.a׮ԨqS51ޭp;7uaW.{`f̘heS.̭[e<^xOO:2e7{$۸ pMUk 1Yu೓.h͇`fx-uAn  >`@h/M|0INnڂYWu?o3($=rp)v8Bځil:Nҥc~NDVvFDkQSh>uL?~UT< W%G>);wʙS # Q߾à~ B`&AkV01{ *.D[ՀW\_w\BL]bIfk!$&Y!v?͠#:3׮8\1E0IShq^YFfIw#5D:{EK-T 6ʗ,YJ={}t05{vd#~+P2lۺŕdF ;?q*W~R 1#FVR߿p̷Oݐt `q &QC`pZq-|̎8ޝjT%UwHٟ2VrLL` ݂f]ߺ={N~?t \<*X:u-i&x2f k׮Q,Hܱx h*tuĘ =!'ѯݻknrF0WZMtsqfѧo,XDzAXl~_/hÁLsըi\>;99)T9,o|/3'r\jkIb7/#m`!xw'ǂbޗP||Ӣu~ʝ9.$t6ȷUsZ0%jнi诞z+VŖ_zEZxVJ&X8k;̳d2y"+W֒w`T/r% m ˗Ћ/ 7;4 &QC@7BëW0>j' I3fXW ѣUj9bU>ҥKY>ȇ  bu 9DSc妍8̄2XkDb-#Gȉ%"m(1j5ƤM7lu2@eܹSc/WqMvEN6SJGB1r,/_pK79s"!.VZq kղYTF0JQp:Tb=ĤZ $ 4l]P~elWaG͎xLIJoTLH;xNJ3']5ԥ#H/^va}< 1A0)9v1zѠf3ȿ3g$ճ;5o;*TYѽ5ifTOIH$aM StL@WsHL`Q$ѣAG{hÆDUu`zWm 9aD.L6#nhaa/ uLuR\fݺ{h%3oKj^y1Enw0e48 | ZKjډ/Wdɏ ?&T,ND &z0 o-b_!Z9*m]D7kJ=͘ &1\L$`kL7m|ϒOqL&J)AyѽR`*+oI\?kwf_P,hL,` :"-q`rl 04*:ə`b'Jj]aӧAƵ5zvBN~͎GJ0` O̾f:~ݎnbү'B*ĴiELtHJa{G"c\cߦ)SXJer:CD6!3ژ·_r![2ާ6}Etn/CJvءN{ ]v+W/aʐ!%]=תc{Trw\ `m„I,Ç 1ȧ%Q9,!nݒjTnfJ:ܸqaAVե ':n; vޤB!xhZtݘ`.GL06 3f̢aGX k˖VGrΝ(}tr6[\2k+OBWY&Z %kذHU&h/иI3A07#:w& {첽u}LGýηev,cu`BMcw}FSel"^v4lEL`kGhܨPʯdX25G'}Tu%/D!B(UisVz/vXzӁnb^^.k}n> T.]b)2Z0G0QZr9QXXe*AyeɨSE:M{3@Lg4EXjkF0|G`;w"LEw#'YN߲wԪ@A7nohX9БeK fV1uqՕ lڸz)_v; 'x}Ĩ'N;ar9/hb?<_`kѲj~-itSswL0e48#\B EnAg/8ȁ\JnqBbNV;:buWQ&ugQ>n 7[xt ,Y_&>lŸlHMÂ㮝=7]E$>PPQaf@ OS QTRV XrW MVԲEsׅ 륌s'`ƹK&W#UO<@Vvhk_`Ju3&1d\FfZ={v"t7ka;r$l< //傍wSϚiמn7N-gP|WJ }ؔ)SY,ҸնVʁ)`&A X}Hq$LP 3^(['!%`\.uS BvBZ^ovoiR˚pN_fuʖ-1k!up}Ղ5թLBG7ؠNՕ}L%:}c77^ ,&A~nu}YWg̔U> {|Va]VE  fLc:v4qUZս{e-Y$95]>h9Do g̠{|X4,jժBEzlYzEKYlA |-jc;T0\cRI6,M~HLfc8~IM:GNK>DRVn<^* d%Y׆ZJ:t3`L,wJ)[a=C0Q9'φ翼nݻH0cS^|7`t1W ,v9ݝo wޙg!u:ukWQ\,eun,B<,4w/O^}-axcl_?XXK$0c ԹN0 .PѢ%.祀4DkW&Z',+`M wM֮6{jF,3F2Qtr6nLk׭fs-ץЋ3MT5L 1BR&r^]\gRۉ=y!˻<(_x ͠!ܘ:}q`L;uJW[d]بqS3~Y43tU1Tt|Vl;{A}#^€[33uY:S=*16NrJ}+/\&@LHqp3pe7OٲZt+ŲՎÁbAu>BXŇ8v7o*Bvm9Fw F{hs wYu!W 9O8t0lZ_{G0LD9sF@نϝꂁRy E0ՑHK̘>ո* F(ydU5rֈE<ۨ# fa agnjְG`ʝ+`׆޷-6y޽k;=FR0ɸnЉZe`HoL 3&PmD0lH8Q8~^gesY4T8er~/h"j#>/6 E/i"0txՠZT;V &[r鯨Ng*+0kre¨RWaɍ`\22΂&'O@PH 3MgN`$Ѐ|ԔC\$SQD^Dȅt`qF6UDZ:˩U\jNn?x4? cƎ.dVnOq-Z̪ցڴ'z7nYgi"ǿ1̘@=w>W9\'qۯӋy $?:p1mՈ[[98|a!Zl߾6r3Ǹ6*V83w6)b'Ǐ}L',.q`C0er#|!a nUVS. (Gaщ`qϞ8`)R$aG /,֫YJabr98 :q`Lu BRuݤӋwu"bDEK,mIFԷOoK|P|%:uꔜd<#2e;,u#GޱL4Pv X¸[9EbUZSFF f#tZNhwV1X|9_AիVPy)4&Xmk]hkχ;)u^3kڼi l4` "0t29k6-5hP_>=S|eΝ;G{p#ɮz !t`"Mº1z+^'gl ːTj9p@+U9_b)RѺѠ`ٝ70;w6 5}0?K<ЋĶUg ?=`7w0@,t.r]G?S,Fcp\$S<!0p8|0ߡCVM[nS5T@s;w5hD!X $\~ԭO &iq +̥O>sM8"ƢSW؞3Ohbdpz⽔=S^K~՟ L0c{͛7oR=zZWtuF)SYؽ|PPMV;5>򀰂tB=U#L{t#\Ȗk$qݺVk\* $45&i˖-Y%Kr~q' -MZqiY:9JC-љć!QəH<+:9a8TfݳÇ5֨J)g֡OkmV+u: k}LԺcbLcǎilY ?uj:ombݺu1,DF-Nb1:PxT\Y)ig|%e!RMHxA-r"?u]*6l@o&}Wwk 3b of`F ~usUM_k#qZgCӢh!Yd)2`2[*]󼝸~th?=Nk5}J0t? RdphH}L"FF<ѣ 1[y̳FX@IDAT/ ;&+T\BUl: ] ؾZ_w:L ⎣G=<Dzq9Nΐzl:G 3fL; 2 jp2{8P ,`?԰kvs jIi6es ҧ??%JjV)4>b 1LcG&t}!:,?ڵΝ} 6-3&0a/+w= _URWnℷb Sh  ZNdwjY3VuF̐gTZV'рXPt#yW(a]ԸUÉ3l 0 - jI6FJ |6۷ߗFj),w/!6Nͼؿ Du尠տ&:!qӴYJ.d}kxW z0遼rf_UG\ZQI}xfZlc_ڵhA|D KNՊ ;TM? 1u|5;r`)h#3ڈ^h`B< y:ܲyn|Zig3`"&?3]xQ[bzWCʷnB>bB2UJ}7 l߱SxyT@߱Qťt?_>C3ghwW5(7$Mh"™xN0=]W7VhP4+}A> ~fzGd# ~)͜9Zߦ>'ee= Dv&u`,/QJKQJ]F`r :iGGxvlQ?ժ.4-JԣBU5Ȃ|j@9+堳6kmvV6P~c`FS7lt;ffO>ȯ4IϷݻWԫo5g/~:a $Ggn0f'j9aD&_%;}8I1D4vHI\h גo|x#tyu*` p^`%X|n٬<ϿdD@j` ~C Mڹ~:imG/б&c, NK[/["r0SN}G2nMoQU &D!A 0)s2Lg-Pc,.4 ]HL`$*y{6HB ؅7oǮZhܤ'⨖1v1uIurBLL)Z7ޤ/F9;6rh{]عsgV.)E?hdN:$休Bn)c);㔮sf G&@9${XE W9o(&Ӧ |Ѓ ͥB^[4ZaGМ9퓚׹sg˵g c<91#pk{~ SUoA%J0!ڊz #+VTrz `UhҴ㮯[`HmŊ[uQ>].6n,}v\ %q 7ދeHbߧN}a9N8X- `q4`MK*Sj{貅 'jqSv0f3"C+mڴ:kkEq ET>z. h?|5̩S| abܥsG),"]ҤS=M=iEzu}Lyq'LwW Z0 q3fjInۦ5uɒ;ёk:^H9|Ajuumiu+^oпwjXx1V5"0,XB_T~_0?!7c,Gv Įq@ޡ<{kcgVa0{5z)ըT0B'+jTm<r˂:?_=z[R܃į\Z(/]r%X/.}qK.\XU , O&,I ]gy:ECBK/"L "Erjƭ:E lo ґ>}:}]ڼy_ G >1}!q.Xa}J&ApuNB#G QxF K6gZN99rq} =h^xg;paF`r`0#0#>ָF`]`޻זG0#0z mǏ}jPgaF&|+0#0@F@CY&Իw8 ``` j\`F`tV]wJӧgaF Z0#0#:tntJ*I3Ou}1#p7 nJGF`F;u5kZ^p>oj#00tO0#0D.R2ĬYMG׭|0]̻rF`F 8FEӦͰ1bV%F`#;V`F`!:reDŊDY`F;L0c9F`F`F`\`bF`F`FLXqNF`F`F``çF`F`F`#;V`F`F`F&.)F`F`F`0dF`F`FpA 8|`F`F`F;L0c9F`F`F`\`bF`F`FLXqNF`F`F``çF`F`F`#;V`F`F`F&.)F`F`F`0dF`F`FpA 8|`F`F`F;L0c9F`F`F`\`bFnG?UСCO^)Hʕۇgb-'O~FϜ<8 (QXWt \Biӥ/V^ܥKرc Aʓ'7%K^"'!3]}V~zƃ>hӧOGY2g+POy./d(+}zG}4d >2L0C+&LN:iM4>}z_& x0YLyc°9{̀݊@4 ݊;x%oL0c9BC fhq(#*DwW :wS'I?L0m:N%tڝV\eF>Z `` 3fcL0ceNxE mUZpj,sVJ>tOę`1A|VjѲT!O}pp$ 3\X6L&pw̨͍@9siࡖ,X Ƅ%;vҹo@>(yr(}]>z9vY`Ko„ [k4SS" }J0pMھ};}y4}w 3f@02g+p}!x"}-x2~Y*&DBDZ hX!9}.\@ҤI#R%3 0믿7o\)p؞5k*Y%IĵcǎQVm իQ#\dې᯿2td8yO yHW~O6o6p(I8̑ zŗO0s_"]2G\ԩRQ,X"DW4m}=ŋGxfq+bsfohΝ;]r9HP@hѣGŻ<z2u**??ex|?~S+W_omO+ T~Jw !`)[g԰a}KC@wޕfp۬/_ _~Gq^$]<0e{*e˚2edrg{ӭNi1!۵cGw&ƊEL2o|zJ'n?tvy4iK `+*۶m7 t7$V-/:gEd"E fqA \s ͎1?* %jظi3͜9:u@͚5DynɃ6`b^BV-Vt|IL:u/bOmO  8u֭ ="<۷igG Fo WַnJVɓնc8Wo$|γgϥqowg|ԦMk1Ik&~˗a)Dmt骻 Ӥ \肛+W<񧻇p4n܈խC)a 4˖3-Vu:gN ]+V鵚5hĈacD *[~2xթg)(QaCSZ@ANN/(߸KZnu+B!zr^[ 6{Iպ,UÔÙӧ9n@DmU,XK3sEcƎb-2>)_W,&uέК5k-YΙ{.?qdZ!#PgU⥿`;Xò饗^q,̛?yG{^Nأ{703x$` TΈ!.9o|0p ; ҰkԿ@ 4 ЏJ T$c4m c7LLjծH ŘT -74Q?q],YPSYŮIiM`b'qf_&2{uQpN֮Sy;Iބ o[|K<&6N@0{N7b)8]ve99Λ%,:KM63vl']5mb,]&n"4>L,Pt؉ nhA ߺuK촷={T֭,=/^\ J0`[ q"wP-nwiiѾf0=fpPpˡfZo)~ z~! 4`Y TB{fHRc];}:^OdN?v(c!Oq>xB '8SlA \S!۱ D8_NU_n't"U͠&WrwFN5Ů!؉Z-wT\7 u׋?Rgہ增 _]v#UD!I1a$EpjVbmod nTΞ=J.Q^G0Hn- C/ z5t`T@>C+ < &v2?ز&Jjѳ8K/,~R$On$$D%QuxfLJСi,?&S}錘HC~tsoURC &C'_p#.]B:Vqgy~aCt1|*С]^wXϐ1?֮]%X'xB[SbL-I*$ij[kǣ\ܹ3{ rU#g׮Yi! +ZY <=e7v }J_97=)J%tXG:v$w=z֝2|1>+|A/}:`(wĸ zH `{l2?'vڷo.+ lb³=dɒi%.Sζ0CS`mԼE+]QB2c x鎣ǎ;>n*;-\sÆĢtm颬ٲt{ɕ0x0 A.UB%:] ~9c, +U XN4(Pv :cCԯooCVB[ ,'تd{-Q!0SZM 9BwimTCNƍo|A_vuYM682XAy\E&u@K']BgTqF+L0"bLL{N!ٽG/T`=裮8XJnˣ br5}WYȘLJj)X`;Y1 Ӏ-Z8e˦KäLc3dBP)Po.S&OՖ-bf\.tWD| :Me {9t m Lɶ~dL/ 6ķ!O3֯ 2I_kձc*uҹmCn3E:"|1M0ϜJ.+wو7o;Sx  p>:2yǎ39R0 ߴ=K>A Kw* l͛7['iUR_{!/x8*ZlxעB϶>hO~pbKfN7 D|۔^P׌UTŶ8~0" =| mES'1tC@1`bBH]U|U [4 m簃 ^XAbju!izJ0ð#mvLfR|P0_a~/ <{kzھ x"}wR~}\/:B>H*’tdحUWrrI$͉`Ĭ:vx%|+\*]71˨^: t0/_oB\Fdr?W'c… ~b-@_02.p%21 KM4Můj ]LAoٮPgt1g]TV-S];1I0uĸPC%.gTY]b˗/z6=+ ?oɧhRY~wD{%>b!J/A0X|'ՠB%N'|,x {,DÁpuD  qUbP N;B\bj_:j0+WKF ǻC"G0^u-T-8H|gXpɔȯdwhi>9km"k#:k9[rY@ D !j(@ۿ_8 )l<&VNp .֮ra5vŧݺI~P7Pĉd׸]1I0{;ϋg}:с|\q'pLft$WB!ؙ.zLԅ;7]u:n&fUVS.C׋8K-'⨾Ӝy";k"`sŝ1&ycpL|0t"<nܸ.>I,ٞ`јaԴisU'_.iSfPՍ`bWZZrUR}YzQ,fFdU+ѭfϞe)tEUc:9fBvVrC>2.4`}3@ŗDЧС'reoLU<%>V}:F7ißGҬxS+$:buwMDWgey[ <1!GmcƸb dщx~a A /l[!u : د-]S'SNlH.ls *,WMaɖ5$t,LS ' b1@8&^{ G%t։Iԙ&Mz9o^R|ԏ0C4G;ŝf>|b Lm~,Bi!6h+^̙3Qɒ%6e3crؿMw6wSF5I8 AuFyg>9zT|㿶/3f``yS, amum3{ԽrAz &e \ &NpzD  ] SLŊ'/tǽ=k%W aQ~g:${&\ޓ'Vmv0iw#ѝW?.JuS_'J\'w(eUثTBz:l[:ù_}Yz{"o׿%NTCG浽dI =y*nEXĎf)B&@b#ƸυB07_лHgXG0XRr7 & XFN7xM>f<o &_ҥoD KL;690x0 A.UtSC 5 *|ٟt]ԡ#ݶˋ:c1^SgR~m>]:]qi=ǔ9ۨ᳓lV2a=eLPvwPqLmL%X¤K˒JzTYl2gԃ~rmY\IS9ݤ;Hx&TޗC͚ZF`_7B!{2|ziyLU HS-# "Ͽ ;cV86I<o @L8sn) f0Hr'`:!hLL MS7Si8Uꊀ,l߶`&:0\c_g`|bN"JLu>cTpw:BJ0tC;߸q zmc rglV'}66k~F'ј$)19sz`Qo4e4 QpLXwьcMB0j8$TSZ rY}߸i3icFq5 wԌ3i{M BLI}0 I.L'd8=V"  \TԬQݶTBt6T&JO||+i+ 3p r˴z5kJ,tP"s:u}EUۡBf( Gxg\ƍE?٬8D 2jC!}\ Qpj7Z;~Wddgfo]E!\>ĮcJR8᝻v K :L0Ebf~c+q,RV%~!GN(5n۶V%:1sv4S'>|^SF"3T|Tة Y2.0Yԭ s[Qy6tb A2ȇ )fkYrJ`7J0e6,\ɅJ0QTQ jK7p m޲U;9Ѕ`׸:ⱕ`0~9̚904Q*\T>MիWѣFX'C ڵkaE~1d3Ta#?w]2t|"`9"M0Ȝ)uzL$vSW5>t7 rU[ŋP}i9jW?P J0=&[}V=u}ӥa-tS"Kg<`|0f.B*=gdۡLLU+0pՐɨU~޾}5mwWJ0uVd-]L00ED~:%Kf~6?`ǭt &*O̘'qo[fnUUnx 9DzS es4Cˁ CE fr"iyuX<@_:`|4Shϰx0ueL.QjϞj +2\\>,):x*)׳y,%ѧG>vd] a N9dZVcFw&ڋD*iSuG\0~0zu:?J$WuڎtS:1VEіJ0u7vPo$cUl̝ME5}Ls@,76LA FAsȦs#mP(SHږOBZy9wr|XŽ#ڊc[;bҖ bؖ4i 2bo0z8[yZm+ѡcg[Y%\~C9?akTwp bNoBϱ0|t6tO>TzmWe,@ίyRm>l~*:lĝᄏ/\j³W)Z}["xԼTsJ8< "bxmh9k> w,Kkj5ՠE8VW4iD}رQ4UE%Jn];\+`WYXK4-[r3oL-Ì[Hx Jz4l؀l0 U'k`NK oˮD&ƪ nT͛7j5m;g4{\KtBϾzUKYѽ/7AY wmҶ`0ԨqS˩ݺRVVqcdЉY:al0HH@RBgNI[Dsr}Vu߉{L*^(+5-[)}t52eI=]ؽ6mk,sD~+RXlc{p }뚲"ӗO R^ޖD}a~AozOWB3+Tݿ uU SH .$bIHY=mxwKf4 &ơӵTWn4hl9:r?!r%D;:z5424p 3(rQC Sg2xZ ytcUev)&bS؊` :<< >v!v@NJ0Q RWŋ&ЋU ԧ­c=j9u֯p"SLus yzu(gΜ>b u6z3Ģ&ɪݺvܹtg6';0W%f9ŎO,ͬ_XNڭ8a+Dbm+a! nBY;~fM(}t1cZza1_,zԮSO[GNqPx_ݓNsÆ.}7kyt tf[5;4_&#FZqLu{jzkPC6;8&MjDmA 2;p@?Cn_xy*XDs^}7L!J ky^:>Cxf[͎\hL|( p.!.L0mpB0 <.}A0/ *&Ŋk„ w/ڜ&&hY$On2|ҦMC?3]xQhe=XؓC0v 7HXRew!X<%~0O@BQUw\"F0[@>d9Lu<&yFoܠ_&S~mg@ )ߢEP\9?BlCe4U*vJɶ>cum=tvvq9QϞEBϓح\Ή`}Qk:0g3Mg|xm9L7KER w씫,%nH\(g?_>o&gΜ—'tbm Dq 2LJBz o?SH|ubY.Z /[3g~AT,DY%*]6y"r{-/ӦN%¨`"vJ Q@&8o ty!hRA5Ѐ]i&D֭,F wt$H\Lmv*uL }"}L!ڸI]֭~2`N8;"LaGҫV7s2u2COzq,=29&!ţtd:u/^v`n&U{VrՀY7k,FFq36mZQڵmr!b*r~807IDATRKFq|JndZx!r^_:HGʻRnܸ}(`:˕V .s\EWF0}IP vq Tl1v_)!?|1 '&MꉬM+z؈Él]Rysrjzg<{,"Xg/bG ,0_2e(tgT1ugD!ѥk7&խ[,fLޝQḊ_ e:u ]:wBr6`hm1#D&Fg8~ʾFkԨNF:T H"6o~&cF 0$\7#0@`mĹ=F%\TSu[0i-ya(=A[_T@0 8.0#+`+/ w`T6mu~-[4`. ‡fϞIŋU*L0 770#a`F``ƒ_|AeUV -7#H!WgVC\~q`b&1}}F`p"3hr]#Q6n,| n`А 0gcF+`yW\&$#@Uurm yy#3s#0QD fF <3hpY5`rРTlE8#`Ffn`F J0 #K.E>sх }PʕfBYe5e$I|9O7u'UT;sc0#0wL0嶺}eWOK&PIF`F`"̈ʕ2#0#0#`9`F`F` 3"r#0#0#=`ƽk#fF`F`F "0\)#0#0#0q&qF`F`FL0#+W0#0#0@C fܻs%{,A6%JHIznx>=xRʑy*SLOK|`F``^`Crb:|hء'O%JD'0!矔$}t}(^xPiK_ R^ۘy70#0L0ɣa{6+s$IS?ƋGɒ>BWJ/,OLG?: q']x?>URNg:ʔ15%;EbџBl[ڍ!CZALS駟ƍt +Vm;;.0#00 1#8~% ǧb'2}0qBʝ#+eI$|OJ v4?w;P: }w?cI[aoڴ 7rRSS=5`wu۔ig`ظ)רmӶ]M{qƷc;{*!^' p2:w(@0[K/@2VI`qr#h=~Zt!;3!u584Q,tbEf&Ȍt& |٪(&1!6ބ%J.Rl> SX9RECyI(*. Oݷb{y<>xp!݌}2D^78LBS˯{{̟wTvQ}/N=dƒKͷC]գ|H <أu˭jysxL /\??0|l{`=;c!׍>7{O; ֐Y+K[V^okp3/YnxiW$xv1[}_GF6;=~qH[ۺ;/vwY?/RMڗ>~{ŋ1[G/ 78{SϽ>ѷ܄s:s`lކPgg;@B, "CduxdhDT#qztbYSJʶeCP`JY@h GF\$$Ibu^) ߋ,c$EKy:r%PYaֆIƍ6c"1SГ~T5Vڵky:S v!wqnv4`js+Q7Urݺ˻%$[ӀzsO5@=P|;Сt]Qo6NۇP{xsRcFF8G֭8J >wޛoM2^22u@ci>f|k7۪_3^rzӀٱc#7E* 8pхU% P2'TéSޫCow{W+W]q^nf5Vkh b !>{7mlV _:89RudcҺFPeet i2v"66VFg+}!Zr>MC鰫E0[eT'L|Ncz/(@ C$A P` \vEeKCEG>!uIK:5\N21G 1C>AR7wP!Q+a/sziPt 1(@FLM(p**MHH Z'G_.aj -ϒS2LaȎ N>[YSoƁ`L!K8Ca]F6O,ZbTKA `ďg#kK b'>zj{S<Ϸ:O!C^ү4:fS!q5`z Zq<팁Ʊ:ɍPwX;oM:t{{w1wZԡm-,Ĺ\l>KÍ?|O*owݳáZ9<8A*is{o'ӴsY'n>Qumzuȳl7 Pع΍x(@ 6XH:ΡI6eCѥm FdX[GJBѵs&+Q:em2iT^^pتʤ"DHx A?HQQU0ofTWsZ90t dm55PhFGF\p4s݁2kߝHsàCMnOkj)`<;\Aaր?74܍n|ѧ'< \"s~Fլw2tX}pWӬs}scSkl'0PN2[{oit|C1[ϜWƽؚO]&ic$ޛٓywC$h؝m:Jϩ<5,&Uf~Vq|L P0[(@&w᯿H Bl\8DiS/31! &evf9tKNDQu1RaJcE[%p -, 5岎, aCT`륃M µz-BC)t_}GuLD=""؉{l)dYCnބ4QLDv%`i-jԱ=W u?TZ^8_!]˯b_UEmHZΞ3=oRÏVu9S^|EQv|mޣuvզGyu_~+M~7I;$Ϳ3١;ӭF*}U>lNǍ 0`^?(p@ҳ1cbb>ڔ~5l'^ F|5e*J+ѳa8KG"L:]N?d.`^ntMBCq)Gb2\DX+[h8&M6 ?:%ݲU*mp:п>% vGjT.auq kMÙynmb_W6PcmLbubvLlսdHjʭ~?:HWs uyTkS+Z9miMp|oқߘ?x{OM< ns8&BJǺO<>\Ewxր̡\ȰzЎme~MF}]+π;kUjѺÏ>vJ=ڍgJVߜv㧵 L}tz !znM3O=]ntn6Ho]s+K.v^ۆ 7w}R}iȰހdӨH'סs"ukur; >_dX+zǽ$}ڑ7^wp\vɞ?`?)@ 0wF 4Y5HoNb$i aώ\̒e2mj?:o"Ce*]6/Cl lYWasʠW'fJYQ!2:T.dgJ*YW#7X2~xdXt|[62%su Ff4 dmP nπUK\ 뚑ŨΚb +rj܅ 6:j[׮FObjBtJ uk05xj5Sϫ]Rl6jv5ϩ]q3tH?͆M+LGNˍAv4pRu5_}(9Wkk3dҙtM 8wW]=}-:XÊz1myqM~6@sC+c 2/ѐ_yR1K{iZ`lކPاWR}#%.Gv턶 >* u2-4$ "\E,ENAkBҊ:k::KP235=bCP&҉3F\bT;#>&h +k F*96$,}^qh)dX#6aZ π/c_2{GM^l1Oz|j @`OCO5$k3xzu$>,|YpZ%%FW_sMN>Nc8uia[+K<(!S6τ )i{#e9{5Y][(@ 9( P^p\Tt=3Z%&HxKFVѥHdme&ѱUN ѡvTKԉBU:mueG&AqpI`r%f-YD{p̧e|N Vo.K*y/?4]EZ9 bM~ϰ.a]c|Ok(unߙ 55Ũڭ]Θ&-ͽޥ>k%:VZCW7XD7\.xs&86!]pM^ǽ㴺x޹gǼ<-B^yywEQcgWT]i5W+Fnp6݊ld}]+o=S@  PT`\\ypRWwl'Mm" Y<> lZ"T4bkvبp8` @rjy y!hf} ( 4Hx&.Y?Ee%((hg˥lJJ*2M~X<Ɩ-yMϭsGkKR'fO>SOXchTVΔj-aSz:z:&=sv1QCnKypܶmNM5l}-Cp'KډWߣ!Rʐ6s%-K^GRR;ܯ6lD(5F͔cvMsלx(@}"0yDT$Y$NI$ U؜2 ҸRIM4 *I-TU[e7s<̍òh߾z9EQV0L:/As"y\/\9|^n]jW>h$3s 4F9Y`%,4`uYY2_0׸Oz;J޽{R0c(@ P 0`6K(@ {2$N D!Y$Ң`!{M&\FձNYڊ %MK*1"f6H”aUulJB,ݐ {XRZ#e.,f°*k+2s &)uyxՉޙ{fCǽtMF o8yRK_s^3BF\= C_[?.^2J:>A,鲯7}-ϣ(@]`%C P` ,s:%@YR-_1/^jA-M6%Q"uذ ?9A:&ϗU2?/4<Q1hץ֣Xr,#{̀p;hKC?gz5*˵UX_v]бSIGj(@ P^`-OL P(@ P0}~j)@ P(@ xMk<1(@ P(@`ͫ(@ P(5L(@ P|Kӷ7(@ P0FS(@ P-Lߺ߼Z P(@ P^`-OL P(@ P0}~j)@ P(@ xMk<1(@ P(@`ͫ(@ P(5L(@ P|Kӷ7(@ P0FS(@ P-Lߺ߼Z P(@ P^`-OL P(@ P0}~j)@ P(@ xMk<1(@ P(@`ͫ(@ P(5L(@ P|Kӷ7(@ P0FS(@ P-Lߺ߼Z P(@ P^`-OL P(@ P0}~j)@ P(@ xMk<1(@ P(@`ͫ(@ P(5L(@ P|Kӷ7(@ P0FS(@ P-Lߺ߼Z P(@ P^`-OL P(@ P0}~j)@ P(@ xMk<1(@ P(@`ͫ(@ P(5L(@ P|Kӷ7(@ P0FS(@ P-Lߺ߼Z P(@ P^`-OL P(@ P0}~j)@ P(@ xMk<1(@ P(@`ͫ(@ P(5L(@ P|Kӷ7(@ P0FS(@ P-Lߺ߼Z P(@ P^`-OL P(@ P0}~j)@ P(@ xMk<1(@ P(@`ͫ(@ P(5L(@ P|Kӷ7(@ P0FS(@ P-Lߺ߼Z P(@ P^`-OL P(@ P0}~j)@ P(@ xMk<1(@ P(@`ͫ(@ P(5L(@ P|Kӷ7(@ P0FS(@ P-Lߺ߼Z P(@ P^`-OL P(@ Pv7`@IDAT] M_d Mf!EJdh4J""2OIȘR$cTL dJYHfTw:o}9w|{};y^{l!E߸E*N(\['E"`X,E"f̬^6 ㏫h:j^CrJ$87EVڰqH")Fs" pQZB3N?*U:DȖ̓E *V\I{nWr?"`"`L^k׮?SXbTp)d]BxbX!оC'Y>;:ȑ#4g\ʛ7/]uYb?i9r40Pè͵3DlD?dũPB?.[F5kԠ%K-[u7j?z RL"gڱc\r?x:˖-[8 ʑ#;vVZ/@e˖ukZ$#~3T}?PK:̧38=UQx,,e03yhي>tNe_ L1&dժ_Gvǩe~em۞>x͂3όs0p1J7o!\4T<\- {,X wh6bץK8d^Ai\0ͼ#Gxl߾9,AY_}18uXI oYU_ѬHn֭W;-[U{=CՋ6fsyf%&MיjW2}mz۶mZN ;&~K!ӫnyŹs.qem 4ƒ`Sb q[?(:*P4s?;HY P\9=1[ٞK秃Dzjaz"厾tcv+PT\ub?PݻΙ\p/`e} bw ChذWt0]:wwY2 _'BUWA2%KFЂ> ʔ)];9Wc8,~,Vm\5g)w?U7#:H;ഛO[~m ~s(mʗbvُl!:.CDlsG])' ?mLh_rZbe01mRr8|pR C˔ӿs?q<` fCuL!5v!)SS/r‹.;P7]y~λK. CKd3}ڶkts{4mRmFׅ-s箐DR"LMp\h?ҋ4CJ3{kÂPO}fgxyN]BH^w#lPumΝh^qR.ĉ:46{PwkQ{_{ mïz㎻B9[x%%sv&Ŋl+zi'|?8Q$R{+lpTNN"|^~'KF|O:W,[u3 pƘ0Vp~oQ=?G49-ٷ߀@b8Ӽy ]8~œ8(I-sc($O`s1gA?'L#Dځ_?{|:"<ż˂O> &},!/qy 2嘽dRL+f4(zkw`va.YY3.)B sJ$EhM;뮺4__}5 vҾ};zyB D)@H/w gI!*iR: ?Mtp[lyz@|nU/se;I)Ǝrz 'NK.vq<[6lp>Of͚j'R VY+u/wl&w4'\8;GMr+]{5:sw/:azѮfR\ߨß͛5emA-j]q=zt-2iԨ10si'/H;7U87ʕ+ьS]A+7N. N5pK^a󢪗:oVq 5)N| jPsv} nF|5%KT٥n$zKgСc%},8`'E 嘍4kӺu>$VرLogǟ Z)p` J)-.;5x!T{N`ʰ.eڑ*v@>`+]r۳gOH]#Zh\~yXCs:Zv 8,B;'P:{tUNInUFڰӋy&b\|aG'{>ቖq3r1"WC6*JpxXV."ܯ].wBgq՗ɓ[Jv'W6'㌧;2U'n`r~n2w#X='g>-^;BvƂv0+I3v !!|O@{;H˽{? u?v8<)/^} 3ŋ砜~Q$RrwG]N!֭;g''$18 ߎ0i>ӽt`cǏ5fk 6H- K$VYxqH-~;a+浃8)=6IR"}Q'Mإü)q=:H;0h\vx*z,dsI!~1?P]u`s&$bkN4M㏷?V0a8+vb%W2f1X8D΄~2 uǘm&lohllYч0 &JoqH&T{Z2xہ|G{-F_$,ؙ$0,2ޫ4˸9L ܰ0n揦{Zb-MhɫHٱ| ;LNV9;L8]sXk`?9Iy^~1Qn`D0Dž`"^%vN~[$ŏYGr^` ^N9w4r`Up/B aA0)`<N^sGL`/W%h}yǯ租s~7 6y5-w/?A;rIIFlr21&,ze8idcDZ2&hB*-we7ѓ d"s[\b٭/QV[Ul*s̔,3Q'xMb2}IRzx\̺d0!w7z-Z#D0-?Bsݐn.=/ߏ8}\ O.813x2vo[&8H1}3^̳k:,:T.:y3*(=]L LNV9A4JJ,YsÄ9j?V;%. NjIɡ6yua@\ӄq0Ls^O9hI&;Gߠ1RDLAN& ;NKa`5룏 =K}3x03^?/AqLt&\L( ] Q ox$y)aD C" l~d,̉Cqn6G9v&$^DM[O˗,7HwxOzfZ)Ӂ:unkhHMki~fԩHMMPӮ`@C5Qmn ._* >|չ6[z=Kj^c@͚pѿs԰= q(QF9j4݅h-](H*fZ)ZjR[n8eQ1E+W,FQ~-^%?jgS]yPIlj4|g[ԕ5K?X;TV>o(v|\e+J4[/"~mE?;w~)?\]5Pb0~eXڽ0H[ï=#x]19Ttc'ɫ"UZZM/^۰k[d} [֗8@^C$rMIM6^12P 1gG:*(|EjՋh{[]Uo֨qR;::KO/I3gMjDkUnA_\5L1ӀH1[.?>>;[_M4'zf͘-/5i@$h} Uɏe0SX8AQ]=Gk+&5 vœN$/atQZUAHK"DvHձ 0}Y~M6;3;|+X`k^ԪUKأȏYeq24TPGkGN7T\.zkWMcBVIxދ-{3L;-|fJ[dnH7~aH!^S.1rܒ0L>x0s DjS.\`ĉkǵJh&X} `,mXL ` M[i5+w˖one}=hGh"a;Ya0w9n^qڠes$.S2jjԼ^cu+T'yMcHvgݻI]~R}W$]p_:k;JŦ#Gyj=tyJ-R"w4/SL /﹭wwoa)#D2єtvc]>+ݒРN~d,W9%L!/k6;rLRW)R*= ݜ%{,O~`Y)oѼy\̏ĎJ н6~}we0݈,\JT^M3n__˕%e(:Rؠb2^˓'6?ʟl#$A)ҿy}L`Sv''ص˗??̡.JQ# OaDʕOb0(YZ,<`SgwoGM:Wpy0ة˝;@"`rіRDT؟}UEpMߢ޽!SG֗Hה+A" fwYpB]>J\r(d0y~*:[VjT_~ٜ%*2Y&а=?Zl\y;|z1ɬѶh!}D˯"1f>wa+J.g$H[ӳha UIDqWCrY#q09#yhxc*.~4FF8ig0=3 (Dj܆Ay9.:U(i:Q`o۷o"ggʍRф_UN?m@<$HZ?X'#^:yV+.SnL'uጛTu:QgD.3/el`Y,N3ܑgN9ʕU~(Wvk1"%^Mxu :$_r%ǐB0G9H3~uc=iLR0+;ڴi{wJ%'lvVsd{'pTndOLLF1Odz ZPL0-~A[0UO3#/pi Z4̍_d>; `fCN`~dNr$Gm/$f1)"(Zu8Lg??R; s0xMn SNfռH)ߤ[d>(PJnq&CI`D3rpWeq1bxd05JxLMЀEhhcf&:H;w`}*ERS\meͽ0OWq\1]/b>ǫd3`f)%0D[vPn.PNO9UdcjVH9xG`b)'U}c>Ad ^]%9;s~n? ~`` gF**Y0`.v1KZ.d$L2.f!Y q7T 8m 9~L̸ rz,IBZ'~pHU"N^1A+A&L&&meAid+lKġUNLuV S %m49r:IIXA^hH20^L@j;0')X󉮤bN )+ 'rAهOAٰ0L^ &Z-JJsvB3HNJcdC{4 w3CId[( )1M`6ծ9^D_ٖB7`Z%?DZMmAg8ǿK=RΝ+. 9 bpthߖ$V+%ӧϤM69a| 9A// 8jJ-)tec7fH4E_|%3;ر/Ԛ7K38Ȕ3JlOtRKRs S&i@X0* sjRh|휻oh%/x>eI-V(G%K'3|jzk %?+ʨatz)4λS}B T]I3? ,{s1@#4J<g3> ogcMR ¾D0%O8DŽyGP5g'N}c{VDm>QoesۭjΒ/>5@&[00X^P!Rt +afΘJ*rV*yL^z/_"o`!5b=iLR2-=fl`??KS#.^ix4}D R_빬#)@'V#h;d3Mu\6=jݨTG"`s@}M)*\i WLL̘QKAmEY⹮㨻+EMruDCNZD[ȑñV"\ceI S={N?%ÐH֭[s6-[p |e HjٳgQ=Njm4MDHСxf@z]Vjtر^rg=AJFV֗`bsKBo|zo3`SAyЈFLӧ. Є:s4*PNN/hd0㭓tb0P-$r?w2{ץnWe7fH:Sh}$%?3g~ᗦD0`uce0L+ WXLQN)aLR?! X /Pa9~qA1.ƫ]:')E}FAtͼ$f!vc5e4G 3(2B锬k^~1…ؾ_1fNgH]+;0Qϯxs+h3ǛHïah/'vl&SO=;粤88A̗7HyWɈBTx+bLi )гC{Pv)B-$ڇ!lĘ+뉙n|['i^`3S]"ώ;|*q@xţ:3IfSe˖*l n߾<ڰspiQRH3bE#ܫNne+͓W)7oޢWF>lϫk\p hQZd'R8'=Uh{ڵ{Wu.:&YZ| ~K*Iy6]mVM:U?:c,X.l9n1e.ɎK-StT}'AMr?q=Hhl)w\vݑRI5N<m){TVC=VB]S-3ԹC=~ΗTGͣYŘdcoPoLxf+n{qivumS48>sFǃh0VuR.*QM.th{i27`Z]6uWVgLDͷ:"sntk-`0ͺ=_Z*2kkD'Q\G@=?(]gC`"`p,,%CJa/3a۷>3اE"=ƽA>kGm{.-X?(HvqmY« +0a`rE 5`Rp"0dm6lH-_bp:KQk]2ڹX,W+E"`X,E"r,rmE"`X,ED2'f\Y,E"`X,#`̔Cn#X,E"`X,'&<1"`X,E"``rE"`X,E"`81 Y6WE"`X,E X3-E"`X,e0OrX,E"`X,)G2)FhX,E"`XNL,yb͕E"`X,E"`H9L96BE"`X,E"pb"`\3mڽjTN9.'Snk}z?K7& m6_ujQ7JH_MnݪtRn,Y2c,}x 7gem-"w˩ t0M6^O?hֿE #`L_DD_SZpav,`~Ԭy ϻK/ SX3&㱯>}t˗~5ϟ?0jCs~ԬYƽ6&нdfX`\+l2;%dӗX3#PMrkP׮N:$;+1f}Dwى f 'OO}Gt 'ѣthΝ:GB۷ w4ktHKFԃԤE!Q RC>cA{uҁm.VȻ.ګ̞=ǩ 5Bٲl[@|e0b)g41&2"<5PtC~3X??EDr 4vf,U266t`X}A$1R^# f$@ f6m[vgΝN=5]A)\̙,gÆ qOThQg7%l' Oy;D~F$ ZzTŋ)%D%}@"{)q JӿK6muQӋҹ*<xEdۓNEvAs?Ҏ;.3<ӕeĿ~z答C]|y*Vlʖ-˝pGnJ(eM9CEPٵcǎ)6;7o`={jsÝ6&:t9el2gC͛ qDJNIY`Y~~A$}atqժ*Ot}0}E ŋ"E8xi^Zt6ر=ue֫|_,|[~޽ mam2c\rRBU?I+(]Uũ*HgN9۷k'M|lzBѣj38]?ѯYc>GS>HCz RvTժ'C<>3οKrzɹ}#֭xy*%}b?Dz'UJ%q͑#{Xdx7W dZޣEyb)X+/BBYw3Cj^/XWAf͛pR\N?s墘c=@ܫ,NY-S 1nq+".-LJ'b<[[zߤi3'Ou0P,@0(SO^lu݋&MkۮO3ńgˇ[nH1m Xݶj +xΈ+mYL`%~6_[>ɤx^/݌/}_PCw.4_4j'[GOpcK쏟 Wws@`nE9ذg,e?Y3S;v ٿ}ZR@|h#|W87merJ4cTHu0]1c+q ^ڴnEݺ=MKweWvG#l颰=[7K/ԫXY\?z7DW_}8 `fBW4 fX))S|xߤgPfkɒ%Oҙ:th -ZW,|>ʕ;G~פ&%}v#|I7T6Ҿ]+n\{5BݸFraqJ=SLPMUk\q9oq͂ >V[IsPF;%5µ}Tr v\&Ot=+_pn7@pjSte71mߡ vr+IM޵Viz^( hz Wœ>m2)ƘԹCmRծm0R[+^[nIka)SG W͛FR M6eo1 'jؿ ߤJ % 4xF۷{l]tmw:W&yCymh\ֲ, 7\OcF4þ ~!Nx)5KVڣ'b x3{Hu_x^K*wa`{!cHg*Uhꔉa;2lnH%v3|qEj7V̰UQ* LO|ʱîIXufp˄6G|êuBCAyM?.'N:q";,&]X*.xv09O%*V;>c݌36DdC?DI.3S^L֭w7w0knPNJ*4[)6+ؽe/Vb yC]yn;~ دLk-[~'DN={Odz3YMYjiYjLZX2r<ҭ,ꁬԁeƎvЧ~'IͷX2ƛonsߔ{,@s}=a(qB;g;솙$ ?A?]ζ٦hMx?f3LO"qgX :TZ":%B[|jqRs.'szbd$r۝xAWDbR-输W-ҹB5s^c˳@8`hź@_!{hX3c%}X5(ƒ,Kc`uN-;l>N=A~3>Yr.u!q<("ĺiddiˤ"0d^Z6O&L"&&e{s"Daa3fLC`q"9$0>h$-&L% I큟dȤ9sMA&SN;J& IzI,J7_OLEdh*jL)@es#/`LD+\qrҋznBLZH.Hx RCyb$uϩ7/fPw|LBVl3p;HJ$ dLhfP~ud^4iE<6_i$ES#E_+xWm,Ǐd d`xH}AuIQr#ؙ}7 h-샑Fs?qoT#`T#G|N*QF;d8b,9bR3! %V$%1~vD9<̝N\Ym<`"E)9 '(S/;Q&"`bbGa68.gr h;܇̗IoD<ٗf8.3j,z}1ȗLEZ+~fE};eɶIXIu.\Jv'"%4}Ʌ6J ^h<ȅDqE X3('GY}Dy{7]n`j@IDATj\4hˏeUq)J 7~vE4 `]< &/3 ,V9~$2b(YF0+0$Ӎc 0A:ցH@1XbD2ʀ@ُ䢆`ʉv.vWQFy1F&θ:E&$9SN"'WRz ̠.7٧"y$ qGp N  Xȱ (~ QJ{`&"m\N i[U( "٦pI5v:)1Mwh\y1(Ui'"f>bKA8aVcR>;~ŶDqΣ}ZRe0SxeB{a͜<8lI`pFO,ADI?|L3yLOO0 ɼMՎԿ`0q#;)ɟGCN+烟N&_;~bGk'- &LׂgJNpDfb0N7ۉ~X“ڣ\ʪ &v._bH)1 6هN2lTF#ǻH=y SM`1r#V3i$ 2>Sj-DL,fp~Lc3UiO.ub`&c" , 38T@\x{ʉ&&xg`09h#9&SDV=8hyS|bb)ͰLq'9F9c" R`"^$z\7-EA`b08]Dґ6I(VL֭s+"Y"/ƈ7CI|T5#M;:? iH=tw?S7D>@e83+Hߋs`0qΜ8D1FfJDRDDQh1 f2h,@2@1I&쒚9pH{P?/H. I1ϰJ1TE#3<ɿۯSxeE `fC) ZqvURQNPU8|rJ4cTȑIu{aTڎL:??jrGA*WqT`A>{7)R.\U*T@+W"5s˗VX|;VZRǻEeW\+= : C_Bv~Jݟ?np *VmݺFƎvkѴ5{ԢvA%4b(G~*OD K/ ;ȵoߎ2ȝ;7)QvRSݘ#u=b%LjYbDw*U$U<\B"uدmTzMU7шW_qMdw/#+hKդBz f|8Z2{`s'/"RM3 &"_^r 3ҙ^Iss2&xѐ`ϒ%Kuޠu8H:v"y A; 5RYt1vکjմd1%AO6ty40,`e˖Nik.#)07# ׏d:gGh&oTx9 `Ԭy xY3FxAtwgL/3L0cBP ~O`&w3~ &=hWRR [ŵz1u Q[%x9=Q}oL$ d'ޡ Shښ˃ǖ-[t5'qhĈWqdtڃH}Ҙh]RRD4x]码~z.;ѵn5n[H$>&8ǽk,@ V ź5MU7jRXEv-eȒAK(is39'!>´hb'S|(ȎCD"^BF*2?bR̉Blbx ,v=)E,W> Q(oyDm[ѣgXꄟm4m-t@4:'2̌LlO^DP_4&ZDMM)B5gRh9%=7~w9;G"Lbf*,lжm){TV:̙3%WN[itg$N V,A:|w*S4͛WZ޽vEgq# f2LďwI%Jvw [YgviMBSD=D Y]F>,\X&aFnrX,΄xmk蒋/N8|E"`X"̊flX,E"`X2!̄bdX,E"`X"̊flX,E"`X2!̄bdX,E"`X"̊flX,E"`X2!̄bdX,E"`X"̊flX,E"`X2!̄bdX,E"`X"̊flX,E"`X2!̄bdX,E"`X"̊flX,E"`X2!̄bdX,E"`X"̊-C˷p^nʟ;_>(7- Kʊȴi筴aF*T"EdڴoxRuũ|r#6NE"B`iɹQJ]Í?43;pcc,e03 ;ta';Jfmca~So8#GꀆFunx'P}@i&bݑ#GhΜ7o^(gΜ1?C_=vMoErJAS%[r08TJ-lYd6n{y}sڂrD[ْib,KtMKܤqbKu^w:|4D-+˞igbȬX3L fYdJ(9` 1bdԠ&h_P-t(wU^<([776u<@"`MY_ic:9Ѻ{K?&KX3KWHlVg0'OBLcGt1b vqAu#jպ?QWg7axqj2sEďY>v;k֬A^O,y_j:S$]<KҶiEU5 慽^GEԼtm?,4ɪ̪%wҝ>}i?=Kb⫭Z~v}ڶi(syiv4fXڱs'5{ UPe0񱶙E1W<.1ڼԻ9zCJtThq.d0}} 5yØhiXkX)q` lX3c* X3 Re0aN:Fi,<19fX,fHX[R'Yi9(+z+~~Rg^Tg )MuAʣ.! I꜂I~vJʡ-/<<aŶig$*EiՊdZNS.86[ƕ剋x%(v(hcT)o>ǮHj*:ŋ%K*{b0 n:ھ};sNY*Wٴy3߷J.EߓO>DZOʖ-~z0{@+*Z!ڻWhIJ.Z/Z;,P۽{g޼yɧEyDV^Myby0?C~ƅ UknYvT;~:(Q>l3P(_zy:tmV|˗OWJ*_ںuJV].HcR|wfܹsөz+_Ү+WN*TWrhv:]B*F,`nsLҹg䤊g夼'4a23_U>I9|KA{vul/AIWo<ԴҘ_[\VSOɓ MOWPyQ?kUK Zvg3sR<ſa1ڸG}sVsx.1b Pa?ti\:~?aA=)VxN=LF)32ChO(y(}LgA9Uu;p<+~zLɯQ&]ovסWviw `"?nVzyFt<ŻS_߾8}rGQj~q ‚iQs nfv1?Yo=jnh3GnQ刲Fkb f*5$Fp1sHAD o}6A}f ~:\2te"WCjq*]_1]m5<2['<1 cBaR4 &F\iS{/)o]3/VeGu aa8-ETPuGz;($0Om۴ c4% ӢKw3ӳ7 3g>%(1~}x2} &4f6}69sNjԨa_f m*ܛOx.R6E:uǺ"I8دstnM/<^v:vh&8y={W^y=ѽUR%`ok֭ǻuu9=x :bVP`ں>(z- T5k֘^= sNcQR崴WzMA?T"ߩp´d07ۯz{;avHs} a4HlbusOW'p(՛ LIU"w^h#MC}?L*>AAjθEaiN ٧6S* Iw< otP+m5N?Y'=Qn&ƓQMN s~sұyMi}qko8;&|c _n^&&虌8M9j*GłxO`)H'',Cו¬Z*=|s~"zXY+̂o꼞 DT&d̥ح{.|l/bIg)Fٺ Z2e:k*WP)CTAC%Aa;gX.v^:,Iۗ_smhLرC9䥡4u4Xe˖]iCPeXzvռ䁴?0+rr`rP>ؔnT}4~lb7)|ɃJ^5nt?UX.]J]j72v|€`u 4XB9wh^^B6meAΝA=GBJ黨M<@R)B>zmg\^ SjE>TZbw{ԧq~}Yה@2a EkVi8_ݠSI` >n_Q^3&aOx!B*%(]]$97JWV" B<bx}L0>L/M߁C)MÝݥp؄gF7@%a8εQW%#)3Gq¢jiC;8o|%5N 0ඤ]q4wPL[UU[kͰ9=^T1ڭL$ N?'nPPvm}3SϬe0@zNId+872@mֱ]:8JBt:k¹wu(D_0pHp¤ '98CSp5o˜&?/"5lv&:':1wD*5t+‹C}}u㠁;IΜ9KiԨM+- ÇUj?3v<%?hFv%Kϧ0oÆhK-SyncR79jkj{ԡ+DL nNE)'ΰBu۲nh1K4'Or1}䂕)ڲZ^ugڎDҚ$ d /FBL:&d09m7Q2ǥF+I0gTZpfK|)Ϋ^gsa`yJTGAp|cYv5#sYo}M3e0`]H,5'9$uԣ; !g?a0E񷪑zt,\U/\J?՘8bWs3WbG.9DOe0@O+PjД ;kUzpLls]&' t3O W>ݑjsgVRVu&sXAPuGh be ̎bebqO30: g5= )!qb'TڵJɍX.TfحYm:v!$ٿ_} *_ruDϩXb;~ٲe ռF d9 JVb0qoЗ8[sz1&v&Sa:F 9GoL&;͘Pq啗5W_d`0aֱqޠ}r%Ա^dHsgSYd ާO=RO,(07VD":L`b|lc /\ 5R5ݑ`pp&Ƥ6^LWQoӏ.%yKV/^c=yuq3z,瞑O^7]/ #/YԒKs|'lȑ# VSwMG`N>].ms$8c:c#V11K&0.]*:\&SOmhćtv.~5zh>Ì͘q6S4s]'J%xT0Y>n ^ѓi2?HR`; MwŽ[3-0o2"88U7OTsZ;a F湫ym?wI-;/lVg 𯐋g L_̥#0q^n\較H*qֳ}J#w!|".ˏY 2^f(i,)071MT+a7T!=} Z1]mZg\ n.o_\Y5ҵ \뛞Rn\Μuz1 ,ӏpMݨ.]mK㯣 Yg0}kb>G0="Ab?5y-b&}-Zg#0.\(="H+ZSI#y0L QH%?H ^<,iYT:"015ƪjK&0%i 1w"01Žst'Qqq$Z5sL[%Z_X%>U|H]h?9fZP̴Du7| Lt T~/9*Ktžܻ_ju?mX}bq0ى)D溟.0k5|Z`" u9UK%c {q~[&sl̴5zj[*w}Qn;7T`jg>Α&Ӆ`Z Loe^SFx~Cm"n}{91p\'L9~|xyӟ~ɔuLC%(.ڵL ]x$g>%EK?te:.u93pI1C~\>*0}Nw-{M08YVեqKE`"~`[0誡-~\q1eN;[9]Lxq5v Gf"[%Y>1Ry=ᒵ`@&\UGvݦ^HYY/e|ޙLOwi7oȿ M8Gg ;]b^?loz 8̀.-n1@~w88 r /S;AO>0a.\̳9/nd6l#$ԦAq*M48Ϥz?O/Qc03"/cf@`ܫ&vmF.S~ps #̟Ə1>c;B ɇ։s)w<ﱗ8v䏂]`휙 9\F`&4QqRY0Sms?36UvX/tb3 Wq}qp/0&7axYwOX/lȺ:%%]o4ۙ%Y>1Ry/W׽(05_,oձߎ][ٱWt)k o&v3x __oZbīعx9|G +7ٓהva~" i j"dS\L!ot@7:nqz4ccBsfϰZ"L3f̔c+: tԞBvJOEQVO<)yUoюeONWرE[ъKq{]z ۹8pצEµx6/>`ZAa; ;6!M{e 6L⏳R59ֺh:]EV_'rx/>0L{ѽqΆ?->NΝky{b0J[s'Dw.] k.{q v 3xmҤ7r1pc^A{`{8z~MA<6/}gڙ>7StIOtwJHwmq^t~;-/onl}lu^Toe|uVw<&XX?2_]h#4D{sCRO<,(0o(6a&b`Ԇvfb1w¢-k}]Y]|͊FĹ.Foz1~U!mDOꂯtqO$>/:1txI?tAnؠe q$_`b駷}Nq EpFQ#[]+6m*ڷaΟ?_ fGT&+zw /FR֮Ȼˏ?h~&/S PxC*f 6[9sslQ*~ݵ= e]v&D^{_O,X ~0eIR^bWد[1βuC"~'PG/VofqǷp̡ Us!_kc_sdCm\p<5{*=i҇A\_`bn+5ʧxɧ[O3~Ҿå6wSN99'ي/0ubr՚ jU~q.ofw]XPR}q+9WL 5sCCTs*0vnzFzц%Z8n=ۥ% \_k؆e-wE)?dp,}0=8*nf?6=?sqej+ji wuE8^fU]"OzBh>9>fq=7> 2r]n~5|_M}) qw_"iܭ#R2x=k򄛬j ww)70exM?M\qaXuxغ%ɴ~ +^\;b87ôDد?$u:}$}P۱qiH=;rg++:ɡ @ah+cD-+VCw0 6_%lluoIッDJiaۥC^(yi3}3L!˴Y!LrL )/o_S OyAWY)p>Ыhk3\$.)?*oW3e0$3J>&0Ä6xSm3d \rJ,C.}ryK֓} `_C %( +^fꠗ9Do0mFS`sT_. Yw=8恫w? to0fUKK/>z~1O%Q?f-pn2}顎Bƍ">B8/ghDQux`|и/-nQ?2?Up>8q1/M7{ &GjƐ_{x}S7Dz]c#{Sr8scK#[};m:Yg3g.C쮰qʫA_'/i_|хҳuq`r +/ S p|a c-хAhlZd1h6y93mt;2!A7n|´68hne{״vNN8thtu`n=.ڄtq,aǣÆ=*>x#|8WbćZwN04ծ(^7+Nhb<^WKM}pC6/ c1dqa fqMKeE[m&>`ܚ.#VAb-06~:mG:gCL7H䅖Ϭsu$Yg>:`Z8w5>~+ n.`*#O78w8t4t\癱7a!<2}~"AtUƹ"ƛwxw'wВ xRHCmНTUʖUML\ycg<;s3LkeѫlI 7i\3}?t 7]P-6zå>X> .5LK%7+نנV9zq3|M}s\Zov1lvi/*Op")gdC _q~g~Ætp%mk%K*˼y?.k9tű# 45˃p<.֫W7'Nc|mqovm[f?{ѢE#D͚GSЊ kԨaFV*UH7fnɱqg푊>g:;|uLM1|,[dƸq3Ɩa|&|ry!568tasW3kg34w<!yjX:duxSd ex׵RcrVeې~!|h8WLZp{!@k#&0($    HfP`dmHHHH6< X Dsg$@$@$@$@K`O-+V ̒q? G3=^]@\0sI;xr&B$@$@$@$@y#@7̈HHHHH fa_֎HHHHHF3o 6 > fP3#     (l}~Y;      ̼fF$@$@$@$@$@$P(0 v$@$@$@$@$@$7yC͌HHHHHH P`eHHHHHH o(0 @a,ڑ @P` 53"     &@Y痵#     jfD$@$@$@$@$@M/kG$@$@$@$@$@y#@7̈HHHHH fa_֎HHHHHF3o 6 > fP3#     (l}~Y;      ̼fF$@$@$@$@$@$P(0 v$@$@$@$@$@$7yC͌HHHHHH P`eHHHHHH o(0 @a,ڑ @P` 53"     &@Y痵#     jfD$@$@$@$@$@M/kG$@$@$@$@$@y#@7̈HHHHH fa_֎HHHHHF3o 6 > fP'X,&&}(3gΒEIݺueˣdvOK%kH7C9LcŊg۲ШR8W!|w|rvmSOիBnK`J9sl}7WަT}֕H`3r…믿Cw޹TR9!ŋHWG}HN  @{UZUO.۸<[IDJ!?3ԩOqE~Aqan㫯uֺʹ5j֔F cp{cx}dJ+D.;Jo#Ŝ9mZߪĒ[dIdjժv5d6sL?erdwO5nߪUdܹqazRJ&ύ͆ &;vc+"C$P`(0 lizq9:Ȅ j#C#Og hNhn79]:/~*[n wܩqʑGCZZOʮ5qi%WwZވ#?SһM'+M7"=x(v_ÆK6](+'~u"4b-JL^\zi֩SGl\^E_@̃H@467̐ocZJjՂ0@2ŋ-(m(KM7Cm&˗O %Wő-ѣ3iB:ck%3fч؊tB=c=\]!0ufx6 }? lz(09/I`ʫr]lmR"\Fb所{2M^婧Ry\t)`/du q "ǿiïJ9KSf`">7vkd6K,d++ hm]]ڵkֵk9,Y|e~pCA8>ʘ:@LG48+VziW^~Iv?Xԙ' l(0/I`s( t-[&{V}՗anqx4ǎa"8 *9Žϝў/tnvDjyL↭ 248saQbe#~3cgMV,U5fnq遲N>Ԅ.`fƥN< ͥ]Kb.!><߯1-0_~e1Æ=ses ٳMu#rc]6>(3ؾ9F}̼z!xѱ1|xP3{ }ziWnes(pٜ9. >om3*V{ NJHu5} } {M&$ҭҥKya݈NC꫽o9H8'+67b3!~׷0zD^5zYcz{p+pԆ{O5xƌxK QFi#|G8駟 ~83| +^ɶ_}ulPnӕ5f>=>١1A"E\}4(eF%fti疒N= i1\K.=ijd6|x=gF$\?9s dS^\:}x8n 92dwNe{OT;y_|"Lʕͱ. .I47C!RQi+T݄D`#3 ١CVGYEd/tZ0dqsfXUtI1gKZs3<\GſҮm]ʹ~UN@SU*TTE`O5_TݗA|D .%"-fHB{ H_ם}7A8FZ^ /h1V-~ ʅVie_2Xqts=?O=5  } &\eA;HghYq9kw[J͝{D z~m0K\c_ظzqK$ɆB g<80`N 4oqN&qi`:}ֽ"t@ >Ct6Ju٥O<kZiq S9r AZ0 {4.=;MyG<\\FkT]->gz}qo|뭷KK"lj!dlhgq~ {ۓY}/~uEwO6^axi :.~q@26Ut " 1ā %o.~v68g,Xsˆ/! K@ HYdS .]JFdZi2}ϽHvf@YWgtUӦ->Hk߻TI7jmݑ.D+>.dk\FE}Zouͱ0!v] !7tѿ5>t9Чϭz]}{}gB&ZHjq;oAIU`Õ~f. kzu.QϝGO{k6؏xCx!>ɓs5L``ސMyq?/9 j oO}}^# gSl (e 8WɗD~9qL!K5a Yi L|Eco8-eb#̌ˀ^%UTF'0r'KW%;^|EWK ƃWR&6rQw$ losia&g:AK6RZ ZJC5pmD {? +n6'-ɒsqE:]-0QO)O&V׌y: QgCD[b#<KL 7<ܳ)93%8wg/0= H7L3l]| ٳ_66  "tKS`&.8a5tqÃptÊ2TuW~Ypkv_fxpuXT?.^TX:J%(%u6xLqlͅg_Y˛N ̕__ڑ1DqP=eimL-^BΙnmwbKYvsL ^"./8seו,JȸcD^#x[CӱH9C7E}f*0E}te5ŒsZp1<]#K- n2݇_-0}uC=-qNyGRPdW lJABOT/cC ( L:4ӉryAҿѠds|y~҉{Q><1ޕmƮpő-2g}c𗶓d <$p` O?nK;T9޽$e1!<* iNTLµs?|__?4s~kZYS%ZCS85f/!i&9zk:th'=w믛9L ]O6"`F;(IqΡlq휙֑Jw(q֕*drQwyyqYoecs*`X;^߮YBHvg-}nvҴ i{>D~qxڶ|1i)a駟fc9'?5~o7ɧQ=RZ:AgL.R6קK#n=:dP0;S~v}PFqDž-q9Ҿ}8GP.}f:MK+l7;`#犝1cu䘄08b:GxA8&|ٔi\Lk&ʝwG59ٜ0ӎlWR@m5S8{rqǚ傃e6ؠ\!R& R>!Qɗ4_|} |Dr\w |Ȯ$M96(WH P`2\%_/x n&w/DvqAuqX Y7C7 Ji L7h_V_̦+0Oj?1anZnzy312dS\4Iڪt/^Z`Gӽm<߿o1a r~Q;7bI7}1m~QSOWxq2-zq9ĴFRnjݷz xЁv=̵ٟm f.rHHVdrՂ˕ʇ)Cp5n1྅>E q/1"V2?5n dJ ]I/SDK{ &J1nMq{M(c-1&ƙ΃~tc0!UӃN"0=%Ec0qMhf)fSdYyCp^3,h"1=l&-`©KAT`ʽ¨ \evi'[1EQu f׷c맽$,8u͸kw v*,^Tƅ!_v@ wL/sߕ Kc>He׎s\>.%1ɜ%+_cxL}|>=q1Էlʫg2~~C{Lg/c]\@iHw &fi!07C{!7-x/SΙ_|$+vk#h)LţKAtT^Xpe`:jٔ;YpXe.l#֕^]"mz_YZDA;So!ʴ(L L}ٯWH?~Nk'?>gD1 ̌~YJfiQ(= ݋Z _h2]l T H~/xԆ<= bAhE!=E`ǃ/sϭlqtԻK'"}x /˨+^w peKe_D>F]h^^(m0xZ`b->S4zKd^!LS8Ktqj6v0>2Q;.ո„kܰsiG LCp\7ߡfs}yLDuܛ\={VUbk1xZ h*뛾W\QbL^KMyQ6vX-Z,9mx|)tjw ѭ[luO="6ABL ]I'?vF82.M婧̸ƅd"1_G6rڗ۱`qf= ^AZj2s,.Ν/bL[!ŷ 8'8nk͞3ǎrk1+">'D-2[:oxyKuCd0y# x5ztH Η7y Yɦ!i%qe.ǒ%?7ߌ`D.mJm5ʧ~&Oma7qFӢEs3N|ŗωdNu^6?FYϰ%} !Q c$q0w3eP_=Ҝk;k=;͊&DԩjU$3Gq3IFN~2$cyr޹<-/!^|q9՜#[;Naw^s6߁~gsx8/8llD9Cm?8zL-ʹ~[RrA 1A(v oyodSximvnR4;>.zK93h)SsN`ZЮÝ~ѳ#j(\^ĉoi۸1O*BӕW]#GPϦ0gB%X ;XxG~rꩧMI&0tŵwzٔ n6ßTL>g3d"0/_!_9)}q2_6lp5t&`rrTz2bͼP]ML+Htz HTףn2C7CJp\Qw}^?]tua y>}nu  3ώc"Q;2-J"r,̍sE7&=4_m..At4hH\7P莅ɽ `{E2x 𻸺 xM֟\1.O;Lq,q50\%$ \Ke{}#= cS5 wNƺAtEwkȱJ\p¦ c[EWU\Qɸ|qJg;u]6o \w!?*\wcGT UWdJJybL:#[\8 afZ㺟oZ21/HCLqf墼O?Lst ׉VXtz9jxV/Ҏr NeZ?ͱQa8 Z@zf|mBW-"Z᫡ +W:_%KwflҨQKއ8dnYCRޝũ5w\)W\[$,7Z\~:44]-s-XPT" >u:tRWZqdbZ6EEI`~$;*+rwc4,M{um印t=CuGwioPbs nk?\pa^EO0p]36)g*e,qwѢe{w3ϝ̿v1m&rQ^s~g;'zʰ2fMw0G|ȰY?>#i#~}ֳFI띙ids.I Wmd\HA7/7[tHb@ ̼eΌBo;osP%sfBH`ĈgeˣXsz;#mp:`tr ~['fJ6|\$4H`$@IJ8@% cf0vF$ǵ`K̸m'+ʱ&$7=:Tv٥xL?>UgxU xSd$P(0 XGxaRLH … ]1c >;bp6WH'; }G/0 ^UA8_+ޭHh̐ fLr9?%K/`o4MGFΨ<9̸]v%r5WŅqH 3fN5c%/;8A$PP`su/of^.W '4 bƫ|76a:Z&>sN4i,wןRlג' K>5N5s~RݴT66=\G2eS̝H b\      HhHHHHHH b\      HhHHHHHH b\      HhHHHHHH b\      HhHHHHHH b\      HhHHHHHH b\      HhHHHHHH b2G`ޯdrJRfvY_Yz3믠PQ,lsHHHHI3W ?w; W-{5l3GC*'5* k %=,FfUm @> P`6;.03&D֮/BwheDZG WHHHH6I`J@i KZ! crsZV}lS`(B$@$@$@$NPzJ[`Y&f+S^bZAĎF*UL8mK>[IS`(B$@$@$@$NPz A`*#b+oT(ϊ3HHHH6, ?1.o¿_ɖI*vElշLjc~\N&},QI*LHi-۪z9+l&},}k=ے囟Ut-n *J}uf5z[,L#3o{3F ?jm;̊kGIƓo0UCJ.+Oz`iϧ0-[KZˆ,Y6(O~GEv'-EUvl5);Oi ͰN.\lZ\[$q'eЪl"/T-^\%)>.\7W]UV~dp\ @&(03 y5IsLQqlڴ@nu;3(6g ^ tu|a0iC+zkU)'onObO&0F:X%C\ۊt毿s./Mh2|ߌөZ8uVwZyMVN#    (Qd6;&w=(nek~hmq]}0-P@2mMeIBq.__Zˋ"ή!hs+dGb';.}tk]r]|Džc Lq &ewAx%GTݓognwfOގfCv.{kRɶc;xAϟ/`eĥ>QNߥ PT%    P`kdr]JV+gEF,ޙUԚ Nݦh@35ܾm Wm.LZ3c =RAf?Zժ'4S.#Vuzv ^f9Ux)1%٣W ^\'k-Z`+&E莵ƘΣUcMZK";t_raVZ x u+Vt&ZNX< =|}]쟮yj[%yQW&.IHHHH Uڀ/VN}hYw#eHeLxE֛sZv𻫭]w}@_:lnVxGZ f.iRZ-ƙJ6]R+JxWDq1Z˅DKjÈ]X&n0T]f -d{s-:ꖷcj ݦi$@$@$@$@$- l Ttn 9a":cKMJgxu>-t·`h lr}ƨS򦲄C+T港Mm.& >n @*(0S碩?$KE!T`f.𤽶$nE &ʻq`8e2w4:qt$093VEz?O%O(:[ӯoqӧ@`?Ֆ^d}V&   Hf6XL_ߖvJ@u~W\ L䉮ln2er~{j9c-|gīcۭۡoc5*. kv1S@O_<4W#l],0cw5MRLE3>^U$@$@$@$@< 2p |6oiuqTxx86UGџ=Ock9mCVƩPEbZn~XTu\8&WŏmxEc*i\`zAF[YykjMLsXYժZPV?{2;&jJy[~^a6d+3W3f1&#&_X)תm-ӾY!)?nX#_d j*Yo&&Q#:8+$@$@$@$@$ 2r) -nTψ)>AhM^0Յ(zEh.5T0Kj^ ̕DC҈0_1]S;.PLk䂥=lV>h?x'-I;`ߒqҵuQ!#0q9K 궿i]zz rǏSwpg9唓sHHHH|>t]Rtbg0aEC+5[Ѵ@ɅDwisc ʂ'͘>)Q.洅.k* }&;V# l?ӭh[(ݾGU.VK-.Nry5EGt&m#EWb=*}϶쇀*鐳n~nh~ 6HHHHB P`bx &3Em\I!,usݪeS(ڛ,Lř2i) X;47ewtqz־Lmu rA8Fuތ fX3'     (h}zY9      fN$@$@$@$@$@$P(0 r$@$@$@$@$@$?c͜HHHHHH P`eHHHHHH (0ǚ9 @A,ʑ @P`5s"     &@YЧ#     kD$@$@$@$@$@MO/+G$@$@$@$@$@#@?̉HHHHH fA^VHHHHHG3 4 ̂> fX3'     (h}zY9      fN$@$@$@$@$@$P(0 r$@$@$@$@$@$?c͜HHHHHH P`eHHHHHH (0ǚ9 @A,ʑ @P`5s"     &@YЧ#     kD$@$@$@$@$@MO/+G$@$@$@$@$@#@?̉HHHHH fA^VHHHHHG3 4 ̂> fX3'     (h}zY9      fN$@$@$@$@$@$P(0 r$@$@$@$@$@$?c͜H`'b -5v1gLf̘)~ѦwЁ'+WYLHHHH 5qfϞ#:r/_Av?)/ ._VARNȪb1Yh̜5[;vmqV/_>8cݺu渹2sLYd㎲KƲfmeD>GI5NsC%G˙gcС-(ի7K*UERB`_*+W_]^xa:WI&8$@$@$@$@9$@CT#[ܹsbwӊ_#uk{tۆV맟~v/ӿJQ:h@w}ɓȅ˽KԯUocB'ZpG6mc ~l׿_is*+fZHHHJf`}%3:yA(9k,9! {^/"Wt R{t;5 G.>2TgU֓ oYI[.څ0f#-ta *:^q   ) ̜,Ĵ2aUV*.{4iBIr9U)J`vX@3V[;4ŋC 'zz]>4m/HڶX֭#ߚn>iôekW!CMW~&~lYZj3-\ytRw6kvtswɃA.95 ;s,iԨa "Wz5r*YNm̙#~ԫWvZJ a8Y|iXzu\r^ eYn]iذa#d3. Kf̘! ,d7\ L]"}8rtb~a E[f=Wvm."_#s]vi"Sŋ%~իWss:(ϊ+m5kHJ'r0|,pnx[o_~Zdw㔱S-e6 G ])9%5rCyb;츳o^J>X^m_ 'O3ق۶sΜoc\xqߕ K]dI\|Nd˩Sʀnjy94hH¡DZafLSOb^}FsI{svoÏ8 M9~1#co # b,gZf;~?˗dž ΅{?Hkķ.v_[2tXWg߄ \qM|ʩAӷaqqcO4~`8 eiӃ:w;3g1c]p2r#Oݯk!_    M ]NN[&'LT6Ƽ4Jc9w w-dAvZ[N8ᤠhUVG ?nwyǟpmeEwyK ..]Ƽlc|%2 z8'3 i-G<~Z6ŝDr"Dg%YbD݄uZ^Auq-F4͏a\6mt9ēmv.zrcm5Ú.}Ͻ6 rfĥx„.(2iJ4װaт0p8Т.p[osI'J ΀xnF`uqtfZnL%zp-}uiͬ%mڜ!-G؟4= :%O/8 lցtknrՂZ9{eˊZtbO?L쭷޶MwOAZ+ډK#bg}n-fSJq$@ˇZb`kւyƙgxI{L]Gt&Z -̮\tϵ§d~ yj1ZΏ+ fP,Z'ucXYUJV 3r7e7bocЊ)(P>vw'b{ HHHpEv{-0/arE]dxZ!?3 y;"@֍EҴH]vJqDl޽ܕ$c² K ѥKaQQ jT:<(7Їu-!i>z.8Xj?@hA4qq%J`]uu˦\ 0\`+Nv!O~5_1K?3rk>Th?AP/rmxN$@$@$P P`{IZ%2-Ñh~E 5B+^2KhIy$da鲖$0Z&C ̥Aa% K&ʴDktHAɿf 9t-Q.m\Oo!BaGcD?P`f @,#T LA`E#mZ`fiGsEm:-ttp-^c@\MY`F N:^ڴMDut頥B - ԦJN~tú=[o.fW$pD[ukvE vϥ̦ʄ%YVTt}(05 @,#V lf#01̽\疤XCMg L]8]U~I3.xi(]d}/Q$ O&t f_ 8:L<;hݵe7-0:ZҺ2j} (+-i]9uد+f懍8 LGK  (len S;ZK2%Ee#05RO.ziJzhPi7\,o'  'w&.^Z4e skBqXb$ċ0>duBROSrM}TBL@9$[G>`e%03-.'z3`eT]R`j\'  %@YFέ1zlt).t۩1߿0mKv3}ca0QLB`FP'%@)vAShL| L-6-Xk:r3U7jlʭ篣+9F>hL>1n @a,#ucZ8%]) S:LwRd{!S]\7KxW6Lf+?zLJM <[V>A:>pGWw$_~ }=nGavqA<:ܴnʎtеR lʭ= 0j޼(hwe8P @aHW`n v_R(%hqdK;wֵKg^zIg&+W?*ɍ{{yI>xԫW/!Ov_ӦMCҠA? r[x!:3-+/ؽ<堃 @ @;|=0{_,Zpzu@3߱(_G 9wi^|h?r<꘿SOG|eyS%AOyZ˿rQGv^P/9KX^úsOmŊ?hF]Gegd׏toA]ߞL60?ʫTofk`ցۜ-I3o|`yA7)墋.w%oL|ouV9CR|ͧV>#c> @ _v\YbE_i;g!oǿ駽Cw駿?i?~[ϟ?aǚ6gn>S3&'Omΐq;&<}l' 7|4?{QZ:ѥAKa0a]\vm9O1xx=w̘dx{Wz߽n\o*'w@S+jW/|g_:'?}Vud.?1gꍼ%GO~s>Z~/]de`ʗ* e͙jyY.V=>g?=iu:ߛ /QG-{n9藖7?gG_]LJ>|V׳gC Mk:XjurH ?sA9WoTs׳^rYn_o^WMW0}v} [_W)k֬n-^L>=D^CԱrfP+5N;UK=Lei^D}ysy̴}```F?~ ۯ}j㑹~է4C}V_Ia @A ̠ňE @lf%@ @@P3h1b @ @ 1y  @ T ZX @&``fkL^ @00# @  @A ̠ňE @lf%@ @@P3h1b @ @ 1y  @ T ZX @&``fkL^ @00# @  @A ̠ňE @lf%@ @@P3h1b @ @ 1y  @ T ZX @&``fkL^ @00# @  @A ̠ňE @lf%@ @@P3h1b @ @ 1y  @ T ZX @&``fkL^ @00# @  @A ̠ňE @lf%@ @@P3h1b @ @ 1y  @ T ZX @&``fkL^ @00# @  @A ̠ňE @lf%@ @@P3h1b @ @ 1y  @ T ZX @&``fkL^ @00# @  @A ̠ňE @lf%@ @@P3h1b @ @ 1y  @ T ZX @&``fkL^ @00# @  @A ̠ňE @lf%@ @@P3h1b @ @ 1y  @ T ZX @&``fkL^ @00# @  @A ̠ňE @lf%@ @@P3h1b @ @ 1y  @ T ZX @&``fkL^ @00# @  @A ̠ňE @lf%@ @@P3h1b @ @ 1y  @ T ZX @&``fkL^ @00# @  @A ̠ňE @lf%@ @@P3h1b @ @ 1y  @ T ZX @&``fkL^ @00# @  @A ̠ňE @lf%@ @@P3h1b @ @ 1y  @ T ZX @&``fkL^ @00# @  @A ̠ňE @lf%@ @@P3h1b @ @ 1y  @ T ZX @&``fkL^ @00# @  @A ̠ňE @lf%@ @@P3h1b @ @ 1y  @ T ZX @&``fkL^ @00# @  @A ̠ňE @lf%@ @@P3h1b @ @ 1y  @ T ZX @&``fkL^ @00# @  @A ̠ňE @lf%@ @@P3h1b @ @ 1y  @ T ZX @&``fkL^ @00# @  @A ̠ňE @lf%@ @@P3h1b @ @ 1y  @ T ZX @&``fkL^ @00# @  @A ̠ňE @lf%@ @@P3h1b @ @ 1y  @ T ZX @&``fkL^ @00# @  @A ̠ňE @lf%@ @@P3h1b @ @ 1y  @ T ZX @&``fkL^ @00# @  @A ̠ňE @lf%@ @@P3h1b @ @ 1y  @ T ZX @&``fkL^ @00# @  @A ̠ňE @lf%@ @@P3h1b @ @ 1y  @ T ZX @&``fkL^ @00# @  @A ̠ňE @lf%@ @@P3h1b @ @ 1y  @ T ZX @&``fkL^ @00# @  @A ̠ňE @lf%@ @@P3h1b @ @ 1y  @ T ZX @&``fkL^ @00# @  @A ̠ňE @lf%@ @@P3h1b @ @ 1y  @ T ZX @&``fkL^ @00# @  @A ̠ňE @lf%@ @@P3h1b @ @ 1y  @ T ZX @&``fkL^ @00# @  @A ̠ňE @lf%@ @@P3h1b @ @ 1y  @ T ZX @&``fkL^ @00# @  @A ̠ňE @lf%@ @@P3h1b @ @ vGRw288E^ @C`ddr۝hvҞӿK/G~]eӦeehhz/Wq  @$xxrʻie^{z=[,}^7wvO7  @)u-sfV{O[f*{.YӍ; @r \lxxc^̚kO灹e˖[hoti8 @r l;Voi<0u&;gA @ K`==c#k`njV]^' @E>}6^Wy޼=P  @E^=0wpnE ? @ @@PVS֯0S+W]FF#3WX @C3.^Kzjln40r{M;2e._wnן @ @n{lڴqxxa1m7YƬA꥾ܹ V @ W*x}ؚ8X:Ϝ1TfΜѮ]vٹYO @ .M1^ye`۫g3`]Yn& @D={2otrcns[l)=Ԭۧ>#iv>O @xWSag ]wQk>0k:7F @i 4U J @f~#@ @@3MU @ @  @ FLS @-``G: @00T%( @b  @i 4U J @f~#@ @@ACIENDB`gargle/R/0000755000176200001440000000000014456265227011731 5ustar liggesusersgargle/R/request_make.R0000644000176200001440000000427214431310014014520 0ustar liggesusers#' Make a Google API request #' #' @description #' Intended primarily for internal use in client packages that provide #' high-level wrappers for users. `request_make()` does relatively little: #' * Calls an HTTP method. #' * Adds a user agent. #' * Enforces `"json"` as the default for `encode`. This differs from httr's #' default behaviour, but aligns better with Google APIs. #' #' Typically the input is created with [request_build()] and the output is #' processed with [response_process()]. #' #' @param x List. Holds the components for an HTTP request, presumably created #' with [request_develop()] or [request_build()]. Must contain a `method` and #' `url`. If present, `body` and `token` are used. #' @param user_agent A user agent string, prepared by [httr::user_agent()]. When #' in doubt, a client package should have an internal function that extends #' `gargle_user_agent()` by prepending its return value with the client #' package's name and version. #' @inheritParams httr::POST #' @param ... Optional arguments passed through to the HTTP method. Currently #' neither gargle nor httr checks that all are used, so be aware that unused #' arguments may be silently ignored. #' #' @return Object of class `response` from [httr]. #' @export #' @family requests and responses #' @examples #' \dontrun{ #' req <- gargle::request_build( #' method = "GET", #' path = "path/to/the/resource", #' token = "PRETEND_I_AM_TOKEN" #' ) #' gargle::request_make(req) #' } request_make <- function(x, ..., encode = "json", user_agent = gargle_user_agent()) { check_string(x$method, allow_empty = FALSE) method <- switch(x$method, GET = httr::GET, POST = httr::POST, PATCH = httr::PATCH, PUT = httr::PUT, DELETE = httr::DELETE, gargle_abort("Not a recognized HTTP method: {.code {x$method}}.") ) method( url = x$url, body = x$body, x$token, encode = encode, user_agent, ... ) } gargle_user_agent <- function() { httr::user_agent(paste0( "gargle/", utils::packageVersion("gargle"), " ", "(GPN:RStudio; )", " ", "httr/", utils::packageVersion("httr") )) } gargle/R/token-info.R0000644000176200001440000001140414431310014014077 0ustar liggesusers# See notes about the userinfo and tokeninfo endpoints below. #' Get info from a token #' #' These functions send the `token` to Google endpoints that return info about a #' token or a user. #' #' @param token A token with class [Token2.0][httr::Token-class] or an object of #' httr's class `request`, i.e. a token that has been prepared with #' [httr::config()] and has a [Token2.0][httr::Token-class] in the #' `auth_token` component. #' @name token-info #' #' @return A list containing: #' * `token_userinfo()`: user info #' * `token_email()`: user's email (obtained from a call to `token_userinfo()`) #' * `token_tokeninfo()`: token info #' #' @examples #' \dontrun{ #' # with service account token #' t <- token_fetch( #' scopes = "https://www.googleapis.com/auth/drive", #' path = "path/to/service/account/token/blah-blah-blah.json" #' ) #' # or with an OAuth token #' t <- token_fetch( #' scopes = "https://www.googleapis.com/auth/drive", #' email = "janedoe@example.com" #' ) #' token_userinfo(t) #' token_email(t) #' tokens_tokeninfo(t) #' } NULL #' @rdname token-info #' @export #' #' @details #' It's hard to say exactly what info will be returned by the "userinfo" #' endpoint targetted by `token_userinfo()`. It depends on the token's scopes. #' Where possible, OAuth2 tokens obtained via the gargle package include the #' `https://www.googleapis.com/auth/userinfo.email` scope, which guarantees we #' can learn the email associated with the token. If the token has the #' `https://www.googleapis.com/auth/userinfo.profile` scope, there will be even #' more information available. But for a token with unknown or arbitrary scopes, #' we can't make any promises about what information will be returned. token_userinfo <- function(token) { if (inherits(token, "request")) { token <- token$auth_token } stopifnot(inherits(token, "Token2.0")) req <- request_build( method = "GET", path = "v1/userinfo", token = token, base_url = "https://openidconnect.googleapis.com" ) resp <- request_make(req) response_process(resp) } #' @rdname token-info #' @export token_email <- function(token) { # Assumes the token was obtained with userinfo.email scope. # This is true of all gargle-mediated tokens, by definition. token_userinfo(token)$email } #' @rdname token-info #' @export token_tokeninfo <- function(token) { if (inherits(token, "request")) { token <- token$auth_token } stopifnot(inherits(token, "Token2.0")) # I only want to explicitly refresh a user token, which I identify in this # rather back-ass-wards way, i.e. by a process of elimination. if (!inherits(token, c("TokenServiceAccount", "WifToken", "GceToken"))) { # A stale token does not fail in a way that leads to auto refresh. # It results in: "Bad Request (HTTP 400)." # Hence, the explicit refresh here. token$refresh() } # https://www.googleapis.com/oauth2/v3/tokeninfo # This can still lead to a doomed attempt to refresh, for example, a GceToken, # where the problem is actually (lack of) scope. That happens inside of httr. req <- request_build( method = "GET", path = "oauth2/v3/tokeninfo", # also works # params = list(access_token = token$credentials$access_token), token = token, base_url = "https://www.googleapis.com" ) resp <- request_make(req) response_process(resp) } # Good 3rd party overview re: learning about your Google user / token: # https://www.oauth.com/oauth2-servers/signing-in-with-google/verifying-the-user-info/ # # It suggests perhaps we could recover, e.g., the email during token initiation? # But I'm not sure and it might require a change in httr, in any case. # # Therefore, I focus on the two post hoc methods for retrieving info based on # the token you've already got: # * userinfo endpoint # * tokeninfo endpoint # Both require an additional HTTP request, so are "not recommended for # production applications". # # I had working code to access both the userinfo and tokeninfo endpoints before # I read this and there are a few differences between what I do and what they # say to do. The discrepancies are around which token to send (ID? access? etc.) # and where/how to send it. I conclude that there must be a few variations that # work. # # Google's docs re: how to get user info: # https://developers.google.com/identity/protocols/OpenIDConnect#obtaininguserprofileinformation # "Add your access token to the authorization header and make an HTTPS GET # request to the userinfo endpoint, which you should retrieve from the Discovery # document using the key userinfo_endpoint." # # Here's the OpenID Connect discovery document: # https://accounts.google.com/.well-known/openid-configuration # # Here's the URL for userinfo_endpoint, at the time of writing: # https://openidconnect.googleapis.com/v1/userinfo gargle/R/sysdata.rda0000644000176200001440000000317514431310014014051 0ustar liggesusersBZh91AY&SY2 ~HA @L`&`0AF` L`&hdȞ!=OȞSM=BzFF&#=L5JCdcg"N?TnlK饖-QQReUQJMhy*kvbbI`f!`ɗ%i z6U0y?IeQ]e&rerJjŲKQ|w,Oe)'юR|EϋFteucQyBШ5%Di)I"¦(Kj-.>K䋢*J_Wrlԩ5s vޅt.r &*J*Q>a(`2͂Z $QRP$*5∍ HhKR\TTJH3QRG.)"觖K!R eDI$m.!.TJQR$RTJUIrmʉiI#QU0GR~{I=C$HT*+J"E$7|hX[ :5%YE6]G8ƙzf~Vppu9Hc4LbpIE6coJʶܢLh&> s#deg7y,xvo;|{a?Cf^G?cxeh06Y"8ؗ/rѸڬ ?׹;i~[_3zu'_2TwשlOJ/z8^#7+cf?$ʖ6Q卸9sVG}mD:w';R4bpAp{z[ߋS|jnZx:Nɿ&į/wckuq1+,]Q;9 QCY{jCR1*;S,<:6'²ؾ2<+r=吥Sv2vTQQ*4F b`~6ݎDب fjd}2;9؍cmWtGeCHƹwk$˰R9O[ar1f7Kcm牼F:1# GaҌ,at=SUljaɜV:[QO۽ZM ycrI61yF=18QdJapaMWejFaJ"s]քq@{;p-v8)51/Q|J"N8,{9.UJs&Ѕ&"T7iW68cfr36;^xpCTIU$QP8+|x?5떼~¼93u|~jlh.p eHgargle/R/request_develop.R0000644000176200001440000001753714440166676015300 0ustar liggesusers#' Build a Google API request #' #' Intended primarily for internal use in client packages that provide #' high-level wrappers for users. The `vignette("request-helper-functions")` #' describes how one might use these functions inside a wrapper package. #' #' @param endpoint List of information about the target endpoint or, in #' Google's vocabulary, the target "method". Presumably prepared from the #' [Discovery #' Document](https://developers.google.com/discovery/v1/getting_started#background-resources) #' for the target API. #' @param params Named list. Values destined for URL substitution, the query, #' or, for `request_develop()` only, the body. For `request_build()`, body #' parameters must be passed via the `body` argument. #' @param base_url Character. #' @param method Character. An HTTP verb, such as `GET` or `POST`. #' @param path Character. Path to the resource, not including the API's #' `base_url`. Examples: `drive/v3/about` or `drive/v3/files/{fileId}`. The #' `path` can be a template, i.e. it can include variables inside curly #' brackets, such as `{fileId}` in the example. Such variables are substituted #' by `request_build()`, using named parameters found in `params`. #' @param body List. Values to send in the API request body. #' @param key API key. Needed for requests that don't contain a token. For more, #' see Google's document Credentials, access, security, and identity #' (`https://support.google.com/googleapi/answer/6158857?hl=en&ref_topic=7013279`). #' A key can be passed as a named component of `params`, but note that the #' formal argument `key` will clobber it, if non-`NULL`. #' @param token Token, ready for inclusion in a request, i.e. prepared with #' [httr::config()]. #' #' @section `request_develop()`: #' #' Combines user input (`params`) with information about an API endpoint. #' `endpoint` should contain these components: #' - `path`: See documentation for argument. #' - `method`: See documentation for argument. #' - `parameters`: Compared with `params` supplied by user. An error is #' thrown if user-supplied `params` aren't named in #' `endpoint$parameters` or if user fails to supply all required #' parameters. In the return value, body parameters are separated from #' those destined for path substitution or the query. #' #' The return value is typically used as input to `request_build()`. #' #' @section `request_build()`: #' #' Builds a request, in a purely mechanical sense. This function does nothing #' specific to any particular Google API or endpoint. #' - Use with the output of `request_develop()` or with hand-crafted input. #' - `params` are used for variable substitution in `path`. Leftover `params` #' that are not bound by the `path` template automatically become HTTP #' query parameters. #' - Adds an API key to the query iff `token = NULL` and removes the API key #' otherwise. Client packages should generally pass their own API key in, but #' note that [gargle_api_key()] is available for small-scale experimentation. #' #' See `googledrive::generate_request()` for an example of usage in a client #' package. googledrive has an internal list of selected endpoints, derived from #' the Drive API Discovery Document #' (`https://www.googleapis.com/discovery/v1/apis/drive/v3/rest`), #' exposed via `googledrive::drive_endpoints()`. An element from such a list is #' the expected input for `endpoint`. `googledrive::generate_request()` is a #' wrapper around `request_develop()` and `request_build()` that inserts a #' googledrive-managed API key and some logic about Team Drives. All user-facing #' functions use `googledrive::generate_request()` under the hood. #' #' @return #' `request_develop()`: `list()` with components `method`, `path`, `params`, #' `body`, and `base_url`. #' #' `request_build()`: `list()` with components `method`, `path` #' (post-substitution), `query` (the input `params` not used in URL #' substitution), `body`, `token`, `url` (the full URL, post-substitution, #' including the query). #' #' @export #' @family requests and responses #' @examples #' \dontrun{ #' ## Example with a prepared endpoint #' ept <- googledrive::drive_endpoints("drive.files.update")[[1]] #' req <- request_develop( #' ept, #' params = list( #' fileId = "abc", #' addParents = "123", #' description = "Exciting File" #' ) #' ) #' req #' #' req <- request_build( #' method = req$method, #' path = req$path, #' params = req$params, #' body = req$body, #' token = "PRETEND_I_AM_A_TOKEN" #' ) #' req #' #' ## Example with no previous knowledge of the endpoint #' ## List a file's comments #' ## https://developers.google.com/drive/v3/reference/comments/list #' req <- request_build( #' method = "GET", #' path = "drive/v3/files/{fileId}/comments", #' params = list( #' fileId = "your-file-id-goes-here", #' fields = "*" #' ), #' token = "PRETEND_I_AM_A_TOKEN" #' ) #' req #' #' # Example with no previous knowledge of the endpoint and no token #' # use an API key for which the Places API is enabled! #' API_KEY <- "1234567890" #' #' # get restaurants close to a location in Vancouver, BC #' req <- request_build( #' method = "GET", #' path = "maps/api/place/nearbysearch/json", #' params = list( #' location = "49.268682,-123.167117", #' radius = 100, #' type = "restaurant" #' ), #' key = API_KEY, #' base_url = "https://maps.googleapis.com" #' ) #' resp <- request_make(req) #' out <- response_process(resp) #' vapply(out$results, function(x) x$name, character(1)) #' } request_develop <- function(endpoint, params = list(), base_url = "https://www.googleapis.com") { check_params(params, endpoint$parameters, endpoint$id) in_body <- vapply( endpoint$parameters, function(x) x$location == "body", logical(1) ) body_param_names <- names(endpoint$parameters[in_body]) other_param_names <- names(endpoint$parameters[!in_body]) list( method = endpoint$httpMethod, path = endpoint$path, params = params[intersect(other_param_names, names(params))], body = params[intersect(body_param_names, names(params))], base_url = base_url ) } #' @rdname request_develop #' @export request_build <- function(method = "GET", path = "", params = list(), body = list(), token = NULL, key = NULL, base_url = "https://www.googleapis.com") { path_param_names <- extract_path_names(path) path_params <- params[path_param_names] query_param_names <- setdiff(names(params), path_param_names) query_params <- params[query_param_names] ## send a token or a key, but never both query_params$key <- if (is.null(token)) { key %||% query_params$key } else { NULL } out <- list( method = method, url = httr::modify_url( url = base_url, path = glue_data(path_params, path), query = query_params ), body = body, token = token ) out } ## check params provided by user against spec ## * error if required params are missing ## * error for unknown params check_params <- function(provided, spec, id, call = caller_env()) { required <- Filter(function(x) isTRUE(x$required), spec) missing <- setdiff(names(required), names(provided)) if (length(missing)) { gargle_abort_bad_params(missing, reason = "missing", id, call = call) } unknown <- setdiff(names(provided), names(spec)) if (length(unknown)) { gargle_abort_bad_params(unknown, reason = "unknown", id, call = call) } invisible(provided) } ## input: /v4/spreadsheets/{spreadsheetId}/sheets/{sheetId}:copyTo ## output: spreadsheetId, sheetId extract_path_names <- function(path) { m <- gregexpr("\\{[^/]*\\}", path) path_param_names <- regmatches(path, m)[[1]] gsub("[\\{\\}]", "", path_param_names) } gargle/R/credentials_service_account.R0000644000176200001440000000771614456265227017620 0ustar liggesusers#' Load a service account token #' #' @inheritParams token_fetch #' @param path JSON identifying the service account, in one of the forms #' supported for the `txt` argument of [jsonlite::fromJSON()] (typically, a #' file path or JSON string). #' @param subject An optional subject claim. Specify this if you wish to use the #' service account represented by `path` to impersonate the `subject`, who is #' a normal user. Before this can work, an administrator must grant the service #' account domain-wide authority. Identify the user to impersonate via their #' email, e.g. `subject = "user@example.com"`. Note that gargle automatically #' adds the non-sensitive `"https://www.googleapis.com/auth/userinfo.email"` #' scope, so this scope must be enabled for the service account, along with #' any other `scopes` being requested. #' #' @details Note that fetching a token for a service account requires a #' reasonably accurate system clock. For more information, see the #' `vignette("how-gargle-gets-tokens")`. #' @seealso Additional reading on delegation of domain-wide authority: #' * #' #' @return An [`httr::TokenServiceAccount`][httr::Token-class] or `NULL`. #' @family credential functions #' @export #' @examples #' \dontrun{ #' token <- credentials_service_account( #' scopes = "https://www.googleapis.com/auth/userinfo.email", #' path = "/path/to/your/service-account.json" #' ) #' } credentials_service_account <- function(scopes = NULL, path = "", ..., subject = NULL) { gargle_debug("trying {.fun credentials_service_account}") info <- jsonlite::fromJSON(path, simplifyVector = FALSE) if (!identical(info[["type"]], "service_account")) { gargle_debug(c( "JSON does not appear to represent a service account", "Did you provide the JSON for an OAuth client instead of for a \\ service account?" )) return() } # I add email scope explicitly, whereas I don't need to do so in # credentials_user_oauth2(), because it's done in Gargle2.0$new(). scopes <- normalize_scopes(add_email_scope(scopes)) token <- httr::oauth_service_token( ## FIXME: not sure endpoint is truly necessary, but httr thinks it is. ## https://github.com/r-lib/httr/issues/576 endpoint = gargle_oauth_endpoint(), secrets = info, scope = scopes, sub = subject ) if (is.null(token$credentials$access_token) || !nzchar(token$credentials$access_token)) { NULL } else { gargle_debug("service account email: {.email {token_email(token)}}") token } } #' Check for a service account #' #' This pre-checks information provided to a high-level, user-facing auth #' function, such as `googledrive::drive_auth()`, before passing the user's #' input along to [token_fetch()], which is designed to silently swallow errors. #' Some users are confused about the difference between an OAuth client and a #' service account and they provide the (path to the) JSON for one, when the #' other is what's actually expected. #' #' @inheritParams credentials_service_account #' @param hint The relevant function to call for configuring an OAuth client. #' @inheritParams rlang::abort #' #' @return Nothing. Exists purely to throw an error. #' @export #' @keywords internal check_is_service_account <- function(path, hint, call = caller_env()) { if (is.null(path)) { return(invisible()) } info <- tryCatch( jsonlite::fromJSON(path, simplifyVector = FALSE), error = function(e) NULL ) if (is.null(info) || !identical(info[["type"]], "service_account")) { cli::cli_abort(c( "{.arg path} does not represent a service account.", "Did you provide the JSON for an OAuth client instead of for a \\ service account?", "Use {.fun {hint}} to configure the OAuth client." ), call = call ) } } gargle/R/utils.R0000644000176200001440000000354014442347245013212 0ustar liggesusersempty_string <- function(x) { check_string(x) !nzchar(x) } is_windows <- function() { tolower(Sys.info()[["sysname"]]) == "windows" } is.oauth_app <- function(x) inherits(x, "oauth_app") is.oauth_endpoint <- function(x) inherits(x, "oauth_endpoint") is_rstudio_server <- function() { Sys.getenv("RSTUDIO") == "1" && Sys.getenv("RSTUDIO_PROGRAM_MODE") == "server" } is_google_colab <- function() { # idea from https://stackoverflow.com/a/74930276 # 2023-02-21 I created new notebook with # https://colab.research.google.com/#create=true&language=r # and I see: # Sys.getenv("COLAB_RELEASE_TAG") returns 'release-colab-20230216-060056-RC01' # # https://github.com/r-lib/gargle/issues/140#issuecomment-1439111627 # via @craigcitro, the existence of this directory is another indicator: # /var/colab/hostname nzchar(Sys.getenv("COLAB_RELEASE_TAG")) } is_hosted_session <- function() { is_rstudio_server() || is_google_colab() } add_line <- function(path, line) { if (file_exists(path)) { lines <- readLines(path, warn = FALSE) lines <- lines[lines != ""] } else { lines <- character() } if (line %in% lines) { return(TRUE) } gargle_info("Adding {.val {line}} to {.file {path}}.") lines <- c(lines, line) writeLines(lines, path) TRUE } ## in the spirit of basename(), but for Google scopes ## for printing purposes base_scope <- function(x) { gsub("/$", "", gsub("(.*)/(.+$)", "...\\2", x)) } normalize_scopes <- function(x) { stats::setNames(sort(unique(x)), NULL) } add_email_scope <- function(scopes = NULL) { gargle_debug("adding {.val userinfo.email} scope") url <- "https://www.googleapis.com/auth/userinfo.email" union(scopes %||% character(), url) } new_srcref <- function(lines) { n <- length(lines) srcref( srcfilecopy("HIDDEN", lines), c(1L, 1L, n, nchar(lines[[n]])) ) } gargle/R/zzz.R0000644000176200001440000000014414411420064012667 0ustar liggesusers.onLoad <- function(lib, pkg) { # nocov start cred_funs_set_default() invisible() } # nocov end gargle/R/secret.R0000644000176200001440000002637414444241216013342 0ustar liggesusers# gargle's new secret management functions ------------------------------------- #' Encrypt/decrypt JSON or an R object #' #' @description #' These functions help to encrypt and decrypt confidential information that you #' might need when deploying gargle-using projects or in CI/CD. They basically #' rely on inlined copies of the [secret functions in the httr2 #' package](https://httr2.r-lib.org/reference/secrets.html). The awkwardness of #' inlining code from httr2 can be removed if/when gargle starts to depend on #' httr2. #' * The `secret_encrypt_json()` + `secret_decrypt_json()` pair is unique to #' gargle, given how frequently Google auth relies on JSON files, e.g., service #' account tokens and OAuth clients. #' * The `secret_write_rds()` + `secret_read_rds()` pair is just a copy of #' functions from httr2. They are handy if you need to secure a user token. #' * `secret_make_key()` and `secret_has_key()` are also copies of functions #' from httr2. Use `secret_make_key` to generate a key. Use `secret_has_key()` #' to condition on key availability in, e.g., examples, tests, or apps. #' #' @param path The path to write to (`secret_encrypt_json()`, #' `secret_write_rds()`) or to read from (`secret_decrypt_json()`, #' `secret_read_rds()`). #' @param key Encryption key, as implemented by httr2's [secret #' functions](https://httr2.r-lib.org/reference/secrets.html). This should #' almost always be the name of an environment variable whose value was #' generated with `secret_make_key()` (which is an inlined copy of #' `httr2::secret_make_key()`). #' #' @return #' * `secret_encrypt_json()`: The encrypted JSON string, invisibly. In typical #' use, this function is mainly called for its side effect, which is to write an #' encrypted file. #' * `secret_decrypt_json()`: The decrypted JSON string, invisibly. #' * `secret_write_rds()`: `x`, invisibly #' * `secret_read_rds()`: the decrypted object. #' * `secret_make_key()`: a random string to use as an encryption key. #' * `secret_has_key()` returns `TRUE` if the key is available and `FALSE` #' otherwise. #' #' @name gargle_secret #' @examplesIf secret_has_key("GARGLE_KEY") #' # gargle ships with JSON for a fake service account #' # here we put the encrypted JSON into a new file #' tmp <- tempfile() #' secret_encrypt_json( #' fs::path_package("gargle", "extdata", "fake_service_account.json"), #' tmp, #' key = "GARGLE_KEY" #' ) #' #' # complete the round trip by providing the decrypted JSON to a credential #' # function #' credentials_service_account( #' scopes = "https://www.googleapis.com/auth/userinfo.email", #' path = secret_decrypt_json( #' fs::path_package("gargle", "secret", "gargle-testing.json"), #' key = "GARGLE_KEY" #' ) #' ) #' #' file.remove(tmp) #' #' # make an artificial Gargle2.0 token #' fauxen <- gargle2.0_token( #' email = "jane@example.org", #' client = gargle_oauth_client( #' id = "CLIENT_ID", secret = "SECRET", name = "CLIENT" #' ), #' credentials = list(token = "fauxen"), #' cache = FALSE #' ) #' fauxen #' #' # store the fake token in an encrypted file #' tmp2 <- tempfile() #' secret_write_rds(fauxen, path = tmp2, key = "GARGLE_KEY") #' #' # complete the round trip by providing the decrypted token to the "BYO token" #' # credential function #' rt_fauxen <- credentials_byo_oauth2( #' token = secret_read_rds(tmp2, key = "GARGLE_KEY") #' ) #' rt_fauxen #' #' file.remove(tmp2) NULL #' @param json A JSON file (or string). #' @rdname gargle_secret #' @export secret_encrypt_json <- function(json, path = NULL, key) { if (!jsonlite::validate(json)) { json <- readChar(json, file.info(json)$size - 1) } enc <- secret_encrypt(json, key = key) if(!is.null(path)) { check_string(path) writeBin(enc, path) } invisible(enc) } #' @rdname gargle_secret #' @export secret_decrypt_json <- function(path, key) { raw <- readBin(path, "raw", file.size(path)) enc <- rawToChar(raw) invisible(secret_decrypt(enc, key = key)) } # httr2's secret management functions ------------------------------------------ # inlined as of: # https://github.com/r-lib/httr2/commit/86127996b98c03f4ada8949969db83bb0c4a7921 # # Basic workflow # # 1. Use `secret_make_key()` to generate a password. Make this available # as an env var (e.g. `{MYPACKAGE}_KEY`) by adding a line to your # `.Renviron`. # # 2. Encrypt strings with `secret_encrypt()` and other data with # `secret_write_rds()`, setting `key = "{MYPACKAGE}_KEY"`. # # 3. In your tests, decrypt the data with `secret_decrypt()` or # `secret_read_rds()` to match how you encrypt it. # # 4. If you push this code to your CI server, it will already "work" because # all functions automatically skip tests when your `{MYPACKAGE}_KEY}` # env var isn't set. To make the tests actually run, you'll need to set # the env var using whatever tool your CI system provides for setting # env vars. Make sure to carefully inspect the test output to check that # the skips have actually gone away. #' #' @rdname gargle_secret #' @export secret_make_key <- function() { I(base64_url_rand(16)) } secret_encrypt <- function(x, key) { check_string(x) key <- as_key(key) value <- openssl::aes_ctr_encrypt(charToRaw(x), key) base64_url_encode(c(attr(value, "iv"), value)) } secret_decrypt <- function(encrypted, key) { check_string(encrypted, arg = "encrypted") key <- as_key(key) bytes <- base64_url_decode(encrypted) iv <- bytes[1:16] value <- bytes[-(1:16)] rawToChar(openssl::aes_ctr_decrypt(value, key, iv = iv)) } #' @param x An R object. #' @rdname gargle_secret #' @export secret_write_rds <- function(x, path, key) { writeBin(secret_serialize(x, key), path) invisible(x) } #' @rdname gargle_secret #' @export secret_read_rds <- function(path, key) { x <- readBin(path, "raw", file.size(path)) secret_unserialize(x, key) } secret_serialize <- function(x, key) { key <- as_key(key) x <- serialize(x, NULL, version = 2) x_cmp <- memCompress(x, "bzip2") x_enc <- openssl::aes_ctr_encrypt(x_cmp, key) c(attr(x_enc, "iv"), x_enc) } secret_unserialize <- function(encrypted, key) { key <- as_key(key) iv <- encrypted[1:16] x_enc <- encrypted[-(1:16)] x_cmp <- openssl::aes_ctr_decrypt(x_enc, key, iv = iv) x <- memDecompress(x_cmp, "bzip2") unserialize(x) } #' @rdname gargle_secret #' @export secret_has_key <- function(key) { check_string(key) key <- Sys.getenv(key) !identical(key, "") } secret_get_key <- function(envvar, call = caller_env()) { key <- Sys.getenv(envvar) if (identical(key, "")) { if (is_testing()) { msg <- glue("Env var {envvar} not defined.") testthat::skip(msg) } else { msg <- gargle_map_cli( envvar, "Env var {.envvar <>} not defined." ) cli::cli_abort(msg, call = call) } } base64_url_decode(key) } ## Helpers ----------------------------------------------------------------- as_key <- function(x) { if (inherits(x, "AsIs") && is_string(x)) { base64_url_decode(x) } else if (is.raw(x)) { x } else if (is_string(x)) { secret_get_key(x) } else { cli::cli_abort(c( "{.arg key} must be one of the following:", "*" = "a string giving the name of an env var", "*" = "a raw vector containing the key", "*" = "a string wrapped in {.fun I} that contains the base64url encoded \\ key" )) } } # https://datatracker.ietf.org/doc/html/rfc7636#appendix-A base64_url_encode <- function(x) { x <- openssl::base64_encode(x) x <- gsub("=+$", "", x) x <- gsub("+", "-", x, fixed = TRUE) x <- gsub("/", "_", x, fixed = TRUE) x } base64_url_decode <- function(x) { mod4 <- nchar(x) %% 4 if (mod4 > 0) { x <- paste0(x, strrep("=", 4 - mod4)) } x <- gsub("_", "/", x, fixed = TRUE) x <- gsub("-", "+", x, fixed = TRUE) # x <- gsub("=+$", "", x) openssl::base64_decode(x) } base64_url_rand <- function(bytes = 32) { base64_url_encode(openssl::rand_bytes(bytes)) } # gargle's legacy, internal secret management functions ------------------------ warn_for_legacy_secret <- function(what, env = caller_env(), user_env = caller_env(2)) { lifecycle::deprecate_soft( when = "1.5.0", what = what, details = c( "Use the new secret functions instead:", "" ), env = env, user_env = user_env, id = "httr2_secret_mgmt" ) } ## Setup support for the NAME=PASSWORD envvar ---------------------------------- # secret_pw_name("gargle") --> "GARGLE_PASSWORD" secret_pw_name <- function(package) { warn_for_legacy_secret("secret_pw_name()") paste0(toupper(gsub("[.]", "_", package)), "_PASSWORD") } # secret_pw_gen() --> "9AkKLa50wf1zHNCnHiQWeFLDoch9MYJHmPNnIVYZgSUt0Emwgi" secret_pw_gen <- function() { warn_for_legacy_secret("secret_pw_gen()") x <- sample(c(letters, LETTERS, 0:9), 50, replace = TRUE) paste0(x, collapse = "") } # secret_pw_exists("gargle") --> TRUE or FALSE secret_pw_exists <- function(package) { pw_name <- secret_pw_name(package) pw <- Sys.getenv(pw_name, "") !identical(pw, "") } # secret_pw_get("gargle") --> error or key-ified PASSWORD = # hash of charToRaw(PASSWORD) secret_pw_get <- function(package) { pw_name <- secret_pw_name(package) pw <- Sys.getenv(pw_name, "") if (identical(pw, "")) { gargle_abort_secret( message = "Env var {.envvar {pw_name}} is not defined.", package = package ) } sodium::sha256(charToRaw(pw)) } ## Store and retrieve encrypted data ------------------------------------------- secret_can_decrypt <- function(package) { warn_for_legacy_secret("secret_can_decrypt()") requireNamespace("sodium", quietly = TRUE) && secret_pw_exists(package) } # input should either be a filepath or a raw vector secret_write <- function(package, name, input) { warn_for_legacy_secret("secret_write()") if (is.character(input)) { input <- readBin(input, "raw", file.size(input)) } else if (!is.raw(input)) { stop_input_type(input, what = c("character", "raw")) } destdir <- fs::path("inst", "secret") fs::dir_create(destdir) destpath <- fs::path(destdir, name) enc <- sodium::data_encrypt( msg = input, key = secret_pw_get(package), nonce = secret_nonce() ) attr(enc, "nonce") <- NULL writeBin(enc, destpath) invisible(destpath) } # Generated with sodium::bin2hex(sodium::random(24)). AFAICT nonces are # primarily used to prevent replay attacks, which shouldn't be a concern here secret_nonce <- function() { sodium::hex2bin("cb36bab652dec6ae9b1827c684a7b6d21d2ea31cd9f766ac") } secret_path <- function(package, name) { fs::path_package(package, "secret", name) } # Returns a raw vector secret_read <- function(package, name) { warn_for_legacy_secret("secret_read()") if (!secret_can_decrypt(package)) { gargle_abort_secret(message = "Decryption not available.", package = package) } path <- secret_path(package, name) raw <- readBin(path, "raw", file.size(path)) sodium::data_decrypt( bin = raw, key = secret_pw_get(package), nonce = secret_nonce() ) } gargle_abort_secret <- function(message, package, call = caller_env()) { gargle_abort( class = "gargle_error_secret", call = call, message = message, package = package ) } gargle/R/token_fetch.R0000644000176200001440000000260314433520365014336 0ustar liggesusers#' Fetch a token for the given scopes #' #' This is a rather magical function that calls a series of concrete #' credential-fetching functions, each wrapped in a `tryCatch()`. #' `token_fetch()` keeps trying until it succeeds or there are no more functions #' to try. See the `vignette("how-gargle-gets-tokens")` for a full description #' of `token_fetch()`. #' #' @seealso [cred_funs_list()] reveals the current registry of #' credential-fetching functions, in order. #' #' @inheritParams credentials_user_oauth2 #' @param ... Additional arguments passed to all credential functions. #' #' @return An [`httr::Token`][httr::Token-class] (often an instance of something #' that inherits from `httr::Token`) or `NULL`. #' @family credential functions #' @export #' @examples #' \dontrun{ #' token_fetch(scopes = "https://www.googleapis.com/auth/userinfo.email") #' } token_fetch <- function(scopes = NULL, ...) { gargle_debug("trying {.fun token_fetch}") for (f in gargle_env$cred_funs) { token <- NULL token <- tryCatch( error = function(e) { gargle_debug(c("Error caught by {.fun token_fetch}:", e$message)) NULL }, withCallingHandlers( f(scopes, ...), warning = function(e) { gargle_debug(c("Warning caught by {.fun token_fetch}:", e$message)) } ) ) if (!is.null(token)) { return(token) } } NULL } gargle/R/credentials_external_account.R0000644000176200001440000003503014431310014017742 0ustar liggesusers#' Get a token for an external account #' #' @description #' `r lifecycle::badge('experimental')` #' Workload identity federation is a new (as of April 2021) keyless #' authentication mechanism that allows applications running on a non-Google #' Cloud platform, such as AWS, to access Google Cloud resources without using a #' conventional service account token. This eliminates the dilemma of how to #' safely manage service account credential files. #' #' Unlike service accounts, the configuration file for workload identity #' federation contains no secrets. Instead, it holds non-sensitive metadata. #' The external application obtains the needed sensitive data "on-the-fly" from #' the running instance. The combined data is then used to obtain a so-called #' subject token from the external identity provider, such as AWS. This is then #' sent to Google's Security Token Service API, in exchange for a very #' short-lived federated access token. Finally, the federated access token is #' sent to Google's Service Account Credentials API, in exchange for a #' short-lived GCP access token. This access token allows the external #' application to impersonate a service account and inherit the permissions of #' the service account to access GCP resources. #' #' This feature is still experimental in gargle and **currently only supports #' AWS**. It also requires installation of the suggested packages #' \pkg{aws.signature} and \pkg{aws.ec2metadata}. Workload identity federation #' **can** be used with other platforms, such as Microsoft Azure or any #' identity provider that supports OpenID Connect. If you would like gargle to #' support this token flow for additional platforms, please [open an issue on #' GitHub](https://github.com/r-lib/gargle/issues) and describe your use case. #' #' @inheritParams token_fetch #' @param path JSON containing the workload identity configuration for the #' external account, in one of the forms supported for the `txt` argument of #' [jsonlite::fromJSON()] (probably, a file path, although it could be a JSON #' string). The instructions for generating this configuration are given at #' [Configuring workload identity federation](https://cloud.google.com/iam/docs/configuring-workload-identity-federation). #' #' Note that external account tokens are a natural fit for use as Application #' Default Credentials, so consider storing the configuration file in one of #' the standard locations consulted for ADC, instead of providing `path` #' explicitly. See [credentials_app_default()] for more. #' #' @seealso There is substantial setup necessary, both on the GCP and AWS side, #' to use this authentication method. These two links provide, respectively, #' a high-level overview and step-by-step instructions. #' * #' * #' @return A [WifToken()] or `NULL`. #' @family credential functions #' @export #' @examples #' \dontrun{ #' credentials_external_account() #' } credentials_external_account <- function(scopes = "https://www.googleapis.com/auth/cloud-platform", path = "", ...) { gargle_debug("trying {.fun credentials_external_account}") if (!detect_aws_ec2() || is.null(scopes)) { return(NULL) } scopes <- normalize_scopes(add_email_scope(scopes)) token <- oauth_external_token(path = path, scopes = scopes) if (is.null(token$credentials$access_token) || !nzchar(token$credentials$access_token)) { NULL } else { gargle_debug("service account email: {.email {token_email(token)}}") token } } #' Generate OAuth token for an external account #' #' @inheritParams credentials_external_account #' #' @keywords internal #' @export oauth_external_token <- function(path = "", scopes = "https://www.googleapis.com/auth/cloud-platform") { info <- jsonlite::fromJSON(path, simplifyVector = FALSE) if (!identical(info[["type"]], "external_account")) { gargle_debug("JSON does not appear to represent an external account") return() } params <- c( list(scopes = scopes), info, # the most pragmatic way to get super$sign() to work # can't implement my own method without needing unexported httr functions # request() or build_request() as_header = TRUE ) WifToken$new(params = params) } #' Token for use with workload identity federation #' #' Not intended for direct use. See [credentials_external_account()] instead. #' #' @keywords internal #' @export WifToken <- R6::R6Class("WifToken", inherit = httr::Token2.0, list( #' @description Get a token via workload identity federation #' @param params A list of parameters for `init_oauth_external_account()`. #' @return A WifToken. initialize = function(params = list()) { gargle_debug("WifToken initialize") # TODO: any desired validity checks on contents of params # NOTE: the final token exchange with # https://cloud.google.com/iam/docs/reference/credentials/rest/v1/projects.serviceAccounts/generateAccessToken # takes scopes as an **array**, not a space delimited string # so we do NOT collapse scopes in this flow params$scope <- params$scopes self$params <- params self$init_credentials() }, #' @description Enact the actual token exchange for workload identity #' federation. init_credentials = function() { gargle_debug("WifToken init_credentials") creds <- init_oauth_external_account(params = self$params) # for some reason, the serviceAccounts.generateAccessToken method of # Google's Service Account Credentials API returns in camelCase, not # snake_case # as in, we get this: # "accessToken":"ya29.c.KsY..." # "expireTime":"2021-06-01T18:01:06Z" # instead of this: # "access_token": "ya29.a0A..." # "expires_in": 3599 snake_case <- function(x) { gsub("([a-z0-9])([A-Z])", "\\1_\\L\\2", x, perl = TRUE) } names(creds) <- snake_case(names(creds)) self$credentials <- creds self }, #' @description Refreshes the token, which means re-doing the entire token #' flow in this case. refresh = function() { gargle_debug("WifToken refresh") # There's something kind of wrong about this, because it's not a true # refresh. But this method is basically required by the way httr currently # works. # This means that some uses of $refresh() aren't really appropriate for a # WifToken. # For example, if I attempt token_userinfo(x) on a WifToken that lacks # appropriate scope, it fails with 401. # httr tries to "fix" things by refreshing the token. But this is # not a problem that refreshing can fix. # I've now prevented that particular phenomenon in token_userinfo(). self$init_credentials() }, #' @description Format a [WifToken()]. #' @param ... Not used. format = function(...) { x <- list( scopes = commapse(base_scope(self$params$scope)), credentials = commapse(names(self$credentials)) ) c( cli::cli_format_method( cli::cli_h1("") ), glue("{fr(names(x))}: {fl(x)}") ) }, #' @description Print a [WifToken()]. #' @param ... Not used. print = function(...) { # a format method is not sufficient for WifToken because the parent class # has a print method cli::cat_line(self$format()) }, #' @description Placeholder implementation of required method. Returns `TRUE`. can_refresh = function() { # TODO: see above re: my ambivalence about the whole notion of refresh with # respect to this flow TRUE }, # TODO: are cache and load_from_cache really required? # alternatively, what if calling them threw an error? #' @description Placeholder implementation of required method. Returns self. cache = function() self, #' @description Placeholder implementation of required method. Returns self. load_from_cache = function() self, # TODO: are these really required? #' @description Placeholder implementation of required method. validate = function() {}, #' @description Placeholder implementation of required method. revoke = function() {} )) # https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/identify_ec2_instances.html # https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/instance-identity-documents.html detect_aws_ec2 <- function() { if (is_installed("aws.ec2metadata")) { return(aws.ec2metadata::is_ec2()) } gargle_debug(" {.pkg aws.ec2metadata} not installed; can't detect whether running on \\ EC2 instance") FALSE } init_oauth_external_account <- function(params) { credential_source <- params$credential_source if (!identical(credential_source$environment_id, "aws1")) { gargle_abort(" {.pkg gargle}'s workload identity federation flow only supports AWS at \\ this time.") } subject_token <- aws_subject_token( credential_source = credential_source, audience = params$audience ) serialized_subject_token <- serialize_subject_token(subject_token) federated_access_token <- fetch_federated_access_token( params = params, subject_token = serialized_subject_token ) fetch_wif_access_token( federated_access_token, impersonation_url = params[["service_account_impersonation_url"]], scope = params[["scope"]] ) } # For AWS, the subject token isn't really a token, but rather the instructions # necessary to get a token: # # From https://cloud.google.com/iam/docs/access-resources-aws#exchange-token # # "The GetCallerIdentity token contains the information that you would normally # include in a request to the AWS GetCallerIdentity() method, as well as the # signature that you would normally generate for the request. # # Also scroll down here, to see the AWS-specific content # https://cloud.google.com/iam/docs/reference/sts/rest/v1/TopLevel/token aws_subject_token <- function(credential_source, audience) { if (!is_installed(c("aws.ec2metadata", "aws.signature"))) { gargle_abort(" Packages {.pkg aws.ec2metadata} and {.pkg aws.signature} must be \\ installed in order to use workload identity federation on AWS.") } region <- aws.ec2metadata::instance_document()$region regional_cred_verification_url <- glue( credential_source[["regional_cred_verification_url"]], region = region ) parsed_url <- httr::parse_url(regional_cred_verification_url) headers_orig <- list( host = parsed_url$hostname, # for some reason, this is not included as a signed header unless I provide # it `x-amz-date` = format(Sys.time(), "%Y%m%dT%H%M%SZ", tz = "UTC"), # in contrast, session token IS automatically included if it exists, which # it should `x-goog-cloud-target-resource` = audience ) verb <- "POST" # https://docs.aws.amazon.com/general/latest/gr/signature-version-4.html signed <- aws.signature::signature_v4_auth( region = region, service = "sts", verb = verb, action = "/", query_args = parsed_url$query, canonical_headers = headers_orig, request_body = "" ) # unfortunately, the headers actually used to make the canonical request are # not returned in the signed object, so we dig them out of the canonical # request req_parts <- strsplit(signed[["CanonicalRequest"]], split = "\n")[[1]] f <- function(needle) { needle <- paste0("^", needle, ":") x <- grep(needle, req_parts, value = TRUE) sub(needle, "", x) } headers <- list( host = f("host"), `x-amz-date` = f("x-amz-date"), `x-amz-security-token` = f("x-amz-security-token"), `x-goog-cloud-target-resource` = f("x-goog-cloud-target-resource") ) list( url = regional_cred_verification_url, method = verb, headers = c( Authorization = signed$SignatureHeader, headers ) ) } serialize_subject_token <- function(x) { # The GCP STS endpoint expects the headers to be formatted as: # [ # {key: 'Authorization', value: '...'}, # {key: 'x-amz-date', value: '...'}, # ... # ] # even though the headers were formatted differently, i.e. in the usual way, # when we generated the V4 signature. # we're using a purrr compat file, so must call with actual function kv <- function(val, nm) list(key = nm, value = val) headers_key_value <- unname(imap(x$headers, kv)) x$headers <- headers_key_value # The GCP STS endpoint expects the prepared request to be serialized as a JSON # string, which is then URL-encoded. utils::URLencode( jsonlite::toJSON(x, auto_unbox = TRUE), reserved = TRUE ) } # https://datatracker.ietf.org/doc/html/rfc8693 # https://cloud.google.com/iam/docs/reference/sts/rest/v1/TopLevel/token # https://cloud.google.com/iam/docs/reference/credentials/rest/v1/projects.serviceAccounts/generateAccessToken#authorization-scopes fetch_federated_access_token <- function(params, subject_token) { req <- list( method = "POST", url = params$token_url, body = list( audience = params[["audience"]], grantType = "urn:ietf:params:oauth:grant-type:token-exchange", requestedTokenType = "urn:ietf:params:oauth:token-type:access_token", # this request must have one of these scopes: # https://www.googleapis.com/auth/cloud-platform # https://www.googleapis.com/auth/iam # I am hard-wiring the iam scope, guided by the least privilege principle, # as it is the narrower of the 2 scopes scope = "https://www.googleapis.com/auth/iam", subjectTokenType = params[["subject_token_type"]], subjectToken = subject_token ) ) # rfc 8693 says to encode as "application/x-www-form-urlencoded" resp <- request_make(req, encode = "form") response_process(resp) } # https://cloud.google.com/iam/docs/creating-short-lived-service-account-credentials#sa-credentials-oauth fetch_wif_access_token <- function(federated_access_token, impersonation_url, scope = "https://www.googleapis.com/auth/cloud-platform") { req <- list( method = "POST", url = impersonation_url, # https://cloud.google.com/iam/docs/reference/credentials/rest/v1/projects.serviceAccounts/generateAccessToken # takes scope as an **array**, not a space delimited string body = list(scope = scope), token = httr::add_headers( Authorization = paste("Bearer", federated_access_token$access_token) ) ) resp <- request_make(req) response_process(resp) } gargle/R/request_retry.R0000644000176200001440000001737414431310014014757 0ustar liggesusers#' Make a Google API request, repeatedly #' #' Intended primarily for internal use in client packages that provide #' high-level wrappers for users. It is a drop-in substitute for #' [request_make()] that also has the ability to retry the request. Codes that #' are considered retryable: 408, 429, 500, 502, 503. #' #' Consider an example where we are willing to make a request up to 5 times. #' #' ``` #' try 1 2 3 4 5 #' |--|----|--------|----------------| #' wait 1 2 3 4 #' ``` #' #' There will be up to 5 - 1 = 4 waits and we generally want the waiting period #' to get longer, in an exponential way. Such schemes are called exponential #' backoff. `request_retry()` implements exponential backoff with "full jitter", #' where each waiting time is generated from a uniform distribution, where the #' interval of support grows exponentially. A common alternative is "equal #' jitter", which adds some noise to fixed, exponentially increasing waiting #' times. #' #' Either way our waiting times are based on a geometric series, which, by #' convention, is usually written in terms of powers of 2: #' #' ``` #' b, 2b, 4b, 8b, ... #' = b * 2^0, b * 2^1, b * 2^2, b * 2^3, ... #' ``` #' #' The terms in this series require knowledge of `b`, the so-called exponential #' base, and many retry functions and libraries require the user to specify #' this. But most users find it easier to declare the total amount of waiting #' time they can tolerate for one request. Therefore `request_retry()` asks for #' that instead and solves for `b` internally. This is inspired by the Opnieuw #' Python library for retries. Opnieuw's interface is designed to eliminate #' uncertainty around: #' * Units: Is this thing given in seconds? minutes? milliseconds? #' * Ambiguity around how things are counted: Are we starting at 0 or 1? #' Are we counting tries or just the retries? #' * Non-intuitive required inputs, e.g., the exponential base. #' #' Let *n* be the total number of tries we're willing to make (the argument #' `max_tries_total`) and let *W* be the total amount of seconds we're willing #' to dedicate to making and retrying this request (the argument #' `max_total_wait_time_in_seconds`). Here's how we determine *b*: #' #' ``` #' sum_{i=0}^(n - 1) b * 2^i = W #' b * sum_{i=0}^(n - 1) 2^i = W #' b * ( (2 ^ n) - 1) = W #' b = W / ( (2 ^ n) - 1) #' ``` #' #' @section Special cases: #' `request_retry()` departs from exponential backoff in three special cases: #' * It actually implements *truncated* exponential backoff. There is a floor #' and a ceiling on random wait times. #' * `Retry-After` header: If the response has a header named `Retry-After` #' (case-insensitive), it is assumed to provide a non-negative integer #' indicating the number of seconds to wait. If present, we wait this many #' seconds and do not generate a random waiting time. (In theory, this header #' can alternatively provide a datetime after which to retry, but we have no #' first-hand experience with this variant for a Google API.) #' * Sheets API quota exhaustion: In the course of googlesheets4 development, #' we've grown very familiar with the `429 RESOURCE_EXHAUSTED` error. As of #' 2023-04-15, the Sheets API v4 has a limit of 300 requests per minute per #' project and 60 requests per minute per user per project. Limits for reads #' and writes are tracked separately. In our experience, the "60 (read or #' write) requests per minute per user" limit is the one you hit most often. #' If we detect this specific failure, the first wait time is a bit more than #' one minute, then we revert to exponential backoff. #' #' #' @param ... Passed along to [request_make()]. #' @param max_tries_total Maximum number of tries. #' @param max_total_wait_time_in_seconds Total seconds we are willing to #' dedicate to waiting, summed across all tries. This is a technical upper #' bound and actual cumulative waiting will be less. #' #' @seealso #' * #' * #' * #' * #' * #' * #' * #' #' @inherit request_make return #' @export #' #' @examples #' \dontrun{ #' req <- gargle::request_build( #' method = "GET", #' path = "path/to/the/resource", #' token = "PRETEND_I_AM_TOKEN" #' ) #' gargle::request_retry(req) #' } request_retry <- function(..., max_tries_total = 5, max_total_wait_time_in_seconds = 100) { resp <- request_make(...) tries_made <- 1 b <- calculate_base_wait( n_waits = max_tries_total - 1, total_wait_time = max_total_wait_time_in_seconds ) while (we_should_retry(tries_made, max_tries_total, resp)) { wait_time <- backoff(tries_made, resp, base = b) # TODO: show progress in some way Sys.sleep(wait_time) resp <- request_make(...) tries_made <- tries_made + 1 } invisible(resp) } retryable_codes <- c("408", "429", "500", "502", "503") we_should_retry <- function(tries_made, max_tries_total, resp) { if (tries_made >= max_tries_total) { FALSE } else if (httr::status_code(resp) %in% retryable_codes) { TRUE } else { FALSE } } backoff <- function(tries_made, resp, base = 1, min_wait = 1, max_wait = 45) { wait_time <- stats::runif(1, 0, base * (2^(tries_made - 1))) wait_rationale <- "exponential backoff, full jitter" if (wait_time < min_wait) { wait_time <- min_wait + stats::runif(1) wait_rationale <- glue( "{wait_rationale}, clipped to floor of {min_wait} seconds" ) } if (wait_time > max_wait) { wait_time <- max_wait + stats::runif(1) wait_rationale <- glue( "{wait_rationale}, clipped to ceiling of {max_wait} seconds" ) } if (sheets_per_user_quota_exhaustion(resp) && tries_made == 1) { wait_time <- 60 + stats::runif(1) wait_rationale <- "fixed 60 second wait for per user quota exhaustion" } retry_after <- retry_after_header(resp) if (!is.null(retry_after)) { wait_time <- retry_after wait_rationale <- "'Retry-After' header" } status_code <- httr::status_code(resp) if (gargle_verbosity() == "debug") { msg <- c( "x" = "Request failed [{status_code}]", " " = gargle_error_message(resp), "i" = "Retry {tries_made} happens in {round(wait_time, 1)} seconds ...", " " = "(strategy: {wait_rationale})" ) gargle_debug(msg) } else { gargle_info(c( "x" = "Request failed [{status_code}]. Retry {tries_made} happens in \\ {round(wait_time, 1)} seconds ..." )) } wait_time } retry_after_header <- function(resp) { # TODO: consider honoring Retry-After with status codes besides 429 if (!(httr::status_code(resp) == "429")) { return(NULL) } h <- httr::headers(resp) retry_after <- resp$headers[["retry-after"]] if (is.null(retry_after)) { NULL } else { as.numeric(retry_after) } } sheets_per_user_quota_exhaustion <- function(resp) { msg <- gargle_error_message(resp) # the structure of this error and the wording of this message have changed # over time any(grepl("per user per 60 seconds", msg)) || any(grepl("per minute per user", msg)) } calculate_base_wait <- function(n_waits, total_wait_time) { stopifnot(is.numeric(n_waits), length(n_waits) == 1L, n_waits > 0) stopifnot(is.numeric(total_wait_time), length(total_wait_time) == 1L, total_wait_time > 0) total_wait_time / (2^(n_waits) - 1) } gargle/R/import-standalone-types-check.R0000644000176200001440000002761614431310014017717 0ustar liggesusers# Standalone file: do not edit by hand # Source: # ---------------------------------------------------------------------- # # --- # repo: r-lib/rlang # file: standalone-types-check.R # last-updated: 2023-03-13 # license: https://unlicense.org # dependencies: standalone-obj-type.R # imports: rlang (>= 1.1.0) # --- # # ## Changelog # # 2023-03-13: # - Improved error messages of number checkers (@teunbrand) # - Added `allow_infinite` argument to `check_number_whole()` (@mgirlich). # - Added `check_data_frame()` (@mgirlich). # # 2023-03-07: # - Added dependency on rlang (>= 1.1.0). # # 2023-02-15: # - Added `check_logical()`. # # - `check_bool()`, `check_number_whole()`, and # `check_number_decimal()` are now implemented in C. # # - For efficiency, `check_number_whole()` and # `check_number_decimal()` now take a `NULL` default for `min` and # `max`. This makes it possible to bypass unnecessary type-checking # and comparisons in the default case of no bounds checks. # # 2022-10-07: # - `check_number_whole()` and `_decimal()` no longer treat # non-numeric types such as factors or dates as numbers. Numeric # types are detected with `is.numeric()`. # # 2022-10-04: # - Added `check_name()` that forbids the empty string. # `check_string()` allows the empty string by default. # # 2022-09-28: # - Removed `what` arguments. # - Added `allow_na` and `allow_null` arguments. # - Added `allow_decimal` and `allow_infinite` arguments. # - Improved errors with absent arguments. # # # 2022-09-16: # - Unprefixed usage of rlang functions with `rlang::` to # avoid onLoad issues when called from rlang (#1482). # # 2022-08-11: # - Added changelog. # # nocov start # Scalars ----------------------------------------------------------------- .standalone_types_check_dot_call <- .Call check_bool <- function(x, ..., allow_na = FALSE, allow_null = FALSE, arg = caller_arg(x), call = caller_env()) { if (!missing(x) && .standalone_types_check_dot_call(ffi_standalone_is_bool_1.0.7, x, allow_na, allow_null)) { return(invisible(NULL)) } stop_input_type( x, c("`TRUE`", "`FALSE`"), ..., allow_na = allow_na, allow_null = allow_null, arg = arg, call = call ) } check_string <- function(x, ..., allow_empty = TRUE, allow_na = FALSE, allow_null = FALSE, arg = caller_arg(x), call = caller_env()) { if (!missing(x)) { is_string <- .rlang_check_is_string( x, allow_empty = allow_empty, allow_na = allow_na, allow_null = allow_null ) if (is_string) { return(invisible(NULL)) } } stop_input_type( x, "a single string", ..., allow_na = allow_na, allow_null = allow_null, arg = arg, call = call ) } .rlang_check_is_string <- function(x, allow_empty, allow_na, allow_null) { if (is_string(x)) { if (allow_empty || !is_string(x, "")) { return(TRUE) } } if (allow_null && is_null(x)) { return(TRUE) } if (allow_na && (identical(x, NA) || identical(x, na_chr))) { return(TRUE) } FALSE } check_name <- function(x, ..., allow_null = FALSE, arg = caller_arg(x), call = caller_env()) { if (!missing(x)) { is_string <- .rlang_check_is_string( x, allow_empty = FALSE, allow_na = FALSE, allow_null = allow_null ) if (is_string) { return(invisible(NULL)) } } stop_input_type( x, "a valid name", ..., allow_na = FALSE, allow_null = allow_null, arg = arg, call = call ) } IS_NUMBER_true <- 0 IS_NUMBER_false <- 1 IS_NUMBER_oob <- 2 check_number_decimal <- function(x, ..., min = NULL, max = NULL, allow_infinite = TRUE, allow_na = FALSE, allow_null = FALSE, arg = caller_arg(x), call = caller_env()) { if (missing(x)) { exit_code <- IS_NUMBER_false } else if (0 == (exit_code <- .standalone_types_check_dot_call( ffi_standalone_check_number_1.0.7, x, allow_decimal = TRUE, min, max, allow_infinite, allow_na, allow_null ))) { return(invisible(NULL)) } .stop_not_number( x, ..., exit_code = exit_code, allow_decimal = TRUE, min = min, max = max, allow_na = allow_na, allow_null = allow_null, arg = arg, call = call ) } check_number_whole <- function(x, ..., min = NULL, max = NULL, allow_infinite = FALSE, allow_na = FALSE, allow_null = FALSE, arg = caller_arg(x), call = caller_env()) { if (missing(x)) { exit_code <- IS_NUMBER_false } else if (0 == (exit_code <- .standalone_types_check_dot_call( ffi_standalone_check_number_1.0.7, x, allow_decimal = FALSE, min, max, allow_infinite, allow_na, allow_null ))) { return(invisible(NULL)) } .stop_not_number( x, ..., exit_code = exit_code, allow_decimal = FALSE, min = min, max = max, allow_na = allow_na, allow_null = allow_null, arg = arg, call = call ) } .stop_not_number <- function(x, ..., exit_code, allow_decimal, min, max, allow_na, allow_null, arg, call) { if (allow_decimal) { what <- "a number" } else { what <- "a whole number" } if (exit_code == IS_NUMBER_oob) { min <- min %||% -Inf max <- max %||% Inf if (min > -Inf && max < Inf) { what <- sprintf("%s between %s and %s", what, min, max) } else if (x < min) { what <- sprintf("%s larger than or equal to %s", what, min) } else if (x > max) { what <- sprintf("%s smaller than or equal to %s", what, max) } else { abort("Unexpected state in OOB check", .internal = TRUE) } } stop_input_type( x, what, ..., allow_na = allow_na, allow_null = allow_null, arg = arg, call = call ) } check_symbol <- function(x, ..., allow_null = FALSE, arg = caller_arg(x), call = caller_env()) { if (!missing(x)) { if (is_symbol(x)) { return(invisible(NULL)) } if (allow_null && is_null(x)) { return(invisible(NULL)) } } stop_input_type( x, "a symbol", ..., allow_na = FALSE, allow_null = allow_null, arg = arg, call = call ) } check_arg <- function(x, ..., allow_null = FALSE, arg = caller_arg(x), call = caller_env()) { if (!missing(x)) { if (is_symbol(x)) { return(invisible(NULL)) } if (allow_null && is_null(x)) { return(invisible(NULL)) } } stop_input_type( x, "an argument name", ..., allow_na = FALSE, allow_null = allow_null, arg = arg, call = call ) } check_call <- function(x, ..., allow_null = FALSE, arg = caller_arg(x), call = caller_env()) { if (!missing(x)) { if (is_call(x)) { return(invisible(NULL)) } if (allow_null && is_null(x)) { return(invisible(NULL)) } } stop_input_type( x, "a defused call", ..., allow_na = FALSE, allow_null = allow_null, arg = arg, call = call ) } check_environment <- function(x, ..., allow_null = FALSE, arg = caller_arg(x), call = caller_env()) { if (!missing(x)) { if (is_environment(x)) { return(invisible(NULL)) } if (allow_null && is_null(x)) { return(invisible(NULL)) } } stop_input_type( x, "an environment", ..., allow_na = FALSE, allow_null = allow_null, arg = arg, call = call ) } check_function <- function(x, ..., allow_null = FALSE, arg = caller_arg(x), call = caller_env()) { if (!missing(x)) { if (is_function(x)) { return(invisible(NULL)) } if (allow_null && is_null(x)) { return(invisible(NULL)) } } stop_input_type( x, "a function", ..., allow_na = FALSE, allow_null = allow_null, arg = arg, call = call ) } check_closure <- function(x, ..., allow_null = FALSE, arg = caller_arg(x), call = caller_env()) { if (!missing(x)) { if (is_closure(x)) { return(invisible(NULL)) } if (allow_null && is_null(x)) { return(invisible(NULL)) } } stop_input_type( x, "an R function", ..., allow_na = FALSE, allow_null = allow_null, arg = arg, call = call ) } check_formula <- function(x, ..., allow_null = FALSE, arg = caller_arg(x), call = caller_env()) { if (!missing(x)) { if (is_formula(x)) { return(invisible(NULL)) } if (allow_null && is_null(x)) { return(invisible(NULL)) } } stop_input_type( x, "a formula", ..., allow_na = FALSE, allow_null = allow_null, arg = arg, call = call ) } # Vectors ----------------------------------------------------------------- check_character <- function(x, ..., allow_null = FALSE, arg = caller_arg(x), call = caller_env()) { if (!missing(x)) { if (is_character(x)) { return(invisible(NULL)) } if (allow_null && is_null(x)) { return(invisible(NULL)) } } stop_input_type( x, "a character vector", ..., allow_na = FALSE, allow_null = allow_null, arg = arg, call = call ) } check_logical <- function(x, ..., allow_null = FALSE, arg = caller_arg(x), call = caller_env()) { if (!missing(x)) { if (is_logical(x)) { return(invisible(NULL)) } if (allow_null && is_null(x)) { return(invisible(NULL)) } } stop_input_type( x, "a logical vector", ..., allow_na = FALSE, allow_null = allow_null, arg = arg, call = call ) } check_data_frame <- function(x, ..., allow_null = FALSE, arg = caller_arg(x), call = caller_env()) { if (!missing(x)) { if (is.data.frame(x)) { return(invisible(NULL)) } if (allow_null && is_null(x)) { return(invisible(NULL)) } } stop_input_type( x, "a data frame", ..., allow_null = allow_null, arg = arg, call = call ) } # nocov end gargle/R/oauth-init.R0000644000176200001440000001376414444241216014135 0ustar liggesusers# This file has its origins in oauth-init.R in httr. # Motivated by the need to support the pseudo-OOB flow. # # Affected functions: # - Modified: init_oauth2.0(). This function is the workhorse for the # $init_credentials() method of the Token2.0/Gargle2.0 class. Previously, # Gargle2.0 ultimately delegated to the Token2.0 method, but now the method is # fully implemented for Gargle2.0. # - Modified: oauth_authorize(). This function gains the ability to do code # exchange *with state*, by calling the new function # oauth_exchanger_with_state(). # - Added: oauth_exchanger_with_state() # - Added: csrf_token(). Used to create the `state` token (example of a # cross-site request forgery token). Switched one existing use of # httr:::nonce() to this, now that I can. # - The internal helper check_scope() got inlined (it was a mix of a checker # and a processor). # - The internal helper check_oob() got modified to use gargle conventions. #' Retrieve OAuth 2.0 access token, but specific to gargle #' #' @param endpoint An OAuth endpoint, presumably the one returned by #' `gargle_oauth_endpoint()`. The fact that this is even an argument is #' because this function is based on `httr::init_oauth2.0()`. #' @param client An OAuth client, preferably an instance of `gargle_oauth_client`. #' @param scope a character vector of scopes to request. #' @param use_oob Whether to use out-of-band auth. Results in conventional OOB #' if the `client` is of type `"installed"` (or if type is unknown) and #' pseudo-OOB if the `client` is of type `"web"`. #' @param oob_value if provided, specifies the value to use for the redirect_uri #' parameter when retrieving an authorization URL. For conventional OOB, this #' defaults to "urn:ietf:wg:oauth:2.0:oob". For pseudo-OOB, this should be the #' (or a) redirect URI configured for the OAuth client. Consulted only when #' `use_oob = TRUE`. #' @param query_authorize_extra Named list of query parameters to include in the #' initial request to the authorization server. #' @noRd init_oauth2.0 <- function(endpoint = gargle_oauth_endpoint(), client = gargle_client(), scope = NULL, use_oob = gargle_oob_default(), oob_value = NULL, is_interactive = interactive(), query_authorize_extra = list()) { check_character(scope, allow_null = TRUE) scope <- glue_collapse(scope, sep = " ") use_oob <- check_oob(use_oob, oob_value) client_type <- if (inherits(client, "gargle_oauth_client")) client$type else NA if (use_oob) { redirect_uri <- oob_value %||% "urn:ietf:wg:oauth:2.0:oob" if (identical(client_type, "web")) { # pseudo-OOB flow # https://developers.google.com/identity/protocols/oauth2/web-server#creatingclient # We need so-called "offline" access, so the access token can be # refreshed without re-prompting the user for permission. # Specifically, this is necessary (though not sufficient!) to make the # authorization server return a **refresh token** in addition to an # access token. # Offline access is the default for installed applications, but it is NOT # the default for web apps, so we must explicitly request it. query_authorize_extra[["access_type"]] <- "offline" # https://stackoverflow.com/questions/10827920/not-receiving-google-oauth-refresh-token # https://developers.google.com/identity/protocols/oauth2/openid-connect#re-consent # By default, for a web app, the user is only prompted for consent once # per project. And this is necessary in order to get a refresh token. # So we must explicitly ask for re-consent. query_authorize_extra[["prompt"]] <- "consent" state <- csrf_token() } else { # conventional oob state <- NULL } } else { redirect_uri <- httr::oauth_callback() state <- csrf_token() } authorize_url <- httr::oauth2.0_authorize_url( endpoint, client, scope = scope, redirect_uri = redirect_uri, state = state, query_extra = query_authorize_extra ) code <- oauth_authorize( authorize_url, oob = use_oob, client_type = client_type, state = state ) # Use authorisation code to get (temporary) access token httr::oauth2.0_access_token( endpoint, client, code = code, redirect_uri = redirect_uri ) } # https://developers.google.com/identity/protocols/oauth2/openid-connect#createxsrftoken # "These tokens are often referred to as cross-site request forgery (CSRF) # tokens. # # One good choice for a state token is a string of 30 or so characters # constructed using a high-quality random-number generator." csrf_token <- function(n_bytes = 16) { paste0(as.character(openssl::rand_bytes(n_bytes)), collapse = "") } oauth_authorize <- function(url, oob = FALSE, client_type = NA, state = NULL) { if (oob) { if (identical(client_type, "web")) { # pseudo oob oauth_exchanger_with_state(url, state)$code } else { httr::oauth_exchanger(url)$code } } else { httr::oauth_listener(url)$code } } oauth_exchanger_with_state <- function(request_url, state) { httr::BROWSE(request_url) info_enc <- trimws(readline("Enter authorization code: ")) info <- jsonlite::fromJSON(rawToChar(openssl::base64_decode(info_enc))) if (!identical(info$state, state)) { stop("state did not match") } list(code = info$code) } check_oob <- function(use_oob, oob_value = NULL) { check_bool(use_oob) if (!use_oob && !is_installed("httpuv")) { gargle_info( "The {.pkg httpuv} package is not installed; using out-of-band auth.") use_oob <- TRUE } if (use_oob && !is_interactive()) { gargle_abort("Out-of-band auth only works in an interactive session.") } if (!is.null(oob_value) && !use_oob) { gargle_abort(" The {.arg oob_value} argument can only be used when {.code use_oob = TRUE}.") } if (use_oob && !is.null(oob_value)) { check_string(oob_value) } use_oob } gargle/R/gargle-package.R0000644000176200001440000000774414437422236014714 0ustar liggesusers#' @keywords internal "_PACKAGE" # The following block is used by usethis to automatically manage # roxygen namespace tags. Modify with care! ## usethis namespace: start #' @import fs #' @import rlang #' @importFrom glue glue #' @importFrom glue glue_collapse #' @importFrom glue glue_data #' @importFrom lifecycle deprecated ## usethis namespace: end NULL #' Options consulted by gargle #' #' @description #' Wrapper functions around options consulted by gargle, which provide: #' * A place to hang documentation. #' * The mechanism for setting a default. #' #' If the built-in defaults don't suit you, set one or more of these options. #' Typically, this is done in the `.Rprofile` startup file, with code along #' these lines: #' ``` #' options( #' gargle_oauth_email = "jane@example.com", #' gargle_oauth_cache = "/path/to/folder/that/does/not/sync/to/cloud" #' ) #' ``` #' #' @name gargle_options #' @examples #' gargle_oauth_email() #' gargle_oob_default() #' gargle_oauth_cache() #' gargle_oauth_client_type() #' gargle_verbosity() NULL #' @rdname gargle_options #' @export #' @section `gargle_oauth_email`: #' `gargle_oauth_email()` returns the option named "gargle_oauth_email", which #' is undefined by default. If set, this option should be one of: #' * An actual email address corresponding to your preferred Google identity. #' Example:`janedoe@gmail.com`. #' * A glob pattern that indicates your preferred Google domain. #' Example:`*@example.com`. #' * `TRUE` to allow email and OAuth token auto-discovery, if exactly one #' suitable token is found in the cache. #' * `FALSE` or `NA` to force the OAuth dance in the browser. gargle_oauth_email <- function() { getOption("gargle_oauth_email") } #' @rdname gargle_options #' @export #' @section `gargle_oob_default`: #' `gargle_oob_default()` returns `TRUE` unconditionally on RStudio Server, #' Posit Workbench, Posit Cloud, or Google Colaboratory, since it is not #' possible to launch a local web server in these contexts. In this case, for #' the final step of the OAuth dance, the user is redirected to a specific URL #' where they must copy a code and paste it back into the R session. #' #' In all other contexts, `gargle_oob_default()` consults the option named #' `"gargle_oob_default"`, then the option named `"httr_oob_default"`, and #' eventually defaults to `FALSE`. #' #' "oob" stands for out-of-band. Read more about out-of-band authentication in #' the vignette `vignette("auth-from-web")`. gargle_oob_default <- function() { if (is_hosted_session()) { TRUE } else { getOption("gargle_oob_default") %||% getOption("httr_oob_default") %||% FALSE } } #' @rdname gargle_options #' @export #' @section `gargle_oauth_cache`: #' `gargle_oauth_cache()` returns the option named "gargle_oauth_cache", #' defaulting to `NA`. If defined, the option must be set to a logical value or #' a string. `TRUE` means to cache using the default user-level cache file, #' `~/.R/gargle/gargle-oauth`, `FALSE` means don't cache, and `NA` means to #' guess using some sensible heuristics. gargle_oauth_cache <- function() { getOption("gargle_oauth_cache", default = NA) } #' @rdname gargle_options #' @export #' @section `gargle_oauth_client_type`: #' `gargle_oauth_client_type()` returns the option named #' "gargle_oauth_client_type", if defined. If defined, the option must be either #' "installed" or "web". If the option is not defined, the function returns: #' * "web" on RStudio Server, Posit Workbench, Posit Cloud, or Google #' Colaboratory #' * "installed" otherwise #' #' Primarily intended to help infer the most suitable OAuth client type when a #' user is relying on a built-in client, such as the tidyverse client used by #' packages like bigrquery, googledrive, and googlesheets4. gargle_oauth_client_type <- function() { opt <- getOption("gargle_oauth_client_type") if (is.null(opt)) { if(is_hosted_session()) "web" else "installed" } else { check_string(opt) arg_match(opt, values = c("installed", "web")) } } gargle/R/inside-the-house.R0000644000176200001440000000212314431310014015176 0ustar liggesusersfrom_permitted_package <- function(env = caller_env()) { env <- topenv(env, globalenv()) if (!isNamespace(env)) { return(FALSE) } nm <- getNamespaceName(env) gargle_debug("attempt to access internal gargle data from: {.pkg {nm}}") nm %in% c("gargle", "googledrive", "bigrquery", "googlesheets4", "gmailr") } check_permitted_package <- function(env = caller_env(), call = caller_env()) { if (!from_permitted_package(env)) { msg <- c( "Attempt to directly access a credential that can only be used within \\ tidyverse packages.", "This error may mean that you need to:", "*" = "Create a new project on Google Cloud Platform.", "*" = "Enable relevant APIs for your project.", "*" = "Create an API key and/or an OAuth client ID.", "*" = "Configure your requests to use your API key and OAuth client ID.", "i" = "See gargle's \"How to get your own API credentials\" vignette for more details:", "i" = "{.url https://gargle.r-lib.org/articles/get-api-credentials.html}" ) gargle_abort(msg, call = call) } invisible(env) } gargle/R/AuthState-class.R0000644000176200001440000002010214440725001015034 0ustar liggesusers#' Create an AuthState #' #' Constructor function for objects of class [AuthState]. #' #' @param package Package name, an optional string. It is recommended to record #' the name of the package whose auth state is being managed. Ultimately, this #' may be used in some downstream messaging. #' @param api_key Optional. API key (a string). Some APIs accept unauthorized, #' "token-free" requests for public resources, but only if the request #' includes an API key. #' @param auth_active Logical. `TRUE` means requests should include a token (and #' probably not an API key). `FALSE` means requests should include an API key #' (and probably not a token). #' @param cred Credentials. Typically populated indirectly via [token_fetch()]. #' @inheritParams gargle2.0_token #' #' @return An object of class [AuthState]. #' @export #' @examples #' my_client <- gargle_oauth_client( #' id = "some_long_client_id", #' secret = "ssshhhhh_its_a_secret", #' name = "my-nifty-oauth-client" #' ) #' #' init_AuthState( #' package = "my_package", #' client = my_client, #' api_key = "api_key_api_key_api_key", #' ) init_AuthState <- function(package = NA_character_, client = NULL, api_key = NULL, auth_active = TRUE, cred = NULL, app = deprecated()) { if (lifecycle::is_present(app)) { lifecycle::deprecate_soft( "1.5.0", "init_AuthState(app)", "init_AuthState(client)" ) client <- app } AuthState$new( package = package, client = client, api_key = api_key, auth_active = auth_active, cred = cred ) } #' Authorization state #' #' @description #' An `AuthState` object manages an authorization state, typically on behalf of #' a wrapper package that makes requests to a Google API. #' #' The `vignette("gargle-auth-in-client-package)` describes a design for wrapper #' packages that relies on an `AuthState` object. This state can then be #' incorporated into the package's requests for tokens and can control the #' inclusion of tokens in requests to the target API. #' #' * `api_key` is the simplest way to associate a request with a specific #' Google Cloud Platform [project](https://cloud.google.com/resource-manager/docs/cloud-platform-resource-hierarchy#projects). #' A few calls to certain APIs, e.g. reading a public Sheet, can succeed #' with an API key, but this is the exception. #' * `client` is an OAuth client ID (and secret) associated with a specific #' Google Cloud Platform [project](https://cloud.google.com/resource-manager/docs/cloud-platform-resource-hierarchy#projects). #' This is used in the OAuth flow, in which an authenticated user authorizes #' the client to access or manipulate data on their behalf. #' * `auth_active` reflects whether outgoing requests will be authorized by an #' authenticated user or are unauthorized requests for public resources. #' These two states correspond to sending a request with a token versus an #' API key, respectively. #' * `cred` is where the current token is cached within a session, once one #' has been fetched. It is generally assumed to be an instance of #' [`httr::TokenServiceAccount`][httr::Token-class] or #' [`httr::Token2.0`][httr::Token-class] (or a subclass thereof), probably #' obtained via [token_fetch()] (or one of its constituent credential #' fetching functions). #' #' An `AuthState` should be created through the constructor function #' [init_AuthState()], which has more details on the arguments. #' #' @param package Package name. #' @param client An OAuth client. #' @param api_key An API key. #' @param auth_active Logical, indicating whether auth is active. #' @param cred Credentials. #' @param app `r lifecycle::badge('deprecated')` Use `client` instead. #' #' @export #' @name AuthState-class AuthState <- R6::R6Class("AuthState", list( #' @field package Package name. package = NULL, #' @field client An OAuth client. client = NULL, #' @field app `r lifecycle::badge('deprecated')` Use `client` instead. app = NULL, #' @field api_key An API key. api_key = NULL, #' @field auth_active Logical, indicating whether auth is active. auth_active = NULL, #' @field cred Credentials. cred = NULL, #' @description Create a new AuthState #' @details For more details on the parameters, see [init_AuthState()] initialize = function(package = NA_character_, client = NULL, api_key = NULL, auth_active = TRUE, cred = NULL, app = deprecated()) { gargle_debug("initializing AuthState") if (lifecycle::is_present(app)) { # I'm using deprecate_warn() intentionally here. If I use # deprecate_soft(), you don't see the warning for a call to # AuthState$new(app). Most folks should be instantiating through # init_AuthState() anyway, so anyone who sees this warning probably needs # to see it. lifecycle::deprecate_warn( "1.5.0", "AuthState$initialize(app)", "AuthState$initialize(client)" ) client <- app } stopifnot( is_scalar_character(package), is.null(client) || is.oauth_app(client), is.null(api_key) || is_string(api_key), is_bool(auth_active), is.null(cred) || inherits(cred, "Token2.0") ) self$package <- package self$client <- client # for backwards compatibility; could eventually be removed self$app <- client self$api_key <- api_key self$auth_active <- auth_active self$cred <- cred self }, #' @description Format an AuthState #' @param ... Not used. format = function(...) { x <- list( package = cli::format_inline("{.pkg {self$package}}"), client = self$client$name, api_key = obfuscate(self$api_key), auth_active = self$auth_active, credentials = cli::format_inline("{.cls {class(self$cred)[[1]]}}") ) c( cli::cli_format_method( cli::cli_h1("") ), glue("{fr(names(x))}: {fl(x)}") ) }, #' @description Set the OAuth client set_client = function(client) { stopifnot(is.null(client) || is.oauth_app(client)) self$client <- client invisible(self) }, #' @description `r lifecycle::badge('deprecated')` Deprecated method to set #' the OAuth client set_app = function(app) { lifecycle::deprecate_soft( "1.5.0", "AuthState$set_app()", "AuthState$set_client()", details = make_package_hint(self$package) ) # needed for backwards compatibility, as long as there are packages out # there consulting .auth$app self$app <- app self$set_client(client = app) }, #' @description Set the API key #' @param value An API key. set_api_key = function(value) { stopifnot(is.null(value) || is_string(value)) self$api_key <- value invisible(self) }, #' @description Set whether auth is (in)active #' @param value Logical, indicating whether to send requests authorized with #' user credentials. set_auth_active = function(value) { stopifnot(isTRUE(value) || isFALSE(value)) self$auth_active <- value invisible(self) }, #' @description Set credentials #' @param cred User credentials. set_cred = function(cred) { self$cred <- cred invisible(self) }, #' @description Clear credentials clear_cred = function() { self$set_cred(NULL) }, #' @description Get credentials get_cred = function() { self$cred }, #' @description Report if we have credentials has_cred = function() { ## FIXME(jennybc): how should this interact with auth_active? should it? !is.null(self$cred) } )) make_package_hint <- function(pkg) { hint <- NULL if (is_string(pkg)) { hint <- glue(" This probably needs to be addressed in the {pkg} package.") url <- pkg_url_bug(pkg) if (!is.null(url)) { hint <- c(hint, glue("Please report the issue at <{url}>.")) } } hint } gargle/R/utils-ui.R0000644000176200001440000002300314433520365013615 0ustar liggesusersgargle_theme <- function() { list( span.field = list(transform = single_quote_if_no_color), # make the default bullet "regular" color, instead of explicitly colored # mostly motivated by consistency with googledrive, where the cumulative # use of color made me want to do this ".bullets .bullet-*" = list( "text-exdent" = 2, before = function(x) paste0(cli::symbol$bullet, " ") ) ) } single_quote_if_no_color <- function(x) quote_if_no_color(x, "'") double_quote_if_no_color <- function(x) quote_if_no_color(x, '"') quote_if_no_color <- function(x, quote = "'") { # TODO: if a better way appears in cli, use it # @gabor says: "if you want to have before and after for the no-color case # only, we can have a selector for that, such as: # span.field::no-color # (but, at the time I write this, cli does not support this yet) if (cli::num_ansi_colors() > 1) { x } else { paste0(quote, x, quote) } } #' @rdname gargle_options #' @export #' @section `gargle_verbosity`: #' `gargle_verbosity()` returns the option named "gargle_verbosity", which #' determines gargle's verbosity. There are three possible values, inspired by #' the logging levels of log4j: #' * "debug": Fine-grained information helpful when debugging, e.g. figuring out #' how `token_fetch()` is working through the registry of credential #' functions. Previously, this was activated by setting an option named #' "gargle_quiet" to `FALSE`. #' * "info" (default): High-level information that a typical user needs to see. #' Since typical gargle usage is always indirect, i.e. gargle is called by #' another package, gargle itself is very quiet. There are very few messages #' emitted when `gargle_verbosity = "info"`. #' * "silent": No messages at all. However, warnings or errors are still thrown #' normally. gargle_verbosity <- function() { gv <- getOption("gargle_verbosity") # help people using the previous option if (is.null(gv)) { gq <- getOption("gargle_quiet") if (isFALSE(gq)) { options(gargle_verbosity = "debug") lifecycle::deprecate_warn( when = "1.1.0", what = I('The "gargle_quiet" option'), with = I('the "gargle_verbosity" option'), details = c( "x" = "Don't do this: `options(gargle_quiet = FALSE)`", "v" = 'Do this instead: `options(gargle_verbosity = "debug")`' ), always = TRUE ) } } gv <- getOption("gargle_verbosity", "info") vals <- c("debug", "info", "silent") if (!is_string(gv) || !(gv %in% vals)) { gargle_abort( 'Option "gargle_verbosity" must be one of: {.or {.field {vals}}}.' ) } gv } #' @rdname gargle_options #' @export #' @param level Verbosity level: "debug" > "info" > "silent" #' @param env The environment to use for scoping local_gargle_verbosity <- function(level, env = caller_env()) { withr::local_options(list(gargle_verbosity = level), .local_envir = env) } #' @rdname gargle_options #' @export #' @param code Code to execute with specified verbosity level with_gargle_verbosity <- function(level, code) { withr::with_options(list(gargle_verbosity = level), code = code) } gargle_debug <- function(text, .envir = caller_env()) { if (gargle_verbosity() == "debug") { cli::cli_div(theme = gargle_theme()) cli::cli_bullets(text, .envir = .envir) } } gargle_info <- function(text, .envir = caller_env()) { if (gargle_verbosity() %in% c("debug", "info")) { cli::cli_div(theme = gargle_theme()) cli::cli_bullets(text, .envir = .envir) } } commapse <- function(...) paste0(..., collapse = ", ") fr <- function(x) format(x, justify = "right") fl <- function(x) format(x, justify = "left") ## obscure part of (sensitive?) strings with '...' ## obfuscate("sensitive", first = 3, last = 2) = "sen...ve" obfuscate <- function(x, first = 7, last = 0) { nc <- nchar(x) ellipsize <- nc > first + last out <- x out[ellipsize] <- paste0( substr(x[ellipsize], start = 1, stop = first), "...", substr(x[ellipsize], start = nc[ellipsize] - last + 1, stop = nc[ellipsize] ) ) out } message <- function(...) { gargle_abort(" Internal error: use {.pkg gargle}'s UI functions, not {.fun message}.") } #' Error conditions for the gargle package #' #' @param class Use only if you want to subclass beyond `gargle_error` #' #' @keywords internal #' @name gargle-conditions #' @noRd NULL gargle_abort <- function(message, ..., class = NULL, .envir = caller_env(), call = caller_env()) { cli::cli_div(theme = gargle_theme()) cli::cli_abort( message, class = c(class, "gargle_error"), .envir = .envir, call = call, ... ) } # my heart's not totally in this because I'm not sure we should really be # throwing any warnings, however we currently do re: token refresh # so this wrapper makes the messaging more humane # I am declining to add a class, e.g. gargle_warning gargle_warn <- function(message, ..., class = NULL, .envir = caller_env()) { cli::cli_div(theme = gargle_theme()) cli::cli_warn(message, .envir = .envir, ...) } gargle_abort_bad_params <- function(names, reason, endpoint_id, call = caller_env()) { gargle_abort( c( "These parameters are {reason}:", bulletize(gargle_map_cli(names), bullet = "x"), "i" = gargle_map_cli( endpoint_id, template = "API endpoint: {.field <>}" ) ), class = "gargle_error_bad_params", call = call, names = names, reason = reason ) } #' Map a cli-styled template over an object #' #' For internal use in gargle, googledrive, and googlesheets4 (for now). #' #' @keywords internal #' @export gargle_map_cli <- function(x, ...) UseMethod("gargle_map_cli") #' @export gargle_map_cli.default <- function(x, ...) { gargle_abort(" Don't know how to {.fun gargle_map_cli} an object of class \\ {.cls {class(x)}}.") } #' @export gargle_map_cli.NULL <- function(x, ...) NULL #' @export gargle_map_cli.character <- function(x, template = "{.field <>}", .open = "<<", .close = ">>", ...) { as.character(glue(template, .open = .open, .close = .close)) } #' Abbreviate a bullet list neatly #' #' For internal use in gargle, googledrive, and googlesheets4 (for now). #' #' @keywords internal #' @export bulletize <- function(x, bullet = "*", n_show = 5, n_fudge = 2) { n <- length(x) n_show_actual <- compute_n_show(n, n_show, n_fudge) out <- utils::head(x, n_show_actual) n_not_shown <- n - n_show_actual out <- set_names(out, rep_along(out, bullet)) if (n_not_shown == 0) { out } else { c(out, " " = glue("{cli::symbol$ellipsis} and {n_not_shown} more")) } } # I don't want to do "... and x more" if x is silly, i.e. 1 or 2 compute_n_show <- function(n, n_show_nominal = 5, n_fudge = 2) { if (n > n_show_nominal && n - n_show_nominal > n_fudge) { n_show_nominal } else { n } } # menu(), but based on readline() + cli and mockable --------------------------- # https://github.com/r-lib/cli/issues/228 # https://github.com/rstudio/rsconnect/blob/main/R/utils-cli.R cli_menu <- function(header, prompt, choices, not_interactive = choices, exit = integer(), .envir = caller_env(), error_call = caller_env()) { if (!is_interactive()) { cli::cli_abort( c(header, not_interactive), .envir = .envir, call = error_call ) } choices <- paste0(cli::style_bold(seq_along(choices)), ": ", choices) cli::cli_inform( c(header, prompt, choices), .envir = .envir ) repeat { selected <- cli_readline("Selection: ") if (selected %in% c("0", seq_along(choices))) { break } cli::cli_inform( "Enter a number between 1 and {length(choices)}, or enter 0 to exit." ) } selected <- as.integer(selected) if (selected %in% c(0, exit)) { if (is_testing()) { cli::cli_abort("Exiting...", call = NULL) } else { cli::cli_alert_danger("Exiting...") # simulate user pressing Ctrl + C invokeRestart("abort") } } selected } cli_readline <- function(prompt) { local_input <- getOption("cli_input", character()) # not convinced that we need to plan for multiple mocked inputs, but leaving # this feature in for now if (length(local_input) > 0) { input <- local_input[[1]] cli::cli_inform(paste0(prompt, input)) options(cli_input = local_input[-1]) input } else { readline(prompt) } } local_user_input <- function(x, env = caller_env()) { withr::local_options( rlang_interactive = TRUE, # trailing 0 prevents infinite loop if x only contains invalid choices cli_input = c(x, "0"), .local_envir = env ) } is_testing <- function() { identical(Sys.getenv("TESTTHAT"), "true") } # taken from lifecycle # https://github.com/r-lib/lifecycle/blob/9417eca8f5091f95b4569fb6c388e4394e2b2157/R/utils.R#L30 pkg_url_bug <- function(pkg) { # First check that package is installed, e.g. in case of # runtime-only namespace created by pkgload if (nzchar(system.file(package = pkg))) { url <- utils::packageDescription(pkg)$BugReports # `url` can be NULL if not part of the description if (is_string(url) && grepl("^https://", url)) { return(url) } } NULL } gargle/R/aaa.R0000644000176200001440000000153514365562124012575 0ustar liggesusers#' Environment used for gargle global state #' #' This environment contains: #' * `$cred_funs` is the ordered list of credential functions to use when trying #' to fetch credentials. It is populated by a call to #' `cred_funs_set_default()` in `.onLoad()`. #' * `$last_response` is the most recent response provided to #' `response_process()`. #' #' @noRd #' @format An environment. #' @keywords internal gargle_env <- new.env(parent = emptyenv()) gargle_env$cred_funs <- list() gargle_env$last_response <- list() gargle_env$last_error <- list() gargle_last_response <- function() { gargle_env$last_response } gargle_last_content <- function() { resp <- gargle_last_response() if (inherits(resp, "response")) { tryCatch( response_as_json(resp), gargle_error_request_failed = function(e) e$message ) } else { list() } } gargle/R/gargle_api_key.R0000644000176200001440000000216014431310014014767 0ustar liggesusers#' API key for demonstration purposes #' #' @description Some APIs accept requests for public resources, in which case #' the request must be sent with an API key in lieu of a token. This function #' provides an API key for limited use in prototyping and for testing and #' documentation of gargle itself. This key may be deleted or rotated at any #' time. There are no guarantees about which APIs are enabled. DO NOT USE THIS #' IN A PACKAGE or for anything other than interactive, small-scale #' experimentation. #' #' You can get your own API key, without these limitations. See the [How to #' get your own API #' credentials](https://gargle.r-lib.org/articles/get-api-credentials.html) #' vignette for more details. #' #' @export #' @keywords internal #' @examples #' gargle_api_key() gargle_api_key <- function() { gak() } #' Assets for internal use #' #' Assets for use inside specific packages maintained by the tidyverse team. #' #' @name internal-assets NULL #' @export #' @keywords internal #' @rdname internal-assets tidyverse_api_key <- function() { check_permitted_package(caller_env()) tak() } gargle/R/credentials_user_oauth2.R0000644000176200001440000000531314433520365016663 0ustar liggesusers#' Get an OAuth token for a user #' #' @description Consults the token cache for a suitable OAuth token and, if #' unsuccessful, gets a token via the browser flow. A cached token is suitable #' if it's compatible with the user's request in this sense: #' * OAuth client must be same. #' * Scopes must be same. #' * Email, if provided, must be same. If specified email is a glob pattern #' like `"*@example.com"`, email matching is done at the domain level. #' #' gargle is very conservative about using OAuth tokens discovered in the user's #' cache and will generally seek interactive confirmation. Therefore, in a #' non-interactive setting, it's important to explicitly specify the `"email"` #' of the target account or to explicitly authorize automatic discovery. See #' [gargle2.0_token()], which this function wraps, for more. Non-interactive use #' also suggests it might be time to use a [service account #' token][credentials_service_account] or [workload identity #' federation][credentials_external_account]. #' #' @param scopes A character vector of scopes to request. Pick from those listed #' at . #' #' For certain token flows, the #' `"https://www.googleapis.com/auth/userinfo.email"` scope is unconditionally #' included. This grants permission to retrieve the email address associated #' with a token; gargle uses this to index cached OAuth tokens. This grants no #' permission to view or send email and is generally considered a low-value #' scope. #' @inheritParams gargle2.0_token #' @inheritDotParams gargle2.0_token -scope -client -package #' #' @return A [Gargle2.0] token. #' @family credential functions #' @export #' @examples #' \dontrun{ #' # Drive scope, built-in gargle demo client #' scopes <- "https://www.googleapis.com/auth/drive" #' credentials_user_oauth2(scopes, client = gargle_client()) #' #' # bring your own client #' client <- gargle_oauth_client_from_json( #' path = "/path/to/the/JSON/you/downloaded/from/gcp/console.json", #' name = "my-nifty-oauth-client" #' ) #' credentials_user_oauth2(scopes, client) #' } credentials_user_oauth2 <- function(scopes = NULL, client = gargle_client(), package = "gargle", ..., app = deprecated()) { gargle_debug("trying {.fun credentials_user_oauth2}") if (lifecycle::is_present(app)) { lifecycle::deprecate_soft( "1.5.0", "credentials_user_oauth2(app)", "credentials_user_oauth2(client)" ) client <- app } gargle2.0_token( client = client, scope = scopes, package = package, ... ) } gargle/R/credentials_gce.R0000644000176200001440000003220714431310014015145 0ustar liggesusers#' Get a token from the Google metadata server #' #' @description #' #' If your code is running on Google Cloud, we can often obtain a token for an #' attached service account directly from a metadata server. This is more secure #' than working with an explicit a service account key, as #' [credentials_service_account()] does, and is the preferred method of auth for #' workloads running on Google Cloud. #' #' The most straightforward scenario is when you are working in a VM on Google #' Compute Engine and it's OK to use the default service account. This should #' "just work" automatically. #' #' `credentials_gce()` supports other use cases (such as GKE Workload Identity), #' but may require some explicit setup, such as: #' * Create a service account, grant it appropriate scopes(s) and IAM roles, #' attach it to the target resource. This prep work happens outside of R, e.g., #' in the Google Cloud Console. On the R side, provide the email address of this #' appropriately configured service account via `service_account`. #' * Specify details for constructing the root URL of the metadata service: #' - The logical option `"gargle.gce.use_ip"`. If undefined, this defaults to #' `FALSE`. #' - The environment variable `GCE_METADATA_URL` is consulted when #' `"gargle.gce.use_ip"` is `FALSE`. If undefined, the default is #' `metadata.google.internal`. #' - The environment variable `GCE_METADATA_IP` is consulted when #' `"gargle.gce.use_ip"` is `TRUE`. If undefined, the default is #' `169.254.169.254`. #' #' * Change (presumably increase) the timeout for requests to the metadata #' server via the `"gargle.gce.timeout"` global option. This timeout is given in #' seconds and is set to a value (strategy, really) that often works well in #' practice. However, in some cases it may be necessary to increase the timeout #' with code such as: #' ``` r #' options(gargle.gce.timeout = 3) #' ``` #' For details on specific use cases, such as Google Kubernetes Engine (GKE), #' see `vignette("non-interactive-auth")`. #' #' @inheritParams token_fetch #' @param service_account Name of the GCE service account to use. #' #' @seealso A related auth flow that can be used on certain non-Google cloud #' providers is workload identity federation, which is implemented in #' [credentials_external_account()]. #' #' #' #' #' #' How to attach a service account to a resource: #' #' #' #' #' #' #' #' #' @return A [GceToken()] or `NULL`. #' @family credential functions #' @export #' @examples #' \dontrun{ #' credentials_gce() #' } credentials_gce <- function(scopes = "https://www.googleapis.com/auth/cloud-platform", service_account = "default", ...) { gargle_debug("Trying {.fun credentials_gce} ...") if (!is_gce()) { gargle_debug(c("x" = "We don't seem to be on GCE.")) return(NULL) } scopes <- scopes %||% "https://www.googleapis.com/auth/cloud-platform" requested_scopes <- normalize_scopes(scopes) dat <- gce_instance_service_accounts() service_account_details <- as.list(dat[dat$name == service_account, ]) account_scopes <- service_account_details$scopes account_scopes <- normalize_scopes(strsplit(account_scopes, split = ",")[[1]]) missing <- setdiff(requested_scopes, account_scopes) if (length(missing) > 0) { gargle_debug(c( "!" = "{cli::qty(length(missing))}{?This/These} requested \\ scope{?s} {?is/are} not among the scopes for the \\ {.val {service_account}} service account:", bulletize(missing, bullet = "x"), "i" = "If there are problems downstream, this might be the root cause." )) } token <- gce_access_token(scopes, service_account = service_account) if (is.null(token$credentials$access_token) || !nzchar(token$credentials$access_token)) { NULL } else { gargle_debug("GCE service account email: {.email {service_account_details$email}}") gargle_debug("GCE service account name: {.val {token$params$service_account}}") gargle_debug("GCE access token scopes: {.val {commapse(base_scope(token$params$scope))}}") token } } #' Fetch access token for a service account on GCE #' #' @inheritParams credentials_gce #' #' @keywords internal #' @export gce_access_token <- function(scopes = "https://www.googleapis.com/auth/cloud-platform", service_account = "default") { params <- list( scope = scopes, service_account = service_account, as_header = TRUE ) GceToken$new( params = params ) } #' Token for use on Google Compute Engine instances #' #' This class uses the metadata service available on GCE VMs to fetch access #' tokens. Not intended for direct use. See [credentials_gce()] instead. #' #' @param ... Not used. #' #' @keywords internal #' @export GceToken <- R6::R6Class("GceToken", inherit = httr::Token2.0, list( #' @description Get an access for a GCE service account. #' @param params A list of parameters for `fetch_gce_access_token()`. #' @return A GceToken. initialize = function(params) { gargle_debug("GceToken initialize") self$params <- params self$init_credentials() }, #' @description Request an access token. init_credentials = function() { gargle_debug("GceToken init_credentials") token <- fetch_gce_access_token( self$params$scope, service_account = self$params$service_account ) # find out the scopes actually obtained # https://www.googleapis.com/oauth2/v3/tokeninfo req <- request_build( method = "GET", path = "oauth2/v3/tokeninfo", params = list(access_token = token$access_token), base_url = "https://www.googleapis.com" ) resp <- request_make(req) info <- response_process(resp) actual_scopes <- normalize_scopes(strsplit(info$scope, split = "\\s+")[[1]]) missing <- setdiff(self$params$scope, actual_scopes) if (length(missing) > 0) { gargle_debug(c( "!" = "{cli::qty(length(missing))}{?This/These} requested \\ scope{?s} {?is/are} not among the scopes for the \\ access token returned by the metadata server:", bulletize(missing, bullet = "x"), "i" = "If there are problems downstream, this might be the root cause." )) } if (!setequal(self$params$scope, actual_scopes)) { gargle_debug(c( "!" = "Updating token scopes to reflect its actual scopes:", bulletize(actual_scopes) )) self$params$scope <- actual_scopes } self$credentials <- token self }, #' @description Refreshes the token. In this case, that just means "ask again #' for an access token". refresh = function() { gargle_debug("GceToken refresh") # There's something kind of wrong about this, because it's not a true # refresh. But this method is basically required by the way httr currently # works. # This means that some uses of $refresh() aren't really appropriate for a # GceToken. # For example, if I attempt token_userinfo(x) on a GceToken that lacks # appropriate scope, it fails with 401. # httr tries to "fix" things by refreshing the token. But this is # not a problem that refreshing can fix. # I've now prevented an explicit refresh in token_userinfo(), but an # implicit one still eventually happens in httr:::request_perform(). self$init_credentials() }, #' @description Placeholder implementation of required method. Returns `TRUE`. can_refresh = function() { TRUE }, #' @description Format a [GceToken()]. #' @param ... Not used. format = function(...) { x <- list( scopes = commapse(base_scope(self$params$scope)), credentials = commapse(names(self$credentials)) ) c( cli::cli_format_method( cli::cli_h1("") ), glue("{fr(names(x))}: {fl(x)}") ) }, #' @description Print a [GceToken()]. #' @param ... Not used. print = function(...) { # a format method is not sufficient for GceToken because the parent class # has a print method cli::cat_line(self$format()) }, # Never cache #' @description Placeholder implementation of required method. cache = function() self, #' @description Placeholder implementation of required method. load_from_cache = function() self, # These methods don't really make sense for GCE access tokens #' @description Placeholder implementation of required method. revoke = function() { gargle_abort("{.fun $revoke} is not implemented for {.cls GceToken}") }, #' @description Placeholder implementation of required method validate = function() { gargle_abort("{.fun $validate} is not implemented for {.cls GceToken}") } )) gce_metadata_hostname <- function() { use_ip <- getOption("gargle.gce.use_ip", FALSE) if (isTRUE(use_ip)) { Sys.getenv("GCE_METADATA_IP", "169.254.169.254") } else { Sys.getenv("GCE_METADATA_URL", "metadata.google.internal") } } gce_metadata_request <- function(path = "", query = NULL, stop_on_error = TRUE) { # TODO(craigcitro): Add options to ignore proxies. if (grepl("^/", path)) { path <- substring(path, 2) } url_parts <- structure( list( scheme = "http", hostname = gce_metadata_hostname(), path = path, query = query ), class = "url" ) url <- httr::build_url(url_parts) response <- try( { httr::with_config(httr::timeout(gce_timeout()), { httr::GET(url, httr::add_headers("Metadata-Flavor" = "Google")) }) }, silent = TRUE ) if (stop_on_error) { if (inherits(response, "try-error")) { gargle_abort(" Error fetching GCE metadata: {attr(response, 'condition')$message}") } else if (httr::http_error(response)) { gargle_abort(" Error fetching GCE metadata: {httr::http_status(response)$message}") } if (response$headers$`metadata-flavor` != "Google") { gargle_abort(" Error fetching GCE metadata: missing/invalid metadata-flavor header") } } response } # https://cloud.google.com/compute/docs/instances/detect-compute-engine is_gce <- function() { response <- gce_metadata_request(stop_on_error = FALSE) !(inherits(response, "try-error") || httr::http_error(response)) } #' List all service accounts available on this GCE instance #' #' @returns A data frame, where each row is a service account. Due to aliasing, #' there is no guarantee that each row represents a distinct service account. #' #' @seealso The return value is built from a recursive query of the so-called #' "directory" of the instance's service accounts as documented in #' . #' #' @export #' @examplesIf gargle:::is_gce() #' credentials_gce() gce_instance_service_accounts <- function() { response <- gce_metadata_request( "computeMetadata/v1/instance/service-accounts", query = list(recursive = "true") ) raw <- transpose(response_as_json(response)) data.frame( name = names(raw$email), email = unlist(raw$email), aliases = map_chr(raw$aliases, function(x) glue_collapse(x, sep = ",")), scopes = map_chr(raw$scopes, function(x) glue_collapse(x, sep = ",")), stringsAsFactors = FALSE, row.names = NULL ) } # TODO: why isn't scopes used here at all? # the python auth library definitely passes scopes: # https://github.com/googleapis/google-auth-library-python/blob/a83af399fe98764ee851997bf3078ec45a9b51c9/google/auth/compute_engine/_metadata.py#L237 # perhaps there are use cases where it would be helpful it we did same: # https://github.com/r-lib/gargle/issues/216 fetch_gce_access_token <- function(scopes, service_account) { path <- glue("computeMetadata/v1/instance/service-accounts/{service_account}/token") scope_string <- glue_collapse(scopes, sep = ",") response <- gce_metadata_request(path, query = list(scopes = scope_string)) httr::content(response, as = "parsed", type = "application/json") } # wrapper to access the "gargle.gce.timeout" option # https://github.com/r-lib/gargle/issues/186 # https://github.com/r-lib/gargle/pull/195 # if called with no argument: # if option is set, return that value # if unset: return a short default, suitable for initial ping of # the metadata server (and not too burdensome for non-GCE users) and set the # option to a longer default, suitable for a subsequent request for all # service accounts or a specific token # if called with an argument: # set the option to that value (and return the old value) gce_timeout <- function(v) { opt <- getOption("gargle.gce.timeout") if (missing(v)) { if (is.null(opt)) { ret <- 0.8 # short default timeout options(gargle.gce.timeout = 2) # long default timeout } else { ret <- opt } } else { ret <- options(gargle.gce.timeout = v)[["gargle.gce.timeout"]] } ret } gargle/R/oauth-refresh.R0000644000176200001440000000640414444241216014621 0ustar liggesusers# this file has its origins in oauth-refresh.R and oauth-error.R in httr # I want to introduce behaviour to error informatively for a deleted OAuth client # Refresh an OAuth 2.0 credential. # # Refreshes the given token, and returns a new credential with a # valid access_token. Based on: # https://developers.google.com/identity/protocols/oauth2/native-app#offline refresh_oauth2.0 <- function(endpoint, client, credentials, package = NULL) { if (is.null(credentials$refresh_token)) { gargle_abort("Refresh token not available.") } refresh_url <- endpoint$access req_params <- list( refresh_token = credentials$refresh_token, client_id = client$key, client_secret = client$secret, grant_type = "refresh_token" ) response <- httr::POST(refresh_url, body = req_params, encode = "form") err <- find_oauth2.0_error(response) if (!is.null(err)) { gargle_refresh_failure(err, client, package) return(NULL) } httr::stop_for_status(response) refresh_data <- httr::content(response) utils::modifyList(credentials, refresh_data) } oauth2.0_error_codes <- c( 400, 401 ) # This implements error checking according to the OAuth2.0 # specification: https://tools.ietf.org/html/rfc6749#section-5.2 find_oauth2.0_error <- function(response) { if (!httr::status_code(response) %in% oauth2.0_error_codes) { return(NULL) } content <- httr::content(response) if (is.null(content$error)) { return(NULL) } list( error = content$error, error_description = content$error_description, error_uri = content$error_uri ) } gargle_refresh_failure <- function(err, client, package = NULL) { if (!identical(err$error, "deleted_client")) { # this is basically what httr does, except we don't have an explicit # whitelist of acceptable values of err$error, because we know Google does # not limit itself to these gargle_warn(c( "Unable to refresh token: {err$error}", "*" = err$error_description, "*" = err$error_uri )) return(invisible()) } # special handling for 'deleted_client' client_name <- client$name %||% client$appname %||% "" is_legacy_app <- grepl(gargle_legacy_app_pattern(), client_name) # client looks like one of "ours" if (is_legacy_app) { main_pkg <- package %||% "gargle" all_pkgs <- if (main_pkg == "gargle") "gargle" else c(main_pkg, "gargle") gargle_warn(c( "Unable to refresh token, because the associated OAuth client \\ has been deleted.", "i" = "You appear to be relying on the default client used by the \\ {.pkg {main_pkg}} package.", " " = "Consider re-installing {.pkg {all_pkgs}}, \\ in case the default client has been updated." )) return(invisible()) } # deleted client doesn't seem to be one of "ours" gargle_warn(c( "Unable to refresh token, because the associated OAuth client \\ has been deleted.", "*" = if (nzchar(client_name)) "Client name: {.field {client_name}}", if (!is.null(package)) { c( "i" = "If you did not configure this OAuth client, it may be built into \\ the {.pkg {package}} package.", " " = "If so, consider re-installing {.pkg {package}} to get an updated \\ client." ) } )) invisible() } gargle/R/Gargle-class.R0000644000176200001440000003722714453545575014377 0ustar liggesusers#' Generate a gargle token #' #' Constructor function for objects of class [Gargle2.0]. #' #' @param email Optional. If specified, `email` can take several different #' forms: #' * `"jane@gmail.com"`, i.e. an actual email address. This allows the user to #' target a specific Google identity. If specified, this is used for token #' lookup, i.e. to determine if a suitable token is already available in the #' cache. If no such token is found, `email` is used to pre-select the targeted #' Google identity in the OAuth chooser. (Note, however, that the email #' associated with a token when it's cached is always determined from the token #' itself, never from this argument). #' * `"*@example.com"`, i.e. a domain-only glob pattern. This can be helpful if #' you need code that "just works" for both `alice@example.com` and #' `bob@example.com`. #' * `TRUE` means that you are approving email auto-discovery. If exactly one #' matching token is found in the cache, it will be used. #' * `FALSE` or `NA` mean that you want to ignore the token cache and force a #' new OAuth dance in the browser. #' #' Defaults to the option named `"gargle_oauth_email"`, retrieved by #' [gargle::gargle_oauth_email()] (unless a wrapper package implements different #' default behavior). #' @param client A Google OAuth client, preferably constructed via #' [gargle::gargle_oauth_client_from_json()], which returns an instance of #' `gargle_oauth_client`. For backwards compatibility, for a limited time, #' gargle will still accept an "OAuth app" created with [httr::oauth_app()]. #' @param package Name of the package requesting a token. Used in messages. #' @param scope A character vector of scopes to request. #' @param use_oob Whether to use out-of-band authentication (or, perhaps, a #' variant implemented by gargle and known as "pseudo-OOB") when first #' acquiring the token. Defaults to the value returned by #' [gargle::gargle_oob_default()]. Note that (pseudo-)OOB auth only affects #' the initial OAuth dance. If we retrieve (and possibly refresh) a #' cached token, `use_oob` has no effect. #' #' If the OAuth client is provided implicitly by a wrapper package, its type #' probably defaults to the value returned by #' [gargle::gargle_oauth_client_type()]. You can take control of the client #' type by setting `options(gargle_oauth_client_type = "web")` or #' `options(gargle_oauth_client_type = "installed")`. #' @param cache Specifies the OAuth token cache. Defaults to the option named #' `"gargle_oauth_cache"`, retrieved via [gargle::gargle_oauth_cache()]. #' @inheritParams httr::oauth2.0_token #' @param ... Absorbs arguments intended for use by other credential functions. #' Not used. #' @param app `r lifecycle::badge('deprecated')` Replaced by the `client` #' argument. #' @return An object of class [Gargle2.0], either new or loaded from the cache. #' @export #' @examples #' \dontrun{ #' gargle2.0_token() #' } gargle2.0_token <- function(email = gargle_oauth_email(), client = gargle_client(), package = "gargle", ## params start scope = NULL, use_oob = gargle_oob_default(), ## params end credentials = NULL, cache = if (is.null(credentials)) gargle_oauth_cache() else FALSE, ..., app = deprecated()) { if (lifecycle::is_present(app)) { lifecycle::deprecate_soft( "1.5.0", "gargle2.0_token(app)", "gargle2.0_token(client)" ) client <- app } params <- list( scope = scope, use_oob = use_oob, as_header = TRUE ) # pseudo-OOB flow client_type <- if (inherits(client, "gargle_oauth_client")) client$type else NA if (use_oob && identical(client_type, "web")) { params$oob_value <- select_pseudo_oob_value(client$redirect_uris) } # params$oob_value is deliberately left unspecified for conventional OOB, # with the intent of falling back to urn:ietf:wg:oauth:2.0:oob # this allows pseudo-OOB auth to work on colab, because: # 1) gargle's attempts to communicate with the user route through readline() # which is shimmed in Jupyter (and therefore Colab) # 2) httr >= 1.4.5 honors the "rlang_interactive" option when deciding whether # it will try the oauth dance if (is_google_colab()) { withr::local_options(rlang_interactive = TRUE) } Gargle2.0$new( email = email, client = client, package = package, params = params, credentials = credentials, cache_path = cache ) } #' OAuth2 token objects specific to Google APIs #' #' @description #' `Gargle2.0` is based on the [`Token2.0`][httr::Token-class] class provided in #' httr. The preferred way to create a `Gargle2.0` token is through the #' constructor function [gargle2.0_token()]. Key differences with `Token2.0`: #' * The key for a cached `Token2.0` comes from hashing the endpoint, client, #' and scopes. For the `Gargle2.0` subclass, the identifier or key is expanded #' to include the email address associated with the token. This makes it easier #' to work with Google APIs with multiple identities. #' * `Gargle2.0` tokens are cached, by default, at the user level, following the #' XDG spec for storing user-specific data and cache files. In contrast, the #' default location for `Token2.0` is `./.httr-oauth`, i.e. in current working #' directory. `Gargle2.0` behaviour makes it easier to reuse tokens across #' projects and makes it less likely that tokens are accidentally synced to a #' remote location like GitHub or DropBox. #' * Each `Gargle2.0` token is cached in its own file. The token cache is a #' directory of such files. In contrast, `Token2.0` tokens are cached as #' components of a list, which is typically serialized to `./.httr-oauth`. #' #' @param email Optional email address. See [gargle2.0_token()] for full #' details. #' @param client An OAuth consumer application. #' @param package Name of the package requesting a token. Used in messages. #' @param credentials Exists largely for testing purposes. #' @param params A list of parameters for the internal function #' `init_oauth2.0()`, which is a modified version of [httr::init_oauth2.0()]. #' gargle actively uses `scope` and `use_oob`, but does not use `user_params`, #' `type`, `as_header` (hard-wired to `TRUE`), `use_basic_auth` (accept #' default of `use_basic_auth = FALSE`), `config_init`, or #' `client_credentials`. #' @param cache_path Specifies the OAuth token cache. Read more in #' [gargle::gargle_oauth_cache()]. #' @param app `r lifecycle::badge('deprecated')` Use `client` instead. #' #' @keywords internal #' @export #' @name Gargle-class Gargle2.0 <- R6::R6Class("Gargle2.0", inherit = httr::Token2.0, list( #' @field email Email associated with the token. email = NULL, #' @field package Name of the package requesting a token. Used in messages. package = NULL, #' @field client An OAuth client. client = NULL, #' @description Create a Gargle2.0 token #' @return A Gargle2.0 token. initialize = function(email = gargle_oauth_email(), client = gargle_client(), package = "gargle", credentials = NULL, params = list(), cache_path = gargle_oauth_cache(), app = deprecated()) { gargle_debug("Gargle2.0 initialize") # I'm using deprecate_warn() intentionally here. Most folks should be # instantiating through gargle2.0_token() anyway, so anyone who sees this # warning probably needs to see it. if (lifecycle::is_present(app)) { lifecycle::deprecate_warn( "1.5.0", "Gargle2.0$initialize(app)", "Gargle2.0$initialize(client)" ) client <- app } stopifnot( is.null(email) || is_scalar_character(email) || isTRUE(email) || isFALSE(email) || is_na(email), is.oauth_app(client), is_string(package), is.list(params) ) if (identical(email, "")) { gargle_abort(c( "{.arg email} must not be \"\" (the empty string).", "i" = "Do you intend to consult an env var, but it's unset?" )) } if (isTRUE(email)) { email <- "*" } if (isFALSE(email) || is_na(email)) { email <- NA_character_ } # https://developers.google.com/identity/protocols/OpenIDConnect#login-hint # optional hint for the auth server to pre-fill the email box login_hint <- if (is_string(email) && !startsWith(email, "*")) email self$endpoint <- gargle_oauth_endpoint() self$email <- email self$client <- client # for backwards compatibility and also because the parent class has $app; # I can never remove it self$app <- client self$package <- package params$scope <- normalize_scopes(add_email_scope(params$scope)) params$query_authorize_extra <- list(login_hint = login_hint) self$params <- params self$cache_path <- cache_establish(cache_path) if (!is.null(credentials)) { # Use credentials created elsewhere - usually for tests gargle_debug("credentials provided directly") self$credentials <- credentials return(self$cache()) } # Are credentials cached already? if (self$load_from_cache()) { self } else { gargle_debug("no matching token in the cache") self$init_credentials() self$email <- token_email(self) %||% NA_character_ self$cache() } }, #' @description Format a Gargle2.0 token #' @param ... Not used. format = function(...) { x <- list( oauth_endpoint = "google", client = self$client$name, email = cli::format_inline("{.email {self$email}}"), scopes = commapse(base_scope(self$params$scope)), credentials = commapse(names(self$credentials)) ) c( cli::cli_format_method( cli::cli_h1("") ), glue("{fr(names(x))}: {fl(x)}") ) }, #' @description Print a Gargle2.0 token #' @param ... Not used. print = function(...) { # a format method is not sufficient for Gargle2.0 because the parent class # has a print method cli::cat_line(self$format()) }, #' @description Generate the email-augmented hash of a Gargle2.0 token hash = function() { paste(super$hash(), self$email, sep = "_") }, #' @description Put a Gargle2.0 token into the cache cache = function() { token_into_cache(self) self }, #' @description (Attempt to) get a Gargle2.0 token from the cache load_from_cache = function() { gargle_debug("loading token from the cache") if (is.null(self$cache_path) || is_na(self$email)) { return(FALSE) } gargle_debug("email: {.email {self$email}}") gargle_debug("oauth client name: {self$client$name}") gargle_debug("oauth client name: {self$client$type}") gargle_debug("oauth client id: {self$client$id}") gargle_debug("scopes: {commapse(base_scope(self$params$scope))}") cached <- token_from_cache(self) if (is.null(cached)) { return(FALSE) } gargle_debug("matching token found in the cache") self$endpoint <- cached$endpoint self$email <- cached$email self$client <- cached$client self$app <- cached$client self$credentials <- cached$credentials self$params <- cached$params TRUE }, #' @description (Attempt to) refresh a Gargle2.0 token refresh = function() { cred <- refresh_oauth2.0( self$endpoint, self$client, self$credentials, package = self$package ) if (is.null(cred)) { token_remove_from_cache(self) # It's tricky to decide what to do here. Currently we return the current, # invalid, unrefreshed token, but we clear the refresh_token field, to # prevent subsequent refresh attempts. # # Analysis from a BYO token POV: # I've decided the status quo may be the best move, because it causes # token_fetch() to return instead of moving on to try other methods. If # someone provides token_fetch(token =), I think it's clear that they # want/hope to use that token and they don't want to end up doing the # OAuth browser dance. If we threw an error or returned NULL, # token_fetch() would just keep going. The refresh failure does throw a # visible warning: # # Warning message: # Unable to refresh token: invalid_grant # • Token has been expired or revoked. # # However, this does mean that functions like PKG_has_token() still return # TRUE and that some other method must be used to find out if we have a # *valid* token. gargle::token_tokeninfo() and API-specific functions for # "tell me about the current user" are good candidates, such as # gmailr::gm_profile() or googledrive::drive_user(). self$credentials$refresh_token <- NULL } else { self$credentials <- cred self$cache() } self }, #' @description Initiate a new Gargle2.0 token init_credentials = function() { gargle_debug("initiating new token") if (is_interactive()) { if (!isTRUE(self$params$use_oob) && !is_hosted_session()) { encourage_httpuv() } self$credentials <- init_oauth2.0( self$endpoint, self$client, scope = self$params$scope, use_oob = self$params$use_oob, oob_value = self$params$oob_value, query_authorize_extra = self$params$query_authorize_extra ) } else { # TODO: good candidate for an eventual sub-classed gargle error # would be useful in testing to know that this is exactly where we aborted gargle_abort("OAuth2 flow requires an interactive session.") } } )) encourage_httpuv <- function() { if (!is_interactive() || isTRUE(is_installed("httpuv"))) { return(invisible()) } choice <- cli_menu( "The {.pkg httpuv} package enables a nicer Google auth experience, in many \\ cases, but it isn't installed.", "Would you like to install it now?", choices = c("Yes", "No") ) if (choice == 1) { utils::install.packages("httpuv") } invisible() } # I want to encourage users to create an OAuth client (newer httr2-y language) # directly from downloaded JSON, using gargle_oauth_client_from_json(). # Sometimes there are multiple URIs and I think we can usually figure out which # one to use for the pseudo-OOB flow. select_pseudo_oob_value <- function(redirect_uris) { # https://developers.google.com/identity/protocols/oauth2/resources/oob-migration#inspect-your-application-code bad_values <- c( "urn:ietf:wg:oauth:2.0:oob", "urn:ietf:wg:oauth:2.0:oob:auto", "oob" ) redirect_uris <- setdiff(redirect_uris, bad_values) # https://developers.google.com/identity/protocols/oauth2/web-server#uri-validation bad_regex <- "^http[s]?://(localhost|127.0.0.1)" redirect_uris <- grep(bad_regex, redirect_uris, value = TRUE, invert = TRUE) redirect_uris <- grep("^https", redirect_uris, value = TRUE) # inspired by these guidelines re: URIs associated with URL shorteners: # 'redirect URI must either contain "/google-callback/" in its path or end # with "/google-callback"' m <- grep("/google-callback(/|$)", redirect_uris) if (length(m) > 0) { redirect_uris <- redirect_uris[m] } if (length(redirect_uris) == 0) { gargle_abort(' OAuth client does not have a redirect URI suitable for the pseudo-OOB \\ flow.') } if (length(redirect_uris) > 1) { msg <- c( "Can't determine which redirect URI to use for the pseudo-OOB flow:", set_names(redirect_uris, ~ rep_along(., "*")) ) gargle_abort(msg) } redirect_uris } gargle/R/response_process.R0000644000176200001440000002760414431310014015433 0ustar liggesusers#' Process a Google API response #' #' @description #' `response_process()` is intended primarily for internal use in client #' packages that provide high-level wrappers for users. Typically applied as the #' final step in this sequence of calls: #' * Request prepared with [request_build()]. #' * Request made with [request_make()]. #' * Response processed with `response_process()`. #' #' All that's needed for a successful request is to parse the JSON extracted via #' `httr::content()`. Therefore, the main point of `response_process()` is to #' handle less happy outcomes: #' * Status codes in the 400s (client error) and 500s (server error). The #' structure of the error payload varies across Google APIs and we try to #' create a useful message for all variants we know about. #' * Non-JSON content type, such as HTML. #' * Status code in the 100s (information) or 300s (redirection). These are #' unexpected. #' #' If `process_response()` results in an error, a redacted version of the `resp` #' input is returned in the condition (auth tokens are removed). #' #' @details #' When `remember = TRUE` (the default), gargle stores the most recently seen #' response internally, for *post hoc* examination. The stored response is #' literally just the most recent `resp` input, but with auth tokens redacted. #' It can be accessed via the unexported function #' `gargle:::gargle_last_response()`. A companion function #' `gargle:::gargle_last_content()` returns the content of the last response, #' which is probably the most useful form for *post mortem* analysis. #' #' The `response_as_json()` helper is exported only as an aid to maintainers who #' wish to use their own `error_message` function, instead of gargle's built-in #' `gargle_error_message()`. When implementing a custom `error_message` #' function, call `response_as_json()` immediately on the input in order to #' inherit gargle's handling of non-JSON input. #' #' @param resp Object of class `response` from [httr]. #' @param error_message Function that produces an informative error message from #' the primary input, `resp`. It must return a character vector. #' @param remember Whether to remember the most recently processed response. #' @inheritParams rlang::abort #' #' @return The content of the request, as a list. An HTTP status code of 204 (No #' content) is a special case returning `TRUE`. #' @family requests and responses #' @export #' @examples #' \dontrun{ #' # get an OAuth2 token with 'userinfo.email' scope #' token <- token_fetch(scopes = "https://www.googleapis.com/auth/userinfo.email") #' #' # see the email associated with this token #' req <- gargle::request_build( #' method = "GET", #' path = "v1/userinfo", #' token = token, #' base_url = "https://openidconnect.googleapis.com" #' ) #' resp <- gargle::request_make(req) #' response_process(resp) #' #' # make a bad request (this token has incorrect scope) #' req <- gargle::request_build( #' method = "GET", #' path = "fitness/v1/users/{userId}/dataSources", #' token = token, #' params = list(userId = 12345) #' ) #' resp <- gargle::request_make(req) #' response_process(resp) #' } response_process <- function(resp, error_message = gargle_error_message, remember = TRUE, call = caller_env()) { if (remember) { gargle_env$last_response <- redact_response(resp) } code <- httr::status_code(resp) if (code >= 200 && code < 300) { if (code == 204) { # HTTP status: No content TRUE } else { response_as_json(resp, call = call) } } else { gargle_abort_request_failed( error_message(resp, call = call), resp, call = call ) } } #' @export #' @rdname response_process response_as_json <- function(resp, call = caller_env()) { check_for_json(resp, call = call) content <- httr::content(resp, type = "raw") content <- rawToChar(content) Encoding(content) <- "UTF-8" jsonlite::fromJSON(content, simplifyVector = FALSE) } check_for_json <- function(resp, call = caller_env()) { type <- httr::http_type(resp) if (grepl("^application/json", type)) { return(invisible(resp)) } gargle_abort_request_failed( "Expected content type {.field application/json}, not {.field {type}}.", call = call, resp = resp ) } gargle_abort_request_failed <- function(message, resp, .envir = caller_env(), call = caller_env()) { gargle_abort( message, class = c( "gargle_error_request_failed", glue("http_error_{httr::status_code(resp)}") ), .envir = .envir, call = call, resp = redact_response(resp) ) } #' @export #' @rdname response_process gargle_error_message <- function(resp, call = caller_env()) { type <- httr::http_type(resp) if (grepl("^text/html", type)) { return(gargle_html_error_message(resp)) } content <- response_as_json(resp, call = call) error <- content[["error"]] # Handle variety of error messages returned by different google APIs # It would be fussy to employ cli styling here, as we would need to # pre-interpolate data from `resp`. So we either don't style or we use simple # "no color" styles. if (identical(names(content), c("error", "error_description"))) { # seen when calling userinfo endpoint with an access token obtained via # workload identity federation message <- c( httr::http_status(resp)$message, "*" = content$error, "*" = content$error_description ) return(message) } if (is.null(error)) { # developed from test fixture from tokeninfo endpoint message <- c( httr::http_status(resp)$message, "*" = content$error_description ) return(message) } errors <- error[["errors"]] if (is.null(errors)) { # developed from test fixtures from "sheets.spreadsheets.get" endpoint status <- httr::http_status(resp) message <- c( glue("{status$category}: ({error$code}) {error$status}"), "*" = rpc_description(error$status), "*" = error$message ) if (!is.null(error$details)) { message <- c( message, "", reveal_details(error$details) ) } return(message) } # developed from # - test fixture from "drive.files.get" endpoint # - response_process() example of under/mis-scoped token errors <- unlist(errors) message <- c( httr::http_status(resp)$message, error$message, error$status, bulletize( # format() has no effect when processed by cli; bring that back later? # glue("{format(names(errors), justify = 'right')}: {errors}"), glue("{names(errors)}: {errors}"), n_show = 10 ) ) message } redact_response <- function(resp) { resp$request$auth_token <- "" resp$request$headers["Authorization"] <- "" resp$handle <- NULL resp } rpc_description <- function(rpc) { m <- match(rpc, oops$RPC) if (is_na(m)) { NULL } else { oops$Description[[m]] } } # https://cloud.google.com/apis/design/errors # @craigcitro says: # "... a published description of how new APIs do errors, which includes the # canonical error codes and http mappings. This view of errors is ... what ... # APIs will ultimately converge on" # https://github.com/googleapis/googleapis/blob/master/google/rpc/error_details.proto oops <- read.csv( text = trimws(c(' HTTP, RPC, Description 200, "OK", "No error." 400, "INVALID_ARGUMENT", "Client specified an invalid argument. Check error message and error details for more information." 400, "FAILED_PRECONDITION", "Request can not be executed in the current system state, such as deleting a non-empty directory." 400, "OUT_OF_RANGE", "Client specified an invalid range." 401, "UNAUTHENTICATED", "Request not authenticated due to missing, invalid, or expired OAuth token." 403, "PERMISSION_DENIED", "Client does not have sufficient permission. This can happen because the OAuth token does not have the right scopes, the client doesn\'t have permission, or the API has not been enabled for the client project." 404, "NOT_FOUND", "A specified resource is not found, or the request is rejected by undisclosed reasons, such as whitelisting." 409, "ABORTED", "Concurrency conflict, such as read-modify-write conflict." 409, "ALREADY_EXISTS", "The resource that a client tried to create already exists." 429, "RESOURCE_EXHAUSTED", "Either out of resource quota or reaching rate limiting. The client should look for google.rpc.QuotaFailure error detail for more information." 499, "CANCELLED", "Request cancelled by the client." 500, "DATA_LOSS", "Unrecoverable data loss or data corruption. The client should report the error to the user." 500, "UNKNOWN", "Unknown server error. Typically a server bug." 500, "INTERNAL", "Internal server error. Typically a server bug." 501, "NOT_IMPLEMENTED", "API method not implemented by the server." 503, "UNAVAILABLE", "Service unavailable. Typically the server is down." 504, "DEADLINE_EXCEEDED", "Request deadline exceeded. This will happen only if the caller sets a deadline that is shorter than the method\'s default deadline (i.e. requested deadline is not enough for the server to process the request) and the request did not finish within the deadline." ')), stringsAsFactors = FALSE, strip.white = TRUE ) # https://github.com/googleapis/googleapis/blob/master/google/rpc/error_details.proto reveal_details <- function(details) { c("Error details:", unlist(lapply(details, reveal_detail))) } # https://github.com/googleapis/googleapis/blob/master/google/rpc/rpc_publish.yaml # https://github.com/googleapis/googleapis/blob/master/google/rpc/error_details.proto reveal_detail <- function(x) { type <- sub("^type.googleapis.com/", "", x$`@type`) rpc_bad_request <- function(e) { f <- function(x) { c( "*" = glue("Field: {x$field}"), " " = glue("Description: {x$description}") ) } bullets <- unlist(map(e[["fieldViolations"]], f)) c("Field violations", bullets) } rpc_help <- function(e) { f <- function(x) { c( "*" = glue("Description: {x$description}"), " " = glue("URL: {x$url}") ) } bullets <- unlist(map(e[["links"]], f)) c("Links", bullets) } rpc_error_info <- function(e) { e <- unlist(e) e <- e[names(e) != "@type"] bulletize(glue_data(as.list(e), "{names(e)}: {e}"), n_show = 10) } switch(type, "google.rpc.BadRequest" = rpc_bad_request(x), "google.rpc.Help" = rpc_help(x), "google.rpc.ErrorInfo" = rpc_error_info(x), # must be an unimplemented type, such as RetryInfo, QuotaFailure, etc. bulletize( map_chr( c( "Error details of type '{type}' may not be fully revealed.", "Workaround: use `gargle:::gargle_last_response()` or \\ `gargle:::gargle_last_content()` to inspect error payload \\ yourself.", "Consider opening an issue at \\ " ), glue ) ) ) } gargle_html_error_message <- function(resp) { stopifnot(httr::http_type(resp) == "text/html") content <- httr::content(resp, as = "text") tmp <- tempfile("gargle-unexpected-html-error-", fileext = ".html") writeLines(content, tmp) browse_hint <- glue('browseURL("{tmp}")') # pre-interpolate, since `tmp` and `browse_hint` are only known here. c( httr::http_status(resp)$message, "x" = "Expected content type {.field application/json}, not \\ {.field text/html}.", "i" = gargle_map_cli(tmp, "See {.file <>} for the html error content."), "i" = gargle_map_cli( browse_hint, "Or execute {.code <>} to view it in your browser." ) ) } gargle/R/gargle_oauth_client.R0000644000176200001440000002120514433520365016043 0ustar liggesusers#' Create an OAuth client for Google #' #' @description #' A `gargle_oauth_client` consists of: #' * A type. gargle only supports the "Desktop app" and "Web application" client #' types. Different types are associated with different OAuth flows. #' * A client ID and secret. #' * Optionally, one or more redirect URIs. #' * A name. This is really a human-facing label. Or, rather, it can be used #' that way, but the default is just a hash. We recommend using the same name #' here as the name used to label the client ID in the [Google Cloud Platform #' Console](https://console.cloud.google.com). #' #' A `gargle_oauth_client` is an adaptation of httr's [oauth_app()] (currently) #' and httr2's `oauth_client()` (which gargle will migrate to in the future). #' @param path JSON downloaded from [Google Cloud #' Console](https://console.cloud.google.com), containing a client id and #' secret, in one of the forms supported for the `txt` argument of #' [jsonlite::fromJSON()] (typically, a file path or JSON string). #' @param name A label for this specific client, presumably the same name used #' to label it in Google Cloud Console. Unfortunately there is no way to #' make that true programmatically, i.e. the JSON representation does not #' contain this information. #' @param id Client ID #' @param secret Client secret #' @param redirect_uris Where your application listens for the response from #' Google's authorization server. If you didn't configure this specifically #' when creating the client (which is only possible for clients of the "web" #' type), you can leave this unspecified. #' @param type Specifies the type of OAuth client. The valid values are a subset #' of possible Google client types and reflect the key used to describe the #' client in its JSON representation: #' * `"installed"` is associated with a "Desktop app" #' * `"web"` is associated with a "Web application" #' @return An OAuth client: An S3 list with class `gargle_oauth_client`. For #' backwards compatibility reasons, this currently also inherits from the httr #' S3 class `oauth_app`, but that is a temporary measure. An instance of #' `gargle_oauth_client` stores more information than httr's `oauth_app`, such #' as the OAuth client's type ("web" or "installed"). #' #' There are some redundant fields in this object during the httr-to-httr2 #' transition period. The legacy fields `appname` and `key` repeat the #' information in the future-facing fields `name` and (client) `id`. Prefer #' `name` and `id` to `appname` and `key` in downstream code. Prefer the #' constructors `gargle_oauth_client_from_json()` and `gargle_oauth_client()` #' to [httr::oauth_app()] and [oauth_app_from_json()]. #' @export #' #' @examples #' \dontrun{ #' gargle_oauth_client_from_json( #' path = "/path/to/the/JSON/you/downloaded/from/gcp/console.json", #' name = "my-nifty-oauth-client" #' ) #' } #' #' gargle_oauth_client( #' id = "some_long_id", #' secret = "ssshhhhh_its_a_secret", #' name = "my-nifty-oauth-client" #' ) gargle_oauth_client_from_json <- function(path, name = NULL) { check_string(path) if (!is.null(name)) { check_string(name) } json <- jsonlite::fromJSON(path, simplifyVector = FALSE) if (length(json) != 1) { gargle_abort(c( "JSON has an unexpected form", "i" = "Are you sure this is the JSON downloaded for an OAuth client?", "i" = "It is easy to confuse the JSON for an OAuth client and a service account." )) } info <- json[[1]] gargle_oauth_client( id = info$client_id, secret = info$client_secret, redirect_uris = info$redirect_uris, type = names(json), name = name %||% glue("{info$project_id}_{hash(info$project_id)}") ) } #' @export #' @rdname gargle_oauth_client_from_json gargle_oauth_client <- function(id, secret, redirect_uris = NULL, type = c("installed", "web"), name = hash(id)) { check_string(id) check_string(secret) check_string(name) type <- arg_match(type) if (!is.null(redirect_uris)) { # httr appears to assume that an OAuth app can have exactly 1 redirect_uri # (gargle has never used the `redirect_uri` field of httr::oauth_app) # httr2 seems to think it can usually construct the redirect_uri? # I think I have to accept multiple URIs, because that can be true in the # downloaded JSON # we'll just have to decide which one to use downstream, based on context redirect_uris <- unlist(redirect_uris) check_character(redirect_uris) } if (type == "web" && length(redirect_uris) == 0) { gargle_abort(' A "web" type OAuth client must have one or more {.field redirect_uris}.') } structure( list( name = name, id = id, secret = secret, type = type, redirect_uris = redirect_uris, # needed for backwards compatibility; I need this class to quack like a # specialization of httr's oauth_app class, for now appname = name, key = id ), class = c("gargle_oauth_client", "oauth_app") # in the future, maybe: # class = c("gargle_oauth_client", "httr2_oauth_client") ) } # adapted from httr2 ---- #' @export print.gargle_oauth_client <- function(x, ...) { # this print method needs work, but not a high priority atm cli::cli_text(cli::style_bold("")) redacted <- list_redact(compact(x), "secret") # quick fix for multiple URIs case if (length(redacted$redirect_uris) > 0) { redacted$redirect_uris <- commapse(redacted$redirect_uris) } # hide redundant fields that exist only for backwards compatibility with # httr's oauth_app redacted$appname <- redacted$key <- NULL cli::cli_dl(redacted) invisible(x) } list_redact <- function(x, names, case_sensitive = TRUE) { if (case_sensitive) { i <- match(names, names(x)) } else { i <- match(tolower(names), tolower(names(x))) } x[i] <- cli::col_grey("") x } #' OAuth client for demonstration purposes #' #' @description #' Invisibly returns an instance of #' [`gargle_oauth_client`][gargle_oauth_client()] that can be used to test drive #' gargle before obtaining your own client ID and secret. This OAuth client may #' be deleted or rotated at any time. There are no guarantees about which APIs #' are enabled. DO NOT USE THIS IN A PACKAGE or for anything other than #' interactive, small-scale experimentation. #' #' You can get your own OAuth client ID and secret, without these limitations. #' See the `vignette("get-api-credentials")` for more details. #' #' @inheritParams gargle_oauth_client_from_json #' #' @return An OAuth client, produced by [gargle_oauth_client()], invisibly. #' @export #' @keywords internal #' @examples #' \dontrun{ #' gargle_client() #' } gargle_client <- function(type = NULL) { if (is.null(type) || is.na(type)) { type <- gargle_oauth_client_type() } check_string(type) type <- arg_match(type, values = c("installed", "web")) switch( type, web = goc_web(), installed = goc_installed() ) } #' @export #' @keywords internal #' @rdname internal-assets tidyverse_client <- function(type = NULL) { check_permitted_package(caller_env()) if (is.null(type) || is.na(type)) { type <- gargle_oauth_client_type() } check_string(type) type <- arg_match(type, values = c("installed", "web")) switch( type, web = toc_web(), installed = toc_installed() ) } # deprecated functions ---- #' Create an OAuth app from JSON #' #' @description #' `r lifecycle::badge("deprecated")` #' #' `oauth_app_from_json()` is being replaced with #' [`gargle_oauth_client_from_json()`], in light of the new #' `gargle_oauth_client` class. Now `oauth_app_from_json()` potentially warns #' about this deprecation and immediately passes its inputs through to #' [`gargle_oauth_client_from_json()`]. #' #' `gargle_app()` is being replaced with [gargle_client()]. #' #' @inheritParams gargle_oauth_client #' @inheritParams httr::oauth_app #' @keywords internal #' @export oauth_app_from_json <- function(path, appname = NULL) { lifecycle::deprecate_soft( "1.3.0", "oauth_app_from_json()", "gargle_oauth_client_from_json()" ) gargle_oauth_client_from_json(path = path, name = appname) } #' @export #' @keywords internal #' @rdname internal-assets tidyverse_app <- function() { lifecycle::deprecate_soft( "1.3.0", "tidyverse_app()", "tidyverse_client()" ) tidyverse_client() } #' @export #' @keywords internal #' @rdname oauth_app_from_json gargle_app <- function() { lifecycle::deprecate_soft( "1.3.0", "gargle_app()", "gargle_client()" ) gargle_client() } gargle/R/cred_funs.R0000644000176200001440000001573514431310014014011 0ustar liggesusers#' Credential function registry #' #' Functions to query or manipulate the registry of credential functions #' consulted by [token_fetch()]. #' #' @name cred_funs #' #' @seealso [token_fetch()], which is where the registry is actually used. #' @return A list of credential functions or `NULL`. #' @examples #' names(cred_funs_list()) #' #' creds_one <- function(scopes, ...) {} #' #' cred_funs_add(one = creds_one) #' cred_funs_add(two = creds_one, three = creds_one) #' names(cred_funs_list()) #' #' cred_funs_add(two = NULL) #' names(cred_funs_list()) #' #' # restore the default list #' cred_funs_set_default() #' #' # remove one specific credential fetcher #' cred_funs_add(credentials_gce = NULL) #' names(cred_funs_list()) #' #' # force the use of one specific credential fetcher #' cred_funs_set(list(credentials_user_oauth2 = credentials_user_oauth2)) #' names(cred_funs_list()) #' #' # restore the default list #' cred_funs_set_default() #' #' # run some code with a temporary change to the registry #' # creds_one ONLY #' with_cred_funs( #' list(one = creds_one), #' names(cred_funs_list()) #' ) #' # add creds_one to the list #' with_cred_funs( #' list(one = creds_one), #' names(cred_funs_list()), #' action = "modify" #' ) #' # remove credentials_gce #' with_cred_funs( #' list(credentials_gce = NULL), #' names(cred_funs_list()), #' action = "modify" #' ) NULL #' @describeIn cred_funs Get the list of registered credential functions. #' @export cred_funs_list <- function() { gargle_env$cred_funs } #' @describeIn cred_funs Register one or more new credential fetching functions. #' Function(s) are added to the *front* of the list. So: #' * "First registered, last tried." #' * "Last registered, first tried." #' #' Can also be used to *remove* a function from the registry. #' #' @param ... <[`dynamic-dots`][rlang::dyn-dots]> One or more credential #' functions, in `name = value` form. Each credential function is subject to a #' superficial check that it at least "smells like" a credential function: its #' first argument must be named `scopes`, and its signature must include #' `...`. To remove a credential function, you can use a specification like #' `name = NULL`. #' @export cred_funs_add <- function(...) { dots <- dots_list( ..., .named = NULL, .ignore_empty = "all", .preserve_empty = FALSE, .homonyms = "error", .check_assign = TRUE ) cred_funs_check(dots, allow_null = TRUE) nms_to_remove <- names(dots)[map_lgl(dots, is.null)] cf <- cred_funs_list() cf[nms_to_remove] <- NULL cred_funs_set(cf) dots <- dots[!map_lgl(dots, is.null)] dup_nm <- names(dots) %in% names(cred_funs_list()) if (any(dup_nm)) { n_dup_nm <- sum(dup_nm) gargle_abort(c( "{cli::qty(n_dup_nm)}{?This/These} name{?s} already {?appears/appear} \\ in the credential function registry:", x = "{.field {names(dots)[dup_nm]}}" )) } # add them in reverse order, to mimic what would happen if they'd been added # one-at-a-time cf <- cred_funs_list() cred_funs_set(c(rev(dots), cf)) invisible(cred_funs_list()) } #' @describeIn cred_funs Register a list of credential fetching functions. #' #' @param funs A named list of credential functions. #' @param ls `r lifecycle::badge("deprecated")` This argument has been renamed #' to `funs`. #' @export cred_funs_set <- function(funs, ls = deprecated()) { if (lifecycle::is_present(ls)) { lifecycle::deprecate_warn( when = "1.3.0", what = "cred_funs_set(ls)", with = "cred_funs_set(funs)", ) funs = ls } cred_funs_check(funs, allow_null = FALSE) gargle_env$cred_funs <- funs invisible(cred_funs_list()) } #' @describeIn cred_funs Clear the credential function registry. #' @export cred_funs_clear <- function() { gargle_env$cred_funs <- list() invisible(cred_funs_list()) } #' @describeIn cred_funs Return the default list of credential functions. #' @export cred_funs_list_default <- function() { list( credentials_byo_oauth2 = credentials_byo_oauth2, credentials_service_account = credentials_service_account, credentials_external_account = credentials_external_account, credentials_app_default = credentials_app_default, credentials_gce = credentials_gce, credentials_user_oauth2 = credentials_user_oauth2 ) } #' @describeIn cred_funs Reset the registry to the gargle default. #' @export cred_funs_set_default <- function() { cred_funs_set(cred_funs_list_default()) } #' @describeIn cred_funs Modify the credential function registry in the current #' scope. It is an example of the `local_*()` functions in \pkg{withr}. #' @param action Whether to use `funs` to replace or modify the registry with #' funs: #' * `"replace"` does `cred_funs_set(funs)` #' * `"modify"` does `cred_funs_add(!!!funs)` #' @param .local_envir The environment to use for scoping. Defaults to current #' execution environment. #' @export local_cred_funs <- function(funs = cred_funs_list_default(), action = c("replace", "modify"), .local_envir = caller_env()) { action <- arg_match(action) cred_funs_orig <- cred_funs_list() withr::defer(cred_funs_set(cred_funs_orig), envir = .local_envir) switch( action, replace = cred_funs_set(funs), modify = cred_funs_add(!!!funs) ) } #' @describeIn cred_funs Evaluate `code` with a temporarily modified credential #' function registry. It is an example of the `with_*()` functions in #' \pkg{withr}. #' @param code Code to run with temporary credential function registry. #' @export with_cred_funs <- function(funs = cred_funs_list_default(), code, action = c("replace", "modify")) { local_cred_funs(funs = funs, action = action) force(code) } cred_funs_check <- function(ls, allow_null = FALSE) { if (allow_null) { not_cred_fun <- !map_lgl(ls, is.null) & !map_lgl(ls, is_cred_fun) } else { not_cred_fun <- !map_lgl(ls, is_cred_fun) } if (any(not_cred_fun)) { gargle_abort(c( "Not a valid credential function:", x = "Element{?s} {as.character(which(not_cred_fun))}" )) } if (!is_dictionaryish(ls)) { gargle_abort("Each credential function must have a unique name") } invisible() } #' Check that f is a viable credential fetching function #' #' In the abstract, a credential fetching function is any function which takes a #' set of scopes and any number of additional arguments, and returns either a #' valid [`httr::Token`][httr::Token-class] or `NULL`. #' #' Here we say that a function is valid if its first argument is named `scopes`, #' and it includes `...` as an argument, since it's difficult to actually check #' the behavior of the function. #' #' @param f A function to check. #' @keywords internal #' @noRd #' @examples #' f <- function(scopes, ...) {} #' is_cred_fun(f) is_cred_fun <- function(f) { if (!is.function(f)) { return(FALSE) } args <- names(formals(f)) args[1] == "scopes" && "..." %in% args } gargle/R/roxygen-templates.R0000644000176200001440000002700014453545575015546 0ustar liggesusers# nocov start # example of data client package needs to provide ----------------------------- # a client package should define a list like this and pass it to the functions # below, to provide data to populate the templates # see googledrive/R/drive_auth.R for an example # this is an exhaustive list of the pieces of data required by the templates # gargle_lookup_table <- list( # PACKAGE = "googledrive", # YOUR_STUFF = "your Drive files", # PRODUCT = "Google Drive", # API = "Drive API", # PREFIX = "drive", # ) # 2023-03 developments related to the 'app' -> 'client' transition: # - `PREFIX_auth_configure_description()` crosslinks to # `PREFIX_oauth_client()` now, not `PREFIX_oauth_app()` # - `PREFIX_auth_configure_params()` gains `client` argument # - `PREFIX_auth_configure_params()` deprecates the `app` argument and uses a # lifecycle badge # - `PREFIX_auth_configure_params() crosslinks to # `gargle::gargle_oauth_client_from_json()` which requires gargle (>= 1.3.0) glue_data_lines <- function(.data, lines, ..., .envir = caller_env()) { # work around name collision of `.x` of map_chr() vs. of glue_data() # and confusion re: `...` of glue_data_lines() vs. `...` of map_chr() # plus: I've only got compat-purrr here, so I have to write a function gd <- function(line) glue_data(.x = .data, line, ..., .envir = .envir) map_chr(lines, gd) } # PREFIX_auth() ---------------------------------------------------------- PREFIX_auth_description <- function(.data = list( PACKAGE = "PACKAGE", YOUR_STUFF = "YOUR STUFF", PRODUCT = "A GOOGLE PRODUCT" )) { glue_data_lines(c( "@description", "Authorize {PACKAGE} to view and manage {YOUR_STUFF}. This function is a", "wrapper around [gargle::token_fetch()].", "", "By default, you are directed to a web browser, asked to sign in to your", "Google account, and to grant {PACKAGE} permission to operate on your", "behalf with {PRODUCT}. By default, with your permission, these user", "credentials are cached in a folder below your home directory, from where", "they can be automatically refreshed, as necessary. Storage at the user", "level means the same token can be used across multiple projects and", "tokens are less likely to be synced to the cloud by accident." ), .data = .data) } PREFIX_auth_details <- function(.data = list( PACKAGE = "PACKAGE", PREFIX = "PREFIX" )) { glue_data_lines(c( "@details", "Most users, most of the time, do not need to call `{PREFIX}_auth()`", "explicitly -- it is triggered by the first action that requires", "authorization. Even when called, the default arguments often suffice.", "", "However, when necessary, `{PREFIX}_auth()` allows the user to explicitly:", " * Declare which Google identity to use, via an `email` specification.", " * Use a service account token or workload identity federation via", " `path`.", " * Bring your own `token`.", " * Customize `scopes`.", " * Use a non-default `cache` folder or turn caching off.", " * Explicitly request out-of-band (OOB) auth via `use_oob`.", "", "If you are interacting with R within a browser (applies to RStudio", "Server, Posit Workbench, Posit Cloud, and Google Colaboratory), you need", "OOB auth or the pseudo-OOB variant. If this does not happen", "automatically, you can request it explicitly with `use_oob = TRUE` or,", "more persistently, by setting an option via", "`options(gargle_oob_default = TRUE)`.", "", "The choice between conventional OOB or pseudo-OOB auth is determined", "by the type of OAuth client. If the client is of the \"installed\" type,", "`use_oob = TRUE` results in conventional OOB auth. If the client is of", "the \"web\" type, `use_oob = TRUE` results in pseudo-OOB auth. Packages", "that provide a built-in OAuth client can usually detect which type of", "client to use. But if you need to set this explicitly, use the", "`\"gargle_oauth_client_type\"` option:", "```r", "options(gargle_oauth_client_type = \"web\") # pseudo-OOB", "# or, alternatively", "options(gargle_oauth_client_type = \"installed\") # conventional OOB", "```", "", "For details on the many ways to find a token, see", "[gargle::token_fetch()]. For deeper control over auth, use", "[{PREFIX}_auth_configure()] to bring your own OAuth client or API key.", "To learn more about gargle options, see [gargle::gargle_options]." ), .data = .data) } PREFIX_auth_params <- function() { c( "@inheritParams gargle::credentials_service_account", "@inheritParams gargle::credentials_external_account", "@inheritParams gargle::credentials_app_default", "@inheritParams gargle::credentials_gce", "@inheritParams gargle::credentials_byo_oauth2", "@inheritParams gargle::credentials_user_oauth2", "@inheritParams gargle::gargle2.0_token" ) } # PREFIX_deauth() ---------------------------------------------------------- PREFIX_deauth_description_with_api_key <- function(.data = list( PACKAGE = "PACKAGE", PREFIX = "PREFIX" ), .fallback_api_key = TRUE) { lines <- c( "@description", "Put {PACKAGE} into a de-authorized state. Instead of sending a token,", "{PACKAGE} will send an API key. This can be used to access public", "resources for which no Google sign-in is required. This is handy for using", "{PACKAGE} in a non-interactive setting to make requests that do not", "require a token. It will prevent the attempt to obtain a token", "interactively in the browser. The user can configure their own API key", "via [{PREFIX}_auth_configure()] and retrieve that key via", "[{PREFIX}_api_key()].", if (.fallback_api_key) { "In the absence of a user-configured key, a built-in default key is used." } ) glue_data_lines(lines, .data = .data) } PREFIX_deauth_description_no_api_key <- function(.data = list( PACKAGE = "PACKAGE", PREFIX = "PREFIX" ), .fallback_api_key = TRUE) { lines <- c( "@description", "Clears any currently stored token. The next time {PACKAGE} needs a token,", "the token acquisition process starts over, with a fresh call to", "[{PREFIX}_auth()] and, therefore, internally, a call to", "[gargle::token_fetch()]. Unlike some other packages that use gargle,", "{PACKAGE} is not usable in a de-authorized state. Therefore, calling", "`{PREFIX}_deauth()` only clears the token, i.e. it does NOT imply that", "subsequent requests are made with an API key in lieu of a token." ) glue_data_lines(lines, .data = .data) } # PREFIX_token() ---------------------------------------------------------- PREFIX_token_description <- function(.data = list( API = "GOOGLE API", PREFIX = "PREFIX" ), .deauth_possible = TRUE) { lines <- c( "@description", "For internal use or for those programming around the {API}.", "Returns a token pre-processed with [httr::config()]. Most users", "do not need to handle tokens \"by hand\" or, even if they need some", "control, [{PREFIX}_auth()] is what they need. If there is no current", "token, [{PREFIX}_auth()] is called to either load from cache or", "initiate OAuth2.0 flow.", if (.deauth_possible) { c( "If auth has been deactivated via [{PREFIX}_deauth()], `{PREFIX}_token()`", "returns `NULL`." ) } ) glue_data_lines(lines, .data = .data) } PREFIX_token_return <- function() { "@return A `request` object (an S3 class provided by [httr][httr::httr])." } # PREFIX_has_token() ---------------------------------------------------------- PREFIX_has_token_description <- function(.data = list(PACKAGE = "PACKAGE")) { glue_data_lines(c( "@description", "Reports whether {PACKAGE} has stored a token, ready for use in downstream", "requests." ), .data = .data) } PREFIX_has_token_return <- function() { "@return Logical." } # PREFIX_auth_configure() ------------------------------------------------------- PREFIX_auth_configure_description <- function(.data = list( PACKAGE = "PACKAGE", PREFIX = "PREFIX" ), .has_api_key = TRUE, .fallbacks = TRUE) { lines <- c( "@description", "These functions give more control over and visibility into the auth", "configuration than [{PREFIX}_auth()] does. `{PREFIX}_auth_configure()`", "lets the user specify their own:", " * OAuth client, which is used when obtaining a user token.", if (.has_api_key) { c( " * API key. If {PACKAGE} is de-authorized via [{PREFIX}_deauth()], all", " requests are sent with an API key in lieu of a token." ) }, "", 'See the `vignette("get-api-credentials", package = "gargle")`', "for more.", if (.fallbacks) { c( "If the user does not configure these settings, internal defaults", "are used." ) }, "", if (.has_api_key) { c( "`{PREFIX}_oauth_client()` and `{PREFIX}_api_key()` retrieve the", "currently configured OAuth client and API key, respectively." ) } else { "`{PREFIX}_oauth_client()` retrieves the currently configured OAuth client." } ) glue_data_lines(lines, .data = .data) } PREFIX_auth_configure_params <- function(.has_api_key = TRUE) { c( "@param client A Google OAuth client, presumably constructed via", "[gargle::gargle_oauth_client_from_json()]. Note, however, that it is", "preferred to specify the client with JSON, using the `path` argument.", "@inheritParams gargle::gargle_oauth_client_from_json", if (.has_api_key) { "@param api_key API key." }, "@param app `r lifecycle::badge('deprecated')` Replaced by the `client`", "argument." ) } PREFIX_auth_configure_return <- function(.data = list( PREFIX = "PREFIX" ), .has_api_key = TRUE) { lines <- c( "@return", " * `{PREFIX}_auth_configure()`: An object of R6 class", " [gargle::AuthState], invisibly.", " * `{PREFIX}_oauth_client()`: the current user-configured OAuth client.", if (.has_api_key) { " * `{PREFIX}_api_key()`: the current user-configured API key." } ) glue_data_lines(lines, .data = .data) } # PREFIX_user() ---------------------------------------------------------- PREFIX_user_description <- function() { c( "@description", "Reveals the email address of the user associated with the current token.", "If no token has been loaded yet, this function does not initiate auth." ) } PREFIX_user_seealso <- function() { c( "@seealso [gargle::token_userinfo()], [gargle::token_email()],", "[gargle::token_tokeninfo()]" ) } PREFIX_user_return <- function() { "@return An email address or, if no token has been loaded, `NULL`." } # nocov end gargle/R/oauth-cache.R0000644000176200001440000003312314433723555014235 0ustar liggesusers## this file has its origins in oauth-cache.R in httr ## nothing there is exported, so copied over, then it evolved # cache setup, loading, validation -------------------------------------------- gargle_default_oauth_cache_path <- function() { path_tidy(rappdirs::user_cache_dir("gargle")) } ## this is the cache setup interface for the Gargle2.0 class ## returns NULL or cache path cache_establish <- function(cache = NULL) { cache <- cache %||% gargle_oauth_cache() if (length(cache) != 1) { gargle_abort("{.arg cache} must have length 1, not {length(cache)}.") } # the inherits() call is so we accept 'fs_path' if (!is.logical(cache) && !is.character(cache) && !inherits(cache, "character")) { stop_input_type(cache, what = c("logical", "character")) } # If NA, consider default cache folder # Request user's permission to create it, if doesn't exist yet # Store outcome of this mission (TRUE or FALSE) in the option for the session if (is_na(cache)) { cache <- cache_available(gargle_default_oauth_cache_path()) options("gargle_oauth_cache" = cache) } ## cache is now TRUE, FALSE or path if (isFALSE(cache)) { return() } if (isTRUE(cache)) { cache <- gargle_default_oauth_cache_path() } ## cache is now a path if (dir_exists(cache)) { cache_clean(cache) } else { cache_create(cache) } return(cache) } cache_available <- function(path) { dir_exists(path) || cache_allowed(path) } cache_allowed <- function(path) { if (!is_interactive()) { return(FALSE) } choice <- cli_menu( header = character(), prompt = "Is it OK to cache OAuth access credentials in the folder \\ {.path {path}} between R sessions?", choices = c("Yes", "No") ) choice == 1 } cache_create <- function(path) { ## owner (and only owner) can read, write, execute dir_create(path, recurse = TRUE, mode = "0700") cache_parent <- path_dir(path) desc <- path(cache_parent, "DESCRIPTION") if (file_exists(desc)) { add_line( path(cache_parent, ".Rbuildignore"), paste0("^", gsub("\\.", "\\\\.", path), "$") ) } git <- path(cache_parent, c(".gitignore", ".git")) if (any(file_exists(git))) { add_line( path(cache_parent, ".gitignore"), path ) } TRUE } cache_ls <- function(path) { names(cache_load(path)) } cache_load <- function(path) { files <- as.character(dir_ls(path)) files <- keep_hash_paths(files) names(files) <- path_file(files) tokens <- map(files, readRDS) hashes <- map_chr(tokens, function(t) t$hash()) mismatch <- names(hashes) != hashes if (any(mismatch)) { # original motivation: # we've seen this with tokens cached on R 3.5 but reloaded on 3.6 # because $hash() calls serialize() and default version changed # # later observation: I suppose this could also get triggered if someone # caches on, e.g., Windows, then moves/deploys the project to *nix n <- sum(mismatch) mismatch_name <- names(hashes)[mismatch] mismatch_hash <- hashes[mismatch] mismatch_name_fmt <- gargle_map_cli( mismatch_name, template = "{.val <>} (name)" ) mismatch_hash_fmt <- gargle_map_cli( mismatch_hash, template = "{.field <>} (hash)" ) msg <- c( "!" = "Cache contains {cli::qty(n)}{?a /}token{?s} with {?a /}name{?s} \\ that do{?es/} not match {?its/their} hash:", bulletize( as.vector(rbind(mismatch_name_fmt, mismatch_hash_fmt)), n_show = 100 ), " " = "Will attempt to repair by renaming" ) gargle_debug(msg) file_move(files[mismatch], path(path, hashes[mismatch])) Recall(path) } else { tokens } } cache_clean <- function(cache, pattern = gargle_legacy_app_pattern()) { dat_tokens <- gargle_oauth_dat(cache) dat_tokens$legacy <- grepl(pattern, dat_tokens$client) n <- sum(dat_tokens$legacy) if (n == 0) { return(FALSE) } gargle_info(c( "v" = "Deleting {n} token{?s} obtained with an old tidyverse OAuth client.", "i" = "Expect interactive prompts to re-auth with the new client.", "!" = "Is this rolling of credentials highly disruptive to your \\ workflow?", " " = "That means you should rely on your own OAuth client \\ (or switch to a service account token).", " " = "Learn more these in these articles:", " " = "{.url https://gargle.r-lib.org/articles/get-api-credentials.html}", " " = "{.url https://gargle.r-lib.org/articles/non-interactive-auth.html}" )) file_delete(dat_tokens$filepath[dat_tokens$legacy]) TRUE } gargle_legacy_app_pattern <- function() "-calliope$" # retrieve and insert tokens from cache ----------------------------------- ## these two functions provide the "current token <--> token cache" interface ## for the Gargle2.0 class token_from_cache <- function(candidate) { cache_path <- candidate$cache_path if (is.null(cache_path)) { return() } existing <- cache_ls(cache_path) this_one <- token_match(candidate$hash(), existing, package = candidate$package) gargle_debug("token(s) found in cache:") gargle_debug(existing) gargle_debug("token we are looking for:") gargle_debug(this_one) if (is.null(this_one)) { NULL } else { readRDS(path(cache_path, this_one)) } } token_into_cache <- function(candidate) { cache_path <- candidate$cache_path if (is.null(cache_path)) { gargle_debug("not caching token") return() } gargle_debug(c("putting token into the cache:", "{.file {cache_path}}")) saveRDS(candidate, path(cache_path, candidate$hash())) } token_remove_from_cache <- function(candidate) { cache_path <- candidate$cache_path if (is.null(cache_path)) { return() } token_path <- path(cache_path, candidate$hash()) # when does token_path not exist? # the first time a token fails to refresh it is removed on disk, # but a package may still have it stored in its auth state if (file_exists(token_path)) { gargle_debug(c("Removing token from the cache:", "{.file {token_path}}")) file_delete(token_path) } } # helpers to compare tokens based on SHORTHASH_EMAIL ------------------------ token_match <- function(candidate, existing, package = "gargle") { if (length(existing) == 0) { return() } m <- match2(candidate, existing) if (!is_na(m)) { stopifnot(length(m) == 1) return(existing[[m]]) } # there is no full match candidate_email <- extract_email(candidate) # possible values what they mean # ------------------ --------------------------------------------------------- # 'blah@example.org' user specified an email # '*@example.org' user specified only the domain # (we still scold for multiple matches) # '*' `email = TRUE`, i.e. permission to use *one* that we find # (we still scold for multiple matches) # '' user gave no email and no instructions # if email was specified, we're done if (!empty_string(candidate_email) && !startsWith(candidate_email, "*")) { return() } # candidate_email is '*' or '' or domain-only, e.g. '*@example.org' # match on the short hash m <- match2(mask_email(candidate), mask_email(existing)) # if no match on short hash, we're done if (is_na(m)) { return() } existing <- existing[m] # existing holds at least one short hash match # filter on domain, if provided if (!empty_string(candidate_email) && startsWith(candidate_email, "*@")) { domain_part <- function(x) sub(".+@(.+)$", "\\1", x) m <- match2(domain_part(candidate_email), domain_part(existing)) if (is_na(m)) { return() } existing <- existing[m] } # if candidate_email is '*' or domain-only, e.g. '*@example.org' AND # we're down to 1 match, we're done if (!empty_string(candidate_email) && length(existing) == 1) { gargle_info(c( "i" = "The {.pkg {package}} package is using a cached token for \\ {.email {extract_email(existing)}}." )) return(existing) } # if we're still here, one of these is true: # - email was partially specified and there are multiple matches # - email was unspecified and there is at least one match if (!is_interactive()) { # proceed, but make sure user sees messaging about how to do # non-interactive auth more properly # https://github.com/r-lib/gargle/issues/92 local_gargle_verbosity("info") if (length(existing) > 1) { emails <- extract_email(existing) emails_fmt <- lapply( emails, function(x) cli::format_inline("{.email {x}}") ) msg <- c( "i" = "Suitable tokens found in the cache, associated with these \\ emails:", set_names(emails_fmt, ~ rep_along(., "*")), " " = "Defaulting to the first email." ) gargle_info(msg) existing <- existing[[1]] } msg <- c( "!" = "Using an auto-discovered, cached token.", " " = "To suppress this message, modify your code or options \\ to clearly consent to the use of a cached token.", " " = "See gargle's \"Non-interactive auth\" vignette for more details:", " " = "{.url https://gargle.r-lib.org/articles/non-interactive-auth.html}" ) # morally, I'd like to throw a warning but current design of token_fetch() # means warnings are caught gargle_info(msg) gargle_info(c( "i" = "The {.pkg {package}} package is using a cached token for \\ {.email {extract_email(existing)}}." )) return(existing) } # we need user to OK our discovery or pick from multiple emails choices <- c( "Send me to the browser for a new auth process.", extract_email(existing) ) choice <- cli_menu( "The {.pkg {package}} package is requesting access to your Google account.", "Enter '1' to start a new auth process or select a pre-authorized account.", choices = choices ) if (choice == 1) { NULL } else { existing[[choice - 1]] } } ## for this token hash: ## 2a46e6750476326f7085ebdab4ad103d_jenny@example.org ## ^ mask_email() returns this ^ ^ extract_email() returns this ^ hash_regex <- "^([0-9a-f]+)_(.*?)$" mask_email <- function(x) sub(hash_regex, "\\1", x) extract_email <- function(x) sub(hash_regex, "\\2", x) keep_hash_paths <- function(x) x[grep(hash_regex, path_file(x))] ## match() but return location of all matches match2 <- function(needle, haystack) { matches <- which(haystack == needle) if (length(matches) == 0) { matches <- NA } matches } # gargle situation report ------------------------------------------------- #' OAuth token situation report #' #' Get a human-oriented overview of the existing gargle OAuth tokens: #' * Filepath of the current cache #' * Number of tokens found there #' * Compact summary of the associated #' - Email = Google identity #' - OAuth client (actually, just its nickname) #' - Scopes #' - Hash (actually, just the first 7 characters) #' Mostly useful for the development of gargle and client packages. #' #' @inheritParams gargle2.0_token #' #' @return A data frame with one row per cached token, invisibly. Note this data #' frame may contain more columns than it seems, e.g. the `filepath` column #' isn't printed by default. #' @export #' #' @examples #' gargle_oauth_sitrep() gargle_oauth_sitrep <- function(cache = NULL) { cache <- cache %||% cache_locate() if (!is_dir(cache)) { gargle_info("No gargle OAuth cache found at {.path {cache}}.") return(invisible()) } dat <- gargle_oauth_dat(cache) gargle_info(c( "{nrow(dat)} token{?s} found in this gargle OAuth cache:", "{.path {cache}}", "" )) if (gargle_verbosity() %in% c("debug", "info")) { cli::cli_verbatim(format(dat)) } invisible(dat) } gargle_oauth_dat <- function(cache = NULL) { cache <- cache %||% gargle_default_oauth_cache_path() if (is_dir(cache)) { tokens <- cache_load(cache) } else { tokens <- list() } nms <- names(tokens) hash <- mask_email(nms) email <- extract_email(nms) client <- map_chr(tokens, function(t) t$client$name %||% t$app$appname) scopes <- map(tokens, function(t) t$params$scope) email_scope <- "https://www.googleapis.com/auth/userinfo.email" scopes <- map(scopes, function(s) s[s != email_scope]) scopes <- map_chr(scopes, function(s) commapse(base_scope(s))) structure( data.frame( email, client, scopes, hash, filepath = path(cache, nms), stringsAsFactors = FALSE, row.names = NULL ), class = c("gargle_oauth_dat", "data.frame") ) } #' @export format.gargle_oauth_dat <- function(x, ...) { format_transformer <- function(text, envir) { res <- eval(parse(text = text, keep.source = FALSE), envir) res <- format(c(text, res)) c( res[1], strrep("_", nchar(res[1])), res[-1] ) } # obfuscate the hash for brevity hash_column <- which(names(x) == "hash") x[[hash_column]] <- obfuscate(x[[hash_column]], first = 7, last = 0) names(x)[hash_column] <- "hash..." # NOTE: the filepath variable is absent from the formatted data frame glue_data( x, "{email} {client} {scopes} {hash...}", .transformer = format_transformer ) } #' @export print.gargle_oauth_dat <- function(x, ...) { cli::cat_line(format(x)) invisible(x) } # main point of this is **passive** cache discovery cache_locate <- function() { option_cache <- gargle_oauth_cache() if (is_scalar_character(option_cache)) { gargle_info(c( "i" = 'Taking cache location from the {.code "gargle_oauth_cache"} option.' )) return(option_cache) } default_cache <- gargle_default_oauth_cache_path() gargle_info(c( "i" = 'Reporting the default cache location.' )) default_cache } gargle/R/credentials_byo_oauth2.R0000644000176200001440000000645214446563364016515 0ustar liggesusers#' Load a user-provided token #' #' @description #' This function is designed to pass its `token` input through, after doing a #' few checks and some light processing: #' * If `token` has class `request`, i.e. it is a token that has been prepared #' with [httr::config()], the `auth_token` component is extracted. For #' example, such input could be returned by `googledrive::drive_token()` #' or `bigrquery::bq_token()`. #' * If `token` is an instance of `Gargle2.0` (so: a gargle-obtained user #' token), checks that it appears to be a Google OAuth token, based on its #' embedded `oauth_endpoint`. Refreshes the token, if it's refreshable. #' * Returns the `token`. #' #' There is no point in providing `scopes`. They are ignored because the #' `scopes` associated with the token have already been baked in to the token #' itself and gargle does not support incremental authorization. The main point #' of `credentials_byo_oauth2()` is to allow `token_fetch()` (and packages that #' wrap it) to accommodate a "bring your own token" workflow. #' #' This also makes it possible to obtain a token with one package and then #' register it for use with another package. For example, the default scope #' requested by googledrive is also sufficient for operations available in #' googlesheets4. You could use a shared token like so: #' ``` #' library(googledrive) #' library(googlesheets4) #' drive_auth(email = "jane_doe@example.com") #' gs4_auth(token = drive_token()) #' # work with both packages freely now, with the same identity #' ``` #' #' @inheritParams token_fetch #' @inheritParams token-info #' #' @return An [Token2.0][httr::Token-class]. #' @family credential functions #' @export #' @examples #' \dontrun{ #' # assume `my_token` is a Token2.0 object returned by a function such as #' # credentials_user_oauth2() #' credentials_byo_oauth2(token = my_token) #' } credentials_byo_oauth2 <- function(scopes = NULL, token, ...) { gargle_debug("Trying {.fun credentials_byo_oauth} ...") if (inherits(token, "request")) { token <- token$auth_token } stopifnot(inherits(token, "Token2.0")) if (!is.null(scopes)) { gargle_debug(c( "!" = "The {.arg scopes} cannot be specified when user brings their own \\ OAuth token.", "i" = "The {.arg scopes} are already implicit in the token.", "i" = "Requested {.arg scopes} are effectively ignored." )) declared_scopes <- normalize_scopes(token$params$scope) requested_scopes <- normalize_scopes(scopes) if (!setequal(requested_scopes, declared_scopes)) { gargle_debug(c( "!" = "Token's declared scopes are not the same as the requested \\ scopes.", "i" = "Scopes declared in token: {commapse(base_scope(declared_scopes))}", "i" = "Requested scopes: {commapse(base_scope(requested_scopes))}" )) } } if (inherits(token, "Gargle2.0")) { check_endpoint(token$endpoint) if (token$can_refresh()) { token$refresh() } } token } check_endpoint <- function(endpoint, call = caller_env()) { stopifnot(inherits(endpoint, "oauth_endpoint")) urls <- endpoint[c("authorize", "access", "validate", "revoke")] urls_ok <- all(grepl("google", urls)) if (!urls_ok) { gargle_abort("Token doesn't use Google's OAuth endpoint.", call = call) } endpoint } gargle/R/credentials_app_default.R0000644000176200001440000001016614365660005016711 0ustar liggesusers#' Load Application Default Credentials #' #' @description #' Loads credentials from a file identified via a search strategy known as #' Application Default Credentials (ADC). The hope is to make auth "just work" #' for someone working on Google-provided infrastructure or who has used Google #' tooling to get started, such as the [`gcloud` command line #' tool](https://cloud.google.com/sdk/gcloud). #' #' A sequence of paths is consulted, which we describe here, with some abuse of #' notation. ALL_CAPS represents the value of an environment variable and `%||%` #' is used in the spirit of a [null coalescing #' operator](https://en.wikipedia.org/wiki/Null_coalescing_operator). #' ``` #' GOOGLE_APPLICATION_CREDENTIALS #' CLOUDSDK_CONFIG/application_default_credentials.json #' # on Windows: #' (APPDATA %||% SystemDrive %||% C:)\gcloud\application_default_credentials.json #' # on not-Windows: #' ~/.config/gcloud/application_default_credentials.json #' ``` #' If the above search successfully identifies a JSON file, it is parsed and #' ingested as a service account, an external account ("workload identity #' federation"), or a user account. Literally, if the JSON describes a service #' account, we call [credentials_service_account()] and if it describes an #' external account, we call [credentials_external_account()]. #' #' @inheritParams credentials_service_account #' #' @seealso #' * #' * #' @return An [`httr::TokenServiceAccount`][httr::Token-class], a [`WifToken`], #' an [`httr::Token2.0`][httr::Token-class] or `NULL`. #' @family credential functions #' @export #' @examples #' \dontrun{ #' credentials_app_default() #' } credentials_app_default <- function(scopes = NULL, ..., subject = NULL) { gargle_debug("trying {.fun credentials_app_default}") # In general, application default credentials only include the cloud-platform # scope. path <- credentials_app_default_path() if (!file_exists(path)) { return(NULL) } gargle_debug(c("file exists at ADC path:", "{.file {path}}")) info <- jsonlite::fromJSON(path, simplifyVector = FALSE) if (info$type == "authorized_user") { # In the case of *user* credentials stored as the application default, only # the cloud-platform scope will be included. This means we need our scopes to # be *implied* by the cloud-platform scope, which is hard to validate; # instead, we just approximate. valid_scopes <- c( "https://www.googleapis.com/auth/bigquery", "https://www.googleapis.com/auth/bigquery", "https://www.googleapis.com/auth/cloud-platform", "https://www.googleapis.com/auth/cloud-platform.readonly" ) if (is.null(scopes) || !all(scopes %in% valid_scopes)) { return(NULL) } gargle_debug("ADC cred type: {.val authorized_user}") app <- httr::oauth_app("google", info$client_id, secret = info$client_secret) scope <- "https://www.googleapis.com/auth/cloud.platform" token <- httr::Token2.0$new( endpoint = gargle_oauth_endpoint(), app = app, credentials = list(refresh_token = info$refresh_token), # ADC is already cached. cache_path = FALSE, params = list(scope = scope, as_header = TRUE) ) token$refresh() token } else if (info$type == "service_account") { gargle_debug("ADC cred type: {.val service_account}") credentials_service_account(scopes, path = path, subject = subject) } else if (info$type == "external_account") { gargle_debug("ADC cred type: {.val external_account}") credentials_external_account(scopes, path = path) } } credentials_app_default_path <- function() { if (nzchar(Sys.getenv("GOOGLE_APPLICATION_CREDENTIALS"))) { return(path_expand(Sys.getenv("GOOGLE_APPLICATION_CREDENTIALS"))) } pth <- "application_default_credentials.json" if (nzchar(Sys.getenv("CLOUDSDK_CONFIG"))) { pth <- c(Sys.getenv("CLOUDSDK_CONFIG"), pth) } else if (is_windows()) { appdata <- Sys.getenv("APPDATA", Sys.getenv("SystemDrive", "C:")) pth <- c(appdata, "gcloud", pth) } else { pth <- path_home(".config", "gcloud", pth) } path_join(pth) } gargle/R/field_mask.R0000644000176200001440000000422014431310014014122 0ustar liggesusers#' Generate a field mask #' #' Many Google API requests take a field mask, via a `fields` parameter, in the #' URL and/or in the body. `field_mask()` generates such a field mask from an R #' list, typically a list that is destined to be part of the body of a request #' that writes or updates a resource. `field_mask()` is designed to help in the #' common case where the attributes you wish to modify are exactly the ones #' represented in the object. It is possible to use a "larger" field mask, that #' is either less specific or that explicitly includes other attributes, in #' which case the attributes covered by the mask but absent from the object are #' reset to default values. This is not exactly the use case `field_mask()` is #' designed for, but its output could still be useful as a first step in #' constructing such a mask. #' #' @param x A named R list, where the requirement for names applies at all #' levels, i.e. recursively. #' #' @return A Google API field mask, as a string. #' @export #' #' @seealso The documentation for the [JSON encoding of a Protocol Buffers #' FieldMask](https://protobuf.dev/reference/protobuf/google.protobuf/#json-encoding-of-field-masks). #' #' @examples #' x <- list(sheetId = 1234, title = "my_favorite_worksheet") #' field_mask(x) #' #' x <- list( #' userEnteredFormat = list( #' backgroundColor = list( #' red = 159 / 255, green = 183 / 255, blue = 196 / 255 #' ) #' ) #' ) #' field_mask(x) #' #' x <- list( #' sheetId = 1234, #' gridProperties = list(rowCount = 5, columnCount = 3) #' ) #' field_mask(x) field_mask <- function(x) { stopifnot(is_dictionaryish(x)) explicit_mask <- imap(x, field_mask_impl_) as.character(glue_collapse(unname(unlist(explicit_mask)), ",")) } field_mask_impl_ <- function(x, y = "") { if (!is_list(x)) { return(y) } stopifnot(is_dictionaryish(x)) leafs <- !map_lgl(x, is_list) if (sum(leafs) <= 1) { leafs <- FALSE } names(x)[!leafs] <- glue(".{names(x)[!leafs]}") if (sum(leafs) > 1) { nm <- glue("({glue_collapse(names(x)[leafs], sep = ',')})") x <- list2(!!nm := NA, !!!x[!leafs]) } map2(x, glue("{y}{names(x)}"), field_mask_impl_) } gargle/R/import-standalone-purrr.R0000644000176200001440000001302014431310014016632 0ustar liggesusers# Standalone file: do not edit by hand # Source: # ---------------------------------------------------------------------- # # --- # repo: r-lib/rlang # file: standalone-purrr.R # last-updated: 2023-02-23 # license: https://unlicense.org # imports: rlang # --- # # This file provides a minimal shim to provide a purrr-like API on top of # base R functions. They are not drop-in replacements but allow a similar style # of programming. # # ## Changelog # # 2023-02-23: # * Added `list_c()` # # 2022-06-07: # * `transpose()` is now more consistent with purrr when inner names # are not congruent (#1346). # # 2021-12-15: # * `transpose()` now supports empty lists. # # 2021-05-21: # * Fixed "object `x` not found" error in `imap()` (@mgirlich) # # 2020-04-14: # * Removed `pluck*()` functions # * Removed `*_cpl()` functions # * Used `as_function()` to allow use of `~` # * Used `.` prefix for helpers # # nocov start map <- function(.x, .f, ...) { .f <- as_function(.f, env = global_env()) lapply(.x, .f, ...) } walk <- function(.x, .f, ...) { map(.x, .f, ...) invisible(.x) } map_lgl <- function(.x, .f, ...) { .rlang_purrr_map_mold(.x, .f, logical(1), ...) } map_int <- function(.x, .f, ...) { .rlang_purrr_map_mold(.x, .f, integer(1), ...) } map_dbl <- function(.x, .f, ...) { .rlang_purrr_map_mold(.x, .f, double(1), ...) } map_chr <- function(.x, .f, ...) { .rlang_purrr_map_mold(.x, .f, character(1), ...) } .rlang_purrr_map_mold <- function(.x, .f, .mold, ...) { .f <- as_function(.f, env = global_env()) out <- vapply(.x, .f, .mold, ..., USE.NAMES = FALSE) names(out) <- names(.x) out } map2 <- function(.x, .y, .f, ...) { .f <- as_function(.f, env = global_env()) out <- mapply(.f, .x, .y, MoreArgs = list(...), SIMPLIFY = FALSE) if (length(out) == length(.x)) { set_names(out, names(.x)) } else { set_names(out, NULL) } } map2_lgl <- function(.x, .y, .f, ...) { as.vector(map2(.x, .y, .f, ...), "logical") } map2_int <- function(.x, .y, .f, ...) { as.vector(map2(.x, .y, .f, ...), "integer") } map2_dbl <- function(.x, .y, .f, ...) { as.vector(map2(.x, .y, .f, ...), "double") } map2_chr <- function(.x, .y, .f, ...) { as.vector(map2(.x, .y, .f, ...), "character") } imap <- function(.x, .f, ...) { map2(.x, names(.x) %||% seq_along(.x), .f, ...) } pmap <- function(.l, .f, ...) { .f <- as.function(.f) args <- .rlang_purrr_args_recycle(.l) do.call("mapply", c( FUN = list(quote(.f)), args, MoreArgs = quote(list(...)), SIMPLIFY = FALSE, USE.NAMES = FALSE )) } .rlang_purrr_args_recycle <- function(args) { lengths <- map_int(args, length) n <- max(lengths) stopifnot(all(lengths == 1L | lengths == n)) to_recycle <- lengths == 1L args[to_recycle] <- map(args[to_recycle], function(x) rep.int(x, n)) args } keep <- function(.x, .f, ...) { .x[.rlang_purrr_probe(.x, .f, ...)] } discard <- function(.x, .p, ...) { sel <- .rlang_purrr_probe(.x, .p, ...) .x[is.na(sel) | !sel] } map_if <- function(.x, .p, .f, ...) { matches <- .rlang_purrr_probe(.x, .p) .x[matches] <- map(.x[matches], .f, ...) .x } .rlang_purrr_probe <- function(.x, .p, ...) { if (is_logical(.p)) { stopifnot(length(.p) == length(.x)) .p } else { .p <- as_function(.p, env = global_env()) map_lgl(.x, .p, ...) } } compact <- function(.x) { Filter(length, .x) } transpose <- function(.l) { if (!length(.l)) { return(.l) } inner_names <- names(.l[[1]]) if (is.null(inner_names)) { fields <- seq_along(.l[[1]]) } else { fields <- set_names(inner_names) .l <- map(.l, function(x) { if (is.null(names(x))) { set_names(x, inner_names) } else { x } }) } # This way missing fields are subsetted as `NULL` instead of causing # an error .l <- map(.l, as.list) map(fields, function(i) { map(.l, .subset2, i) }) } every <- function(.x, .p, ...) { .p <- as_function(.p, env = global_env()) for (i in seq_along(.x)) { if (!rlang::is_true(.p(.x[[i]], ...))) return(FALSE) } TRUE } some <- function(.x, .p, ...) { .p <- as_function(.p, env = global_env()) for (i in seq_along(.x)) { if (rlang::is_true(.p(.x[[i]], ...))) return(TRUE) } FALSE } negate <- function(.p) { .p <- as_function(.p, env = global_env()) function(...) !.p(...) } reduce <- function(.x, .f, ..., .init) { f <- function(x, y) .f(x, y, ...) Reduce(f, .x, init = .init) } reduce_right <- function(.x, .f, ..., .init) { f <- function(x, y) .f(y, x, ...) Reduce(f, .x, init = .init, right = TRUE) } accumulate <- function(.x, .f, ..., .init) { f <- function(x, y) .f(x, y, ...) Reduce(f, .x, init = .init, accumulate = TRUE) } accumulate_right <- function(.x, .f, ..., .init) { f <- function(x, y) .f(y, x, ...) Reduce(f, .x, init = .init, right = TRUE, accumulate = TRUE) } detect <- function(.x, .f, ..., .right = FALSE, .p = is_true) { .p <- as_function(.p, env = global_env()) .f <- as_function(.f, env = global_env()) for (i in .rlang_purrr_index(.x, .right)) { if (.p(.f(.x[[i]], ...))) { return(.x[[i]]) } } NULL } detect_index <- function(.x, .f, ..., .right = FALSE, .p = is_true) { .p <- as_function(.p, env = global_env()) .f <- as_function(.f, env = global_env()) for (i in .rlang_purrr_index(.x, .right)) { if (.p(.f(.x[[i]], ...))) { return(i) } } 0L } .rlang_purrr_index <- function(x, right = FALSE) { idx <- seq_along(x) if (right) { idx <- rev(idx) } idx } list_c <- function(x) { inject(c(!!!x)) } # nocov end gargle/R/import-standalone-obj-type.R0000644000176200001440000002032314431310014017215 0ustar liggesusers# Standalone file: do not edit by hand # Source: # ---------------------------------------------------------------------- # # --- # repo: r-lib/rlang # file: standalone-obj-type.R # last-updated: 2022-10-04 # license: https://unlicense.org # imports: rlang (>= 1.1.0) # --- # # ## Changelog # # 2022-10-04: # - `obj_type_friendly(value = TRUE)` now shows numeric scalars # literally. # - `stop_friendly_type()` now takes `show_value`, passed to # `obj_type_friendly()` as the `value` argument. # # 2022-10-03: # - Added `allow_na` and `allow_null` arguments. # - `NULL` is now backticked. # - Better friendly type for infinities and `NaN`. # # 2022-09-16: # - Unprefixed usage of rlang functions with `rlang::` to # avoid onLoad issues when called from rlang (#1482). # # 2022-08-11: # - Prefixed usage of rlang functions with `rlang::`. # # 2022-06-22: # - `friendly_type_of()` is now `obj_type_friendly()`. # - Added `obj_type_oo()`. # # 2021-12-20: # - Added support for scalar values and empty vectors. # - Added `stop_input_type()` # # 2021-06-30: # - Added support for missing arguments. # # 2021-04-19: # - Added support for matrices and arrays (#141). # - Added documentation. # - Added changelog. # # nocov start #' Return English-friendly type #' @param x Any R object. #' @param value Whether to describe the value of `x`. Special values #' like `NA` or `""` are always described. #' @param length Whether to mention the length of vectors and lists. #' @return A string describing the type. Starts with an indefinite #' article, e.g. "an integer vector". #' @noRd obj_type_friendly <- function(x, value = TRUE) { if (is_missing(x)) { return("absent") } if (is.object(x)) { if (inherits(x, "quosure")) { type <- "quosure" } else { type <- paste(class(x), collapse = "/") } return(sprintf("a <%s> object", type)) } if (!is_vector(x)) { return(.rlang_as_friendly_type(typeof(x))) } n_dim <- length(dim(x)) if (!n_dim) { if (!is_list(x) && length(x) == 1) { if (is_na(x)) { return(switch( typeof(x), logical = "`NA`", integer = "an integer `NA`", double = if (is.nan(x)) { "`NaN`" } else { "a numeric `NA`" }, complex = "a complex `NA`", character = "a character `NA`", .rlang_stop_unexpected_typeof(x) )) } show_infinites <- function(x) { if (x > 0) { "`Inf`" } else { "`-Inf`" } } str_encode <- function(x, width = 30, ...) { if (nchar(x) > width) { x <- substr(x, 1, width - 3) x <- paste0(x, "...") } encodeString(x, ...) } if (value) { if (is.numeric(x) && is.infinite(x)) { return(show_infinites(x)) } if (is.numeric(x) || is.complex(x)) { number <- as.character(round(x, 2)) what <- if (is.complex(x)) "the complex number" else "the number" return(paste(what, number)) } return(switch( typeof(x), logical = if (x) "`TRUE`" else "`FALSE`", character = { what <- if (nzchar(x)) "the string" else "the empty string" paste(what, str_encode(x, quote = "\"")) }, raw = paste("the raw value", as.character(x)), .rlang_stop_unexpected_typeof(x) )) } return(switch( typeof(x), logical = "a logical value", integer = "an integer", double = if (is.infinite(x)) show_infinites(x) else "a number", complex = "a complex number", character = if (nzchar(x)) "a string" else "\"\"", raw = "a raw value", .rlang_stop_unexpected_typeof(x) )) } if (length(x) == 0) { return(switch( typeof(x), logical = "an empty logical vector", integer = "an empty integer vector", double = "an empty numeric vector", complex = "an empty complex vector", character = "an empty character vector", raw = "an empty raw vector", list = "an empty list", .rlang_stop_unexpected_typeof(x) )) } } vec_type_friendly(x) } vec_type_friendly <- function(x, length = FALSE) { if (!is_vector(x)) { abort("`x` must be a vector.") } type <- typeof(x) n_dim <- length(dim(x)) add_length <- function(type) { if (length && !n_dim) { paste0(type, sprintf(" of length %s", length(x))) } else { type } } if (type == "list") { if (n_dim < 2) { return(add_length("a list")) } else if (is.data.frame(x)) { return("a data frame") } else if (n_dim == 2) { return("a list matrix") } else { return("a list array") } } type <- switch( type, logical = "a logical %s", integer = "an integer %s", numeric = , double = "a double %s", complex = "a complex %s", character = "a character %s", raw = "a raw %s", type = paste0("a ", type, " %s") ) if (n_dim < 2) { kind <- "vector" } else if (n_dim == 2) { kind <- "matrix" } else { kind <- "array" } out <- sprintf(type, kind) if (n_dim >= 2) { out } else { add_length(out) } } .rlang_as_friendly_type <- function(type) { switch( type, list = "a list", NULL = "`NULL`", environment = "an environment", externalptr = "a pointer", weakref = "a weak reference", S4 = "an S4 object", name = , symbol = "a symbol", language = "a call", pairlist = "a pairlist node", expression = "an expression vector", char = "an internal string", promise = "an internal promise", ... = "an internal dots object", any = "an internal `any` object", bytecode = "an internal bytecode object", primitive = , builtin = , special = "a primitive function", closure = "a function", type ) } .rlang_stop_unexpected_typeof <- function(x, call = caller_env()) { abort( sprintf("Unexpected type <%s>.", typeof(x)), call = call ) } #' Return OO type #' @param x Any R object. #' @return One of `"bare"` (for non-OO objects), `"S3"`, `"S4"`, #' `"R6"`, or `"R7"`. #' @noRd obj_type_oo <- function(x) { if (!is.object(x)) { return("bare") } class <- inherits(x, c("R6", "R7_object"), which = TRUE) if (class[[1]]) { "R6" } else if (class[[2]]) { "R7" } else if (isS4(x)) { "S4" } else { "S3" } } #' @param x The object type which does not conform to `what`. Its #' `obj_type_friendly()` is taken and mentioned in the error message. #' @param what The friendly expected type as a string. Can be a #' character vector of expected types, in which case the error #' message mentions all of them in an "or" enumeration. #' @param show_value Passed to `value` argument of `obj_type_friendly()`. #' @param ... Arguments passed to [abort()]. #' @inheritParams args_error_context #' @noRd stop_input_type <- function(x, what, ..., allow_na = FALSE, allow_null = FALSE, show_value = TRUE, arg = caller_arg(x), call = caller_env()) { # From standalone-cli.R cli <- env_get_list( nms = c("format_arg", "format_code"), last = topenv(), default = function(x) sprintf("`%s`", x), inherit = TRUE ) if (allow_na) { what <- c(what, cli$format_code("NA")) } if (allow_null) { what <- c(what, cli$format_code("NULL")) } if (length(what)) { what <- oxford_comma(what) } message <- sprintf( "%s must be %s, not %s.", cli$format_arg(arg), what, obj_type_friendly(x, value = show_value) ) abort(message, ..., call = call, arg = arg) } oxford_comma <- function(chr, sep = ", ", final = "or") { n <- length(chr) if (n < 2) { return(chr) } head <- chr[seq_len(n - 1)] last <- chr[n] head <- paste(head, collapse = sep) # Write a or b. But a, b, or c. if (n > 2) { paste0(head, sep, final, " ", last) } else { paste0(head, " ", final, " ", last) } } # nocov end gargle/R/gargle_oauth_endpoint.R0000644000176200001440000000105014431310014016363 0ustar liggesusers#' OAuth endpoint for Google APIs #' #' Inlined from httr in case it changes. Internal function to centralize the #' info. #' #' @return An OAuth endpoint, produced by [httr::oauth_endpoint()]. #' @keywords internal #' @noRd #' @examples #' gargle_oauth_endpoint() gargle_oauth_endpoint <- function() { out <- httr::oauth_endpoint( base_url = "https://oauth2.googleapis.com", authorize = "", access = "token", validate = "tokeninfo", revoke = "revoke" ) out$authorize <- "https://accounts.google.com/o/oauth2/v2/auth" out } gargle/NEWS.md0000644000176200001440000006057514456275066012645 0ustar liggesusers# gargle 1.5.2 * Fixed a bug in an internal helper that validates input specifying a service account. The helper targets a common mistake where the JSON for an OAuth client is provided to an argument that is meant for a service account (#270). # gargle 1.5.1 * Completed some overlooked, unfinished work around the OAuth "app" to "client" transition that affected out-of-bound auth (#263, #264). * The `secret_*()` functions are more discoverable via documentation. # gargle 1.5.0 * gargle's existing unexported `secret_*()` functions are deprecated, in favor of new, exported `secret_*()` functions that are built on or inlined from httr2. The `vignette("managing-tokens-securely")` is updated to reflect the new, recommended strategy for encrypting secrets. - `secret_encrypt_json()` / `secret_decrypt_json()` are new gargle-specific functions. - `secret_write_rds()` / `secret_read_rds()`, `secret_make_key()`, and `secret_had_key()` are basically copies of their httr2 counterparts. - Legacy functions to move away from: `secret_pw_name()`, `secret_pw_gen()`, `secret_pw_exists()`, `secret_pw_get()`, `secret_can_decrypt()`, `secret_read()`, `secret_write()`. - The new approach makes it much easier to use gargle functions to encrypt and decrypt credentials in a project that is *not* necessarily an R package. * The transition from OAuth "app" to OAuth "client" is fully enacted now. This process tarted in v1.3.0, when the `"gargle_oauth_client"` class was introduced, to support the new pseudo-OOB auth flow. The deprecations are implemented to preserve backwards compatibility for some time. In this release, function, argument, and field names are all updated to the "client" terminology: - `init_AuthState(client =)` instead of `init_AuthState(app =)` - `AuthState$client` instead of `AuthState$app` - `AuthState$set_client()` instead of `AuthState$set_app()` - `gargle2.0_token(client =)` instead of `gargle2.0_token(app =)` - `credentials_user_oauth2(client =)` instead of `credentials_user_oauth2(app =)` A new `vignette("oauth-client-not-app")` explains how a wrapper package should adapt. * When the `"gargle_verbosity"` option is set to `"debug"`, there are more debugging messages around user credentials. Specifically, more information is available on the email, OAuth client, and scopes, with the goal of better understanding why a cached token is (or is not) being used. * `check_is_service_account()` is a new function for use in wrapper packages to throw a more informative error when a user provides JSON for an OAuth client to an argument that is expecting JSON for a service account. * `response_process()` has improved handling of responses that represent an HTTP error with HTML content (as opposed to the expected and preferred JSON) (#254). * `response_process(call = caller_env())` is a new argument that is passed along to various helpers, which can improve error reporting for user-facing functions that call `response_process()` (#255). # gargle 1.4.0 ## Google Compute Engine * `credentials_gce(scopes = NULL)` is now equivalent to `credentials_gce(scopes = "https://www.googleapis.com/auth/cloud-platform")`, i.e. there's an even stronger current towards the recommended "cloud-platform" scope. * `credentials_gce(scopes =)` now includes those `scopes` in its request to the metadata server for an access token (#216). Note that the scopes for a GCE access token are generally pre-determined for the instance and its associated service account at creation/launch time and these requested `scopes` will have no effect. But this seems to do no harm and it is possible that there are contexts where this is useful. * `credentials_gce()` now emits considerably more information when the `"gargle_verbosity"` option is set to `"debug"`. For example, it reports mismatches between requested scopes and instance scopes and between requested scopes and the access token's actual scopes. * `credentials_gce()` stores the actual scopes of the received access token, which can differ from the requested scopes. This is also noted when the `"gargle_verbosity"` option is set to `"debug"`. * The `GceToken` R6 class gains a better `$print()` method that is more similar to gargle's treatment of tokens obtained with other flows. ## Behaviour in a cloud/server context * gargle is better able to detect when it's running on Posit Workbench or RStudio Server, e.g., in a subprocess. * `gargle_oauth_client_type()` is a new function that returns either "installed" or "web". It returns the value of the new global option by the same name (`"gargle_oauth_client_type"`), if defined. If the option is not defined, returns "web" on RStudio Server, Posit Workbench, Posit Cloud, or Google Colaboratory and "installed" otherwise. In the context of out-of-band (OOB) auth, an "installed" client type leads to the conventional OOB flow (only available for GCP projects in testing mode) and a "web" client leads to the new pseudo-OOB flow. The option and accessor have been added to cover contexts other than those mentioned above where it is helpful to request a "web" client. * `credentials_user_oauth2()` now works in Google Colaboratory (#140). ## Everything else * gargle now elicits user input via `readline()`, instead of via `utils::menu()`, which is favorable for interacting with the user in a Jupyter notebook (#242). * The roxygen templating functions that wrapper packages can use to generate standardized documentation around auth have been updated to reflect gargle's pivot from OAuth "app" to "client". Changes of note: - `PREFIX_auth_configure_description()` crosslinks to `PREFIX_oauth_client()` now, not `PREFIX_oauth_app()`. So this assumes the package has indeed introduced the `PREFIX_oauth_client()` function (and, presumably, has deprecated `PREFIX_oauth_app()`). - `PREFIX_auth_configure_params()` gains `client` argument. - `PREFIX_auth_configure_params()` deprecates the `app` argument and uses a lifecycle badge. It is assumed that the badge SVG is present, which can be achieved with `usethis::use_lifecycle()`. - `PREFIX_auth_configure_params()` crosslinks to `gargle::gargle_oauth_client_from_json()`. The wrapper package therefore needs to state a minimum version for gargle, e.g. `gargle (>= 1.3.0)` (or higher). * `credentials_byo_oauth2()` works now for (variations of) service account tokens, as intended, not just for user tokens (#250). It also emits more information about scopes when the `"gargle_verbosity"` option is set to `"debug"`. # gargle 1.3.0 ## (Partial) deprecation out-of-band (OOB) auth flow On February 16, 2022, Google announced the gradual deprecation of the out-of-band (OOB) OAuth flow. OOB **still works** if the OAuth client is associated with a GCP project that is in testing mode and this is not going away. But OOB is no longer supported for projects in production mode. To be more accurate, some production-mode projects have gotten an extension to permit the use of OOB auth for a bit longer, but that's just a temporary reprieve. The typical user who will (eventually) be impacted is: * Using R via RStudio Server, Posit Workbench, or Posit Cloud. * Using tidyverse packages such as googledrive, googlesheets4, or bigrquery. * Relying on the built-in OAuth client. Importantly, this client is associated with a GCP project that is in production mode. The phased deprecation of OOB is nearly complete and we expect conventional OOB to stop working with the built-in tidyverse OAuth client on February 1, 2023, at the latest. **In preparation for this, gargle has gained support for a new flow, which we call pseudo-OOB (in contrast to conventional OOB)**. The pseudo-OOB flow is triggered when `use_oob = TRUE` (an existing convention in gargle and gargle-using packages) and the configured OAuth client is of "Web application" type. The gargle/googledrive/googlesheets4/bigrquery packages should now default to a "Web application" client on RStudio Server, Posit Workbench and Posit Cloud, leading the user through the pseudo-OOB flow. Other than needing to re-auth once, affected users should still find that things "just work". Read the `vignette("auth-from-web")` for more. ## gargle-specific notion of OAuth client `gargle_oauth_client()` is a new constructor for an S3 class by the same name. There are two motivations: - To adjust to Google's deprecation of conventional OOB and to support gargle's new pseudo-OOB flow, it is helpful for gargle to know whether an OAuth client ID is of type "Web application" or "Desktop app". That means we need a Google- and gargle-specific notion of an OAuth client, so we can introduce a `type` field. - A transition from httr to httr2 is on the horizon, so it makes sense to look more toward `httr2:oauth_client()` than to `httr::oauth_app()`. gargle's vocabulary is generally shifting towards "client" and away from "app". `oauth_app_from_json()` has therefore been (soft) deprecated, in favor of a new function `gargle_oauth_client_from_json()`, which is the preferred way to instantiate an OAuth client, since the downloaded JSON conveys the client type and redirect URI(s). As a bridging measure, `gargle_oauth_client` currently inherits from httr's `oauth_app`, but this probably won't be true in the long-term. `gargle_client(type =)` replaces `gargle_app()`. ## Google Compute Engine and Google Kubernetes Engine `credentials_gce()` no longer asks the user about initiating an OAuth cache, which is not relevant to that flow (#221). `gce_instance_service_accounts()` is a newly exported utility that exposes the service accounts available from the metadata server for the current instance (#234). The global option `"gargle.gce.timeout"` is newly documented in `credentials_gce()`. This controls the timeout, in seconds, for requests to the metadata server. The default value (or strategy) for setting this should often suffice, but the option exists for those with an empirical need to increase the timeout (#186, #195). `vignette("non-interactive-auth")` has a new section "Workload Identity on Google Kubernetes Engine (GKE)" that explains how gargle supports the use of workload identity for applications running on GKE. This is the recommended method of auth in R code running on GKE that needs to access other Google Cloud services, such as the BigQuery API (#197, #223, @MarkEdmondson1234). ## Credential function registry It's gotten a bit easier to work with the credential registry. The primary motivation is that, for example, on Google Compute Engine, you might actually want to suppress auth with the default service account and auth as a normal user instead. This is especially likely to come up with gmailr / the Gmail API. * The credential-fetcher `credentials_byo_oauth2()` has been moved to the very beginning of the default registry. The logic is that a user who has specified a non-`NULL` value of `token` must mean business and does not want automagic auth methods like ADC or GCE to be tried before using their `token` (#187, #225). * The `...` in `cred_funs_all()` are now [dynamic dots](https://rlang.r-lib.org/reference/dyn-dots.html) (#224). * Every registered credential function must have a unique name now. This is newly enforced by `cred_funs_add()` and `cred_funs_set()` (#224). * `cred_funs_list_default()` is a new function that returns gargle's default list of credential functions (#226). * `cred_funs_add(cred_fun = NULL)` is now available to remove a credential function from the registry (#224). * `with_cred_funs()` and `local_cred_funs()` are new helpers for making narrowly scoped changes to the registry (#226). * The `ls` argument of `cred_funs_set()` has been renamed to `funs` (#226). * In general, credential registry functions now return the current registry, invisibly (#224). # gargle 1.2.1 * Help files below `man/` have been re-generated, so that they give rise to valid HTML5. (This is the impetus for this release, to keep the package safely on CRAN.) * We have switched to newer oauth2.googleapis.com-based OAuth2 URIs, moving away from the accounts.google.com and googleapis.com/oauth2 equivalents. * `credentials_gce()` no longer validates the requested scopes against instance scopes. In practice, it's easy for this check to be more of a nuisance than a help (#161, #185 @craigcitro). * `request_retry()` retries for an expanded set of HTTP codes: 408, 429, 500, 502, 503. Previously, retries were limited to 429 (#169). ## Dependency changes * The minimum versions of rlang and testthat have been bumped. The motivation is to exploit and adapt to the changes to the display of error messages. # gargle 1.2.0 ## Workload identity federation `credentials_external_account()` is a new function that implements "workload identity federation", a new (as of April 2021) keyless authentication mechanism. This allows applications running on a non-Google Cloud platform, such as AWS, to access Google Cloud resources without using a conventional service account token, eliminating the security problem posed by long-lived, powerful service account credential files. `credentials_external_account()` has been inserted into the default registry of credential-fetchers tried by `token_fetch()`, which makes it automatically available in certain wrapper packages, such as bigrquery. `credentials_app_default()` recognizes the JSON configuration for an external account and passes such a call along to `credentials_external_account()`. This new feature is still experimental and currently only supports AWS. This [blog post](https://cloud.google.com/blog/products/identity-security/enable-keyless-access-to-gcp-with-workload-identity-federation) provides a good high-level introduction to workload identity federation. ## Other changes The `email` argument of `credentials_user_oauth2()` accepts domain-only email specification via a glob pattern. The goal is to make it possible for code like `PKG_auth(email = "*@example.com")` to identify a suitable cached token when executed on the machine of either `alice@example.com` or `bob@example.com`. gargle now throws errors via `cli::cli_abort()`, which means error messages now have the same styling as informational messages. ## Dependency changes aws.ec2metadata and aws.signature are new in Suggests. # gargle 1.1.0 ## OAuth token cache Two changes affect stored user OAuth tokens: * The default cache location has moved, to better align with general conventions around where to cache user data. Here's how that looks for a typical user: - Typical before, macOS: `~/.R/gargle/gargle-oauth` - Typical after, macOS: `~/Library/Caches/gargle` - Typical before, Windows: `C:/Users/jane/.R/gargle/gargle-oauth` - Typical after, Windows: `C:/Users/jane/AppData/Local/gargle/gargle/Cache` * Tokens created with one of the built-in OAuth apps provided by the tidyverse packages are checked for validity. Tokens made with an old app are deleted. Note that we introduced a new OAuth app in gargle v1.0.0 and the previous app could be disabled at any time. - Nickname of previous tidyverse OAuth app: `tidyverse-calliope` - Nickname of tidyverse OAuth app as of gargle v1.0.0: `tidyverse-clio` For users who accept all default behaviour around OAuth, these changes just mean you will see some messages about cleaning and moving the token cache. These users can also expect to go through interactive auth (approximately once per package / API), to obtain fresh tokens made with the current tidyverse OAuth app. If the rolling of the tidyverse OAuth app is highly disruptive to your workflow, this is a good wake-up call that you should be using your own OAuth app or, perhaps, an entirely different auth method, such as using a service account token in non-interactive settings. As always, these articles explain how to take more control of auth: * * ## User interface The user interface has gotten more stylish, thanks to the cli package (). All errors thrown by gargle route through `rlang::abort()`, providing better access to the backtrace and, potentially, error data. These errors have, at the very least, the `gargle_error` class and may also have additional subclasses. `gargle_verbosity()` replaces `gargle_quiet()`. Each such function is (or was) a convenience wrapper to query the option with that name. Therefore, the option named "gargle_verbosity" now replaces "gargle_quiet". If "gargle_verbosity" is unset, the old "gargle_quiet" is still consulted, but the user is advised to update their usage. The new "gargle_verbosity" option is more expressive and has three levels: * "debug", equivalent to the previous `gargle_quiet = FALSE`. Use for debugging and troubleshooting. * "info" (the default), basically equivalent to the previous `gargle_quiet = TRUE`. Since gargle is not a user-facing package, it has very little to say and only emits messages that end users really need to see. * "silent", no previous equivalent and of little practical significance. But it can be used to suppress all gargle messages. The helpers `with_gargle_verbosity()` and `local_gargle_verbosity()` make it easy to temporarily modify the verbosity level, in the spirit of the [withr package](https://withr.r-lib.org). ## Other changes There is special error handling when OAuth token refresh fails, due to deletion of the associated OAuth app. This should help users who are relying on the default app provided by a package and, presumably, they need to update that package (#168). `gargle_oob_default()` returns `TRUE` unconditionally when running in RStudio Server. `response_process()` gains a `remember` argument. When `TRUE` (the default), gargle stores the most recent response internally (with auth tokens redacted). Unexported functions `gargle:::gargle_last_response()` and `gargle:::gargle_last_content()` facilitate *post mortem* analysis of, e.g., a failed request (#152). `google.rpc.ErrorInfo` errors are explicitly handled now, resulting in a more informative error message. `request_retry()` is better able to detect when the per-user quota has been exhausted (vs. the per-project quota), resulting in a more informed choice of backoff. ## Dependency changes cli is new in Imports. rstudioapi is new in Imports. rappdirs is new in Imports. httpuv is new in Suggests. We encourage its installation in interactive sessions, if we're about to initiate OAuth flow, unless it's clear that out-of-band auth is inevitable. gargle now relies on testthat >= 3.0.0 and, specifically, uses third edition features. mockr is new in Suggests, since `testthat::use_mock()` is superseded. # gargle 1.0.0 * Better handling of `BadRequest` errors, i.e. more specifics are revealed. * `oauth_app_from_json` now supports JSON files from the "Web application" client type (#155). * `request_retry()` is a drop-in substitute for `request_make()` that uses (modified) exponential backoff to retry requests that fail with error `429 RESOURCE_EXHAUSTED` (#63). * Credentials used in selected client packages have been rolled. Users of bigrquery, googledrive, and googlesheets4 can expect a prompt to re-authorize the "Tidyverse API Packages" when using an OAuth user token. This has no impact on users who use their own OAuth app (i.e. client ID and secret) or those who use service account tokens. # gargle 0.5.0 * [Troubleshooting gargle auth](https://gargle.r-lib.org/articles/troubleshooting.html) is a new vignette. * All user-facing messaging routes through `rlang::inform()`, which (as of rlang 0.4.2) prints to standard output in interactive sessions and to standard error in non-interactive sessions (#133). Messaging remains under the control of the `"gargle_quiet"` option, which defaults to `TRUE`. * The `Gargle2.0` class gains its own `$refresh()` method, which removes a token from gargle's cache when it cannot be refreshed (#79). * `credentials_service_account()` and `credentials_app_default()` gain an optional `subject` argument, which can be used to pass a subject claim along to `httr::oauth_service_token()` (#131, @samterfa). * `request_make()` defaults to `encode = "json"`, which differs from the httr default, but aligns better with Google APIs (#124). * `field_mask()` is a utility function for constructing a Protocol-Buffers-style, JSON-encoded field mask from a named R list. * All R6 classes use the new documentation capabilities that appeared in roxygen2 7.0.0. * OAuth2 flow can only be initiated when `rlang::is_interactive()` is `TRUE`. If a new token is needed in a non-interactive session, gargle now throws an error (#113). * The application default credentials path is fixed on non-Windows platforms (#115, @acroz). * `request_develop()` can accept a parameter that appears in both the path and the body (#123). * `response_process()` explicitly declares the UTF-8 encoding of the content in Google API responses [tidyverse/googlesheets4#26](https://github.com/tidyverse/googlesheets4/issues/26). * `response_process()` is able to expose details for a wider set of errors. # gargle 0.4.0 * Eliminated uninformative failure when OAuth tokens cached on R <= 3.5 are re-loaded on R >= 3.6. The change to the default serialization version (2 vs. 3) creates an apparent mismatch between a token's hash and its key. Instead of inexplicably failing, now we attempt to repair the cache and carry on (#109, [tidyverse/googledrive#274](https://github.com/tidyverse/googledrive/issues/274). * In a non-interactive context, gargle will use a cached OAuth token, if it discovers (at least) one, even if the user has not given explicit instructions. We emit a recommendation that the user make their intent unambiguous and link to the vignette on non-interactive auth (#92). * gargle consults the option `"httr_oob_default"`, if the option `"gargle_oob_default"` is unset. This is part of an effort to automatically detect the need for out-of-bound auth in more situations (#102). * `credentials_service_account()` checks explicitly that `type` is `"service_account"`. This makes it easier to detect a common mistake, where the JSON for an OAuth client is provided instead of the JSON representing a service account (#93). * `credentials_gce()` gains `cloud-platform` as a default scope, assuming that the typical user wants to "View and manage your data across Google Cloud Platform services" (#110, @MarkEdmondson1234). # gargle 0.3.1 * [Non-interactive auth](https://gargle.r-lib.org/articles/non-interactive-auth.html) is a new vignette that serves as a guide for any client packages that use gargle for auth. * `credentials_gce()` might actually work now (#97, @wlongabaugh). * `credentials_app_default()` got a small bug fix relating to putting the token in the header (r-dbi/bigrquery#336) * `token_fetch()` silently catches warnings, in addition to errors, as it falls through the registry of credential-fetching methods (#89). * The yes/no asking if it's OK to cache OAuth tokens prints fully now (r-dbi/bigrquery#333). # gargle 0.3.0 * The unexported functions available for generating standardized docs for `PKG_auth` functions in client packages have been updated. * `token_userinfo()`, `token_email()`, and `token_tokeninfo()` are newly exported helpers that retrieve information for a token. * `AuthState$set_app()` and `AuthState$set_api_key()` now allow setting a value of `NULL`, i.e. these fields are easier to clear. * `credentials_byo_oauth2()` gains the ability to ingest a token from an object of class `httr::request`, i.e. to retrieve the `auth_token` component that holds an object of class `httr::Token2.0` that has been processed with `httr::config()`. # gargle 0.2.0 * All built-in API credentials have been rotated and are stored internally in a way that reinforces appropriate use. There is a new [Privacy policy](https://www.tidyverse.org/google_privacy_policy/) as well as a [policy for authors of packages or other applications](https://www.tidyverse.org/google_privacy_policy/#policies-for-authors-of-packages-or-other-applications). This is related to a process to get the gargle project verified, which affects the OAuth2 capabilities and the consent screen. * New vignette on "How to get your own API credentials", to help other package authors or users obtain their own API key or OAuth client ID and secret. * `credentials_byo_oauth2()` is a new credential function. It is included in the default registry consulted by `token_fetch()` and is tried just before `credentials_user_oauth2()`. # gargle 0.1.3 * Initial CRAN release gargle/MD50000644000176200001440000003305414456300740012033 0ustar liggesusersf47897975416a9a4f020586dd6aa5d2b *DESCRIPTION a5032aa8787905db930d574fcf7adeac *LICENSE ad3fdcb83a72df38779fa4e1d5e3c482 *NAMESPACE fbba4ebf6bc7aba73d0f1dc3c6ce31f9 *NEWS.md 1a1cf40f20f877e9d5209361301cab2b *R/AuthState-class.R 3e3d6ed1d8cd164c037deeadf97956f5 *R/Gargle-class.R 24698131ab19f67c36e4b602352bcb12 *R/aaa.R 82acbef644ba89bf18a9a635fd0a282f *R/cred_funs.R eafa0d83e81722a63aef863a91873957 *R/credentials_app_default.R f0eb93f575c2556b7b9a8eb1639bf1ac *R/credentials_byo_oauth2.R 1f49b7a50306efb71eb351a0fdbd057a *R/credentials_external_account.R 7bd1a100a7e3e39c6075d11678ae2ac2 *R/credentials_gce.R 028ccd0c4f17eb62d18039d1ec06aa87 *R/credentials_service_account.R 202bf1a34c2e7d3c0d1a91953a1ca2c6 *R/credentials_user_oauth2.R 5647bb608b436b0d30526d857a37a4a0 *R/field_mask.R 497d39da8645c14c7fe8959a99b39c9e *R/gargle-package.R 82b7aba5e4ffcedabd59635f7fbaaf42 *R/gargle_api_key.R 1a71f400c4d953f479439ceca3b21937 *R/gargle_oauth_client.R ad0972d93fc2fef64386710552fc2389 *R/gargle_oauth_endpoint.R 70258ae742d15ec71336f7a42bf23956 *R/import-standalone-obj-type.R 17bb123964057b839a42eda1c3da214b *R/import-standalone-purrr.R c40f882046a958444c6058a9e2cb9a3b *R/import-standalone-types-check.R 7414f29864f1917da792b23446c468ef *R/inside-the-house.R da2fb2913a8f6ae7c08095b06c3b8c5b *R/oauth-cache.R 2f91b2ae4e720d4a115fbbca7dccd6c9 *R/oauth-init.R fee758da4a0a5e50072f176236e292ae *R/oauth-refresh.R e73e1e1d2b6dc54ce2f593609aac7328 *R/request_develop.R b8ca08ac73265947087ab777952325f5 *R/request_make.R 6270762f9601918e32de332ebbe157bb *R/request_retry.R 210c4e115e25d832dd7ff59b9781008f *R/response_process.R 0bb3728923c2b59b19907bcd91e256f3 *R/roxygen-templates.R a10b8a1087720d910048f31a94b9f1ed *R/secret.R 6e5a935c01f8a4da2d53e5909a3b6b00 *R/sysdata.rda b6381ed4b14e24e3ea79e82cdebd6f96 *R/token-info.R 9e20b0d4be513412cdde497ebe426f0d *R/token_fetch.R 3f1777a9c6f97f06a49d06595f15bf93 *R/utils-ui.R e1851e56aecd20e815133672e10ab562 *R/utils.R 6b2fc88bd96e5bb0a64c603199ee329d *R/zzz.R c8499b1b6a13d74c9ff6429c6677cfb5 *README.md 177ad78b12f666c036c26e1495692194 *build/vignette.rds 36c920329bf9e35302cdf763564897e2 *inst/WORDLIST c12488389652b88ae0d3e384f4eb641d *inst/discovery-doc-ingest/api-wide-parameter-names.txt abf96b69438faadc419df7106110f379 *inst/discovery-doc-ingest/api-wide-parameters-humane.txt 3c28bc4104b8aaa1e742559a81dfd5ac *inst/discovery-doc-ingest/api-wide-parameters.csv 151010fc3a3ac441ee167ea1ea5340ac *inst/discovery-doc-ingest/discover-discovery.R 17d7f9e20b6f77cb0ef771d9fc0b392d *inst/discovery-doc-ingest/drive-example.R 9276fe884512e98561eda2e2ac537daa *inst/discovery-doc-ingest/ingest-functions.R 6e7b55e25e8d32878aa076bee6b05460 *inst/discovery-doc-ingest/method-properties-humane.txt 1108fb1eff01eabe54dd56b9a00239a0 *inst/discovery-doc-ingest/method-properties.csv c12488389652b88ae0d3e384f4eb641d *inst/discovery-doc-ingest/method-property-names.txt d8da9a0b7b12823297dfcd6e908371fd *inst/discovery-doc-ingest/parameter-properties-humane.txt 819e735bddad77e8035b158afc8a3e0d *inst/discovery-doc-ingest/parameter-properties.csv 9b3deb1f99eb1e42143c45ab10e1ca7c *inst/discovery-doc-ingest/parameter-property-names.txt 949aaff31f64b05dd01b5ddbcfcf2b3f *inst/doc/auth-from-web.R 5ff8af182f686a2ebab8c996c60788d8 *inst/doc/auth-from-web.Rmd 68770c15b8c4f1dd41884b29eb275410 *inst/doc/auth-from-web.html 750a6e3ce5ac48b001ed53a0b68256ca *inst/doc/gargle-auth-in-client-package.R 452b90a3341c56647b64c6d1bd67c061 *inst/doc/gargle-auth-in-client-package.Rmd ac335d34e6ce1c3fde3af2383a21700c *inst/doc/gargle-auth-in-client-package.html 8e7d2f113ba00d24a7fbbdc5d68ae48f *inst/doc/get-api-credentials.R c3861d010b15c274b6f3138fca42112d *inst/doc/get-api-credentials.Rmd a30dc5ef00352048503530cbc973e629 *inst/doc/get-api-credentials.html 17eb59c62f7d9ae0f07609f6af12d8e8 *inst/doc/how-gargle-gets-tokens.R 9b628a791c712421580782bb35c8f7d3 *inst/doc/how-gargle-gets-tokens.Rmd 731e922df64e75874f802a78bbf2f625 *inst/doc/how-gargle-gets-tokens.html 2c55f7d256cf12e2b3e47f798f911459 *inst/doc/non-interactive-auth.R 4f3f64637663bcd8ec008b3a00413876 *inst/doc/non-interactive-auth.Rmd f91fc638f8f00c6781d97df10610aecd *inst/doc/non-interactive-auth.html 276cab0ade57a733f8bb1ddee28f5d4f *inst/doc/oauth-client-not-app.R 4cdd4186d18e6d8ec69129c6148af293 *inst/doc/oauth-client-not-app.Rmd c4279be45354183ba63c6c3b7ae674a2 *inst/doc/oauth-client-not-app.html 22fd3602f7b66785cb19618c51285013 *inst/doc/request-helper-functions.R e7c5f90a2d47fb7ad1761dfb4e6b9732 *inst/doc/request-helper-functions.Rmd 8a51d326211fe5c99e5be55e067cc63e *inst/doc/request-helper-functions.html 19c68de78179ad867d6f820644c41575 *inst/doc/troubleshooting.R d8d39c6f4a9b9877eabe954137259de2 *inst/doc/troubleshooting.Rmd e8f3b625ab685c068c6bb9f1f3a75aa3 *inst/doc/troubleshooting.html b067ac42a32d493d170aa8fe81d427f7 *inst/extdata/client_secret_installed.googleusercontent.com.json 1b8125b11b196162e93d4918b1969887 *inst/extdata/client_secret_web.googleusercontent.com.json 49c1f9e4b3d7fd5dde00b8844c491cca *inst/extdata/fake_service_account.json 2d5aaa28681bd91086ef226fe61e3cf7 *inst/pseudo-oob/google-callback/index.html 971c6e532255429eefb18f112ee490bb *inst/secret/gargle-testing.json a1f8baeeaee04de756d841a24508d96c *man/AuthState-class.Rd 7404e42ac0fa65874a4712ffa6e189b8 *man/Gargle-class.Rd 84ba52268334287da5a218556efaf402 *man/GceToken.Rd 53b10bf9de6a3ba2364c8b81c0a3f6e0 *man/WifToken.Rd 7c6eed21b606f1a6b9290abb24698f77 *man/bulletize.Rd 0f1785dfa504bac319e44440986f3fd0 *man/check_is_service_account.Rd 41d413261c90f55caaf3526d1c237626 *man/cred_funs.Rd 12e89593e3231c3cd17e8742b243da1d *man/credentials_app_default.Rd a360b10b2d0d1d8ecd03fa42197ac593 *man/credentials_byo_oauth2.Rd a5793894bc20e5f4fa90bd18de42ed6d *man/credentials_external_account.Rd dc3089a3865bc950b9251e15b513d35f *man/credentials_gce.Rd b582eeff3c032a2d2705c688015b8abc *man/credentials_service_account.Rd 562defc8a284d4273bbb3c35765469e6 *man/credentials_user_oauth2.Rd b502e08c8e7040307092ceb1682223ec *man/field_mask.Rd a1cbaf3f328e8d74e747faacf640c7fc *man/figures/lifecycle-archived.svg 6f521fb1819410630e279d1abf88685a *man/figures/lifecycle-defunct.svg 391f696f961e28914508628a7af31b74 *man/figures/lifecycle-deprecated.svg 691b1eb2aec9e1bec96b79d11ba5e631 *man/figures/lifecycle-experimental.svg 405e252e54a79b33522e9699e4e9051c *man/figures/lifecycle-maturing.svg f41ed996be135fb35afe00641621da61 *man/figures/lifecycle-questioning.svg 306bef67d1c636f209024cf2403846fd *man/figures/lifecycle-soft-deprecated.svg ed42e3fbd7cc30bc6ca8fa9b658e24a8 *man/figures/lifecycle-stable.svg bf2f1ad432ecccee3400afe533404113 *man/figures/lifecycle-superseded.svg f136230d83fc8a466fe0243834993b45 *man/gargle-package.Rd c0402d4efc352f74afec70b167030c3f *man/gargle2.0_token.Rd e3fcce58c983ca430ab89eedddfbd89d *man/gargle_api_key.Rd 05d54d32524be790dc6ba3aaa62424c0 *man/gargle_client.Rd c6efd4d574f082f577134e994f6430e3 *man/gargle_map_cli.Rd be10b9a365fe2fe612873f966022770a *man/gargle_oauth_client_from_json.Rd 655d6428b86a93210036d690e9ecb39e *man/gargle_oauth_sitrep.Rd ccae2b3e36e1c02834830f01367a1370 *man/gargle_options.Rd 70fcb947ca570fb9d43e7f261576522f *man/gargle_secret.Rd d5c7f3010d4c8a0df0c9ddd5f70fc953 *man/gce_access_token.Rd fcf6cd1220c6ac6481def3b9f422f83d *man/gce_instance_service_accounts.Rd 02482ad8a453b57e701507ccbd1243d8 *man/init_AuthState.Rd a14c6b00a0b05ff8654d6e6cc7d61008 *man/internal-assets.Rd 8f99b1778c9652ff5f9518532b5ea5be *man/oauth_app_from_json.Rd 6a000f333caaf2fe2fbcac461c72594c *man/oauth_external_token.Rd 3edd99d3397296b499f6e0f8591b9264 *man/request_develop.Rd d6a53a6320d8e4f9c7b78d121c064f19 *man/request_make.Rd 5274fb16b573a20808cd3feac9b72adc *man/request_retry.Rd b786f4679f8dead631f23422ba29cf1f *man/response_process.Rd 5bfd79ef4fceab9bc029b0753e3740dc *man/token-info.Rd 70457292c7e2d94b7dadd69c441377b2 *man/token_fetch.Rd 50f330eeca8db092d6807e04457bd06d *tests/spelling.R 786cfb0c126a9cf1f08a94ce90325209 *tests/testthat.R 70dc34593861434e49faab70eb17c52f *tests/testthat/_snaps/AuthState-class.md 85398e40c523577cea4ca32ac33175e9 *tests/testthat/_snaps/Gargle-class.md 86bb1859438c2cf304b996cd3cb0fb5b *tests/testthat/_snaps/cred_funs.md 069161142d8cb44475e59db5020978ee *tests/testthat/_snaps/credentials_byo_oauth2.md b00fa447fd149b39189908ff1c4a32c1 *tests/testthat/_snaps/credentials_service_account.md 2bb742a4e19eaf9436cb12711b10f1d6 *tests/testthat/_snaps/gargle_oauth_client.md 836416fc996cc1286c30542b35913acd *tests/testthat/_snaps/gargle_oauth_endpoint.md 43d5b7dda2068e1765c555f23e95b77a *tests/testthat/_snaps/inside-the-house.md 3be65b2630366beb1e99c866ec17ffe4 *tests/testthat/_snaps/oauth-cache.md e475c5aea068a86ad6e16f4dfe7723c7 *tests/testthat/_snaps/oauth-init.md e8de9ebfbcd9ed0fde00bfa38106fc38 *tests/testthat/_snaps/oauth-refresh.md 4b333aae7e0b5dd2013b8e43d71936dd *tests/testthat/_snaps/request_develop.md 5b374994209e84a5d4d226b154cedef1 *tests/testthat/_snaps/request_make.md 2d124a0c1c164db976a5d3292e7cb5dd *tests/testthat/_snaps/request_retry.md 3174972637937a4d51d1e44687c3e2e8 *tests/testthat/_snaps/response_process.md 5864b750ddd576068d4d7c6a9d7a65f3 *tests/testthat/_snaps/roxygen-templates.md 5fb9488f0459307940f944ddfbb099c1 *tests/testthat/_snaps/secret.md 1eb311c8e12b005f57126393d4177f3e *tests/testthat/_snaps/utils-ui.md cf49072267b3d82111361c900d2436d0 *tests/testthat/fixtures/drive-automated-queries_429.R 7d98090d96816200aabc8ff7b5a08b84 *tests/testthat/fixtures/drive-automated-queries_429.rds f5a3952f3d8f9a0f6cfff8b1fcf6beee *tests/testthat/fixtures/drive-files-get-nonexistent-file-id_404.R c08a8e0d5e41eae0792319a6557fbe3a *tests/testthat/fixtures/drive-files-get-nonexistent-file-id_404.rds c7d68aadddaf86e57b3e15df40d3aa9f *tests/testthat/fixtures/fitness-get-wrong-scope_403.R 17e8a8cd52ef2be744cf7edbcadc13fb *tests/testthat/fixtures/fitness-get-wrong-scope_403.rds 35a6dfd39825d3e775d86ff5392a8b2e *tests/testthat/fixtures/service-account-token.json 7e2c99055e95d1825ce5f19063a0ad0c *tests/testthat/fixtures/sheets-spreadsheets-get-api-key-not-enabled_403.R 0037df138035d86ebc9b1ca7a03bbec5 *tests/testthat/fixtures/sheets-spreadsheets-get-api-key-not-enabled_403.rds 787a0b7c9f838148003ab04e86384638 *tests/testthat/fixtures/sheets-spreadsheets-get-bad-field-mask_400.R 0323773104b23551e5cdfc70c8d2c415 *tests/testthat/fixtures/sheets-spreadsheets-get-bad-field-mask_400.rds 2d5e744c767e2aacb7d8632e1072ba2d *tests/testthat/fixtures/sheets-spreadsheets-get-nonexistent-range_400.R 9139d92971f5a2cea6c28bca5d234792 *tests/testthat/fixtures/sheets-spreadsheets-get-nonexistent-range_400.rds 4b6e634a1150ffb87ed0f534fc509d1d *tests/testthat/fixtures/sheets-spreadsheets-get-nonexistent-sheet-id_404.R ee2c34bf50a68adcf21e5f89f398adf1 *tests/testthat/fixtures/sheets-spreadsheets-get-nonexistent-sheet-id_404.rds 7468f0ab2450899bd34499400daf7a1c *tests/testthat/fixtures/sheets-spreadsheets-get-quota-exceeded-readgroup_429.R 18f4776a8ef1ea4f32920d7d89424785 *tests/testthat/fixtures/sheets-spreadsheets-get-quota-exceeded-readgroup_429.rds fcbe61d90196072d5950e283d4845512 *tests/testthat/fixtures/tokeninfo-bad-path_404.rds c0d36ed5fbcdb9afe9834b42a84b7570 *tests/testthat/fixtures/tokeninfo-stale_400.rds bcf8ca6fcfbcbd17e314ee63d54d3f20 *tests/testthat/fixtures/tokeninfo_40X.R 8110ec6c2518572ad744a99ceb4348aa *tests/testthat/helper.R ed638ce51178649148ece47d97af262c *tests/testthat/test-AuthState-class.R 351cc95d8a294f60b48e0ee4e6924af7 *tests/testthat/test-Gargle-class.R 32828a79f683260cff2e5360b5ed686e *tests/testthat/test-aaa.R 9429469de4c69c82e24b35b45a0ef48d *tests/testthat/test-assets.R 97d50ce16e5ac2ff08c78ac30b4e70da *tests/testthat/test-cred_funs.R 0c43e337a39722beda8e325dc96fcfde *tests/testthat/test-credentials_app_default.R 22cb69d6ac24ac1b00f51e9ec7dca78c *tests/testthat/test-credentials_byo_oauth2.R 43cfe8d062c1501fcda615861225c874 *tests/testthat/test-credentials_gce.R 4969aaf306062fc6358afd60363fd836 *tests/testthat/test-credentials_service_account.R cbf82e656938ff7f826786de8b0417b8 *tests/testthat/test-field_mask.R 46b3275e4f993d7357e65c05d7f1488e *tests/testthat/test-gargle_oauth_client.R af298bd49825c0a711499e254b04efc1 *tests/testthat/test-gargle_oauth_endpoint.R 04ac4b790e12de68d0c630e160020152 *tests/testthat/test-inside-the-house.R 66d098866d626c167115f1abc8949063 *tests/testthat/test-oauth-cache.R 7fb7ae3ff18f4b3e46e395cc5d1b9309 *tests/testthat/test-oauth-init.R 579816a53a7cb67d3994c3d163cab3a3 *tests/testthat/test-oauth-refresh.R 1ad6c0eb746684da4b5b08a822f477e7 *tests/testthat/test-request_develop.R 8915713f887221fa2e7d10eec4e43a20 *tests/testthat/test-request_make.R 589a13150ae25eab8b5a8eefde31a247 *tests/testthat/test-request_retry.R e2e6c7a82e477a102a6cde395e664fa2 *tests/testthat/test-response_process.R 763b834deee9230f4ce6031ce4c83fad *tests/testthat/test-roxygen-templates.R 1684579766811ce0eb39e72f961350e7 *tests/testthat/test-secret.R e329fcdd2ccfa1890af2042cb8adb0dc *tests/testthat/test-token-info.R 110ced380f9397e5b318f4820e4f0750 *tests/testthat/test-token_fetch.R 54c921ec3ba52fdd50bc58d0a4251ea4 *tests/testthat/test-utils-ui.R cb377fe21bf4a461903c9eaad67367dc *tests/testthat/test-utils.R 5ff8af182f686a2ebab8c996c60788d8 *vignettes/auth-from-web.Rmd f69dd93a4f263fa467ab61fc6ca787b3 *vignettes/deleted_client.png 452b90a3341c56647b64c6d1bd67c061 *vignettes/gargle-auth-in-client-package.Rmd c3861d010b15c274b6f3138fca42112d *vignettes/get-api-credentials.Rmd 9b628a791c712421580782bb35c8f7d3 *vignettes/how-gargle-gets-tokens.Rmd b5fd4bde43acf6ddd85cb64a1967cd25 *vignettes/invalid_request.png 4f3f64637663bcd8ec008b3a00413876 *vignettes/non-interactive-auth.Rmd 4cdd4186d18e6d8ec69129c6148af293 *vignettes/oauth-client-not-app.Rmd e7c5f90a2d47fb7ad1761dfb4e6b9732 *vignettes/request-helper-functions.Rmd d8d39c6f4a9b9877eabe954137259de2 *vignettes/troubleshooting.Rmd gargle/inst/0000755000176200001440000000000014456275120012476 5ustar liggesusersgargle/inst/secret/0000755000176200001440000000000014436207160013760 5ustar liggesusersgargle/inst/secret/gargle-testing.json0000644000176200001440000000605514436207160017575 0ustar liggesusersvqR_XddoJH7vZk0600OEspJ5U3Ysa4oEWK3Kjk2-sqT3vWASgDQoVH7U5WLWyXl_vPs5_80gk2QiJnPbqgWOdsy4LatP1VEjMswpvePf2ZnsepKBQzbf7t2D-DMbvO32zYu1jBROykyjV2CuMHQd7qPZQ0vKhbJONGHFHloDyiylQ6a2J9UMtgZx7lV01FDtCF6juaEQczPdfLrOeocXmIoqZI2TZVUCxJ3lva7ERgoPe6UIM0MInksDU-uF8QtIQPpm71BHlEFXf_Xrp_T1eZDD5cFqWdKami5TdgFk7UDtRDdNgo4oHtwRtL-5yoYO3xqa-2ByxpEpw8UrqB3oqxnX79u6cjRZj9BMxNFiAzF4PJOw1Eh872ZpuAAJ97AL5zckN1GCdgbJOk5SMmpttA5CACr9_Objc-1oDCCJcgK3rL5z8xDOMGSSDs_dIqniFljt5lr4ph-1PmGvuh1TPvlzzw0_QG1DB0kxvKXnB-lsedNHzTrHowJoRpuZ8yZkoMN4RinapZBkWai-YGypViQCH3_DtfY1ZdY_ANygjRJR4y5Vk7Qnv3umHJ3mo39wr00XXCrH-uFmL5C9gFDDUTqzevtesqlqanXnx3edOv2l3LcKsnn5zHAg6fqx1zQKGUs5Mt2MpOuP1yc939OX01-is0Ce3l43xhQNFoC9pqRas1VVmVxZ4HIxBYt1DpB6CF_62qrZYm-oOoOclsKcQFTgb380LeuB1fHuaIcwY7satd5BjNxxIWa_ZmoFQha2f-V9bS_NbGbj04vU-D8G_Rp2HO9vdAtKSYZaoZ_asnMm2DEhGsDMykSruiVB06AKkuxLHkQ-bjLvpVp_LsO28J47oEmHaGNgHqWPCunFXP6vGCSLlTFS1gtNHVPBuJNDHZliMo5PoGnO2SSPgPtyZG0MllQVpr9RwWTATCI-R45SUWoJKq0F-IsIue2Zp4wBiAyp_uo63VuRfgrefEhWUfwFUbOCh6pNeD6jTkHaw1VESjRzB8ACv_b5H6y9a_SFOGVXBgVTk4wGeBshl_SR2VpykgK9Rh7RSNZ9Fs_HzL1dIqFiWhsiZ6bks9JKIa2ARIUds1su9EO2iQ45itF9zB7MgPedAVvbtwkEH1O-sVL6RwWudP3svfjzcsMFXG7-ExiU_7QcIPr0gK13J6dcZ-qFw6mXzeznF9yYGnpcMv-hFb15s6WXF_P7DEwg-t7yMsQXGSt9fwfBCIXP1Wqx1X84y_Zz4DSFVtvsbce8cSHygotGdbmHZjl3jPOPthYEqDoNTQXRuI9uiVHmAAARCqiZiMo8THh55GnFu4R5N9Tc4KBRum97AKJUYZd72CLH04tQ4qB957ZHvJSblkOPgbEAFVCQ1dm15MwkA_9Zk_y0cZIcy8MrMTcGGcAO89aElt_QXOFohpdvJjqDpWXXznTBwCDy1I3A8xJ14qSZpa4tjwQF7h3ikHJTCHxpYZVR16pQryavSf5g3f_20N_rXKqNFsqIRI_fKQ918tZhIt3IDwOaoD27egO-CkbJoRq0mC_lyxlzKFwkmBx3dUNIxQpSdbaj4-Si2FFSL-Pu0TD70LP5K4n_Oot2WQi5P2jX0usEOqRY5T2krdvq_TYpa8ahZn2ISx26GXOObfTIsPGuviX3jNGCTnPttK41JwPfdWR61VDjWKtEASpzXrrhRdly7zft9BqXdzMu7wIpkHZ-TB7X7qsZRSPG5fiNAzYrRM1NxDqnK-irRHES2eSXBpKgFSNKQbGOL6TjQCWLRqw36k-WjqQYZuWuvbWCMscoq-bAPpEeZ2jQqk5sZSASAuZlT2k0ElTdOxzISNYYbswyzpWt57nBKLtA90KlcdkC8SdBdCTmB8yk-womgZyeWw1XRiHayRsBvSIWP1yCpyeO2rSBnx5al2ejjBtZeGJNdQ2N62-c3OTftBomAOSsZ76wxG8P3VmlJcfiw-zXoG3hrIqW4q2aQLC1wDt80_e5JqdpbuE7Q0wVClJJ1Az7eG9iee0G_vAbH76qkcc6IOcxOx5wTOfKvCBYMD11rB83SjyFsJQRkRKQG6vuACfQOXg9Vn4RWtoknRH3By-NrvQswkkkGkv7StfQbsjOgH6SxAVefN1JhYzMUCufAAo21W2C7mkLCFPCJ4VmdDI20L8nlEvjM_s_dkfm33AuUso--NlGCCv0lT86P9xpURmfr6gvqt1Rx8OBUcU4bQAtcr3ISkdNd0SK2qz_zE1YW8JXCHqQdGhhF9AdW0Blr5MNkPDTUJp5T9InZn4DsAKn_Vunil7JlfuhxIkc1k6LqaLv6__4pURV3AyIkNxVaU-6KYhv1wWRABXdZBaYZwsgmT8sNq9bwZXPaxBisEgQWTsvshpWrdeapvKoVmJwBn3Yj0IywdznFURy1-Nta4X0WImSUry0LuhMSGSkxOF2PQnD5Q0PAn_lTX_M8gcCXU4RrgZQMkHE809ls7F4RWXfIZ7U5b5AIY_Ehx6Jys6Ob3vtfc-eozolUv7jlSDVWzYA5VHmH36KkJdgeCrFRY1-eqB09yfrXkYRtxMVIvcd_JiOCMX3nQM7nhARWvRm4aGHe3jJ3kNQnkH1n4hCl2acI2Bj5-BlliBmZVQHgOb2gVHIZUKV2M5W4r2r1DCS6OH0CgexZq7l2tBaDMujF98eoPBv-wSYkZ7S1rKPseBJ-p0xp1prYNvjGGi9tx5KxakT5WmhDrAmnom4A3FXgORjrhlW4Tcuxrjxtq8vReOl8qmWEiNerqfIuYEbbptidT2bRCjCh7XuPqP2dHcz3Q2Qk495sBCPCU0FvweO9BC3wX6wqPShWemqso124M1g-O5TUHjToStI2pZ73Ck_olqdhmtAAWFTXOYtT4vIInUvfShD2i5gBCSZx_w8LqBneKHxR5e51Eots40-e6pg5_MdrY_xFKRmZQvW1EH5J32uKyUCzIGBqWzx7F66tgErWTcjvocpr796RM-pDMNhJWNQV3FmGu_LRinD2hlaWOCXPWJRCLiW7RSoW5E3pPdmiobAVRRJ4j39IvcnSOTHBXUZ_0lZ0XZ8m4A9cOKBAzEJs5MhOkFIOPuwC_tG87ZczEwpxQz2Kq7A7GKnaazYdBCcgbd2gargle/inst/doc/0000755000176200001440000000000014456275120013243 5ustar liggesusersgargle/inst/doc/request-helper-functions.R0000644000176200001440000000520214456275117020346 0ustar liggesusers## ---- include = FALSE--------------------------------------------------------- knitr::opts_chunk$set( collapse = TRUE, comment = "#>" ) ## ----setup-------------------------------------------------------------------- library(gargle) ## ----------------------------------------------------------------------------- ddi_dir <- system.file("discovery-doc-ingest", package = "gargle") list.files(ddi_dir) ## ---- eval = FALSE------------------------------------------------------------ # .endpoints[["drive.files.create"]] ## ----eval = FALSE------------------------------------------------------------- # # googledrive:: # request_generate <- function(endpoint = character(), # params = list(), # key = NULL, # token = drive_token()) { # ept <- .endpoints[[endpoint]] # if (is.null(ept)) { # stop_glue("\nEndpoint not recognized:\n * {endpoint}") # } # # ## modifications specific to googledrive package # params$key <- key %||% params$key %||% drive_api_key() # if (!is.null(ept$parameters$supportsTeamDrives)) { # params$supportsTeamDrives <- TRUE # } # # req <- gargle::request_develop(endpoint = ept, params = params) # gargle::request_build( # path = req$path, # method = req$method, # params = req$params, # body = req$body, # token = token # ) # } ## ---- eval = FALSE------------------------------------------------------------ # # gargle:: # request_make <- function(x, ..., user_agent = gargle_user_agent()) { # stopifnot(is.character(x$method)) # method <- switch( # x$method, # GET = httr::GET, # POST = httr::POST, # PATCH = httr::PATCH, # PUT = httr::PUT, # DELETE = httr::DELETE, # abort(glue("Not a recognized HTTP method: {bt(x$method)}")) # ) # method( # url = x$url, # body = x$body, # x$token, # user_agent, # ... # ) # } ## ---- eval = FALSE------------------------------------------------------------ # # googledrive:: # request_make <- function(x, ...) { # gargle::request_make(x, ..., user_agent = drive_ua()) # } ## ----asis = TRUE, echo = FALSE, comment = NA---------------------------------- cat(readLines(fs::path(ddi_dir, "method-properties-humane.txt")), sep = "\n") ## ----asis = TRUE, echo = FALSE, comment = NA---------------------------------- cat(readLines(fs::path(ddi_dir, "api-wide-parameters-humane.txt")), sep = "\n") ## ----asis = TRUE, echo = FALSE, comment = NA---------------------------------- cat(readLines(fs::path(ddi_dir, "parameter-properties-humane.txt")), sep = "\n") gargle/inst/doc/request-helper-functions.Rmd0000644000176200001440000002601614017730626020667 0ustar liggesusers--- title: "Request helper functions" output: rmarkdown::html_vignette vignette: > %\VignetteIndexEntry{Request helper functions} %\VignetteEngine{knitr::rmarkdown} %\VignetteEncoding{UTF-8} --- ```{r, include = FALSE} knitr::opts_chunk$set( collapse = TRUE, comment = "#>" ) ``` This vignette explains the purpose and usage of: * `request_develop(endpoint, params, base_url)` * `request_build(method, path, params, body, token, key, base_url)` * `request_make(x, ..., user_agent)` The target audience is someone writing an R package to wrap a Google API. ```{r setup} library(gargle) ``` ## Why use gargle's request helpers? Why would the developer of a Google-API-wrapping package care about the request helpers in gargle? You can write less code and safer code, in return for a modest investment in studying your target API. That is done by ingesting the API's so-called Discovery Document. Hundreds of Google APIs -- the ones addressed by the [API Discovery Service](https://developers.google.com/discovery/) -- share a great deal of behaviour. By ingesting the metadata provided by this service, you can use gargle's request helpers to exploit this shared data and logic, while also decreasing the chance that you and your users will submit ill-formed requests. The request helpers in gargle check the combined inputs from user and developer against suitably prepared API metadata: * If required parameters are missing, an error is thrown. * If unrecognized parameters are submitted, an error is thrown. * Parameters are automatically placed in their correct location: URL substitution, query, or body. * *Is there something else you care about? It is possible to do more, but it would help to have concrete requests.* Google provides [API libraries for several languages](https://developers.google.com/api-client-library/), including Java, Go, Python, JavaScript, Ruby and more (but not R). All of these libraries are machine-generated from the metadata provided by the API Discovery Service. It is the [official recommendation](https://developers.google.com/discovery/v1/using#build) to use the Discovery Document when building client libraries. The gargle package aims to implement key parts of this strategy, in a way that is also idiomatic for R and its developers. ## High-level design pattern gargle facilitates this design for API-wrapping packages: * A machine-assisted low-level interface driven by the Discovery Document: - Your package exports thin wrapper functions around gargle's helpers to form and make HTTP requests, that inject package-specific logic and data, such as an API key and user agent. This is for power users and yourself. * High-level, task-oriented, user-facing functions that constitute the main interface of your package. - These functions convert user input into the form required by the API and pass it along to your low-level interface functions. Later, specific examples are given, using the googledrive package. ## gargle's HTTP request helpers gargle provides support for creating and sending HTTP requests via these functions: `request_develop(endpoint, params, base_url)`: a.k.a. The Smart One. * Processes the info in `params` relative to detailed knowledge about the `endpoint`, derived from an API Discovery Document. * Checks for required and unrecognized parameters. * Peels off `params` destined for the body into their own part. * Returns request data in a form that anticipates the `httr::VERB()` call that is on the horizon. `request_build(method, path, params, body, token, key, base_url)`: a.k.a. The Dumb One. * Typically consumes the output of `request_develop()`, although that is not required. It can be called directly to enjoy a few luxuries even when making one-off API calls in the absence of an ingested Discovery Document. * Integrates `params` into a URL via substitution and the query string. * Sends either an API key or an OAuth token, but it provides no default values or logic for either. `request_make(x, ..., user_agent)`: actually makes the HTTP request. * Typically consumes the output of `request_build()`, although that is not required. However, if you have enough info to form a `request_make()` request, you would probably just make the `httr::VERB()` call yourself. * Consults `x$method` to determine which `httr::VERB()` to call, then calls it with the rest of `x`, `...`, and `user_agent` passed as arguments. They are usually called in the above order, though they don't have to be used that way. It is also fine to ignore this part of gargle and use it only for help with auth. They are separate parts of the package. ## Discovery Documents Google's [API Discovery Service](https://developers.google.com/discovery/) "provides a lightweight, JSON-based API that exposes machine-readable metadata about Google APIs". We recommend ingesting this metadata into an R list, stored as internal data in an API-wrapping client package. Then, HTTP requests inside high-level functions can be made concisely and safely, by referring to this metadata. The combined use of this data structure and gargle's request helpers can eliminate a lot of boilerplate data and logic that are shared across Google APIs and across endpoints within an API. The gargle package ships with some functions and scripts to facilitate the ingest of a Discovery Document. You can find these files in the gargle installation like so: ```{r} ddi_dir <- system.file("discovery-doc-ingest", package = "gargle") list.files(ddi_dir) ``` Main files of interest to the developer of a client package: * `ingest-functions.R` is a collection of functions for downloading and ingesting a Discovery Document. * `drive-example.R` uses those functions to ingest metadata on the Drive v3 API and store it as an internal data object for use in [googledrive](https://googledrive.tidyverse.org). The remaining files present an analysis of the Discovery Document for the Discovery API itself (very meta!) and write files that are useful for reference. Several are included at the end of this vignette. Why aren't the ingest functions exported by gargle? First, we regard this as functionality that is needed at development time, not install or run time. This is something you'll do every few months, probably associated with preparing a release of a wrapper package. Second, the packages that are useful for wrangling JSON and lists are not existing dependencies of gargle, so putting these function in gargle would require some unappealing compromises. ## Method (or endpoint) data Our Discovery Document ingest process leaves you with an R list. Let's assume it's available in your package's namespace as an internal object named `.endpoints`. Each item represents one method of the API (Google's vocabulary) or an endpoint (gargle's vocabulary). Each endpoint has an `id`. These `id`s are also used as names for the list. Examples of some `id`s from the Drive and Sheets APIs: ``` drive.about.get drive.files.create drive.teamdrives.list sheets.spreadsheets.create sheets.spreadsheets.values.clear sheets.spreadsheets.sheets.copyTo ``` Retrieve the metadata for one endpoint by name, e.g.: ```{r, eval = FALSE} .endpoints[["drive.files.create"]] ``` That info can be passed along to `request_develop(endpoint, params, base_url)`, which conducts sanity checks and combines this external knowledge with the data coming from the user and developer via `params`. ## Design suggestion: forming requests Here's the model used in googledrive. There is a low-level request helper, `googledrive::request_generate()`, that is used to form every request in the package. It is exported as part of a low-level API for expert use, but most users will never know it exists. ```{r eval = FALSE} # googledrive:: request_generate <- function(endpoint = character(), params = list(), key = NULL, token = drive_token()) { ept <- .endpoints[[endpoint]] if (is.null(ept)) { stop_glue("\nEndpoint not recognized:\n * {endpoint}") } ## modifications specific to googledrive package params$key <- key %||% params$key %||% drive_api_key() if (!is.null(ept$parameters$supportsTeamDrives)) { params$supportsTeamDrives <- TRUE } req <- gargle::request_develop(endpoint = ept, params = params) gargle::request_build( path = req$path, method = req$method, params = req$params, body = req$body, token = token ) } ``` The `endpoint` argument specifies an endpoint by its name, a.k.a. its `id`. `params` is where the processed user input goes. `key` and `token` refer to an API key and OAuth2 token, respectively. Both can be populated by default, but it is possible to pass them explicitly. If your package ships with a default API key, you should append it above as the final fallback value for `params$key`. Do not "borrow" an API key from gargle or another package; always send a key associated with your package or provided by your user. Per the Google User Data Policy , your application must accurately represent itself when authenticating to Google API services. After `googledrive::request_generate()` takes care of everything specific to the Drive API and the user's input and task, we call `gargle::request_develop()`. We finish preparing the request with `gargle::request_build()`, which enforces the rule that we always send exactly **one** of `key` and `token`. ## Design suggestion: making requests The output of `gargle::request_build()` specifies an HTTP request. `gargle::request_make()` can be used to actually execute it. ```{r, eval = FALSE} # gargle:: request_make <- function(x, ..., user_agent = gargle_user_agent()) { stopifnot(is.character(x$method)) method <- switch( x$method, GET = httr::GET, POST = httr::POST, PATCH = httr::PATCH, PUT = httr::PUT, DELETE = httr::DELETE, abort(glue("Not a recognized HTTP method: {bt(x$method)}")) ) method( url = x$url, body = x$body, x$token, user_agent, ... ) } ``` `request_make()` consults `x$method` to identify the `httr::VERB()` and then calls it with the remainder of `x`, `...` and the `user_agent`. In googledrive we have a thin wrapper around this that injects the googledrive user agent: ```{r, eval = FALSE} # googledrive:: request_make <- function(x, ...) { gargle::request_make(x, ..., user_agent = drive_ua()) } ``` ## Reference *derived from the Discovery Document for the Discovery Service* Properties of an endpoint ```{r asis = TRUE, echo = FALSE, comment = NA} cat(readLines(fs::path(ddi_dir, "method-properties-humane.txt")), sep = "\n") ``` API-wide endpoint parameters (taken from Discovery API but, empirically, are shared with other APIs): ```{r asis = TRUE, echo = FALSE, comment = NA} cat(readLines(fs::path(ddi_dir, "api-wide-parameters-humane.txt")), sep = "\n") ``` Properties of an endpoint parameters: ```{r asis = TRUE, echo = FALSE, comment = NA} cat(readLines(fs::path(ddi_dir, "parameter-properties-humane.txt")), sep = "\n") ``` gargle/inst/doc/non-interactive-auth.html0000644000176200001440000014114614456275116020211 0ustar liggesusers Non-interactive auth

Non-interactive auth

Here we describe how to do auth with a package that uses gargle, without requiring any user interaction. This comes up in a wide array of contexts, ranging from simple rendering of a local R Markdown document to deploying a data product on a remote server.

We assume the wrapper package uses the design described in vignette("gargle-auth-in-client-package"). Examples include:

Full details on gargle::token_fetch(), which powers this strategy, are given in vignette("how-gargle-gets-tokens").

Provide a token or pre-authorize token discovery

The main principle for auth that does not require user interaction:

Provide a token directly or take advance measures that indicate you want a token to be discovered.

We present several ways to achieve this, basically in order of preference.

Embrace credentials available in certain cloud settings

In certain cloud computing contexts, a service account token may be ambiently available (or you can arrange for that to be true). Think about it: if your workload is running on Google Compute Engine (GCE), it’s already “inside the Google house”. It seems like there should be a way to avoid another round of auth and that is indeed the case.

Another advantage of these cloud auth workflows is that there is never any need to download and carefully manage a file that contains sensitive information. This is why they are often described as “keyless”. If you can use one of these methods, you should seriously consider doing so.

Google Compute Engine

This section applies to code running on a GCE instance, either literally, or on another Google Cloud product built on top of GCE. You should consider Google’s own documentation to be definitive, but we’ll try to give a useful summary here and to explain how gargle works with GCE:

https://cloud.google.com/compute/docs/access/create-enable-service-accounts-for-instances

A Google Cloud Platform (GCP) project generally has a GCE default service account and, by default, a new GCE instance runs as that service account. (If you wish, you can use a different service account by taking explicit steps when you create an instance or by modifying it while it’s stopped.) The main point is that, for an application running on GCE, a service account identity is generally available.

GCE allows applications to get an OAuth access token from its metadata server and this is what gargle::credentials_gce() does (which is one of functions tried by gargle::token_fetch(), which is called by wrapper packages). This token request can be made for specific scopes and, in general, most wrapper packages will indeed be asking for specific scopes relevant to the API they access. Consider the signature of googledrive::drive_auth():

drive_auth <- function(email = gargle::gargle_oauth_email(),
                       path = NULL,
                       scopes = "https://www.googleapis.com/auth/drive",
                       cache = gargle::gargle_oauth_cache(),
                       use_oob = gargle::gargle_oob_default(),
                       token = NULL) { ... }

The googledrive package asks for a token with "drive" scope, by default. This brings up one big gotcha when using packages like googledrive or googlesheets4 on GCE.

By default, a GCE instance will be running as the default service account, with the "cloud-platform" scope and this will, generally speaking, allow the service account to work with various Cloud products. However, the "cloud-platform" scope does not permit operations with non-Cloud APIs, such as Drive and Sheets. If you want the service account identity for your GCE instance to be able to get an access token for use with Drive and Sheets, you will need to explicitly add, e.g., the "drive" scope when you create the instance (or stop the instance and add that scope). (Note that, in contrast, BigQuery is considered a Cloud product and therefore bigrquery can operate with the "cloud-platform" scope.)

Be aware that you might also need to explicitly grant the service account an appropriate level of access (e.g. read or write) to any Drive files you intend to work on.

Finally, if you want to opt-out of using the default service account and, instead, auth as a normal user, even though you are on GCE, that is also possible. One way to achieve that is to remove credentials_gce() from the set of auth functions tried by gargle::token_fetch() by executing this command before any explicit or implicit auth happens:

# removes `credentials_gce()` from gargle's registry
gargle::cred_funs_add(credentials_gce = NULL)

You can make a similar change in more scoped way with the helpers gargle::with_cred_funs() or gargle::local_cred_funs().

Workload Identity on Google Kubernetes Engine (GKE)

Here we discuss how gargle’s GCE auth can work for a related service, Google Kubernetes Engine (GKE), using Workload Identity. This is more complicated that direct usage of GCE and some extra configuration is needed to make a service account’s metadata available for the GKE instance to discover. GKE is the underlying technology behind Google’s managed Airflow service, Cloud Composer, so this also applies to R docker files being called in that environment.

Workload Identity is the recommended way to do authentication on GKE and other places, if possible, since it eliminates the use of a file that holds the service key, which is a potential security risk.

  1. Following the Workload Identity docs, you create a service account as normal and give it permissions and scopes needed to, say, upload to BigQuery. Imagine that my-service-key@my-project.iam.gserviceaccount.com has the https://www.googleapis.com/auth/bigquery scope.
  2. Instead of downloading a JSON key, you instead migrate that permission by adding a policy binding to another service account within Kubernetes.
  3. Create the service account within Kubernetes, ideally within a new namespace:
# create namespace
kubectl create namespace my-namespace
# Create Kubernetes service account
kubectl create serviceaccount --namespace my-namespace bq-service-account 
  1. Bind that Kubernetes service account to the service account outside of Kubernetes you created in step 1, and assign it an annotation:
# Create IAM policy binding betwwen k8s SA and GSA
gcloud iam service-accounts add-iam-policy-binding my-service-key@my-project.iam.gserviceaccount.com \
    --role roles/iam.workloadIdentityUser \
    --member "serviceAccount:my-project.svc.id.goog[my-namespace/bq-service-account]"
# Annotate k8s SA
kubectl annotate serviceaccount bq-service-account \
    --namespace my-namespace \
    iam.gke.io/gcp-service-account=my-service-key@my-project.iam.gserviceaccount.com

This key will now be available to add to pods within the cluster. For Airflow, you can pass them in using the Python code GKEStartPodOperator(...., namespace='my-namespace', service_account_name='bq-service-account'). Documentation around GKEStartPodOperator() within Cloud Composer can be found here.

  1. In order for the R function gargle::gce_credentials() do the right thing, you need to do two things:
  • Set "gargle.gce.use_ip" option to TRUE, in order to use the metadata server that’s relevant on GKE.
  • Specify the target service account, i.e. you can’t just passively accept the default, which is to use the "default" service account. gce_instance_service_accounts() can be helpful, e.g., if you want to know which service accounts your Docker container can see.

Here is example code that you might execute in your Docker container:

options(gargle.gce.use_ip = TRUE)
t <- gargle::credentials_gce("my-service-key@my-project.iam.gserviceaccount.com")
# ... do authenticated stuff with the token t ...

Let’s assume that PKG is an R package that implements gargle auth in the standard way, such as bigrquery or googledrive. At the time of writing the service_account argument is not exposed in the usual, high-level PKG_auth() function (https://github.com/r-lib/gargle/issues/249. So if you need to use a non-default service account, you need to call credentials_gce() directly and pass that token to PKG_auth(): Here’s an example of how that might look:

library(PKG)

options(gargle.gce.use_ip = TRUE)
t <- gargle::credentials_gce(
  "my-service-key@my-project.iam.gserviceaccount.com", # use YOUR service account
  scopes = "https://www.googleapis.com/auth/PKG"       # use REAL scopes
)
PKG_auth(token = t)
# ... do authenticated stuff...

AWS

Keyless auth is even possible from non-Google cloud platforms, using Workload identity federation.

This is implemented in the experimental function credentials_external_account(), which currently only supports AWS.

Provide a service account token directly

When two computers are talking to each other, possibly with no human involvement, the most appropriate type of token to use is a service account token. If you’re not working in cloud context with automatic access to a service account (see previous section), you can still use a service account, but it will require more explicit effort.

  1. Create a service account and then download its credentials as a JSON file. This is described in vignette("get-api-credentials"), specifically in the Service account token section.
  2. Call the wrapper package’s main auth function proactively and provide the path to this JSON file.

Example using googledrive:

library(googledrive)

drive_auth(path = "/path/to/your/service-account-token.json")

If this code is running on, e.g., a continuous integration service and you need to use an encrypted token, see vignette("managing-tokens-securely").

For certain APIs, service accounts are inherently awkward, because you often want to do things on behalf of a specific user. Gmail is a good example. If you are sending email programmatically, you probably want to send it as yourself (or from some other specific email account) instead of from zestybus-geosyogl@fuffapster-654321.iam.gserviceaccount.com. This is, in fact, possible but is described as “impersonation”, which should tip you off that Google does not exactly encourage this workflow. Some details:

  • This requires “delegating domain-wide authority” to the service account.
  • It is only possible in the context of a G Suite domain and only an administrator of the domain can set this up.
  • The domain-wide authority is granted only for specific scopes, so those can be as narrow as possible. This may make a domain administrator more receptive to the idea.
  • This is documented in a few different places, such as:
  • The subject argument of credentials_service_account() and credentials_app_default() is available to specify which user to impersonate, e.g. subject = "user@example.com". This argument first appeared in gargle 0.5.0, so it may not necessarily be exposed yet in user-facing auth functions like drive_auth(). If you need subject in a client package, that is a reasonable feature request. It is also possible to get a token with an explicit call to, e.g., credentials_service_account() and then pass that token to the auth function:
t <- gargle::credentials_service_account(
  path = "/path/to/your/service-account-token.json",
  scopes = ...,
  subject = "user@example.com"
)
googledrive::dive_auth(token = t)

If delegation of domain-wide authority is impossible or unappealing, you must use an OAuth user token, as described below.

Rig a service or external account for use with Application Default Credentials

Wrapper packages that use gargle::token_fetch() in the recommended way have access to the token search strategy known as Application Default Credentials.

You need to put the JSON corresponding to your service or external account in a very specific location or, alternatively, record the location of this JSON file in a specific environment variable.

Full details are in the credentials_app_default() section of vignette("how-gargle-gets-tokens").

If you have your token rigged properly, you do not need to do anything else, i.e. you do not need to call PACKAGE_auth() explicitly. Your token should just get discovered upon first need.

For troubleshooting purposes, you can set a gargle option to see verbose output about the execution of gargle::token_fetch():

options(gargle_verbosity = "debug")

withr-style convenience helpers also exist: with_gargle_verbosity() and local_gargle_verbosity().

Provide an OAuth token directly

If you somehow have the OAuth token you want to use as an R object, you can provide it directly to the token argument of the main auth function. Example using googledrive:

library(googledrive)

my_oauth_token <- # some process that results in the token you want to use
drive_auth(token = my_oauth_token)

gargle caches each OAuth user token it obtains to an .rds file, by default. If you know the filepath to the token you want to use, you could use readRDS() to read it and provide as the token argument to the wrapper’s auth function. Example using googledrive:

# googledrive
drive_auth(token = readRDS("/path/to/your/oauth-token.rds"))

How would you know this filepath? That requires some attention to the location of gargle’s OAuth token cache folder, which is described in the next section.

Full details are in the credentials_byo_oauth2() section of vignette("how-gargle-gets-tokens").

Arrange for an OAuth user token to be re-discovered

This is the least recommended strategy, but it appeals to many users, because it doesn’t require creating a service account. Just remember that the perceived ease of using the token you already have (an OAuth user token) is quickly cancelled out by the greater difficulty of managing such tokens for non-interactive use. You might be forced to use this strategy with certain APIs, such as Gmail, that are difficult to use with a service account.

Two main principles:

  1. Take charge of – or at least notice – the folder where OAuth tokens are being cached.
  2. Make sure exactly one cached token will be identified and pre-authorize its use.

There are many ways to do this. We’ll work several examples using that convey the range of what’s possible.

I just want my .Rmd to render

Step 1: Get that first token. You must run your code at least once, interactively, do the auth dance, and allow gargle to store the token in its cache.

library(googledrive)

# do anything that triggers auth
drive_find(n_max)

Step 2: Revise your code to pre-authorize the use of that token next time. Now your .Rmd can be rendered or your .R script can run, without further interaction.

You have two choices to make:

  • Set the gargle_oauth_email option or call PACKAGE_auth(email = ...).
    • The option-based approach can be implemented in each .Rmd or .R or in a user-level or project level .Rprofile startup file.
  • Authorize the use of the “matching token”:
    • email = TRUE works if we’re only going to find, at most, 1 token, i.e. you always auth with the same identity
    • email = "jane@example.com" pre-authorizes use of a token associated with a specific identity
    • email = "*@example.com" pre-authorizes use of a token associated with an identity from a specific domain; good for code that might be executed on the machines of both alice@example.com and bob@example.com

This sets an option that allows gargle to use cached tokens whenever there’s a unique match:

options(gargle_oauth_email = TRUE)

This sets an option to use tokens associated with a specific email address:

options(gargle_oauth_email = "jenny@example.com")

This sets an option to use tokens associated with an email address with a specific domain:

options(gargle_oauth_email = "*@example.com")

This gets a token right now and allows the use of a matching token, using googledrive as an example:

drive_auth(email = TRUE)

This gets a token right now, for the user with a specific email address:

drive_auth(email = "jenny@example.com")

This gets a token right now, first checking the cache for a token associated with a specific domain:

drive_auth(email = "*@example.com")

Project-level OAuth cache

This is like the previous example, but with an added twist: we use a project-level OAuth cache. This is good for deployed data products.

Step 1: Obtain the token intended for non-interactive use and make sure it’s cached in a (hidden) directory of the current project. Using googledrive as an example:

library(googledrive)

# designate project-specific cache
options(gargle_oauth_cache = ".secrets")

# check the value of the option, if you like
gargle::gargle_oauth_cache()

# trigger auth on purpose --> store a token in the specified cache
drive_auth()

# see your token file in the cache, if you like
list.files(".secrets/")

Do this setup once per project.

Another way to accomplish the same setup is to specify the desired cache location directly in the call to the auth function:

library(googledrive)

# trigger auth on purpose --> store a token in the specified cache
drive_auth(cache = ".secrets")

Step 2: In all downstream use, announce the location of the cache and pre-authorize the use of a suitable token discovered there. Continuing the googledrive example:

library(googledrive)

options(
  gargle_oauth_cache = ".secrets",
  gargle_oauth_email = TRUE
)

# now use googledrive with no need for explicit auth
drive_find(n_max = 5)

Setting the option gargle_oauth_email = TRUE says that googledrive is allowed to use a token that it finds in the cache, without interacting with a user, as long as it discovers EXACTLY one matching token. This option-setting code needs to appear in each script, .Rmd, or app that needs to use this token non-interactively. Depending on the context, it might be suitable to accomplish this in a startup file, e.g. project-level .Rprofile.

Here’s a variation where we say which token to use by explicitly specifying the associated email. This is handy if there’s a reason to have more than one token in the cache.

library(googledrive)

options(
  gargle_oauth_cache = ".secrets",
  gargle_oauth_email = "jenny@example.com"
)

# now use googledrive with no need for explicit auth
drive_find(n_max = 5)

Here’s another variation where we specify the necessary info directly in an auth call, instead of in options:

library(googledrive)

drive_auth(cache = ".secrets", email = TRUE)

# now use googledrive with no need for explicit auth
drive_find(n_max = 5)

Here’s one last variation that’s applicable when the local cache could contain multiple tokens:

library(googledrive)

drive_auth(cache = ".secrets", email = "jenny@example.com")

# now use googledrive with no need for explicit auth
drive_find(n_max = 5)

Be very intentional about paths and working directory. Personally I would use here::here(".secrets)" everywhere above, to make things more robust.

For troubleshooting purposes, you can set a gargle option to see verbose output about the execution of gargle::token_fetch():

options(gargle_verbosity = "debug")

withr-style convenience helpers also exist: with_gargle_verbosity() and local_gargle_verbosity().

For a cached token to be considered a “match”, it must match the current request with respect to user’s email, scopes, and OAuth client (client ID or key and secret). By design, these settings have very low visibility, because we usually want to use the defaults. If your token is not being discovered, consider if any of these fields might explain the mismatch.

gargle/inst/doc/get-api-credentials.R0000644000176200001440000000204114456275115017210 0ustar liggesusers## ---- include = FALSE--------------------------------------------------------- knitr::opts_chunk$set( collapse = TRUE, comment = "#>" ) ## ----eval = FALSE------------------------------------------------------------- # library(googlesheets4) # # gs4_auth_configure(api_key = "YOUR_API_KEY_GOES_HERE") # gs4_deauth() # # # now you can read public resources, such as official example Sheets, # # without any need for auth # gs4_example("gapminder") %>% # read_sheet() ## ---- eval = FALSE------------------------------------------------------------ # library(googledrive) # # google_client <- gargle::gargle_oauth_client_from_json( # path = "/path/to/the/JSON/that/was/downloaded/from/gcp/console.json", # name = "acme-corp-google-client" # ) # drive_auth_configure(app = google_client) # # # now any new OAuth tokens are obtained with the configured client ## ----eval = FALSE------------------------------------------------------------- # # googledrive # drive_auth(path = "/path/to/your/service-account-token.json") gargle/inst/doc/troubleshooting.html0000644000176200001440000020440514456275120017365 0ustar liggesusers Troubleshooting gargle auth

Troubleshooting gargle auth

library(gargle)

"gargle_verbosity" option

There is a package-wide option that controls gargle’s verbosity: "gargle_verbosity". The function gargle_verbosity() reveals the current value:

gargle_verbosity()
#> [1] "info"

It defaults to "info", which is fairly quiet. This is because gargle is designed to try a bunch of auth methods (many of which will fail) and persist doggedly until one succeeds. If none succeeds, gargle tries to guide the user through auth or, in a non-interactive session, it throws an error.

If you need to see all those gory details, set the "gargle_verbosity" option to "debug" and you’ll get much more output as gargle works through various auth approaches. It is normal to see lots of errors, as gargle tries various auth methods in succession, most of which will often fail.

# save current value
op <- options(gargle_verbosity = "debug")

gargle_verbosity()
#> [1] "debug"

# restore original value
options(op)

Note there are also withr-style helpers: with_gargle_verbosity() and local_gargle_verbosity().

gargle_verbosity()
#> [1] "info"

with_gargle_verbosity(
  "debug",
  gargle_verbosity()
)
#> [1] "debug"

gargle_verbosity()
#> [1] "info"

f <- function() {
  local_gargle_verbosity("debug")
  gargle_verbosity()
}

f()
#> [1] "debug"

gargle_verbosity()
#> [1] "info"

gargle_oauth_sitrep()

gargle_oauth_sitrep() provides an OAuth2 “situation report”.

gargle_oauth_sitrep() is only relevant to OAuth2 user tokens. If you are using (or struggling to use) a service account token, workload identity federation, Application Default Credentials, or credentials from the GCE metadata service, gargle_oauth_sitrep() isn’t going to help you figure out what’s going on.

Here is indicative output of gargle_oauth_sitrep(), for someone who has accepted the default OAuth cache location and has played with several APIs via gargle-using packages.

gargle_oauth_sitrep()
#' > 14 tokens found in this gargle OAuth cache:
#' '~/Library/Caches/gargle'
#' 
#' email                         app         scope                          hash...   
#' ----------------------------- ----------- ------------------------------ ----------
#' abcdefghijklm@gmail.com       thingy      ...bigquery, ...cloud-platform 128f9cc...
#' buzzy@example.org             gargle-demo                                15acf95...
#' stella@example.org            gargle-demo ...drive                       4281945...
#' abcdefghijklm@gmail.com       gargle-demo ...drive                       48e7e76...
#' abcdefghijklm@gmail.com       tidyverse                                  69a7353...
#' nopqr@ABCDEFG.com             tidyverse   ...spreadsheets.readonly       86a70b9...
#' abcdefghijklm@gmail.com       tidyverse   ...drive                       d9443db...
#' nopqr@HIJKLMN.com             tidyverse   ...drive                       d9443db...
#' nopqr@ABCDEFG.com             tidyverse   ...drive                       d9443db...
#' stuvwzyzabcd@gmail.com        tidyverse   ...drive                       d9443db...
#' efghijklmnopqrtsuvw@gmail.com tidyverse   ...drive                       d9443db...
#' abcdefghijklm@gmail.com       tidyverse   ...drive.readonly              ecd11fa...
#' abcdefghijklm@gmail.com       tidyverse   ...bigquery, ...cloud-platform ece63f4...
#' nopqr@ABCDEFG.com             tidyverse   ...spreadsheets                f178dd8...

It is relatively harmless to delete the folder serving as the OAuth cache. Or, if you have reason to believe one specific cached token is causing you pain, you could delete a specific token (an .rds file) from the cache. OAuth user tokens are meant to be perishable and replaceable.

If you choose to delete your cache (or a specific token), here is the fallout you can expect:

  • You will need to re-auth (usually, meaning the browser dance) in projects that have been using the deleted tokens.
  • If you have .R or .Rmd files that you execute or render non-interactively, presumably with code such as PKG_auth(email = "janedoe@example.com"), those won’t run non-interactively until you’ve obtained and cached a token for the package and that identity (email) interactively once.

Why do good tokens go bad?

Sometimes it feels like auth was working and then suddenly it stops working. If you’ve cached a token and used it successfully, why would it stop working?

Too many tokens

An existing token can go bad if you’ve created too many Google tokens, causing your oldest tokens to “fall off the edge”.

A specific Google user (email) can only have a certain number of OAuth tokens at a time (something like 50 per OAuth client). So, whenever you get a new token (as opposed to refreshing an existing token), there is the potential for it to invalidate an older token. This is unlikely to be an issue for a casual user, but it can absolutely become noticeable for someone who is developing against a Google API or someone working from many different machines / caches.

Another reason that an existing token stops working is if it was obtained with an OAuth client that is in “testing” mode. Refresh tokens obtained that way only last for one week, whereas it’s more typical for refresh tokens to last almost indefinitely (or, at least, for several months).

Credential rolling

Many users of packages like googlesheets4 or googledrive tacitly rely on the default OAuth client used by those packages. Periodically the maintainer of such a package will need to roll the client, i.e. create a new OAuth client and disable the old one. This will make it impossible to refresh existing tokens, made with the old, disabled client Those tokens will stop working.

In gargle v1.0.0, in March 2021, we rolled the client used in googlesheets4, googledrive, and bigrquery. We reserve the right to disable the old client at any time. Anyone relying on the default client will have to upgrade.

The solution is to update the package in question, e.g. googlesheets4:

install.packages("googlesheets4")

Restart R! Resume your work. Chances are you’ll be prompted to re-auth with the new client and you’ll be back in business.

What does this problem look like in the wild?

With gargle versions up to v1.0.0, you will probably see this:

Auto-refreshing stale OAuth token.
Error in get("refresh_oauth2.0", asNamespace("httr"))(self$endpoint, self$app,  :
  Unauthorized (HTTP 401).

If you’re trying to create a token, instead of refreshing one, you might see this in the browser, while R is waiting to receive input:

Google Authorization Error

Error 401: deleted_client
The OAuth client was deleted.

It might look something like this:

Screenshot with the following text: "Google", "Authorization Error", "Error 401: deleted_client", "The OAuth client was deleted."

As of gargle version v1.1.0, we’re trying harder to recognize this specific problem and to provide a more detailed and actionable error message:

Auto-refreshing stale OAuth token.
Error: Client error: (401) UNAUTHENTICATED
  * Request not authenticated due to missing, invalid, or expired OAuth token.
  * Request had invalid authentication credentials. Expected OAuth 2 access token, login cookie or other valid authentication credential. See https://developers.google.com/identity/sign-in/web/devconsole-project.
Run `rlang::last_error()` to see where the error occurred.
In addition: Warning message:
Unable to refresh token, because the associated OAuth app has been deleted
* You appear to be relying on the default app used by the googlesheets4 package
* Consider re-installing googlesheets4 and gargle, in case the default app has been updated

How to avoid auth pain

If you have rigged some remote mission critical thing (e.g. a Shiny app or cron job) to use a cached user OAuth token, one day, one of the problems described above will happen and your mission critical token will stop working. Your thing (e.g. the Shiny app or cron job) will mysteriously fail because the OAuth token can’t be refreshed and a new token can’t be obtained in a non-interactive setting. This is why cached user tokens are a poor fit for such applications.

If you choose to use a cached user token anyway, be prepared to deal with this headache periodically. (You may not have much of a choice if you are using, for example, the gmailr package to work with the Gmail API, which has limited support for service accounts.) Consider using your own OAuth client to eliminate your exposure to a third-party deciding to roll their client. Be prepared to generate a fresh token interactively and upload it to the token cache consulted by your remote mission critical thing. Better yet, upgrade to a more robust strategy for non-interactive auth, such as a service account token.

How to inspect the last response

By default, gargle::response_process() stores the most recently processed response in an internal environment. You can access this response with the nonexported helper gargle:::gargle_last_response(). Prior to storage, a few parts of the response are redacted or deleted, such as the access token and the handle. These are either sensitive (the token) or useless (the handle) and they have more downside than upside for downstream debugging use.

Here’s an example of accessing the most recent response and writing it to file, which could be shared with someone else for debugging. The response is in this example has HTTP status 200, i.e. it is not an error. But this process works the same even if in the case of an error, e.g. an HTTP status >= 400.

library(gargle)

req <- request_build(
  method = "GET",
  path = "webfonts/v1/webfonts",
  params = list(
    sort = "popularity"
  ),
  key = gargle_api_key(),
  base_url = "https://www.googleapis.com"
)
resp <- request_make(req)
out <- response_process(resp)

lr <- gargle:::gargle_last_response()
tmp <- tempfile("gargle-last-response-")
saveRDS(lr, tmp)
# you could share this .rds file with a colleague or the gargle maintainer

# how it would look to complete the round trip, i.e. load this on the other end
rt_lr <- readRDS(tmp)

all.equal(lr, rt_lr)
#> [1] TRUE

# clean up
unlink("tmp")
gargle/inst/doc/non-interactive-auth.R0000644000176200001440000001347114456275116017445 0ustar liggesusers## ---- include = FALSE--------------------------------------------------------- knitr::opts_chunk$set( collapse = TRUE, comment = "#>", eval = FALSE ) ## ----------------------------------------------------------------------------- # # Approach #1: use an option. # # Either specify the user: # options(gargle_oauth_email = "jenny@example.com") # # Or, if you only use one Google identity, you can be more vague: # options(gargle_oauth_email = TRUE) # # Or, you can specify the identity to use at the domain level: # options(gargle_oauth_email = "*@example.com") # # # Approach #2: call PACKAGE_auth() proactively. # library(googledrive) # # Either specify the user: # drive_auth(email = "jenny@example.com") # # Or, if you only use one Google identity, you can be more vague: # drive_auth(email = TRUE) # # Or, you can specify the identity to use at the domain level: # drive_auth(email = "*@example.com") ## ----------------------------------------------------------------------------- # drive_auth <- function(email = gargle::gargle_oauth_email(), # path = NULL, # scopes = "https://www.googleapis.com/auth/drive", # cache = gargle::gargle_oauth_cache(), # use_oob = gargle::gargle_oob_default(), # token = NULL) { ... } ## ----------------------------------------------------------------------------- # # removes `credentials_gce()` from gargle's registry # gargle::cred_funs_add(credentials_gce = NULL) ## ----------------------------------------------------------------------------- # options(gargle.gce.use_ip = TRUE) # t <- gargle::credentials_gce("my-service-key@my-project.iam.gserviceaccount.com") # # ... do authenticated stuff with the token t ... ## ----------------------------------------------------------------------------- # library(PKG) # # options(gargle.gce.use_ip = TRUE) # t <- gargle::credentials_gce( # "my-service-key@my-project.iam.gserviceaccount.com", # use YOUR service account # scopes = "https://www.googleapis.com/auth/PKG" # use REAL scopes # ) # PKG_auth(token = t) # # ... do authenticated stuff... ## ----------------------------------------------------------------------------- # library(googledrive) # # drive_auth(path = "/path/to/your/service-account-token.json") ## ----------------------------------------------------------------------------- # t <- gargle::credentials_service_account( # path = "/path/to/your/service-account-token.json", # scopes = ..., # subject = "user@example.com" # ) # googledrive::dive_auth(token = t) ## ----------------------------------------------------------------------------- # options(gargle_verbosity = "debug") ## ----------------------------------------------------------------------------- # library(googledrive) # # my_oauth_token <- # some process that results in the token you want to use # drive_auth(token = my_oauth_token) ## ----------------------------------------------------------------------------- # # googledrive # drive_auth(token = readRDS("/path/to/your/oauth-token.rds")) ## ----------------------------------------------------------------------------- # library(googledrive) # # # do anything that triggers auth # drive_find(n_max) ## ----------------------------------------------------------------------------- # options(gargle_oauth_email = TRUE) ## ----------------------------------------------------------------------------- # options(gargle_oauth_email = "jenny@example.com") ## ----------------------------------------------------------------------------- # options(gargle_oauth_email = "*@example.com") ## ----------------------------------------------------------------------------- # drive_auth(email = TRUE) ## ----------------------------------------------------------------------------- # drive_auth(email = "jenny@example.com") ## ----------------------------------------------------------------------------- # drive_auth(email = "*@example.com") ## ----------------------------------------------------------------------------- # library(googledrive) # # # designate project-specific cache # options(gargle_oauth_cache = ".secrets") # # # check the value of the option, if you like # gargle::gargle_oauth_cache() # # # trigger auth on purpose --> store a token in the specified cache # drive_auth() # # # see your token file in the cache, if you like # list.files(".secrets/") ## ----------------------------------------------------------------------------- # library(googledrive) # # # trigger auth on purpose --> store a token in the specified cache # drive_auth(cache = ".secrets") ## ----------------------------------------------------------------------------- # library(googledrive) # # options( # gargle_oauth_cache = ".secrets", # gargle_oauth_email = TRUE # ) # # # now use googledrive with no need for explicit auth # drive_find(n_max = 5) ## ----------------------------------------------------------------------------- # library(googledrive) # # options( # gargle_oauth_cache = ".secrets", # gargle_oauth_email = "jenny@example.com" # ) # # # now use googledrive with no need for explicit auth # drive_find(n_max = 5) ## ----------------------------------------------------------------------------- # library(googledrive) # # drive_auth(cache = ".secrets", email = TRUE) # # # now use googledrive with no need for explicit auth # drive_find(n_max = 5) ## ----------------------------------------------------------------------------- # library(googledrive) # # drive_auth(cache = ".secrets", email = "jenny@example.com") # # # now use googledrive with no need for explicit auth # drive_find(n_max = 5) ## ----------------------------------------------------------------------------- # options(gargle_verbosity = "debug") gargle/inst/doc/auth-from-web.Rmd0000644000176200001440000003367614433520365016402 0ustar liggesusers--- title: "Auth when using R from the browser" output: rmarkdown::html_vignette vignette: > %\VignetteIndexEntry{Auth when using R from the browser} %\VignetteEngine{knitr::rmarkdown} %\VignetteEncoding{UTF-8} --- ```{r, include = FALSE} knitr::opts_chunk$set( collapse = TRUE, comment = "#>" ) ``` If you are working with R in a web-based context, such as [RStudio Server](https://posit.co/download/rstudio-server/), [Posit Cloud](https://posit.cloud/), [Posit Workbench](https://posit.co/products/enterprise/workbench/), or [Google Colaboratory](https://colab.research.google.com/), your experience of browser-based auth flows will be different from those using R on their local machine. You need to use **out-of-band authentication**, sometimes denoted "oob" or "OOB". After the usual auth dance, instead of seeing "authentication successful, return to R!", you are presented with an authorization code to copy and paste back into your R session. For folks who are running R on their local machine, this final exchange can be done automagically, using a temporary local webserver, but that is not possible for those accessing a remote R session through the browser. On February 16, 2022, Google announced the (partial) deprecation of the OAuth out-of-band (OOB) flow, to be enacted no later than February 1, 2023. The deprecation applies to Google Cloud Platform (GCP) projects that are in production mode. OOB still works for projects that are in testing mode. The built-in tidyverse client (used by googledrive, googlesheets4, and bigrquery) is associated with a GCP project that is in production mode. Therefore, conventional OOB auth stopped working for the built-in client in February 2023. In anticipation of this, gargle gained a new auth flow in version 1.3.0 that we call "pseudo-OOB", which should allow casual users to continue to enjoy a low-friction auth experience, even from RStudio Server, Posit Cloud, Posit Workbench, and Google Colaboratory. If you attempt to do conventional OOB auth with a client that no longer supports it, you'll see something like this: ```{r, echo = FALSE, out.width = "400px"} #| fig-cap: > #| Access blocked: Tidyverse API Packages's request is invalid. #| Error 400: invalid_request #| fig-alt: > #| Screenshot with the following text: "Access blocked: Tidyverse API #| Packages's request is invalid", "You can't sign in because Tidyverse API #| Packages sent an invalid request. You can try again later, or contact the #| developer about this issue. Learn more about this error", "If you are a #| developer of Tidyverse API Packages, see error details.", "Error 400: #| invalid_request". knitr::include_graphics("invalid_request.png") ``` If you work on any of the affected platforms and are experiencing new auth problems, your first move should be to update all packages involved (gargle and one or more of googledrive, googlesheets4, bigrquery). **Restart R.** Re-execute your code in an interactive context that will allow you to re-auth. This vignette documents various matters around OOB auth, both conventional and pseudo-OOB, for users who want to understand this more deeply. Some of the packages that use gargle for auth and for which this article applies: * [bigrquery](https://bigrquery.r-dbi.org) * [googledrive](https://googledrive.tidyverse.org) * [googlesheets4](https://googlesheets4.tidyverse.org) * [gmailr](https://gmailr.r-lib.org) *note: gmailr does not use the built-in tidyverse OAuth client* ## Consider using a service account token (or no token!) If you have concerns about using OOB auth, consider whether your task truly requires auth as a specific, normal user. Can the task be completed with _no auth_, i.e. you are accessing something that is world readable or readable for "anyone with a link"? In that case, the wrapper package probably provides a function to go into a de-authorized state, such as `googledrive::drive_deauth()` or `googlesheets4::gs4_deauth()`. If the task requires auth, consider whether it really must be as a specific user. You may be able to accomplish the task with a service account, which you create for this specific purpose. A service account token is much easier to work with on a server and in non-interactive contexts than a user token. A service account can also be given much more selective permissions than a user account and can be more easily deleted, once it is no longer needed. Remember that the service account will need to be explicitly given permission to access any necessary resources (e.g. permission to read or write a specific Drive file or Sheet). A service account doesn't somehow inherit permissions indirectly from the user who owns the GCP project in which it lives. To learn more about using a service account, see `vignette("non-interactive-auth")`. ## When and how to use OOB In the absence of any user instructions, the function `gargle::gargle_oob_default()` is used to decide whether to use OOB auth. By default, OOB auth is used on RStudio Server, Posit Cloud, Posit Workbench, and Google Colaboratory, or if the option `"gargle_oob_default"` is set to `TRUE`. (Note that we use the term "OOB auth" here to include both the existing, conventional form of OOB and gargle's new pseudo-OOB.) Wrapper packages generally also allow the user to opt-in to OOB auth when making a direct call to an auth function. For example, the functions `googledrive::drive_auth()`, `googlesheets4::gs4_auth()`, `bigrquery::bq_auth()`, and `gmailr::gm_auth()` all have a `use_oob` argument. Notably, all of these `use_oob` arguments default to `gargle::gargle_oob_default()`. gargle usually automatically detects when it should use OOB auth, but here is what it could look like if we are not using OOB, but should be. During auth, you are redirected to localhost on port 1410 and receive an error along these lines: ``` Chrome: This site can't be reached; localhost refused to connect. Firefox: Unable to connect; can't establish a connection. ``` If this happens you might need to explicitly request OOB. Below we review two different methods. ## Request OOB auth in the `PKG_auth()` call Packages like googledrive and bigrquery aim to make auth "just work" for most users, i.e. it's automatically triggered upon first need. However, it is always possible to initiate auth yourself, which gives you the opportunity to specify non-default values of certain parameters. Here's how you could request OOB auth, using googledrive as an example: ```{r eval = FALSE} library(googledrive) drive_auth(use_oob = TRUE) # now carry on with your work drive_find(n_max = 5) ``` ## Set the `"gargle_oob_default"` option If you know that you *always* want to use OOB, as a user or within a project, the best way to express this is to set the `"gargle_oob_default"` option. ```{r eval = FALSE} options(gargle_oob_default = TRUE) ``` This code could appear at the top of a script, in a setup chunk for `.Rmd`, or in a Shiny app. But it probably makes even more sense in a `.Rprofile` startup file, at the user or project level. Once the `"gargle_oob_default"` option has been set, it is honored by downstream calls to `PKG_auth()`, explicit or implicit, because the default value of `use_oob` is `gargle::gargle_oob_default()`, which consults the option. ## Conventional vs. pseudo-OOB auth gargle now supports two OOB flows, which we call "conventional OOB" (the existing, legacy OOB flow) and "pseudo-OOB" (the new flow introduced in response to the partial deprecation of conventional OOB). If we are using OOB auth, the decision between conventional or pseudo-OOB is made based on the currently configured OAuth client. * If the OAuth client is of type `"installed"` (shows as "Desktop" in Google Cloud Console) or is of unknown type, gargle uses conventional OOB. Note that this will not necessarily succeed, due to the deprecation process described above. * If the OAuth client is of type `"web"` (shows as "Web application" in Google Cloud Console), gargle uses the new pseudo-OOB flow. ```{=html}
use_oob
FALSE TRUE
client type installed use httpuv to spin up
a temporary web server
conventional OOB
web --not possible-- pseudo-OOB
``` Packages that use a built-in tidyverse OAuth client (googledrive, googlesheets4, and bigrquery) should automatically select a "web" client on RStudio Server, Posit Cloud, Posit Workbench, and Google Colaboratory and an "installed" client otherwise. If you need to explicitly request a "web" client in some other setting, you can use the global option `"gargle_oauth_client_type"`: ```{r eval = FALSE} options(gargle_oauth_client_type = "web") ``` Users who configure their own OAuth client will need to be intentional when choosing the client type, depending on where the code is running. On the R side, it is recommended to setup an OAuth client using `gargle_oauth_client_from_json()`, which allows the client type (`"installed"` vs. `"web"`) to be detected programmatically from the downloaded JSON. The less-preferred approach is to use `gargle_oauth_client()` and provide the information yourself. ## How pseudo-OOB works Pseudo-OOB works just like non-OOB and conventional OOB in terms of the user's interactions with Google authorization server. This is where the user authenticates themselves with Google and consents to the type of access being requested by the R code. These flows differ in how they handle a successful response from the authorization server. Specifically, the flows use different redirect URIs. * A (temporary) local webserver is used to listen for this response at, e.g., `http://localhost:1410/` if R is running locally and the httpuv package is available (i.e. a non-OOB flow). * In conventional OOB, a special redirect value is used, typically `urn:ietf:wg:oauth:2.0:oob`, and the authorization code is provided to the user via a browser window for manual copy/paste. This page is served by Google. Google has deprecated conventional OOB for projects in production mode (but it is still allowed for projects in testing mode). * In gargle's pseudo-OOB, a redirect URI from the configured OAuth client is used to receive the response. This page is responsible for exposing a code that the user can copy/paste, similar to conventional OOB (except the page is *not* served by Google). Unlike conventional OOB, this is not the authorization code itself, but is something from which the code can be extracted, along with a state token to mitigate cross-site request forgery. This is actually implemented using an [OAuth flow for web server applications](https://developers.google.com/identity/protocols/oauth2/web-server). Note that we (gargle) call this pseudo-OOB, but it is not technically OOB from Google's point-of-view. The built-in OAuth client used for pseudo-OOB by tidyverse packages redirects to . This is a static landing page that does not collect any data and exists solely to give the interactive R user a way to convey the authorization token back to the waiting R process and thereby complete the auth process. ### More details about the deprecation of conventional OOB Key links: * Blog post: [Making Google OAuth interactions safer by using more secure OAuth flows](https://developers.googleblog.com/2022/02/making-oauth-flows-safer.html) * [Out-Of-Band (OOB) flow Migration Guide](https://developers.google.com/identity/protocols/oauth2/resources/oob-migration#web-application) * [Using OAuth 2.0 to Access Google APIs](https://developers.google.com/identity/protocols/oauth2) ## But I didn't need OOB yesterday! Sometimes the usual OAuth web flow suddenly stops working for people working directly with R (so NOT via the browser) and they use OOB auth to get unstuck again. What's going on in this case? The initial error looks something like this: ``` createTcpServer: address already in use Error in httpuv::startServer(use$host, use$port, list(call = listen)) : Failed to create server ``` It's characteristic of some other process sitting on port 1410, which is what gargle is trying to use for auth. It's true that using OOB auth is a workaround. But OOB auth is, frankly, more clunky, so why use it if you don't have to? Here are ways to fix this. * Restart your system. This will almost certainly kill the offending process, which is usually a zombie process. * Hunt down the offending process, verify it looks expendable, and kill it. On *nix-y systems, use `lsof` to get the process ID: ``` sudo lsof -i :1410 ``` The output will look something like this: ``` COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME R 16664 jenny 20u IPv4 0x63761a50856c65f 0t0 TCP localhost:hiq (LISTEN) ``` In this case, as is typical, this is a zombie R process and I feel confident killing it. The process ID is listed there as PID. Note that and kill the process, like so, filling in the PID you found: ``` kill -9 ``` So, to be clear, in this example, the command would be: ``` kill -9 16664 ``` The normal, non-OOB auth web flow should work again now. ## Further reading If you're working on a data product that will be deployed (for example on [shinyapps.io](https://www.shinyapps.io) or [Posit Connect](https://posit.co/products/enterprise/connect/)), you will also need to consider how the deployed content will authenticate non-interactively, which is covered in `vignette("non-interactive-auth")`. gargle/inst/doc/oauth-client-not-app.Rmd0000644000176200001440000002367714433520365017675 0ustar liggesusers--- title: "Transition from OAuth app to OAuth client" output: rmarkdown::html_vignette vignette: > %\VignetteIndexEntry{Transition from OAuth app to OAuth client} %\VignetteEngine{knitr::rmarkdown} %\VignetteEncoding{UTF-8} --- ```{r, include = FALSE} knitr::opts_chunk$set( collapse = TRUE, comment = "#>" ) ``` ```{r setup} library(gargle) ``` Over the course of several releases (v1.3.0, v1.4.0, and v1.5.0), gargle has shifted to using an OAuth **client** in the user flow facilitated by `gargle::credentials_user_oauth2()`, instead the previous OAuth "app". This is a more than just a vocabulary change (but it is also a vocabulary change). This vignette explains what actually changed and how wrapper packages should adjust. ## Why change was needed In 2022, Google partially deprecated the out-of-band (OOB) OAuth flow. The OOB flow is used by R users who are working with Google APIs and who use R in the browser, such as via RStudio Server, Posit Workbench, Posit Cloud, or Google Colaboratory. Conventional OOB auth **still works** under certain conditions, for example, if the OAuth client is associated with a GCP project that is in testing mode or that is internal to a Google Workspace. But conventional OOB is no longer supported for projects that serve external users that are in production mode. In particular, this means that conventional OOB is no longer supported for the GCP project that has historically made auth "just work" for casual users of packages such as googledrive, googlesheets4, and bigrquery. The default OAuth client used by these package no longer works with conventional OOB. In response, as of v1.3.0, gargle implements a new variant of OOB, called **pseudo-OOB**, to continue to provide a user-friendly auth flow for googledrive/googlesheets4/bigrquery on RStudio Server/Posit Workbench/Posit Cloud/Google Colaboratory. The pseudo-OOB flow is also available for other developers to use. This flow is triggered when `use_oob = TRUE` (an existing convention in gargle and gargle-using packages) **and** the configured OAuth client is of the *web* type (when creating an OAuth client, this is called the "Web application" type). ```{=html}
use_oob
FALSE TRUE
client type installed use httpuv to spin up
a temporary web server
conventional OOB
web --not possible-- pseudo-OOB
``` In the past, gargle basically assumed that every OAuth client was of the *installed* type (when creating an OAuth client, this is called the "Desktop app" type). Therefore, the introduction of pseudo-OOB meant that gargle had to learn about different OAuth client types (web vs. installed). And that didn't play well with `httr::oauth_app()`, which gargle had been using to store the client ID and secret. That's why there is a new S3 class, `"gargle_oauth_client"`, with a constructor of the same name. Since more information is now necessary to instantiate a client (e.g. its type and, potentially, redirect URIs), the recommended way to create a client is to provide JSON downloaded from the GCP console to `gargle_oauth_client_from_json()`. Since we had to introduce a new S3 class and supporting functions, we also took this chance to make the vocabulary pivot from "OAuth app" to "OAuth client". Google's documentation has always talked about the "OAuth client", so this is more natural. This vocabulary is also more future-facing, anticipating the day when gargle might shift from httr to httr2, which uses `httr2:oauth_client()`. As a bridging measure, the `"gargle_oauth_client"` class currently inherits from httr's `"oauth_app"`, but this probably won't be true in the long-term. ### How to instantiate an OAuth client in R If you do auth via gargle, here are some recommended changes: 1. Stop using `httr::oauth_app()` or `gargle::oauth_app_from_json()` to instantiate an OAuth client. 2. Start using `gargle_oauth_client_from_json()` (strongly recommended) or `gargle_oauth_client()` instead. This advice applies to anything you do inside your package and also to what you encourage and document for your users. gargle ships with JSON files for two non-functional OAuth clients, just to make this all more concrete: ```{r} (path_to_installed_client <- system.file( "extdata", "client_secret_installed.googleusercontent.com.json", package = "gargle" )) jsonlite::prettify(scan(path_to_installed_client, what = character())) (client <- gargle_oauth_client_from_json(path_to_installed_client)) class(client) (path_to_web_client <- system.file( "extdata", "client_secret_web.googleusercontent.com.json", package = "gargle" )) jsonlite::prettify(scan(path_to_web_client, what = character())) (client <- gargle_oauth_client_from_json(path_to_web_client)) class(client) ``` Notice the difference in the JSON for the installed vs. web client. Note the class of the `client` object, the new `type` field, and the `redirect_uris`. ## `AuthState` class There are two gargle classes that are impacted by the OAuth-app-to-client switch: `AuthState` and `Gargle2.0`. We cover `AuthState` here and `Gargle2.0` in the next section. If a wrapper package follows the design laid out in `vignette("gargle-auth-in-client-package")`, it will use an instance of `AuthState` to manage the package's auth state. Let's assume that internal object is named `.auth`, which it usually is. Here are the changes you need to know about in `AuthState`: * The `app` field is deprecated, in favor of a new field `client`. If you request `.auth$app`, there will be a deprecation message and the `client` field is returned. * The `$set_app()` method is deprecated, in favor of a new `$set_client()` method. If you call `.auth$set_app()`, there will be a deprecation message and the input is used, instead, to set the `client` field. * The `app` argument of the `init_AuthState()` constructor is deprecated in favor of the new `client` argument. If you call `init_AuthState(app = x)`, there will be a deprecation message and the input `x` is used as the `client` argument instead. Here are the changes you probably need to make in your package: * The first argument of the user-facing function, `PKG_auth_configure()`, should become `client` (which is new). Move the existing `app` argument to the last position and deprecate it. * Deprecate `PKG_oauth_app()` (the function to reveal the user's configured OAuth client). * Introduce `PKG_oauth_client()` to replace `PKG_oauth_app()`. Here's how `googledrive::drive_auth_configure()` and `googledrive::drive_oauth_client()` looked before and after the transition: ```{r, eval = FALSE} # BEFORE drive_auth_configure <- function(app, path, api_key) { # not showing this code .auth$set_app(app) # more code we're not showing } drive_oauth_app <- function() .auth$app # AFTER drive_auth_configure <- function(client, path, api_key, app = deprecated()) { if (lifecycle::is_present(app)) { lifecycle::deprecate_warn( "2.1.0", "drive_auth_configure(app)", "drive_auth_configure(client)" ) drive_auth_configure(client = app, path = path, api_key = api_key) } # not showing this code .auth$set_client(client) # more code we're not showing } drive_oauth_client <- function() .auth$client drive_oauth_app <- function() { lifecycle::deprecate_warn( "2.1.0", "drive_oauth_app()", "drive_oauth_client()" ) drive_oauth_client() } ``` The approach above follows various conventions explained in `vignette("communicate", package = "lifecycle")`. If you also choose to use the lifecycle package to assist in this process, `usethis::use_lifecycle()` function does some helpful one-time setup in your package: ```{r eval = FALSE} usethis::use_lifecycle() ``` The roxygen documentation helpers in gargle assume `PKG_auth_configure()` is adapted as shown above: * `PREFIX_auth_configure_description()` crosslinks to `PREFIX_oauth_client()` now, not `PREFIX_oauth_app()`. * `PREFIX_auth_configure_params()` documents the `client` argument * `PREFIX_auth_configure_params()` uses a lifecycle badge and text to communicate that `app` is deprecated. * `PREFIX_auth_configure_params()` crosslinks to `gargle::gargle_oauth_client_from_json()` which requires gargle (>= 1.3.0) ## `Gargle2.0` class `Gargle2.0` is the second gargle class that is impacted by the OAuth-app-to-client switch. Here are the changes you probably need to make in your package: * Inside `PKG_auth()`, you presumably call `gargle::token_fetch()`. If you are passing `app = `, change that to `client = `. Neither `app` nor `client` are formal arguments of `gargle::token_fetch()`, instead, these are intended for eventual use by `gargle::credentials_user_oauth2()`. Here's a sketch of how this looks in `googledrive::drive_auth()`: ```{r eval = FALSE} drive_auth <- function(...) { # code not shown cred <- gargle::token_fetch( scopes = scopes, # app = drive_oauth_client() %||% , # BEFORE client = drive_oauth_client() %||% , # AFTER email = email, path = path, package = "googledrive", cache = cache, use_oob = use_oob, token = token ) # code not shown } ``` * If you ever call `gargle::credentials_user_oauth2()` directly, use the new `client` argument instead of the deprecated `app` argument. gargle/inst/doc/non-interactive-auth.Rmd0000644000176200001440000005551614433520365017766 0ustar liggesusers--- title: "Non-interactive auth" output: rmarkdown::html_vignette vignette: > %\VignetteIndexEntry{Non-interactive auth} %\VignetteEngine{knitr::rmarkdown} %\VignetteEncoding{UTF-8} --- ```{r, include = FALSE} knitr::opts_chunk$set( collapse = TRUE, comment = "#>", eval = FALSE ) ``` Here we describe how to do auth with a package that uses gargle, without requiring any user interaction. This comes up in a wide array of contexts, ranging from simple rendering of a local R Markdown document to deploying a data product on a remote server. We assume the wrapper package uses the design described in `vignette("gargle-auth-in-client-package")`. Examples include: * [bigrquery](https://bigrquery.r-dbi.org) * [googledrive](https://googledrive.tidyverse.org) * [googlesheets4](https://googlesheets4.tidyverse.org) * [gmailr](https://gmailr.r-lib.org) *note: gmailr does not use the built-in tidyverse OAuth client* Full details on `gargle::token_fetch()`, which powers this strategy, are given in `vignette("how-gargle-gets-tokens")`. ## Provide a token or pre-authorize token discovery The main principle for auth that does not require user interaction: > Provide a token directly or take advance measures that indicate you want a token to be discovered. We present several ways to achieve this, basically in order of preference. ## Sidebar 1: Deployment First, a word about deployed environments. If this doesn't apply to you, skip this section. Let's identify a specific type of project: it is developed in one place, with interactivity -- such as your local computer -- and then deployed elsewhere, where it must run without further interaction -- such as on [Posit Connect](https://posit.co/products/enterprise/connect/) or [shinyapps.io](https://www.shinyapps.io). In this context, it may make sense to depart from gargle's default behaviour, which is to store tokens outside the project, and to embed them in the project instead. An example at the end of this vignette demonstrates the use of a project-level OAuth cache. A service account token could also be stored in the project. When you embed tokens in the project and deploy, remember, that, by default, they are no more secure or hidden than the other source files in the project. `vignette("managing-tokens-securely")` describes a method for embedding an encrypted token in the project, which is an extra level of care needed if you want to access credentials within, e.g., a continuous integration service, such as GitHub Actions. ## Sidebar 2: I just want my `.Rmd` to render TL;DR is that you need to successfully authenticate *once* in an interactive session and then, in your code, give gargle permission to use a token it finds in the cache. These sorts of commands achieve that: ```{r} # Approach #1: use an option. # Either specify the user: options(gargle_oauth_email = "jenny@example.com") # Or, if you only use one Google identity, you can be more vague: options(gargle_oauth_email = TRUE) # Or, you can specify the identity to use at the domain level: options(gargle_oauth_email = "*@example.com") # Approach #2: call PACKAGE_auth() proactively. library(googledrive) # Either specify the user: drive_auth(email = "jenny@example.com") # Or, if you only use one Google identity, you can be more vague: drive_auth(email = TRUE) # Or, you can specify the identity to use at the domain level: drive_auth(email = "*@example.com") ``` At the end of this article, this scenario is explained in detail, if you want to understand why this works. ## Embrace credentials available in certain cloud settings In certain cloud computing contexts, a service account token may be ambiently available (or you can arrange for that to be true). Think about it: if your workload is running on Google Compute Engine (GCE), it's already "inside the Google house". It seems like there should be a way to avoid another round of auth and that is indeed the case. Another advantage of these cloud auth workflows is that there is never any need to download and carefully manage a file that contains sensitive information. This is why they are often described as "keyless". If you *can* use one of these methods, you should seriously consider doing so. ### Google Compute Engine This section applies to code running on a GCE instance, either literally, or on another Google Cloud product built on top of GCE. You should consider Google's own documentation to be definitive, but we'll try to give a useful summary here and to explain how gargle works with GCE: A Google Cloud Platform (GCP) project generally has a GCE default service account and, by default, a new GCE instance runs as that service account. (If you wish, you can use a *different* service account by taking explicit steps when you create an instance or by modifying it while it's stopped.) The main point is that, for an application running on GCE, a service account identity is generally available. GCE allows applications to get an OAuth access token from its metadata server and this is what `gargle::credentials_gce()` does (which is one of functions tried by `gargle::token_fetch()`, which is called by wrapper packages). This token request can be made for specific scopes and, in general, most wrapper packages will indeed be asking for specific scopes relevant to the API they access. Consider the signature of `googledrive::drive_auth()`: ```{r} drive_auth <- function(email = gargle::gargle_oauth_email(), path = NULL, scopes = "https://www.googleapis.com/auth/drive", cache = gargle::gargle_oauth_cache(), use_oob = gargle::gargle_oob_default(), token = NULL) { ... } ``` The googledrive package asks for a token with `"drive"` scope, by default. This brings up one big gotcha when using packages like googledrive or googlesheets4 on GCE. By default, a GCE instance will be running as the default service account, with the `"cloud-platform"` scope and this will, generally speaking, allow the service account to work with various Cloud products. However, the `"cloud-platform"` scope does not permit operations with non-Cloud APIs, such as Drive and Sheets. If you want the service account identity for your GCE instance to be able to get an access token for use with Drive and Sheets, you will need to explicitly add, e.g., the `"drive"` scope when you create the instance (or stop the instance and add that scope). (Note that, in contrast, BigQuery is considered a Cloud product and therefore bigrquery can operate with the `"cloud-platform"` scope.) Be aware that you might also need to explicitly grant the service account an appropriate level of access (e.g. read or write) to any Drive files you intend to work on. Finally, if you want to opt-out of using the default service account and, instead, auth as a normal user, even though you are on GCE, that is also possible. One way to achieve that is to remove `credentials_gce()` from the set of auth functions tried by `gargle::token_fetch()` by executing this command before any explicit or implicit auth happens: ```{r} # removes `credentials_gce()` from gargle's registry gargle::cred_funs_add(credentials_gce = NULL) ``` You can make a similar change in more scoped way with the helpers `gargle::with_cred_funs()` or `gargle::local_cred_funs()`. ### Workload Identity on Google Kubernetes Engine (GKE) Here we discuss how gargle's GCE auth can work for a related service, Google Kubernetes Engine (GKE), using [Workload Identity](https://cloud.google.com/kubernetes-engine/docs/how-to/workload-identity). This is more complicated that direct usage of GCE and some extra configuration is needed to make a service account's metadata available for the GKE instance to discover. GKE is the underlying technology behind Google's managed Airflow service, [Cloud Composer](https://cloud.google.com/composer), so this also applies to R docker files being called in that environment. Workload Identity is the recommended way to do authentication on GKE and other places, if possible, since it eliminates the use of a file that holds the service key, which is a potential security risk. 1. Following the [Workload Identity docs](https://cloud.google.com/kubernetes-engine/docs/how-to/workload-identity), you create a service account as normal and give it permissions and scopes needed to, say, upload to BigQuery. Imagine that `my-service-key@my-project.iam.gserviceaccount.com` has the `https://www.googleapis.com/auth/bigquery` scope. 2. Instead of downloading a JSON key, you instead migrate that permission by adding a policy binding to another service account within Kubernetes. 3. Create the service account within Kubernetes, ideally within a new namespace: ```sh # create namespace kubectl create namespace my-namespace # Create Kubernetes service account kubectl create serviceaccount --namespace my-namespace bq-service-account ``` 4. Bind that Kubernetes service account to the service account outside of Kubernetes you created in step 1, and assign it an annotation: ```sh # Create IAM policy binding betwwen k8s SA and GSA gcloud iam service-accounts add-iam-policy-binding my-service-key@my-project.iam.gserviceaccount.com \ --role roles/iam.workloadIdentityUser \ --member "serviceAccount:my-project.svc.id.goog[my-namespace/bq-service-account]" # Annotate k8s SA kubectl annotate serviceaccount bq-service-account \ --namespace my-namespace \ iam.gke.io/gcp-service-account=my-service-key@my-project.iam.gserviceaccount.com ``` This key will now be available to add to pods within the cluster. For Airflow, you can pass them in using the Python code `GKEStartPodOperator(...., namespace='my-namespace', service_account_name='bq-service-account')`. Documentation around `GKEStartPodOperator()` within Cloud Composer can be found [here](https://cloud.google.com/composer/docs/composer-2/use-gke-operator). 5. In order for the R function `gargle::gce_credentials()` do the right thing, you need to do two things: - Set `"gargle.gce.use_ip"` option to `TRUE`, in order to use the metadata server that's relevant on GKE. - Specify the target service account, i.e. you can't just passively accept the default, which is to use the `"default"` service account. `gce_instance_service_accounts()` can be helpful, e.g., if you want to know which service accounts your Docker container can see. Here is example code that you might execute in your Docker container: ```{r} options(gargle.gce.use_ip = TRUE) t <- gargle::credentials_gce("my-service-key@my-project.iam.gserviceaccount.com") # ... do authenticated stuff with the token t ... ``` Let's assume that PKG is an R package that implements gargle auth in the standard way, such as bigrquery or googledrive. At the time of writing the `service_account` argument is not exposed in the usual, high-level `PKG_auth()` function (. So if you need to use a non-`default` service account, you need to call `credentials_gce()` directly and pass that token to `PKG_auth()`: Here's an example of how that might look: ```{r} library(PKG) options(gargle.gce.use_ip = TRUE) t <- gargle::credentials_gce( "my-service-key@my-project.iam.gserviceaccount.com", # use YOUR service account scopes = "https://www.googleapis.com/auth/PKG" # use REAL scopes ) PKG_auth(token = t) # ... do authenticated stuff... ``` ### AWS Keyless auth is even possible from non-Google cloud platforms, using [Workload identity federation](https://cloud.google.com/iam/docs/configuring-workload-identity-federation). This is implemented in the experimental function `credentials_external_account()`, which currently only supports AWS. ## Provide a service account token directly When two computers are talking to each other, possibly with no human involvement, the most appropriate type of token to use is a service account token. If you're not working in cloud context with automatic access to a service account (see previous section), you can still use a service account, but it will require more explicit effort. 1. Create a service account and then download its credentials as a JSON file. This is described in `vignette("get-api-credentials")`, specifically in the *Service account token* section. 1. Call the wrapper package's main auth function proactively and provide the path to this JSON file. Example using googledrive: ```{r} library(googledrive) drive_auth(path = "/path/to/your/service-account-token.json") ``` If this code is running on, e.g., a continuous integration service and you need to use an encrypted token, see `vignette("managing-tokens-securely")`. For certain APIs, service accounts are inherently awkward, because you often want to do things *on behalf of a specific user*. Gmail is a good example. If you are sending email programmatically, you probably want to send it as yourself (or from some other specific email account) instead of from `zestybus-geosyogl@fuffapster-654321.iam.gserviceaccount.com`. This is, in fact, possible but is described as "impersonation", which should tip you off that Google does not exactly encourage this workflow. Some details: * This requires "delegating domain-wide authority " to the service account. * It is only possible in the context of a G Suite domain and only an administrator of the domain can set this up. * The domain-wide authority is granted only for specific scopes, so those can be as narrow as possible. This may make a domain administrator more receptive to the idea. * This is documented in a few different places, such as: - [Delegating domain-wide authority to the service account](https://developers.google.com/identity/protocols/oauth2/service-account#delegatingauthority) from Google Identity Platform docs - [Perform G Suite Domain-Wide Delegation of Authority](https://developers.google.com/admin-sdk/directory/v1/guides/delegation) from G Suite Admin SDK docs * The `subject` argument of `credentials_service_account()` and `credentials_app_default()` is available to specify which user to impersonate, e.g. `subject = "user@example.com"`. This argument first appeared in gargle 0.5.0, so it may not necessarily be exposed yet in user-facing auth functions like `drive_auth()`. If you need `subject` in a client package, that is a reasonable feature request. It is also possible to get a token with an explicit call to, e.g., `credentials_service_account()` and then pass that token to the auth function: ```{r} t <- gargle::credentials_service_account( path = "/path/to/your/service-account-token.json", scopes = ..., subject = "user@example.com" ) googledrive::dive_auth(token = t) ``` If delegation of domain-wide authority is impossible or unappealing, you must use an OAuth user token, as described below. ## Rig a service or external account for use with Application Default Credentials Wrapper packages that use `gargle::token_fetch()` in the recommended way have access to the token search strategy known as **Application Default Credentials**. You need to put the JSON corresponding to your service or external account in a very specific location or, alternatively, record the location of this JSON file in a specific environment variable. Full details are in the `credentials_app_default()` section of `vignette("how-gargle-gets-tokens")`. If you have your token rigged properly, you **do not** need to do anything else, i.e. you do not need to call `PACKAGE_auth()` explicitly. Your token should just get discovered upon first need. For troubleshooting purposes, you can set a gargle option to see verbose output about the execution of `gargle::token_fetch()`: ```{r} options(gargle_verbosity = "debug") ``` withr-style convenience helpers also exist: `with_gargle_verbosity()` and `local_gargle_verbosity()`. ## Provide an OAuth token directly If you somehow have the OAuth token you want to use as an R object, you can provide it directly to the `token` argument of the main auth function. Example using googledrive: ```{r} library(googledrive) my_oauth_token <- # some process that results in the token you want to use drive_auth(token = my_oauth_token) ``` gargle caches each OAuth user token it obtains to an `.rds` file, by default. If you know the filepath to the token you want to use, you could use `readRDS()` to read it and provide as the `token` argument to the wrapper's auth function. Example using googledrive: ```{r} # googledrive drive_auth(token = readRDS("/path/to/your/oauth-token.rds")) ``` How would you know this filepath? That requires some attention to the location of gargle's OAuth token cache folder, which is described in the next section. Full details are in the `credentials_byo_oauth2()` section of `vignette("how-gargle-gets-tokens")`. ## Arrange for an OAuth user token to be re-discovered This is the least recommended strategy, but it appeals to many users, because it doesn't require creating a service account. Just remember that the perceived ease of using the token you already have (an OAuth user token) is quickly cancelled out by the greater difficulty of managing such tokens for non-interactive use. You might be forced to use this strategy with certain APIs, such as Gmail, that are difficult to use with a service account. Two main principles: 1. Take charge of -- or at least notice -- the folder where OAuth tokens are being cached. 2. Make sure exactly one cached token will be identified and pre-authorize its use. There are many ways to do this. We'll work several examples using that convey the range of what's possible. ### I just want my `.Rmd` to render **Step 1**: Get that first token. You must run your code at least once, interactively, do the auth dance, and allow gargle to store the token in its cache. ```{r} library(googledrive) # do anything that triggers auth drive_find(n_max) ``` **Step 2**: Revise your code to pre-authorize the use of that token next time. Now your `.Rmd` can be rendered or your `.R` script can run, without further interaction. You have two choices to make: * Set the `gargle_oauth_email` option or call `PACKAGE_auth(email = ...)`. - The option-based approach can be implemented in each `.Rmd` or `.R` or in a user-level or project level `.Rprofile` startup file. * Authorize the use of the "matching token": - `email = TRUE` works if we're only going to find, at most, 1 token, i.e. you always auth with the same identity - `email = "jane@example.com"` pre-authorizes use of a token associated with a specific identity - `email = "*@example.com"` pre-authorizes use of a token associated with an identity from a specific domain; good for code that might be executed on the machines of both `alice@example.com` and `bob@example.com` This sets an option that allows gargle to use cached tokens whenever there's a unique match: ```{r} options(gargle_oauth_email = TRUE) ``` This sets an option to use tokens associated with a specific email address: ```{r} options(gargle_oauth_email = "jenny@example.com") ``` This sets an option to use tokens associated with an email address with a specific domain: ```{r} options(gargle_oauth_email = "*@example.com") ``` This gets a token *right now* and allows the use of a matching token, using googledrive as an example: ```{r} drive_auth(email = TRUE) ``` This gets a token *right now*, for the user with a specific email address: ```{r} drive_auth(email = "jenny@example.com") ``` This gets a token *right now*, first checking the cache for a token associated with a specific domain: ```{r} drive_auth(email = "*@example.com") ``` ### Project-level OAuth cache This is like the previous example, but with an added twist: we use a project-level OAuth cache. This is good for deployed data products. **Step 1**: Obtain the token intended for non-interactive use and make sure it's cached in a (hidden) directory of the current project. Using googledrive as an example: ```{r} library(googledrive) # designate project-specific cache options(gargle_oauth_cache = ".secrets") # check the value of the option, if you like gargle::gargle_oauth_cache() # trigger auth on purpose --> store a token in the specified cache drive_auth() # see your token file in the cache, if you like list.files(".secrets/") ``` Do this setup once per project. Another way to accomplish the same setup is to specify the desired cache location directly in the call to the auth function: ```{r} library(googledrive) # trigger auth on purpose --> store a token in the specified cache drive_auth(cache = ".secrets") ``` **Step 2**: In all downstream use, announce the location of the cache and pre-authorize the use of a suitable token discovered there. Continuing the googledrive example: ```{r} library(googledrive) options( gargle_oauth_cache = ".secrets", gargle_oauth_email = TRUE ) # now use googledrive with no need for explicit auth drive_find(n_max = 5) ``` Setting the option `gargle_oauth_email = TRUE` says that googledrive is allowed to use a token that it finds in the cache, without interacting with a user, as long as it discovers EXACTLY one matching token. This option-setting code needs to appear in each script, `.Rmd`, or app that needs to use this token non-interactively. Depending on the context, it might be suitable to accomplish this in a startup file, e.g. project-level `.Rprofile`. Here's a variation where we say which token to use by explicitly specifying the associated email. This is handy if there's a reason to have more than one token in the cache. ```{r} library(googledrive) options( gargle_oauth_cache = ".secrets", gargle_oauth_email = "jenny@example.com" ) # now use googledrive with no need for explicit auth drive_find(n_max = 5) ``` Here's another variation where we specify the necessary info directly in an auth call, instead of in options: ```{r} library(googledrive) drive_auth(cache = ".secrets", email = TRUE) # now use googledrive with no need for explicit auth drive_find(n_max = 5) ``` Here's one last variation that's applicable when the local cache could contain multiple tokens: ```{r} library(googledrive) drive_auth(cache = ".secrets", email = "jenny@example.com") # now use googledrive with no need for explicit auth drive_find(n_max = 5) ``` Be very intentional about paths and working directory. Personally I would use `here::here(".secrets)"` everywhere above, to make things more robust. For troubleshooting purposes, you can set a gargle option to see verbose output about the execution of `gargle::token_fetch()`: ```{r} options(gargle_verbosity = "debug") ``` withr-style convenience helpers also exist: `with_gargle_verbosity()` and `local_gargle_verbosity()`. For a cached token to be considered a "match", it must match the current request with respect to user's email, scopes, and OAuth client (client ID or key and secret). By design, these settings have very low visibility, because we usually want to use the defaults. If your token is not being discovered, consider if any of these fields might explain the mismatch. gargle/inst/doc/how-gargle-gets-tokens.R0000644000176200001440000001507114456275116017674 0ustar liggesusers## ---- include = FALSE--------------------------------------------------------- knitr::opts_chunk$set( collapse = TRUE, comment = "#>" ) ## ----setup-------------------------------------------------------------------- library(gargle) ## ---- eval = FALSE------------------------------------------------------------ # token_fetch(scopes, ...) ## ----------------------------------------------------------------------------- writeLines(names(cred_funs_list())) ## ---- eval = FALSE------------------------------------------------------------ # token_fetch(token = ) # # credentials_byo_oauth2( # token = # ) ## ---- eval = FALSE------------------------------------------------------------ # token_fetch(scopes = , path = "/path/to/your/service-account.json") # # # credentials_byo_oauth2() fails because no `token`, # # which leads to this call: # credentials_service_account( # scopes = , # path = "/path/to/your/service-account.json" # ) ## ---- eval = FALSE------------------------------------------------------------ # token_fetch(scopes = , path = "/path/to/your/external-account.json") # # # credentials_byo_oauth2() fails because no `token`, # # credentials_service_account() fails because the JSON provided via # # `path` is not of type "service_account", # # which leads to this call: # credentials_external_account( # scopes = , # path = "/path/to/your/external-account.json" # ) ## ---- eval = FALSE------------------------------------------------------------ # token_fetch(scopes = ) # # # credentials_byo_oauth2() fails because no `token`, # # credentials_service_account() fails because no `path`, # # credentials_external_account() fails because no `path`, # # which leads to this call: # credentials_app_default( # scopes = # ) ## ---- eval = FALSE------------------------------------------------------------ # ${GOOGLE_APPLICATION_CREDENTIALS} # ${CLOUDSDK_CONFIG}/application_default_credentials.json # # # on Windows: # %APPDATA%\gcloud\application_default_credentials.json # %SystemDrive%\gcloud\application_default_credentials.json # C:\gcloud\application_default_credentials.json # # # on not-Windows: # ~/.config/gcloud/application_default_credentials.json ## ---- eval = FALSE------------------------------------------------------------ # token_fetch(scopes = ) # # or perhaps # token_fetch(scopes = , service_account = ) # # # credentials_byo_oauth2() fails because no `token`, # # credentials_service_account() fails because no `path`, # # credentials_external_account() fails because no `path`, # # credentials_app_default() fails because no ADC found, # # which leads to one of these calls: # credentials_gce( # scopes = , # service_account = "default" # ) # # or # credentials_gce( # scopes = , # service_account = # ) ## ---- eval = FALSE------------------------------------------------------------ # token_fetch(scopes = ) # # # credentials_byo_oauth2() fails because no `token`, # # credentials_service_account() fails because no `path`, # # credentials_external_account() fails because no `path`, # # credentials_app_default() fails because no ADC found, # # credentials_gce() fails because not on GCE, # # which leads to this call: # credentials_user_oauth2( # scopes = , # app = , # package = "" # ) ## ---- eval = FALSE------------------------------------------------------------ # # user initiates auth or does something that triggers it indirectly # THINGY_auth() # # # which then calls # gargle::token_fetch( # scopes = , # app = thingy_app(), # package = "thingyr" # ) # # # which leads to this call: # credentials_user_oauth2( # scopes = , # app = thingy_app(), # package = "thingyr" # ) ## ---- eval = FALSE------------------------------------------------------------ # gargle2.0_token( # email = gargle_oauth_email(), # app = thingy_app(), # package = "thingyr", # scope = , # cache = gargle_oauth_cache() # ) ## ---- eval = FALSE------------------------------------------------------------ # The thingyr package is requesting access to your Google account. # Enter '1' to start a new auth process or select a pre-authorized account. # 1: Send me to the browser for a new auth process. # 2: janedoe_personal@gmail.com # 3: janedoe@example.com # 4: janedoe_work@gmail.com # Selection: ## ---- eval = FALSE------------------------------------------------------------ # thingy_auth(email = "janedoe_work@gmail.com") ## ---- eval = FALSE------------------------------------------------------------ # gargle_oauth_sitrep() # #> 14 tokens found in this gargle OAuth cache: # #> ~/Library/Caches/gargle # #' # #' email app scopes hash... # #' ----------------------------- ----------- ------------------------------ ---------- # #' abcdefghijklm@gmail.com thingy ...bigquery, ...cloud-platform 128f9cc... # #' buzzy@example.org gargle-demo 15acf95... # #' stella@example.org gargle-demo ...drive 4281945... # #' abcdefghijklm@gmail.com gargle-demo ...drive 48e7e76... # #' abcdefghijklm@gmail.com tidyverse 69a7353... # #' nopqr@ABCDEFG.com tidyverse ...spreadsheets.readonly 86a70b9... # #' abcdefghijklm@gmail.com tidyverse ...drive d9443db... # #' nopqr@HIJKLMN.com tidyverse ...drive d9443db... # #' nopqr@ABCDEFG.com tidyverse ...drive d9443db... # #' stuvwzyzabcd@gmail.com tidyverse ...drive d9443db... # #' efghijklmnopqrtsuvw@gmail.com tidyverse ...drive d9443db... # #' abcdefghijklm@gmail.com tidyverse ...drive.readonly ecd11fa... # #' abcdefghijklm@gmail.com tidyverse ...bigquery, ...cloud-platform ece63f4... # #' nopqr@ABCDEFG.com tidyverse ...spreadsheets f178dd8... ## ----------------------------------------------------------------------------- writeLines(names(cred_funs_list())) ## ----eval = FALSE------------------------------------------------------------- # gargle::cred_funs_add(credentials_gce = NULL) gargle/inst/doc/gargle-auth-in-client-package.html0000644000176200001440000012407214456275115021614 0ustar liggesusers How to use gargle for auth in a client package

How to use gargle for auth in a client package

gargle provides common infrastructure for use with Google APIs. This vignette describes one possible design for using gargle to deal with auth, in a client package that provides a high-level wrapper for a specific API.

There are frequent references to googledrive, which uses the design described here, along with bigrquery (v1.2.0 and higher), gmailr (v1.0.0 and higher), and googlesheets4 (the successor to googlesheets).

Key choices

Getting a token requires several pieces of information and there are stark differences in how much users (need to) know or control about this process. Let’s review them, with an eye towards identifying the responsibilities of the package author versus the user.

  • Overall config: OAuth client and API key. Who provides?
  • Token-level properties: Google identity (email) and scopes.
  • Request-level: Who manages tokens and injects them into requests?

User-facing auth

In googledrive, the main user-facing auth function is googledrive::drive_auth(). Here is its definition (at least approximately, remember this is static code):

# googledrive::
drive_auth <- function(email = gargle::gargle_oauth_email(),
                       path = NULL,
                       scopes = "https://www.googleapis.com/auth/drive",
                       cache = gargle::gargle_oauth_cache(),
                       use_oob = gargle::gargle_oob_default(),
                       token = NULL) {
  # this catches a common error, where the user passes JSON for an OAuth client
  # to the `path` argument, which only expects a service account token
  gargle::check_is_service_account(path, hint = "drive_auth_configure")

  cred <- gargle::token_fetch(
    scopes = scopes,
    client = drive_oauth_client() %||% <BUILT_IN_DEFAULT_CLIENT>,
    email = email,
    path = path,
    package = "googledrive",
    cache = cache,
    use_oob = use_oob,
    token = token
  )
  if (!inherits(cred, "Token2.0")) {
    # throw an informative error here
  }
  .auth$set_cred(cred)
  .auth$set_auth_active(TRUE)

  invisible()
}

drive_auth() is called automatically upon the first need of a token and that can lead to user interaction, but does not necessarily do so. drive_auth() can be called explicitly by the user, but usually that is not necessary. token_fetch() is described in the vignette("how-gargle-gets-tokens"). The internal .auth object maintains googledrive’s auth state and is explained next.

Auth state

A client package can use an internal object of class gargle::AuthState to hold the auth state. In googledrive, the main auth file defines a placeholder .auth object:

.auth <- NULL

The actual initialization happens in .onLoad():

.onLoad <- function(libname, pkgname) {
  utils::assignInMyNamespace(
    ".auth",
    gargle::init_AuthState(package = "googledrive", auth_active = TRUE)
  )
  
  # other stuff
}

The initialization of .auth is done this way to ensure that we get an instance of the AuthState class using the current, installed version of gargle (vs. the ambient version from whenever gargle was built, perhaps by CRAN).

An AuthState instance has other fields which, in this googledrive example, are not set at this point. The OAuth client and api_key are configurable by the user and, when NULL, downstream functions can fall back to internal credentials. The cred field is populated by the first call to drive_auth() (direct or indirectly via drive_token()).

OAuth client

Most users should present OAuth user credentials to Google APIs. However, most users would love to be spared the fiddly details surrounding this. The OAuth client is one example. (Historically, following the lead of the httr package, we have used the term OAuth app, but we now use the term OAuth client.) The client is a component that most users do not even know about and they are content to use the same client for all work through a wrapper package: possibly, the client built into the package.

There is a field in the .auth auth state to hold the OAuth client. Exported auth helpers, drive_oauth_client() and drive_auth_configure(), retrieve and modify the current client to support users who want to (or must) take that level of control.

library(googledrive)

# first: download the OAuth client as a JSON file
drive_auth_configure(
  path = "/path/to/the/JSON/that/was/downloaded/from/gcp/console.json"
)

drive_oauth_client()
#> <gargle_oauth_client>
#> name: acme-corp-google-client
#> id: 123456789.apps.googleusercontent.com
#> secret: <REDACTED>
#> type: installed
#> redirect_uris: http://localhost

Do not “borrow” an OAuth client ID and secret from gargle or any other package; always use credentials associated with your package or provided by your user. Per the Google User Data Policy https://developers.google.com/terms/api-services-user-data-policy, your application must accurately represent itself when authenticating to Google API services.

Some APIs and scopes are considered so sensitive that is essentially impossible for a package to provide a built-in OAuth client. Users must get and configure their own client. Among the packages mentioned as examples, this is true of gmailr.

API key

Some Google APIs can be used in an unauthenticated state, if and only if requests include an API key. For example, this is a great way to read a Google Sheet that is world-readable or readable by “anyone with a link” from a Shiny app, thereby designing away the need to manage user credentials on the server.

The user can provide their own API key via drive_auth_configure(api_key =) and retrieve that value with drive_api_key(), just as with the OAuth client. The API key is stored in the api_key field of the .auth auth state.

library(googledrive)

drive_auth_configure(api_key = "123456789")

drive_api_key()
#> "123456789"

Many users aren’t motivated to take this level of control and appreciate when a package provides a built-in default API key. As with the client, packages should obtain their own API key and not borrow the gargle or tidyverse key.

Some APIs are not usable without a token, in which case a wrapper package may not even expose functionality for managing an API key. Among the packages mentioned as examples, this is true of bigrquery.

Email or Google identity

In contrast to the OAuth client and API key, every user must express which identity they wish to present to the API. This is a familiar concept and users expect to specify this. Since users may have more than one Google account, it’s quite likely that they will want to switch between accounts, even within a single R session, or that they might want to explicitly declare the identity to be used in a specific script or app.

That explains why drive_auth() has the optional email argument that lets users proactively specify their identity. drive_auth() is usually called indirectly upon first need, but a user can also call it proactively in order to specify their target email:

# googledrive::
drive_auth(email = "janedoe_work@gmail.com")

If email is not given, gargle also checks for an option named "gargle_oauth_email". The email is used to look up tokens in the cache and, if no suitable token is found, it is used to pre-configure the OAuth chooser in the browser. Read more in the help for gargle::gargle_oauth_email().

Scopes

Most users have no concept of scopes. They just know they want to work with, e.g., Google Drive or Google Sheets. A client package can usually pick sensible default scopes, that will support what most users want to do.

Here’s a reminder of the signature of googledrive::drive_auth():

# googledrive::
drive_auth <- function(email = gargle::gargle_oauth_email(),
                       path = NULL,
                       scopes = "https://www.googleapis.com/auth/drive",
                       cache = gargle::gargle_oauth_cache(),
                       use_oob = gargle::gargle_oob_default(),
                       token = NULL) { ... }

googledrive ships with a default scope, but a motivated user could call drive_auth() preemptively at the start of the session and request different scopes. For example, if they intend to only read data and want to guard against inadvertent file modification, they might opt for the drive.readonly scope.

# googledrive::
drive_auth(scopes = "https://www.googleapis.com/auth/drive.readonly")

OAuth cache and Out-of-bound auth

The location of the token cache and whether to prefer out-of-bound auth are two aspects of OAuth where most users are content to go along with sensible default behavior. For those who want to exert control, that can be done in direct calls to drive_auth() or by configuring an option. Read the help for gargle::gargle_oauth_cache() and gargle::gargle_oob_default() and vignette("auth-from-web") for more.

Overview of mechanics

Here’s a concrete outline of how one could set up a client package to get its auth functionality from gargle.

  1. Add gargle to your package’s Imports.
  2. Create a file R/YOURPKG_auth.R.
  3. Create an internal gargle::AuthClass object to hold auth state. Follow the googledrive example above.
  4. Define standard functions for the auth interface between gargle and your package; do this in R/YOURPKG_auth.R. Examples: tidyverse/googledrive/R/drive_auth.R and r-dbi/bigrquery/R/bq_auth.R.
  5. Use gargle’s roxygen helpers to create the docs for your auth functions. This relieves you from writing docs and you inherit standard wording. See previously cited examples for inspiration.
  6. Use the functions YOURPKG_token() and YOURPKG_api_key() (defined in the standard auth interface) to insert a token or API key in your package’s requests.

Getting that first token

I focus on early use, by the naive user, with the OAuth flow. When the user first calls a high-level googledrive function such as drive_find(), a Drive request is ultimately generated with a call to googledrive::request_generate(). Here is its definition, at least approximately:

# googledrive::
request_generate <- function(endpoint = character(),
                             params = list(),
                             key = NULL,
                             token = drive_token()) {
  ept <- drive_endpoint(endpoint)
  if (is.null(ept)) {
    # throw error about unrecognized endpoint
  }

  ## modifications specific to googledrive package
  params$key <- key %||% params$key %||%
    drive_api_key() %||% <BUILT_IN_DEFAULT_API_KEY>
  if (!is.null(ept$parameters$supportsAllDrives)) {
    params$supportsAllDrives <- TRUE
  }

  req <- gargle::request_develop(endpoint = ept, params = params)
  gargle::request_build(
    path = req$path,
    method = req$method,
    params = req$params,
    body = req$body,
    token = token
  )
}

googledrive::request_generate() is a thin wrapper around gargle::request_develop() and gargle::request_build() that only implements details specific to googledrive, before delegating to more general functions in gargle. The vignette("request-helper-functions") documents these gargle functions.

googledrive::request_generate() gets a token with drive_token(), which is defined like so:

# googledrive::
drive_token <- function() {
  if (isFALSE(.auth$auth_active)) {
    return(NULL)
  }
  if (!drive_has_token()) {
    drive_auth()
  }
  httr::config(token = .auth$cred)
}

where drive_has_token() in a helper defined as:

# googledrive::
drive_has_token <- function() {
  inherits(.auth$cred, "Token2.0")
}

By default, auth is active, and, for a fresh start, we won’t have a token stashed in .auth yet. So this will result in a call to drive_auth() to obtain a credential, which is then cached in .auth$cred for the remainder of the session. All subsequent calls to drive_token() will just spit back this token.

Above, we discussed scenarios where an advanced user might call drive_auth() proactively, with non-default arguments, possibly even loading a service token or using alternative flows, like an external account. Any token loaded in that way is stashed in .auth$cred and will be returned by subsequent calls to drive_token().

Multiple gargle-using packages can use a shared token by obtaining a suitably scoped token with one package, then registering that token with the other packages. For example, the default scope requested by googledrive is also sufficient for operations available in googlesheets4. You could use a shared token like so:

library(googledrive)
library(googlesheets4)

drive_auth(email = "jane_doe@example.com") # gets a suitably scoped token
                                           # and stashes for googledrive use

gs4_auth(token = drive_token())            # registers token with googlesheets4

# now work with both packages freely ...

It is important to make sure that the token-requesting package (googledrive, above) is using an OAuth client for which all the necessary APIs and scopes are enabled.

Auth interface

The exported functions like drive_auth(), drive_token(), etc. constitute the auth interface between googledrive and gargle and are centralized in tidyverse/googledrive/R/drive_auth.R. That is a good template for how to use gargle to manage auth in a client package. In addition, the docs for these gargle-backed functions are generated automatically from standard information maintained in the gargle package.

  • drive_token() retrieves the current credential, in a form that is ready for inclusion in HTTP requests. If auth_active is TRUE and cred is NULL, drive_auth() is called to obtain a credential. If auth_active is FALSE, NULL is returned; client packages should be designed to fall back to including an API key in affected HTTP requests, if sensible for the API.
  • drive_auth() ensures we are dealing with an authenticated user and have a credential on hand with which to place authorized requests. Sets auth_active to TRUE. Can be called directly, but drive_token() will also call it as needed.
  • drive_deauth() clears the current token. It might also toggle auth_active, depending on the features of the target API. See below.
  • drive_oauth_client() returns .auth$client.
  • drive_api_key() returns .auth$api_key.
  • drive_auth_configure() can be used to configure auth. This is how an advanced user would enter their own OAuth client and API key into the auth config, in order to affect all subsequent requests.
  • drive_user() reports some information about the user associated with the current token. The Drive API offers an actual endpoint for this, which is not true for most Google APIs. Therefore the analogous function in bigrquery, bq_user() is a better general reference.

De-auth

APIs split into two classes: those that can be used, at least partially, without a token and those that cannot. If an API is usable without a token – which is true for the Drive API – such requests must include an API key. Therefore, the auth design for a client package is different for these two types of APIs.

For an API that can be used without a token: drive_deauth() can be used at any time to enter a de-authorized state. It sets auth_active to FALSE and .auth$cred to NULL. In this state, requests are sent out with an API key and no token. This is a great way to eliminate any friction re: auth if there’s no need for it, i.e. if all requests are for resources that are world readable or available to anyone who knows how to ask for it, such as files shared via “Anyone with the link”. The de-authorized state is especially useful in non-interactive settings or where user interaction is indirect, such as via Shiny.

For an API that cannot be used without a token: BigQuery is an example of such an API. bq_deauth() just clears the current token, so that the auth flow starts over the next time a token is needed.

Bring Your Own Client and Key

Advanced users can use their own OAuth client and API key. drive_auth_configure() lives in R/drive_auth.R and it provides the ability to modify the current client and api_key. Recall that drive_oauth_client() and drive_api_key() also exist for targeted, read-only access.

The vignette("get-api-credentials") describes how to get an API key and OAuth client.

Packages that always send a token will omit the API key functionality here.

Changing identities (and more)

One reason for a user to call drive_auth() directly and proactively is to switch from one Google identity to another or to make sure they are presenting themselves with a specific identity. drive_auth() accepts an email argument, which is honored when gargle determines if there is already a suitable token on hand. Here is a sketch of how a user could switch identities during a session, possibly non-interactive:

library(googledrive)

drive_auth(email = "janedoe_work@gmail.com")
# do stuff with Google Drive here, with Jane Doe's "work" account

drive_auth(email = "janedoe_personal@gmail.com")
# do other stuff with Google Drive here, with Jane Doe's "personal" account

drive_auth(path = "/path/to/a/service-account.json")
# do other stuff with Google Drive here, using a service account
gargle/inst/doc/auth-from-web.html0000644000176200001440000060544214456275115016625 0ustar liggesusers Auth when using R from the browser

Auth when using R from the browser

If you are working with R in a web-based context, such as RStudio Server, Posit Cloud, Posit Workbench, or Google Colaboratory, your experience of browser-based auth flows will be different from those using R on their local machine. You need to use out-of-band authentication, sometimes denoted “oob” or “OOB”. After the usual auth dance, instead of seeing “authentication successful, return to R!”, you are presented with an authorization code to copy and paste back into your R session. For folks who are running R on their local machine, this final exchange can be done automagically, using a temporary local webserver, but that is not possible for those accessing a remote R session through the browser.

On February 16, 2022, Google announced the (partial) deprecation of the OAuth out-of-band (OOB) flow, to be enacted no later than February 1, 2023. The deprecation applies to Google Cloud Platform (GCP) projects that are in production mode. OOB still works for projects that are in testing mode.

The built-in tidyverse client (used by googledrive, googlesheets4, and bigrquery) is associated with a GCP project that is in production mode. Therefore, conventional OOB auth stopped working for the built-in client in February 2023. In anticipation of this, gargle gained a new auth flow in version 1.3.0 that we call “pseudo-OOB”, which should allow casual users to continue to enjoy a low-friction auth experience, even from RStudio Server, Posit Cloud, Posit Workbench, and Google Colaboratory.

If you attempt to do conventional OOB auth with a client that no longer supports it, you’ll see something like this:

Screenshot with the following text: "Access blocked: Tidyverse API Packages's request is invalid", "You can't sign in because Tidyverse API Packages sent an invalid request. You can try again later, or contact the developer about this issue. Learn more about this error", "If you are a developer of Tidyverse API Packages, see error details.", "Error 400: invalid_request".

Access blocked: Tidyverse API Packages’s request is invalid. Error 400: invalid_request

If you work on any of the affected platforms and are experiencing new auth problems, your first move should be to update all packages involved (gargle and one or more of googledrive, googlesheets4, bigrquery). Restart R. Re-execute your code in an interactive context that will allow you to re-auth.

This vignette documents various matters around OOB auth, both conventional and pseudo-OOB, for users who want to understand this more deeply.

Some of the packages that use gargle for auth and for which this article applies:

Consider using a service account token (or no token!)

If you have concerns about using OOB auth, consider whether your task truly requires auth as a specific, normal user.

Can the task be completed with no auth, i.e. you are accessing something that is world readable or readable for “anyone with a link”? In that case, the wrapper package probably provides a function to go into a de-authorized state, such as googledrive::drive_deauth() or googlesheets4::gs4_deauth().

If the task requires auth, consider whether it really must be as a specific user. You may be able to accomplish the task with a service account, which you create for this specific purpose. A service account token is much easier to work with on a server and in non-interactive contexts than a user token. A service account can also be given much more selective permissions than a user account and can be more easily deleted, once it is no longer needed. Remember that the service account will need to be explicitly given permission to access any necessary resources (e.g. permission to read or write a specific Drive file or Sheet). A service account doesn’t somehow inherit permissions indirectly from the user who owns the GCP project in which it lives. To learn more about using a service account, see vignette("non-interactive-auth").

When and how to use OOB

In the absence of any user instructions, the function gargle::gargle_oob_default() is used to decide whether to use OOB auth. By default, OOB auth is used on RStudio Server, Posit Cloud, Posit Workbench, and Google Colaboratory, or if the option "gargle_oob_default" is set to TRUE. (Note that we use the term “OOB auth” here to include both the existing, conventional form of OOB and gargle’s new pseudo-OOB.)

Wrapper packages generally also allow the user to opt-in to OOB auth when making a direct call to an auth function. For example, the functions googledrive::drive_auth(), googlesheets4::gs4_auth(), bigrquery::bq_auth(), and gmailr::gm_auth() all have a use_oob argument. Notably, all of these use_oob arguments default to gargle::gargle_oob_default().

gargle usually automatically detects when it should use OOB auth, but here is what it could look like if we are not using OOB, but should be. During auth, you are redirected to localhost on port 1410 and receive an error along these lines:

Chrome: This site can't be reached; localhost refused to connect.
Firefox: Unable to connect; can't establish a connection.

If this happens you might need to explicitly request OOB. Below we review two different methods.

Request OOB auth in the PKG_auth() call

Packages like googledrive and bigrquery aim to make auth “just work” for most users, i.e. it’s automatically triggered upon first need. However, it is always possible to initiate auth yourself, which gives you the opportunity to specify non-default values of certain parameters. Here’s how you could request OOB auth, using googledrive as an example:

library(googledrive)

drive_auth(use_oob = TRUE)

# now carry on with your work
drive_find(n_max = 5)

Set the "gargle_oob_default" option

If you know that you always want to use OOB, as a user or within a project, the best way to express this is to set the "gargle_oob_default" option.

options(gargle_oob_default = TRUE)

This code could appear at the top of a script, in a setup chunk for .Rmd, or in a Shiny app. But it probably makes even more sense in a .Rprofile startup file, at the user or project level.

Once the "gargle_oob_default" option has been set, it is honored by downstream calls to PKG_auth(), explicit or implicit, because the default value of use_oob is gargle::gargle_oob_default(), which consults the option.

Conventional vs. pseudo-OOB auth

gargle now supports two OOB flows, which we call “conventional OOB” (the existing, legacy OOB flow) and “pseudo-OOB” (the new flow introduced in response to the partial deprecation of conventional OOB). If we are using OOB auth, the decision between conventional or pseudo-OOB is made based on the currently configured OAuth client.

  • If the OAuth client is of type "installed" (shows as “Desktop” in Google Cloud Console) or is of unknown type, gargle uses conventional OOB. Note that this will not necessarily succeed, due to the deprecation process described above.
  • If the OAuth client is of type "web" (shows as “Web application” in Google Cloud Console), gargle uses the new pseudo-OOB flow.
use_oob
FALSE TRUE
client type installed use httpuv to spin up
a temporary web server
conventional OOB
web --not possible-- pseudo-OOB

Packages that use a built-in tidyverse OAuth client (googledrive, googlesheets4, and bigrquery) should automatically select a “web” client on RStudio Server, Posit Cloud, Posit Workbench, and Google Colaboratory and an “installed” client otherwise. If you need to explicitly request a “web” client in some other setting, you can use the global option "gargle_oauth_client_type":

options(gargle_oauth_client_type = "web")

Users who configure their own OAuth client will need to be intentional when choosing the client type, depending on where the code is running.

On the R side, it is recommended to setup an OAuth client using gargle_oauth_client_from_json(), which allows the client type ("installed" vs. "web") to be detected programmatically from the downloaded JSON. The less-preferred approach is to use gargle_oauth_client() and provide the information yourself.

How pseudo-OOB works

Pseudo-OOB works just like non-OOB and conventional OOB in terms of the user’s interactions with Google authorization server. This is where the user authenticates themselves with Google and consents to the type of access being requested by the R code.

These flows differ in how they handle a successful response from the authorization server. Specifically, the flows use different redirect URIs.

  • A (temporary) local webserver is used to listen for this response at, e.g., http://localhost:1410/ if R is running locally and the httpuv package is available (i.e. a non-OOB flow).
  • In conventional OOB, a special redirect value is used, typically urn:ietf:wg:oauth:2.0:oob, and the authorization code is provided to the user via a browser window for manual copy/paste. This page is served by Google. Google has deprecated conventional OOB for projects in production mode (but it is still allowed for projects in testing mode).
  • In gargle’s pseudo-OOB, a redirect URI from the configured OAuth client is used to receive the response. This page is responsible for exposing a code that the user can copy/paste, similar to conventional OOB (except the page is not served by Google). Unlike conventional OOB, this is not the authorization code itself, but is something from which the code can be extracted, along with a state token to mitigate cross-site request forgery. This is actually implemented using an OAuth flow for web server applications. Note that we (gargle) call this pseudo-OOB, but it is not technically OOB from Google’s point-of-view.

The built-in OAuth client used for pseudo-OOB by tidyverse packages redirects to https://www.tidyverse.org/google-callback/. This is a static landing page that does not collect any data and exists solely to give the interactive R user a way to convey the authorization token back to the waiting R process and thereby complete the auth process.

But I didn’t need OOB yesterday!

Sometimes the usual OAuth web flow suddenly stops working for people working directly with R (so NOT via the browser) and they use OOB auth to get unstuck again. What’s going on in this case?

The initial error looks something like this:

createTcpServer: address already in use
Error in httpuv::startServer(use$host, use$port, list(call = listen)) : 
  Failed to create server

It’s characteristic of some other process sitting on port 1410, which is what gargle is trying to use for auth.

It’s true that using OOB auth is a workaround. But OOB auth is, frankly, more clunky, so why use it if you don’t have to? Here are ways to fix this.

  • Restart your system. This will almost certainly kill the offending process, which is usually a zombie process.
  • Hunt down the offending process, verify it looks expendable, and kill it.

On *nix-y systems, use lsof to get the process ID:

sudo lsof -i :1410

The output will look something like this:

COMMAND   PID  USER   FD   TYPE            DEVICE SIZE/OFF NODE NAME
R       16664 jenny   20u  IPv4 0x63761a50856c65f      0t0  TCP localhost:hiq (LISTEN)

In this case, as is typical, this is a zombie R process and I feel confident killing it. The process ID is listed there as PID. Note that and kill the process, like so, filling in the PID you found:

kill -9 <PID>

So, to be clear, in this example, the command would be:

kill -9 16664

The normal, non-OOB auth web flow should work again now.

Further reading

If you’re working on a data product that will be deployed (for example on shinyapps.io or Posit Connect), you will also need to consider how the deployed content will authenticate non-interactively, which is covered in vignette("non-interactive-auth").

gargle/inst/doc/gargle-auth-in-client-package.R0000644000176200001440000001246614456275115021054 0ustar liggesusers## ---- include = FALSE--------------------------------------------------------- knitr::opts_chunk$set( collapse = TRUE, comment = "#>" ) ## ---- eval = FALSE------------------------------------------------------------ # # googledrive:: # drive_auth <- function(email = gargle::gargle_oauth_email(), # path = NULL, # scopes = "https://www.googleapis.com/auth/drive", # cache = gargle::gargle_oauth_cache(), # use_oob = gargle::gargle_oob_default(), # token = NULL) { # # this catches a common error, where the user passes JSON for an OAuth client # # to the `path` argument, which only expects a service account token # gargle::check_is_service_account(path, hint = "drive_auth_configure") # # cred <- gargle::token_fetch( # scopes = scopes, # client = drive_oauth_client() %||% , # email = email, # path = path, # package = "googledrive", # cache = cache, # use_oob = use_oob, # token = token # ) # if (!inherits(cred, "Token2.0")) { # # throw an informative error here # } # .auth$set_cred(cred) # .auth$set_auth_active(TRUE) # # invisible() # } ## ----eval = FALSE------------------------------------------------------------- # .auth <- NULL ## ----------------------------------------------------------------------------- .onLoad <- function(libname, pkgname) { utils::assignInMyNamespace( ".auth", gargle::init_AuthState(package = "googledrive", auth_active = TRUE) ) # other stuff } ## ---- eval = FALSE------------------------------------------------------------ # library(googledrive) # # # first: download the OAuth client as a JSON file # drive_auth_configure( # path = "/path/to/the/JSON/that/was/downloaded/from/gcp/console.json" # ) # # drive_oauth_client() # #> # #> name: acme-corp-google-client # #> id: 123456789.apps.googleusercontent.com # #> secret: # #> type: installed # #> redirect_uris: http://localhost ## ---- eval = FALSE------------------------------------------------------------ # library(googledrive) # # drive_auth_configure(api_key = "123456789") # # drive_api_key() # #> "123456789" ## ----eval = FALSE------------------------------------------------------------- # # googledrive:: # drive_auth(email = "janedoe_work@gmail.com") ## ---- eval = FALSE------------------------------------------------------------ # # googledrive:: # drive_auth <- function(email = gargle::gargle_oauth_email(), # path = NULL, # scopes = "https://www.googleapis.com/auth/drive", # cache = gargle::gargle_oauth_cache(), # use_oob = gargle::gargle_oob_default(), # token = NULL) { ... } ## ---- eval = FALSE------------------------------------------------------------ # # googledrive:: # drive_auth(scopes = "https://www.googleapis.com/auth/drive.readonly") ## ----eval = FALSE------------------------------------------------------------- # # googledrive:: # request_generate <- function(endpoint = character(), # params = list(), # key = NULL, # token = drive_token()) { # ept <- drive_endpoint(endpoint) # if (is.null(ept)) { # # throw error about unrecognized endpoint # } # # ## modifications specific to googledrive package # params$key <- key %||% params$key %||% # drive_api_key() %||% # if (!is.null(ept$parameters$supportsAllDrives)) { # params$supportsAllDrives <- TRUE # } # # req <- gargle::request_develop(endpoint = ept, params = params) # gargle::request_build( # path = req$path, # method = req$method, # params = req$params, # body = req$body, # token = token # ) # } ## ----eval = FALSE------------------------------------------------------------- # # googledrive:: # drive_token <- function() { # if (isFALSE(.auth$auth_active)) { # return(NULL) # } # if (!drive_has_token()) { # drive_auth() # } # httr::config(token = .auth$cred) # } ## ----eval = FALSE------------------------------------------------------------- # # googledrive:: # drive_has_token <- function() { # inherits(.auth$cred, "Token2.0") # } ## ----eval = FALSE------------------------------------------------------------- # library(googledrive) # library(googlesheets4) # # drive_auth(email = "jane_doe@example.com") # gets a suitably scoped token # # and stashes for googledrive use # # gs4_auth(token = drive_token()) # registers token with googlesheets4 # # # now work with both packages freely ... ## ----eval = FALSE------------------------------------------------------------- # library(googledrive) # # drive_auth(email = "janedoe_work@gmail.com") # # do stuff with Google Drive here, with Jane Doe's "work" account # # drive_auth(email = "janedoe_personal@gmail.com") # # do other stuff with Google Drive here, with Jane Doe's "personal" account # # drive_auth(path = "/path/to/a/service-account.json") # # do other stuff with Google Drive here, using a service account gargle/inst/doc/troubleshooting.R0000644000176200001440000000704714456275117016633 0ustar liggesusers## ---- include = FALSE--------------------------------------------------------- knitr::opts_chunk$set( collapse = TRUE, comment = "#>" ) ## ----setup-------------------------------------------------------------------- library(gargle) ## ----------------------------------------------------------------------------- gargle_verbosity() ## ----------------------------------------------------------------------------- # save current value op <- options(gargle_verbosity = "debug") gargle_verbosity() # restore original value options(op) ## ----------------------------------------------------------------------------- gargle_verbosity() with_gargle_verbosity( "debug", gargle_verbosity() ) gargle_verbosity() f <- function() { local_gargle_verbosity("debug") gargle_verbosity() } f() gargle_verbosity() ## ---- eval = FALSE------------------------------------------------------------ # gargle_oauth_sitrep() # #' > 14 tokens found in this gargle OAuth cache: # #' '~/Library/Caches/gargle' # #' # #' email app scope hash... # #' ----------------------------- ----------- ------------------------------ ---------- # #' abcdefghijklm@gmail.com thingy ...bigquery, ...cloud-platform 128f9cc... # #' buzzy@example.org gargle-demo 15acf95... # #' stella@example.org gargle-demo ...drive 4281945... # #' abcdefghijklm@gmail.com gargle-demo ...drive 48e7e76... # #' abcdefghijklm@gmail.com tidyverse 69a7353... # #' nopqr@ABCDEFG.com tidyverse ...spreadsheets.readonly 86a70b9... # #' abcdefghijklm@gmail.com tidyverse ...drive d9443db... # #' nopqr@HIJKLMN.com tidyverse ...drive d9443db... # #' nopqr@ABCDEFG.com tidyverse ...drive d9443db... # #' stuvwzyzabcd@gmail.com tidyverse ...drive d9443db... # #' efghijklmnopqrtsuvw@gmail.com tidyverse ...drive d9443db... # #' abcdefghijklm@gmail.com tidyverse ...drive.readonly ecd11fa... # #' abcdefghijklm@gmail.com tidyverse ...bigquery, ...cloud-platform ece63f4... # #' nopqr@ABCDEFG.com tidyverse ...spreadsheets f178dd8... ## ----eval = FALSE------------------------------------------------------------- # install.packages("googlesheets4") ## ---- echo = FALSE, out.width = "400px"--------------------------------------- knitr::include_graphics("deleted_client.png") ## ----include = FALSE---------------------------------------------------------- # only run the chunk below in settings that are known to be safe, i.e. where # occasional, incidental failure is OK can_decrypt <- gargle::secret_has_key("GARGLE_KEY") ## ----eval = can_decrypt, purl = can_decrypt----------------------------------- library(gargle) req <- request_build( method = "GET", path = "webfonts/v1/webfonts", params = list( sort = "popularity" ), key = gargle_api_key(), base_url = "https://www.googleapis.com" ) resp <- request_make(req) out <- response_process(resp) lr <- gargle:::gargle_last_response() tmp <- tempfile("gargle-last-response-") saveRDS(lr, tmp) # you could share this .rds file with a colleague or the gargle maintainer # how it would look to complete the round trip, i.e. load this on the other end rt_lr <- readRDS(tmp) all.equal(lr, rt_lr) # clean up unlink("tmp") gargle/inst/doc/get-api-credentials.Rmd0000644000176200001440000003344214431310014017517 0ustar liggesusers--- title: "How to get your own API credentials" output: rmarkdown::html_vignette vignette: > %\VignetteIndexEntry{How to get your own API credentials} %\VignetteEngine{knitr::rmarkdown} %\VignetteEncoding{UTF-8} --- ```{r, include = FALSE} knitr::opts_chunk$set( collapse = TRUE, comment = "#>" ) ``` Here we describe how to obtain different types of credentials that can be important when working with a Google API: * API key (not relevant to all APIs) * OAuth 2.0 client ID and secret * Service account token * External account token ("workload identity federation") This can be important for both users and developers: * Package authors: If you are writing a package to wrap a Google API, you may provide some built-in auth assets so that things "just work" for your users. Regardless, you will need credentials to use during package development and in testing. * Package users: Wrapper packages may or may not provide default auth assets. If they don't, you are required to provide your own. Even if they do, you may prefer to bring your own, to have more control over your own destiny. With your own credentials, you avoid sharing quota with other users, which can reduce time-consuming errors and retries. If you use your own credentials, you are no longer at the mercy of someone else choosing to roll the credentials when you least expect it. * Everyone: The best method for auth in non-interactive settings is to use a service account token or workload identity federation, which require some advance setup. Note that most users of gargle-using packages do not need to read this and can just enjoy the automatic token flow. This article is for people who have a specific reason to be more proactive about auth. ## Get a Google Cloud Platform project You will need a Google Cloud Platform (GCP) project to hold your credentials. Go to the Google Cloud Platform Console: * * This may involve logging in or selecting your preferred Google identity. * This may involve selecting the relevant organization. This console is your general destination for inspecting and modifying your GCP projects. Create a new project here, if necessary. Otherwise, select the project of interest, if you have more than one. ## Enable API(s) Enable the relevant APIs(s) for your GCP project. In the left sidebar, navigate to *APIs & Services > Library*. Identify the API of interest. Click Enable. If you get this wrong, i.e. need to enable more APIs later, you can always come back and do this then. ## Think about billing For some APIs, you won't be able to do anything interesting with the credentials hosted in your project unless you have also linked a billing account. This is true, for example, for BigQuery and anything that has to do with Maps. This is NOT true, for example, for Drive or Sheets or Gmail. If your target API requires a billing account, that obviously raises the stakes for how you manage any API keys, OAuth clients, or service account tokens. Plan accordingly. If you're new to Google Cloud Platform, you'll get to enjoy [GCP Free Tier](https://cloud.google.com/free/). At the time of writing, this means you get a $300 credit and no additional billing will happen without your express consent. So there is a low-stress way to experiment with APIs, with a billing account enabled, without putting actual money on the line. ## API Key Some APIs accept requests to read public resources, in which case the request can be sent with an API key in lieu of a token. If this is possible, it's a good idea to expose this workflow in a wrapper package, because then your users can decide to go into a "de-authed" mode. When using the package in a non-interactive or indirect fashion (e.g. a scheduled job on a remote server or via Shiny), it is wonderful to NOT have to manage a token, if the work can be done with an API key instead. *Some APIs aren't really usable without a token, in which case an API key may not be relevant and you can ignore this section.* * From the GCP Console, in the target GCP Project, go to *APIs & Services > Credentials*. * Do *Create credentials > API key*. * You can capture the new API key via clipboard right away or close this pop-up and copy it later from the Credentials page. * In any case, I suggest you take the opportunity to edit the API key from the Credentials page and give it a nickname. Package maintainers might want to build an API key in as a fallback, possibly taking some measures to obfuscate the key and limit its use to your package. ### What does a user do with an API key? Package users could register an API key for use with a wrapper package. For example, in googlesheets4, one would use `googlesheets4::gs4_auth_configure()` to store a key for use in downstream requests, i.e. after a call to `googlesheets4::gs4_deauth()`: ```{r eval = FALSE} library(googlesheets4) gs4_auth_configure(api_key = "YOUR_API_KEY_GOES_HERE") gs4_deauth() # now you can read public resources, such as official example Sheets, # without any need for auth gs4_example("gapminder") %>% read_sheet() ``` ## OAuth client ID and secret Most APIs are used to create and modify resources on behalf of the user and these requests must include the user's token. A regular user will generally need to send an OAuth2 token, which is obtained under the auspices of an OAuth "app" or "client". This is called three-legged OAuth, where the 3 legs are the client, the user, and Google. The basic steps are described in the [Prerequisites section](https://developers.google.com/identity/protocols/oauth2/native-app) for doing Google OAuth 2.0 for Mobile & Desktop Apps: * From the GCP Console, in the target GCP Project, go to *APIs & Services > Credentials*. * Do *Create credentials > OAuth client ID*. * Select Application type, either "Desktop app" (the most common type used with gargle) or "Web application" (useful for the pseudo-OOB flow). * You can capture the client ID and secret via clipboard at this point or, as we recommend, download the full information as JSON. We recommend using the JSON, as this conveys the client type (desktop vs. web) and any redirect URIs (important for the web type). * At any time, you can navigate to a particular client ID and click "Download JSON". Two ways to package this info for use with gargle: 1. Use `gargle::gargle_oauth_client_from_json()`. This is the preferred workflow, because the JSON conveys the client type (desktop vs. web) and any redirect URIs (important for the web type). - Provide the path to the downloaded JSON file. 1. Use `gargle::gargle_oauth_client()`. In both cases, I suggest you devise a nickname for each OAuth credential and use it as the credential's name in GCP Console and as the `name` argument to `gargle::gargle_oauth_client_from_json()` or `gargle::gargle_oauth_client()`. Package maintainers might want to build this client in as a fallback, possibly taking some measures to obfuscate the client ID and secret and limit its use to your package. * Note that three-legged OAuth always requires the involvement of a user, so the word "secret" here can be somewhat confusing. It is not a secret in the same sense as a password or token. But you probably still want to store it in an opaque way, so that someone else cannot easily "borrow" it and present an OAuth consent screen that impersonates your package. ### What does a user do with an OAuth client? Package users could register their own client for use with a wrapper package. For example, in googledrive, one would use `googledrive::drive_auth_configure()` to do this: ```{r, eval = FALSE} library(googledrive) google_client <- gargle::gargle_oauth_client_from_json( path = "/path/to/the/JSON/that/was/downloaded/from/gcp/console.json", name = "acme-corp-google-client" ) drive_auth_configure(app = google_client) # now any new OAuth tokens are obtained with the configured client ``` ## Service account token For a long time, the recommended way to make authorized requests to an API in a non-interactive context was to use a service account token. As of April 2021, an alternative exists -- workload identity federation -- which is available to applications running on specific non-Google Cloud platforms, such as AWS. If you **can** use workload identity federation, you probably should (see the next section). But for those who can't, here we outline the use of a conventional service account. An official overview of service accounts is given in this [official documentation by Google](https://cloud.google.com/iam/docs/service-accounts?_ga=2.215917847.-1040593195.1558621244). But note that it's not necessary to understand all of that in order to use a service account token. * From the GCP Console, in the target GCP Project, go to *IAM & Admin > Service accounts*. * Give it a decent name and description. - For example, the service account used to create the googledrive docs has name "googledrive-docs" and description "Used when generating googledrive documentation". * Service account permissions. Whether you need to do anything here depends on the API(s) you are targetting. You can also modify roles later and iteratively sort this out. - For example, the service account used to create the googledrive docs does not have any explicit roles. - The service account used to test bigrquery has roles BigQuery Admin and Storage Admin. * Grant users access to this service account? So far, I have not done this, so feel free to do nothing here. Or if you know this is useful to you, then by all means do so. * Do *Create key* and download as JSON. This file is what we mean when we talk about a "service account token" in the documentation of gargle and packages that use gargle. `gargle::credentials_service_account()` expects the `path` to this file. * Appreciate that this JSON file holds sensitive information. Treat it like a username & password combo! This file holds credentials that potentially have a lot of power and that don't expire. * Consider storing this file in such a way that it will be automatically discovered by the Application Default Credentials search strategy. See `credentials_app_default()` for details. * You will notice the downloaded JSON file has an awful name, so sometimes I create a symlink that uses the service account's name, to make it easier to tell what this file is. * Remember to grant this service account the necessary permissions on any resources you plan to access, e.g., read or write permission on a specific Google Sheet. The service account has no formal relationship to you as a Google user and won't automatically inherit permissions. Authors of wrapper packages can use the symmetric encryption strategy described in [Managing tokens securely](https://gargle.r-lib.org/articles/articles/managing-tokens-securely.html) to use this token on remote servers, such as continuous integration services like GitHub Actions. ### What does a user do with a service account token? You could provide the token's filepath to a wrapper package's main auth function, e.g.: ```{r eval = FALSE} # googledrive drive_auth(path = "/path/to/your/service-account-token.json") ``` Alternatively, you could put the token somewhere (or store its location in an environment variable) so that it is auto-discovered by the [Application Default Credentials](https://gargle.r-lib.org/articles/how-gargle-gets-tokens.html#credentials_app_default) search strategy. ## Workload identity federation Workload identity federation is a new (as of April 2021) keyless authentication mechanism that allows applications running on a non-Google Cloud platform, such as AWS, to access Google Cloud resources without using a conventional service account token. This eliminates the dilemma of how to safely manage service account credential files. Unlike service accounts, the configuration file for workload identity federation contains no secrets. Instead, it holds non-sensitive metadata. The external application obtains the needed sensitive data "on-the-fly" from the running instance. The combined data is then used for a token exchange that ultimately yields a short-lived GCP access token. This access token allows the external application to impersonate a service account and inherit the permissions of the service account to access GCP resources. So what's not to love? Well, first, this auth flow is only available if your code is running on AWS, Azure, or another platform that supports the OpenID Connect protocol. Second, there's a non-trivial amount of pre-configuration necessary on both ends. But once that is done, you can download a configuration file that makes auth work automagically with gargle. This feature is still experimental in gargle and **currently only supports AWS**. For more, see the documentation for `credentials_external_account()`. Like conventional service account tokens, workload identity federation is a great fit for the Application Default Credentials strategy for discovering credentials. See `credentials_app_default()` for more about that. These two links provide, respectively, a high-level overview and step-by-step instructions for this flow: * Blog post: [Keyless API authentication — Better cloud security through workload identity federation, no service account keys necessary](https://cloud.google.com/blog/products/identity-security/enable-keyless-access-to-gcp-with-workload-identity-federation/) * Documentation: [Configuring workload identity federation](https://cloud.google.com/iam/docs/configuring-workload-identity-federation) ## Further reading Learn more in Google's documentation: * Credentials, access, security, and identity: `https://support.google.com/googleapi/answer/6158857?hl=en&ref_topic=7013279` * [Using OAuth 2.0 for Installed Applications](https://developers.google.com/identity/protocols/oauth2/native-app) gargle/inst/doc/request-helper-functions.html0000644000176200001440000010174714456275117021124 0ustar liggesusers Request helper functions

Request helper functions

This vignette explains the purpose and usage of:

  • request_develop(endpoint, params, base_url)
  • request_build(method, path, params, body, token, key, base_url)
  • request_make(x, ..., user_agent)

The target audience is someone writing an R package to wrap a Google API.

library(gargle)

Why use gargle’s request helpers?

Why would the developer of a Google-API-wrapping package care about the request helpers in gargle?

You can write less code and safer code, in return for a modest investment in studying your target API. That is done by ingesting the API’s so-called Discovery Document.

Hundreds of Google APIs – the ones addressed by the API Discovery Service – share a great deal of behaviour. By ingesting the metadata provided by this service, you can use gargle’s request helpers to exploit this shared data and logic, while also decreasing the chance that you and your users will submit ill-formed requests.

The request helpers in gargle check the combined inputs from user and developer against suitably prepared API metadata:

  • If required parameters are missing, an error is thrown.
  • If unrecognized parameters are submitted, an error is thrown.
  • Parameters are automatically placed in their correct location: URL substitution, query, or body.
  • Is there something else you care about? It is possible to do more, but it would help to have concrete requests.

Google provides API libraries for several languages, including Java, Go, Python, JavaScript, Ruby and more (but not R). All of these libraries are machine-generated from the metadata provided by the API Discovery Service. It is the official recommendation to use the Discovery Document when building client libraries. The gargle package aims to implement key parts of this strategy, in a way that is also idiomatic for R and its developers.

High-level design pattern

gargle facilitates this design for API-wrapping packages:

  • A machine-assisted low-level interface driven by the Discovery Document:
    • Your package exports thin wrapper functions around gargle’s helpers to form and make HTTP requests, that inject package-specific logic and data, such as an API key and user agent. This is for power users and yourself.
  • High-level, task-oriented, user-facing functions that constitute the main interface of your package.
    • These functions convert user input into the form required by the API and pass it along to your low-level interface functions.

Later, specific examples are given, using the googledrive package.

gargle’s HTTP request helpers

gargle provides support for creating and sending HTTP requests via these functions:

request_develop(endpoint, params, base_url): a.k.a. The Smart One.

  • Processes the info in params relative to detailed knowledge about the endpoint, derived from an API Discovery Document.
  • Checks for required and unrecognized parameters.
  • Peels off params destined for the body into their own part.
  • Returns request data in a form that anticipates the httr::VERB() call that is on the horizon.

request_build(method, path, params, body, token, key, base_url): a.k.a. The Dumb One.

  • Typically consumes the output of request_develop(), although that is not required. It can be called directly to enjoy a few luxuries even when making one-off API calls in the absence of an ingested Discovery Document.
  • Integrates params into a URL via substitution and the query string.
  • Sends either an API key or an OAuth token, but it provides no default values or logic for either.

request_make(x, ..., user_agent): actually makes the HTTP request.

  • Typically consumes the output of request_build(), although that is not required. However, if you have enough info to form a request_make() request, you would probably just make the httr::VERB() call yourself.
  • Consults x$method to determine which httr::VERB() to call, then calls it with the rest of x, ..., and user_agent passed as arguments.

They are usually called in the above order, though they don’t have to be used that way. It is also fine to ignore this part of gargle and use it only for help with auth. They are separate parts of the package.

Discovery Documents

Google’s API Discovery Service “provides a lightweight, JSON-based API that exposes machine-readable metadata about Google APIs”. We recommend ingesting this metadata into an R list, stored as internal data in an API-wrapping client package. Then, HTTP requests inside high-level functions can be made concisely and safely, by referring to this metadata. The combined use of this data structure and gargle’s request helpers can eliminate a lot of boilerplate data and logic that are shared across Google APIs and across endpoints within an API.

The gargle package ships with some functions and scripts to facilitate the ingest of a Discovery Document. You can find these files in the gargle installation like so:

ddi_dir <- system.file("discovery-doc-ingest", package = "gargle")
list.files(ddi_dir)
#>  [1] "api-wide-parameter-names.txt"    "api-wide-parameters-humane.txt" 
#>  [3] "api-wide-parameters.csv"         "discover-discovery.R"           
#>  [5] "drive-example.R"                 "ingest-functions.R"             
#>  [7] "method-properties-humane.txt"    "method-properties.csv"          
#>  [9] "method-property-names.txt"       "parameter-properties-humane.txt"
#> [11] "parameter-properties.csv"        "parameter-property-names.txt"

Main files of interest to the developer of a client package:

  • ingest-functions.R is a collection of functions for downloading and ingesting a Discovery Document.
  • drive-example.R uses those functions to ingest metadata on the Drive v3 API and store it as an internal data object for use in googledrive.

The remaining files present an analysis of the Discovery Document for the Discovery API itself (very meta!) and write files that are useful for reference. Several are included at the end of this vignette.

Why aren’t the ingest functions exported by gargle? First, we regard this as functionality that is needed at development time, not install or run time. This is something you’ll do every few months, probably associated with preparing a release of a wrapper package. Second, the packages that are useful for wrangling JSON and lists are not existing dependencies of gargle, so putting these function in gargle would require some unappealing compromises.

Method (or endpoint) data

Our Discovery Document ingest process leaves you with an R list. Let’s assume it’s available in your package’s namespace as an internal object named .endpoints. Each item represents one method of the API (Google’s vocabulary) or an endpoint (gargle’s vocabulary).

Each endpoint has an id. These ids are also used as names for the list. Examples of some ids from the Drive and Sheets APIs:

drive.about.get
drive.files.create
drive.teamdrives.list
sheets.spreadsheets.create
sheets.spreadsheets.values.clear
sheets.spreadsheets.sheets.copyTo

Retrieve the metadata for one endpoint by name, e.g.:

.endpoints[["drive.files.create"]]

That info can be passed along to request_develop(endpoint, params, base_url), which conducts sanity checks and combines this external knowledge with the data coming from the user and developer via params.

Design suggestion: forming requests

Here’s the model used in googledrive. There is a low-level request helper, googledrive::request_generate(), that is used to form every request in the package. It is exported as part of a low-level API for expert use, but most users will never know it exists.

# googledrive::
request_generate <- function(endpoint = character(),
                             params = list(),
                             key = NULL,
                             token = drive_token()) {
  ept <- .endpoints[[endpoint]]
  if (is.null(ept)) {
    stop_glue("\nEndpoint not recognized:\n  * {endpoint}")
  }

  ## modifications specific to googledrive package
  params$key <- key %||% params$key %||% drive_api_key()
  if (!is.null(ept$parameters$supportsTeamDrives)) {
    params$supportsTeamDrives <- TRUE
  }

  req <- gargle::request_develop(endpoint = ept, params = params)
  gargle::request_build(
    path = req$path,
    method = req$method,
    params = req$params,
    body = req$body,
    token = token
  )
}

The endpoint argument specifies an endpoint by its name, a.k.a. its id.

params is where the processed user input goes.

key and token refer to an API key and OAuth2 token, respectively. Both can be populated by default, but it is possible to pass them explicitly. If your package ships with a default API key, you should append it above as the final fallback value for params$key.

Do not “borrow” an API key from gargle or another package; always send a key associated with your package or provided by your user. Per the Google User Data Policy https://developers.google.com/terms/api-services-user-data-policy, your application must accurately represent itself when authenticating to Google API services.

After googledrive::request_generate() takes care of everything specific to the Drive API and the user’s input and task, we call gargle::request_develop(). We finish preparing the request with gargle::request_build(), which enforces the rule that we always send exactly one of key and token.

Design suggestion: making requests

The output of gargle::request_build() specifies an HTTP request.

gargle::request_make() can be used to actually execute it.

# gargle::
request_make <- function(x, ..., user_agent = gargle_user_agent()) {
  stopifnot(is.character(x$method))
  method <- switch(
    x$method,
    GET    = httr::GET,
    POST   = httr::POST,
    PATCH  = httr::PATCH,
    PUT    = httr::PUT,
    DELETE = httr::DELETE,
    abort(glue("Not a recognized HTTP method: {bt(x$method)}"))
  )
  method(
    url = x$url,
    body = x$body,
    x$token,
    user_agent,
    ...
  )
}

request_make() consults x$method to identify the httr::VERB() and then calls it with the remainder of x, ... and the user_agent.

In googledrive we have a thin wrapper around this that injects the googledrive user agent:

# googledrive::
request_make <- function(x, ...) {
  gargle::request_make(x, ..., user_agent = drive_ua())
}

Reference

derived from the Discovery Document for the Discovery Service

Properties of an endpoint

description             string  Description of this method.
etagRequired            boolean Whether this method requires an ETag to be
                                specified. The ETag is sent as an HTTP If-
                                Match or If-None-Match header.
httpMethod              string  HTTP method used by this method.
id                      string  A unique ID for this method. This property
                                can be used to match methods between
                                different versions of Discovery.
mediaUpload             object  Media upload parameters.
parameterOrder          array   Ordered list of required parameters, serves
                                as a hint to clients on how to structure
                                their method signatures. The array is ordered
                                such that the "most-significant" parameter
                                appears first.
parameters              object  Details for all parameters in this method.
path                    string  The URI path of this REST method. Should
                                be used in conjunction with the basePath
                                property at the api-level.
request                 object  The schema for the request.
response                object  The schema for the response.
scopes                  array   OAuth 2.0 scopes applicable to this method.
supportsMediaDownload   boolean Whether this method supports media downloads.
supportsMediaUpload     boolean Whether this method supports media uploads.
supportsSubscription    boolean Whether this method supports subscriptions.
useMediaDownloadService boolean Indicates that downloads from this method
                                should use the download service URL (i.e.
                                "/download"). Only applies if the method
                                supports media download.

API-wide endpoint parameters (taken from Discovery API but, empirically, are shared with other APIs):

alt         string  Data format for the response.
fields      string  Selector specifying which fields to include
                    in a partial response.
key         string  API key. Your API key identifies your project
                    and provides you with API access, quota, and
                    reports. Required unless you provide an OAuth
                    2.0 token.
oauth_token string  OAuth 2.0 token for the current user.
prettyPrint boolean Returns response with indentations and line
                    breaks.
quotaUser   string  An opaque string that represents a user
                    for quota purposes. Must not exceed 40
                    characters.
userIp      string  Deprecated. Please use quotaUser instead.

Properties of an endpoint parameters:

$ref                 string  A reference to another schema. The value of
                             this property is the "id" of another schema.
additionalProperties NULL    If this is a schema for an object, this
                             property is the schema for any additional
                             properties with dynamic keys on this object.
annotations          object  Additional information about this property.
default              string  The default value of this property (if one
                             exists).
description          string  A description of this object.
enum                 array   Values this parameter may take (if it is an
                             enum).
enumDescriptions     array   The descriptions for the enums. Each position
                             maps to the corresponding value in the "enum"
                             array.
format               string  An additional regular expression or key that
                             helps constrain the value. For more details
                             see: http://tools.ietf.org/html/draft-zyp-
                             json-schema-03#section-5.23
id                   string  Unique identifier for this schema.
items                NULL    If this is a schema for an array, this
                             property is the schema for each element in
                             the array.
location             string  Whether this parameter goes in the query or
                             the path for REST requests.
maximum              string  The maximum value of this parameter.
minimum              string  The minimum value of this parameter.
pattern              string  The regular expression this parameter must
                             conform to. Uses Java 6 regex format: http://
                             docs.oracle.com/javase/6/docs/api/java/util/
                             regex/Pattern.html
properties           object  If this is a schema for an object, list the
                             schema for each property of this object.
readOnly             boolean The value is read-only, generated by the
                             service. The value cannot be modified by the
                             client. If the value is included in a POST,
                             PUT, or PATCH request, it is ignored by the
                             service.
repeated             boolean Whether this parameter may appear multiple
                             times.
required             boolean Whether the parameter is required.
type                 string  The value type for this schema. A list
                             of values can be found here: http://
                             tools.ietf.org/html/draft-zyp-json-
                             schema-03#section-5.1
variant              object  In a variant data type, the value of
                             one property is used to determine how to
                             interpret the entire entity. Its value must
                             exist in a map of descriminant values to
                             schema names.
gargle/inst/doc/gargle-auth-in-client-package.Rmd0000644000176200001440000004371714433520365021373 0ustar liggesusers--- title: "How to use gargle for auth in a client package" output: rmarkdown::html_vignette vignette: > %\VignetteIndexEntry{How to use gargle for auth in a client package} %\VignetteEngine{knitr::rmarkdown} %\VignetteEncoding{UTF-8} --- ```{r, include = FALSE} knitr::opts_chunk$set( collapse = TRUE, comment = "#>" ) ``` gargle provides common infrastructure for use with Google APIs. This vignette describes one possible design for using gargle to deal with auth, in a client package that provides a high-level wrapper for a specific API. There are frequent references to [googledrive](https://googledrive.tidyverse.org), which uses the design described here, along with [bigrquery](https://bigrquery.r-dbi.org) (v1.2.0 and higher), [gmailr](https://gmailr.r-lib.org) (v1.0.0 and higher), and [googlesheets4](https://googlesheets4.tidyverse.org) (the successor to [googlesheets](https://github.com/jennybc/googlesheets)). ## Key choices Getting a token requires several pieces of information and there are stark differences in how much users (need to) know or control about this process. Let's review them, with an eye towards identifying the responsibilities of the package author versus the user. * Overall config: OAuth client and API key. Who provides? * Token-level properties: Google identity (email) and scopes. * Request-level: Who manages tokens and injects them into requests? ### User-facing auth In googledrive, the main user-facing auth function is `googledrive::drive_auth()`. Here is its definition (at least approximately, remember this is static code): ```{r, eval = FALSE} # googledrive:: drive_auth <- function(email = gargle::gargle_oauth_email(), path = NULL, scopes = "https://www.googleapis.com/auth/drive", cache = gargle::gargle_oauth_cache(), use_oob = gargle::gargle_oob_default(), token = NULL) { # this catches a common error, where the user passes JSON for an OAuth client # to the `path` argument, which only expects a service account token gargle::check_is_service_account(path, hint = "drive_auth_configure") cred <- gargle::token_fetch( scopes = scopes, client = drive_oauth_client() %||% , email = email, path = path, package = "googledrive", cache = cache, use_oob = use_oob, token = token ) if (!inherits(cred, "Token2.0")) { # throw an informative error here } .auth$set_cred(cred) .auth$set_auth_active(TRUE) invisible() } ``` `drive_auth()` is called automatically upon the first need of a token and that can lead to user interaction, but does not necessarily do so. `drive_auth()` can be called explicitly by the user, but usually that is not necessary. `token_fetch()` is described in the `vignette("how-gargle-gets-tokens")`. The internal `.auth` object maintains googledrive's auth state and is explained next. ### Auth state A client package can use an internal object of class `gargle::AuthState` to hold the auth state. In googledrive, the main auth file defines a placeholder `.auth` object: ```{r eval = FALSE} .auth <- NULL ``` The actual initialization happens in `.onLoad()`: ```{r} .onLoad <- function(libname, pkgname) { utils::assignInMyNamespace( ".auth", gargle::init_AuthState(package = "googledrive", auth_active = TRUE) ) # other stuff } ``` The initialization of `.auth` is done this way to ensure that we get an instance of the `AuthState` class using the current, installed version of gargle (vs. the ambient version from whenever gargle was built, perhaps by CRAN). An `AuthState` instance has other fields which, in this googledrive example, are not set at this point. The OAuth `client` and `api_key` are configurable by the user and, when `NULL`, downstream functions can fall back to internal credentials. The `cred` field is populated by the first call to `drive_auth()` (direct or indirectly via `drive_token()`). ### OAuth client Most users should present OAuth user credentials to Google APIs. However, most users would love to be spared the fiddly details surrounding this. The OAuth client is one example. (Historically, following the lead of the httr package, we have used the term OAuth *app*, but we now use the term OAuth *client*.) The client is a component that most users do not even know about and they are content to use the same client for all work through a wrapper package: possibly, the client built into the package. There is a field in the `.auth` auth state to hold the OAuth `client`. Exported auth helpers, `drive_oauth_client()` and `drive_auth_configure()`, retrieve and modify the current client to support users who want to (or must) take that level of control. ```{r, eval = FALSE} library(googledrive) # first: download the OAuth client as a JSON file drive_auth_configure( path = "/path/to/the/JSON/that/was/downloaded/from/gcp/console.json" ) drive_oauth_client() #> #> name: acme-corp-google-client #> id: 123456789.apps.googleusercontent.com #> secret: #> type: installed #> redirect_uris: http://localhost ``` Do not "borrow" an OAuth client ID and secret from gargle or any other package; always use credentials associated with your package or provided by your user. Per the Google User Data Policy , your application must accurately represent itself when authenticating to Google API services. Some APIs and scopes are considered so sensitive that is essentially impossible for a package to provide a built-in OAuth client. Users **must** get and configure their own client. Among the packages mentioned as examples, this is true of gmailr. ### API key Some Google APIs can be used in an unauthenticated state, if and only if requests include an API key. For example, this is a great way to read a Google Sheet that is world-readable or readable by "anyone with a link" from a Shiny app, thereby designing away the need to manage user credentials on the server. The user can provide their own API key via `drive_auth_configure(api_key =)` and retrieve that value with `drive_api_key()`, just as with the OAuth client. The API key is stored in the `api_key` field of the `.auth` auth state. ```{r, eval = FALSE} library(googledrive) drive_auth_configure(api_key = "123456789") drive_api_key() #> "123456789" ``` Many users aren't motivated to take this level of control and appreciate when a package provides a built-in default API key. As with the client, packages should obtain their own API key and not borrow the gargle or tidyverse key. Some APIs are not usable without a token, in which case a wrapper package may not even expose functionality for managing an API key. Among the packages mentioned as examples, this is true of bigrquery. ### Email or Google identity In contrast to the OAuth client and API key, every user must express which identity they wish to present to the API. This is a familiar concept and users expect to specify this. Since users may have more than one Google account, it's quite likely that they will want to switch between accounts, even within a single R session, or that they might want to explicitly declare the identity to be used in a specific script or app. That explains why `drive_auth()` has the optional `email` argument that lets users proactively specify their identity. `drive_auth()` is usually called indirectly upon first need, but a user can also call it proactively in order to specify their target `email`: ```{r eval = FALSE} # googledrive:: drive_auth(email = "janedoe_work@gmail.com") ``` If `email` is not given, gargle also checks for an option named `"gargle_oauth_email"`. The `email` is used to look up tokens in the cache and, if no suitable token is found, it is used to pre-configure the OAuth chooser in the browser. Read more in the help for `gargle::gargle_oauth_email()`. ### Scopes Most users have no concept of scopes. They just know they want to work with, e.g., Google Drive or Google Sheets. A client package can usually pick sensible default scopes, that will support what most users want to do. Here's a reminder of the signature of `googledrive::drive_auth()`: ```{r, eval = FALSE} # googledrive:: drive_auth <- function(email = gargle::gargle_oauth_email(), path = NULL, scopes = "https://www.googleapis.com/auth/drive", cache = gargle::gargle_oauth_cache(), use_oob = gargle::gargle_oob_default(), token = NULL) { ... } ``` googledrive ships with a default scope, but a motivated user could call `drive_auth()` preemptively at the start of the session and request different scopes. For example, if they intend to only read data and want to guard against inadvertent file modification, they might opt for the `drive.readonly` scope. ```{r, eval = FALSE} # googledrive:: drive_auth(scopes = "https://www.googleapis.com/auth/drive.readonly") ``` ### OAuth cache and Out-of-bound auth The location of the token cache and whether to prefer out-of-bound auth are two aspects of OAuth where most users are content to go along with sensible default behavior. For those who want to exert control, that can be done in direct calls to `drive_auth()` or by configuring an option. Read the help for `gargle::gargle_oauth_cache()` and `gargle::gargle_oob_default()` and `vignette("auth-from-web")` for more. ## Overview of mechanics Here's a concrete outline of how one could set up a client package to get its auth functionality from gargle. 1. Add gargle to your package's `Imports`. 1. Create a file `R/YOURPKG_auth.R`. 1. Create an internal `gargle::AuthClass` object to hold auth state. Follow the googledrive example above. 1. Define standard functions for the auth interface between gargle and your package; do this in `R/YOURPKG_auth.R`. Examples: [`tidyverse/googledrive/R/drive_auth.R`](https://github.com/tidyverse/googledrive/blob/main/R/drive_auth.R) and [`r-dbi/bigrquery/R/bq_auth.R`](https://github.com/r-dbi/bigrquery/blob/main/R/bq-auth.R). 1. Use gargle's roxygen helpers to create the docs for your auth functions. This relieves you from writing docs and you inherit standard wording. See previously cited examples for inspiration. 1. Use the functions `YOURPKG_token()` and `YOURPKG_api_key()` (defined in the standard auth interface) to insert a token or API key in your package's requests. ## Getting that first token I focus on early use, by the naive user, with the OAuth flow. When the user first calls a high-level googledrive function such as `drive_find()`, a Drive request is ultimately generated with a call to `googledrive::request_generate()`. Here is its definition, at least approximately: ```{r eval = FALSE} # googledrive:: request_generate <- function(endpoint = character(), params = list(), key = NULL, token = drive_token()) { ept <- drive_endpoint(endpoint) if (is.null(ept)) { # throw error about unrecognized endpoint } ## modifications specific to googledrive package params$key <- key %||% params$key %||% drive_api_key() %||% if (!is.null(ept$parameters$supportsAllDrives)) { params$supportsAllDrives <- TRUE } req <- gargle::request_develop(endpoint = ept, params = params) gargle::request_build( path = req$path, method = req$method, params = req$params, body = req$body, token = token ) } ``` `googledrive::request_generate()` is a thin wrapper around `gargle::request_develop()` and `gargle::request_build()` that only implements details specific to googledrive, before delegating to more general functions in gargle. The `vignette("request-helper-functions")` documents these gargle functions. `googledrive::request_generate()` gets a token with `drive_token()`, which is defined like so: ```{r eval = FALSE} # googledrive:: drive_token <- function() { if (isFALSE(.auth$auth_active)) { return(NULL) } if (!drive_has_token()) { drive_auth() } httr::config(token = .auth$cred) } ``` where `drive_has_token()` in a helper defined as: ```{r eval = FALSE} # googledrive:: drive_has_token <- function() { inherits(.auth$cred, "Token2.0") } ``` By default, auth is active, and, for a fresh start, we won't have a token stashed in `.auth` yet. So this will result in a call to `drive_auth()` to obtain a credential, which is then cached in `.auth$cred` for the remainder of the session. All subsequent calls to `drive_token()` will just spit back this token. Above, we discussed scenarios where an advanced user might call `drive_auth()` proactively, with non-default arguments, possibly even loading a service token or using alternative flows, like an external account. Any token loaded in that way is stashed in `.auth$cred` and will be returned by subsequent calls to `drive_token()`. Multiple gargle-using packages can use a shared token by obtaining a suitably scoped token with one package, then registering that token with the other packages. For example, the default scope requested by googledrive is also sufficient for operations available in googlesheets4. You could use a shared token like so: ```{r eval = FALSE} library(googledrive) library(googlesheets4) drive_auth(email = "jane_doe@example.com") # gets a suitably scoped token # and stashes for googledrive use gs4_auth(token = drive_token()) # registers token with googlesheets4 # now work with both packages freely ... ``` It is important to make sure that the token-requesting package (googledrive, above) is using an OAuth client for which all the necessary APIs and scopes are enabled. ## Auth interface The exported functions like `drive_auth()`, `drive_token()`, etc. constitute the auth interface between googledrive and gargle and are centralized in [`tidyverse/googledrive/R/drive_auth.R`](https://github.com/tidyverse/googledrive/blob/main/R/drive_auth.R). That is a good template for how to use gargle to manage auth in a client package. In addition, the docs for these gargle-backed functions are generated automatically from standard information maintained in the gargle package. * `drive_token()` retrieves the current credential, in a form that is ready for inclusion in HTTP requests. If `auth_active` is `TRUE` and `cred` is `NULL`, `drive_auth()` is called to obtain a credential. If `auth_active` is `FALSE`, `NULL` is returned; client packages should be designed to fall back to including an API key in affected HTTP requests, if sensible for the API. * `drive_auth()` ensures we are dealing with an authenticated user and have a credential on hand with which to place authorized requests. Sets `auth_active` to `TRUE`. Can be called directly, but `drive_token()` will also call it as needed. * `drive_deauth()` clears the current token. It might also toggle `auth_active`, depending on the features of the target API. See below. * `drive_oauth_client()` returns `.auth$client`. * `drive_api_key()` returns `.auth$api_key`. * `drive_auth_configure()` can be used to configure auth. This is how an advanced user would enter their own OAuth client and API key into the auth config, in order to affect all subsequent requests. * `drive_user()` reports some information about the user associated with the current token. The Drive API offers an actual endpoint for this, which is not true for most Google APIs. Therefore the analogous function in bigrquery, `bq_user()` is a better general reference. ## De-auth APIs split into two classes: those that can be used, at least partially, without a token and those that cannot. If an API is usable without a token -- which is true for the Drive API -- such requests must include an API key. Therefore, the auth design for a client package is different for these two types of APIs. For an API that can be used without a token: `drive_deauth()` can be used at any time to enter a de-authorized state. It sets `auth_active` to `FALSE` and `.auth$cred` to `NULL`. In this state, requests are sent out with an API key and no token. This is a great way to eliminate any friction re: auth if there's no need for it, i.e. if all requests are for resources that are world readable or available to anyone who knows how to ask for it, such as files shared via "Anyone with the link". The de-authorized state is especially useful in non-interactive settings or where user interaction is indirect, such as via Shiny. For an API that cannot be used without a token: BigQuery is an example of such an API. `bq_deauth()` just clears the current token, so that the auth flow starts over the next time a token is needed. ## Bring Your Own Client and Key Advanced users can use their own OAuth client and API key. `drive_auth_configure()` lives in `R/drive_auth.R` and it provides the ability to modify the current `client` and `api_key`. Recall that `drive_oauth_client()` and `drive_api_key()` also exist for targeted, read-only access. The `vignette("get-api-credentials")` describes how to get an API key and OAuth client. Packages that always send a token will omit the API key functionality here. ## Changing identities (and more) One reason for a user to call `drive_auth()` directly and proactively is to switch from one Google identity to another or to make sure they are presenting themselves with a specific identity. `drive_auth()` accepts an `email` argument, which is honored when gargle determines if there is already a suitable token on hand. Here is a sketch of how a user could switch identities during a session, possibly non-interactive: ```{r eval = FALSE} library(googledrive) drive_auth(email = "janedoe_work@gmail.com") # do stuff with Google Drive here, with Jane Doe's "work" account drive_auth(email = "janedoe_personal@gmail.com") # do other stuff with Google Drive here, with Jane Doe's "personal" account drive_auth(path = "/path/to/a/service-account.json") # do other stuff with Google Drive here, using a service account ``` gargle/inst/doc/how-gargle-gets-tokens.Rmd0000644000176200001440000005021714431310014020172 0ustar liggesusers--- title: "How gargle gets tokens" output: rmarkdown::html_vignette vignette: > %\VignetteIndexEntry{How gargle gets tokens} %\VignetteEngine{knitr::rmarkdown} %\VignetteEncoding{UTF-8} --- ```{r, include = FALSE} knitr::opts_chunk$set( collapse = TRUE, comment = "#>" ) ``` This vignette explains the purpose and usage of `token_fetch()` and the functions it subsequently calls. The goal of `token_fetch()` is to secure a token for use in downstream requests. The target audience is someone who works directly with a Google API. These people roughly fall into two camps: * The author of an R package that wraps a Google API. * The useR who is writing a script or app, without using such a wrapper, either because the wrapper does not exist or there's a reason to avoid the dependency. This vignette might also be useful to the user of a wrapper package who needs to influence the operations of `token_fetch()`, e.g. by telling it to try auth methods in a non-default order or to not try certain methods at all. `token_fetch()` is aimed at whoever is going to manage the returned token, e.g., incorporate it into downstream requests. It can be very nice for users if wrapper packages assume this responsibility, as opposed to requiring users to explicitly acquire and manage their tokens. We give a few design suggestions here and cover this in more depth in `vignette("gargle-auth-in-client-package")`. ```{r setup} library(gargle) ``` ## `token_fetch()` `token_fetch()` is a rather magical function for getting a token. The goal is to make auth relatively painless for users, while allowing developers and power users to take control when and if they need to. Most users will presumably interact with `token_fetch()` only in an indirect way, mediated through an API wrapper package. That is not because the interface of `token_fetch()` is unfriendly -- it's very flexible! The objective of `token_fetch()` is to allow package developers to take responsibility for *managing* the user's token, without having to implement all the different ways of *obtaining* that token in the first place. The signature of `token_fetch()` is very simple and, therefore, not very informative: ```{r, eval = FALSE} token_fetch(scopes, ...) ``` Under the hood, `token_fetch()` calls a sequence of much more specific credential functions, each wrapped in a `tryCatch()` and returning `NULL` if unsuccessful. The only formal argument these functions have in common is `scopes`, with the rest being passed via `...`. This gives a sense of the credential functions and reflects the order in which they are called: ```{r} writeLines(names(cred_funs_list())) ``` It is possible to manipulate this registry of functions. The help for `cred_funs_list()` is a good place to learn more and we present a concrete example in the last section of this vignette. For now, however, we assume you're working with the default registry that ships with gargle. Note also that these credential functions are exported and can be called directly. ## Get verbose output To see more information about what gargle is up to, set the `"gargle_verbosity"` option to "debug". Read more in the docs for `gargle_verbosity()`. ## `credentials_byo_oauth2()` The first function tried is `credentials_byo_oauth2()`. Here's how a call to `token_fetch()` might work: ```{r, eval = FALSE} token_fetch(token = ) credentials_byo_oauth2( token = ) ``` `credentials_byo_oauth2()` provides a back door for a "bring your own token" workflow. This function accounts for the scenario where an OAuth token has been obtained through external means and it's convenient to be able to put it into force. `credentials_byo_oauth2()` checks that `token` is of class `httr::Token2.0` and that it appears to be associated with Google. A `token` of class `request` is also acceptable, in which case the `auth_token` component is extracted and treated as the input. This is how a `Token2.0` object would present, if processed with `httr::config()`, as functions like `googledrive::drive_token()` and `bigrquery::bq_token()` do. If `token` is not provided or if it doesn't satisfy these requirements, we fail and `token_fetch()`'s execution moves on to the next function in the registry. ## `credentials_service_account()` The next function tried is `credentials_service_account()`. Here's how a call to `token_fetch()` with service account inputs plays out: ```{r, eval = FALSE} token_fetch(scopes = , path = "/path/to/your/service-account.json") # credentials_byo_oauth2() fails because no `token`, # which leads to this call: credentials_service_account( scopes = , path = "/path/to/your/service-account.json" ) ``` The `scopes` are often provided by the API wrapper function that is mediating the calls to `token_fetch()` and `credential_service_account()`. The `path` argument is presumably coming from the user. It is treated as a JSON representation of service account credentials, in any form that is acceptable to `jsonlite::fromJSON()`. In the above example, that is a file path, but it could also be a JSON string. If there is no named `path` argument or if it can't be parsed as a service account credential, we fail and `token_fetch()`'s execution moves on to the next function in the registry. Here is some Google documentation about service accounts: * [Cloud Identity and Access Management > Understanding service accounts](https://cloud.google.com/iam/docs/understanding-service-accounts) For R users, a service account is a great option for credentials that will be used in a script or application running remotely or in an unattended fashion. In particular, this is a better approach than trying to move OAuth2 credentials from one machine to another. For example, a service account is the preferred method of auth when testing and documenting a package on a continuous integration service. The JSON key file must be managed securely. In particular, it should not be kept in, e.g., a GitHub repository (unless it is encrypted). The encryption strategy used by gargle and other packages is described in the article [Managing tokens securely](https://gargle.r-lib.org/articles/articles/managing-tokens-securely.html). Note that fetching a token for a service account requires a reasonably accurate system clock. This is of particular importance for users running gargle inside a Docker container, as Docker for Windows has [intermittently seen problems with clock drift](https://github.com/docker/for-win/issues/4526). If your service account token requests fail with "Bad Request" inside a container, but succeed locally, check that the container's system clock is accurate. ## `credentials_external_account()` The next function tried is `credentials_external_account()`. Here's how a call to `token_fetch()` with an external account inputs plays out: ```{r, eval = FALSE} token_fetch(scopes = , path = "/path/to/your/external-account.json") # credentials_byo_oauth2() fails because no `token`, # credentials_service_account() fails because the JSON provided via # `path` is not of type "service_account", # which leads to this call: credentials_external_account( scopes = , path = "/path/to/your/external-account.json" ) ``` `credentials_external_account()` implements something called *workload identity federation* and is available to applications running on specific non-Google Cloud platforms. At the time of writing, gargle only supports AWS, but this could be expanded to other providers, such as Azure, if there is a documented need. Similar to `credentials_service_account()`, the `path` is treated as a JSON representation of the account's configuration and it's probably a file path. However, in contrast to `credentials_service_account()`, this JSON only contains non-sensitive metadata, which is, indeed, the main point of this flow. The secrets needed to complete auth are obtained "on-the-fly" from, e.g., the running EC2 instance. `credentials_external_account()` will fail for many reasons: there is no named `path` argument, the JSON at `path` can't be parsed as configuration for an external AWS account, we don't appear to running on AWS, suggested packages for AWS functionality are not installed, or the workload identity pool is misconfigured. If any of that happens, we fail and `token_fetch()`'s execution moves on to the next function in the registry. Here is some Google documentation about workload identity federation and the specifics for AWS: * Blog post: [Keyless API authentication — Better cloud security through workload identity federation, no service account keys necessary](https://cloud.google.com/blog/products/identity-security/enable-keyless-access-to-gcp-with-workload-identity-federation/) * Documentation: [Configuring workload identity federation](https://cloud.google.com/iam/docs/configuring-workload-identity-federation) ## `credentials_app_default()` The next function tried is `credentials_app_default()`. Here's how a call to `token_fetch()` might work: ```{r, eval = FALSE} token_fetch(scopes = ) # credentials_byo_oauth2() fails because no `token`, # credentials_service_account() fails because no `path`, # credentials_external_account() fails because no `path`, # which leads to this call: credentials_app_default( scopes = ) ``` `credentials_app_default()` loads credentials from a file identified via a search strategy known as [Application Default Credentials (ADC)](https://cloud.google.com/docs/authentication#adc). The credentials themselves are conventional service account, external account, or user credentials that happen to be stored in a pre-ordained location and format. The hope is to make auth "just work" for someone working on Google-provided infrastructure or who has used Google tooling to get started, such as the [`gcloud` command line tool](https://cloud.google.com/sdk/gcloud). A sequence of paths is consulted, which we describe here, with some abuse of notation. ALL_CAPS represents the value of an environment variable. ```{r, eval = FALSE} ${GOOGLE_APPLICATION_CREDENTIALS} ${CLOUDSDK_CONFIG}/application_default_credentials.json # on Windows: %APPDATA%\gcloud\application_default_credentials.json %SystemDrive%\gcloud\application_default_credentials.json C:\gcloud\application_default_credentials.json # on not-Windows: ~/.config/gcloud/application_default_credentials.json ``` If the above search successfully identifies a JSON file, it is parsed and ingested either as a service account token, an external account configuration, or an OAuth2 user credential. In the case of an OAuth2 credential, the requested `scopes` must also meet certain criteria. Note that this will NOT work for OAuth2 credentials initiated by gargle, which are stored on disk in `.rds` files. The storage of OAuth2 user credentials as JSON is unique to certain Google tools -- possibly just the [`gcloud` CLI](https://cloud.google.com/sdk/gcloud/reference/auth/application-default/login) -- and should probably be regarded as deprecated. It is recommended to use ADC with a service account or workload identity federation. If this quest is unsuccessful, we fail and `token_fetch()`'s execution moves on to the next function in the registry. The main takeaway lesson: * You can make auth "just work" by storing the JSON for a service account or an external account at one of the filepaths listed above. It will be automagically discovered when `token_fetch()` is called with only the `scopes` argument specified. Again, remember that the JSON key file for a conventional service account must be managed securely and should NOT live in a directory that syncs to the cloud. The JSON configuration for an external account is not actually sensitive and this is one of the benefits of this flow, but it's only available in a very narrow set of circumstances. ## `credentials_gce()` The next function tried is `credentials_gce()`. Here's how a call to `token_fetch()` might work: ```{r, eval = FALSE} token_fetch(scopes = ) # or perhaps token_fetch(scopes = , service_account = ) # credentials_byo_oauth2() fails because no `token`, # credentials_service_account() fails because no `path`, # credentials_external_account() fails because no `path`, # credentials_app_default() fails because no ADC found, # which leads to one of these calls: credentials_gce( scopes = , service_account = "default" ) # or credentials_gce( scopes = , service_account = ) ``` `credentials_gce()` retrieves service account credentials from a metadata service that is specific to virtual machine instances running on Google Cloud Engine (GCE). Basically, if you have to ask what this is about, this is not the auth method for you. Let us move on. If this seems to happening to you and it's not what you want, see the last section for how to remove this auth method. ## `credentials_user_oauth2()` The next and final function tried is `credentials_user_oauth2()`. Here's how a call to `token_fetch()` might work: ```{r, eval = FALSE} token_fetch(scopes = ) # credentials_byo_oauth2() fails because no `token`, # credentials_service_account() fails because no `path`, # credentials_external_account() fails because no `path`, # credentials_app_default() fails because no ADC found, # credentials_gce() fails because not on GCE, # which leads to this call: credentials_user_oauth2( scopes = , app = , package = "" ) ``` `credentials_user_oauth2()` is where the vast majority of users will end up. This is the function that choreographs the traditional "OAuth dance" in the browser. User credentials are cached locally, at the user level, by default. Therefore, after first use, there are scenarios in which gargle can determine unequivocally that it already has a suitable token on hand and can load (and possibly refresh) it, without additional user intervention. The `scopes`, `app` (likely to be renamed `client` in a future version of gargle), and `package` are generally provided by the API wrapper function that is mediating the calls to `token_fetch()`. Do not "borrow" an OAuth client from gargle or any other package; always use credentials associated with your package or provided by your user. Per the Google User Data Policy , your application must accurately represent itself when authenticating to Google API services. The wrapper package would presumably also declare itself as the package requesting a token (this is used in messages). So here's how a call to `token_fetch()` and `credentials_user_oauth2()` might look when initiated from `THINGY_auth()`, a function in the fictional thingyr wrapper package: ```{r, eval = FALSE} # user initiates auth or does something that triggers it indirectly THINGY_auth() # which then calls gargle::token_fetch( scopes = , app = thingy_app(), package = "thingyr" ) # which leads to this call: credentials_user_oauth2( scopes = , app = thingy_app(), package = "thingyr" ) ``` See `vignette("gargle-auth-in-client-package")` for design ideas for a function like `THINGY_auth()`. What happens tomorrow or next week? Do we make this user go through the browser dance again? How do we get to that happy place where we don't bug them constantly about auth? First, we define "suitable", i.e. what it means to find a matching token in the cache. `credentials_user_oauth2()` is a thin wrapper around `gargle2.0_token()` which is the constructor for the `gargle::Gargle2.0` class used to hold an OAuth2 token. And that call might look something like this (simplified for communication purposes): ```{r, eval = FALSE} gargle2.0_token( email = gargle_oauth_email(), app = thingy_app(), package = "thingyr", scope = , cache = gargle_oauth_cache() ) ``` gargle looks in the cache specified by `gargle_oauth_cache()` for a token that has these scopes, this client, and the Google identity specified by `email`. By default `email` is `NA`, so we might find one or more tokens that have the necessary scopes and client. In that case, gargle reveals the `email` associated with the matching token(s) and asks the user for explicit instructions about how to proceed. That looks something like this: ```{r, eval = FALSE} The thingyr package is requesting access to your Google account. Enter '1' to start a new auth process or select a pre-authorized account. 1: Send me to the browser for a new auth process. 2: janedoe_personal@gmail.com 3: janedoe@example.com 4: janedoe_work@gmail.com Selection: ``` If none of the tokens has the right scopes and client (or if the user declines to use a pre-existing token), we head to the browser to initiate OAuth2 flow *de novo*. A user can reduce the need for interaction by passing the target `email` to `thingy_auth()`: ```{r, eval = FALSE} thingy_auth(email = "janedoe_work@gmail.com") ``` or by specifying same in the `"gargle_oauth_email"` option. A value of `email = TRUE`, passed directly or via the option, is an alternative strategy: `TRUE` means that gargle is allowed to use a matching token whenever there is exactly one match. The elevated status of `email` for `gargle::Gargle2.0` tokens is motivated by the fact that many of us have multiple Google identities and need them to be very prominent when working with Google APIs. This is one of the main motivations for `gargle::Gargle2.0`, which extends `httr::Token2.0`. The `gargle::Gargle2.0` class also defaults to a user-level token cache, as opposed to project-level. An overview of the current OAuth cache is available via `gargle_oauth_cache()` and the output looks something like this: ```{r, eval = FALSE} gargle_oauth_sitrep() #> 14 tokens found in this gargle OAuth cache: #> ~/Library/Caches/gargle #' #' email app scopes hash... #' ----------------------------- ----------- ------------------------------ ---------- #' abcdefghijklm@gmail.com thingy ...bigquery, ...cloud-platform 128f9cc... #' buzzy@example.org gargle-demo 15acf95... #' stella@example.org gargle-demo ...drive 4281945... #' abcdefghijklm@gmail.com gargle-demo ...drive 48e7e76... #' abcdefghijklm@gmail.com tidyverse 69a7353... #' nopqr@ABCDEFG.com tidyverse ...spreadsheets.readonly 86a70b9... #' abcdefghijklm@gmail.com tidyverse ...drive d9443db... #' nopqr@HIJKLMN.com tidyverse ...drive d9443db... #' nopqr@ABCDEFG.com tidyverse ...drive d9443db... #' stuvwzyzabcd@gmail.com tidyverse ...drive d9443db... #' efghijklmnopqrtsuvw@gmail.com tidyverse ...drive d9443db... #' abcdefghijklm@gmail.com tidyverse ...drive.readonly ecd11fa... #' abcdefghijklm@gmail.com tidyverse ...bigquery, ...cloud-platform ece63f4... #' nopqr@ABCDEFG.com tidyverse ...spreadsheets f178dd8... ``` ## Manipulate the credential function registry Recall that you can get an overview of the credential functions that `token_fetch()` works through like so: ```{r} writeLines(names(cred_funs_list())) ``` Sometimes more than one of these auth methods "work", but only one of them actually "works" and, sadly, it's not the first one. In this case, gargle successfully gets a token, but then you experience token-related failure in downstream work. The most common example of this is someone who is working on Google Compute Engine (GCE), but they prefer to auth as a normal user, not as the default service account. Let's say you want to prevent `token_fetch()` from even trying one specific auth method, clearing the way for it to automagically use the method you want. You can remove a specific credential function from the registry. Here's how to do this for the scenario described above, where you want to skip GCE-specific auth: ```{r eval = FALSE} gargle::cred_funs_add(credentials_gce = NULL) ``` Learn more in the docs for `cred_funs_list()`. You can even make narrowly scoped changes to the registry with `local_cred_funs()` and `with_cred_funs()`. gargle/inst/doc/get-api-credentials.html0000644000176200001440000006266014456275116017771 0ustar liggesusers How to get your own API credentials

How to get your own API credentials

Here we describe how to obtain different types of credentials that can be important when working with a Google API:

  • API key (not relevant to all APIs)
  • OAuth 2.0 client ID and secret
  • Service account token
  • External account token (“workload identity federation”)

This can be important for both users and developers:

  • Package authors: If you are writing a package to wrap a Google API, you may provide some built-in auth assets so that things “just work” for your users. Regardless, you will need credentials to use during package development and in testing.
  • Package users: Wrapper packages may or may not provide default auth assets. If they don’t, you are required to provide your own. Even if they do, you may prefer to bring your own, to have more control over your own destiny. With your own credentials, you avoid sharing quota with other users, which can reduce time-consuming errors and retries. If you use your own credentials, you are no longer at the mercy of someone else choosing to roll the credentials when you least expect it.
  • Everyone: The best method for auth in non-interactive settings is to use a service account token or workload identity federation, which require some advance setup.

Note that most users of gargle-using packages do not need to read this and can just enjoy the automatic token flow. This article is for people who have a specific reason to be more proactive about auth.

Get a Google Cloud Platform project

You will need a Google Cloud Platform (GCP) project to hold your credentials.

Go to the Google Cloud Platform Console:

  • https://console.cloud.google.com
  • This may involve logging in or selecting your preferred Google identity.
  • This may involve selecting the relevant organization.

This console is your general destination for inspecting and modifying your GCP projects.

Create a new project here, if necessary. Otherwise, select the project of interest, if you have more than one.

Enable API(s)

Enable the relevant APIs(s) for your GCP project.

In the left sidebar, navigate to APIs & Services > Library.

Identify the API of interest. Click Enable.

If you get this wrong, i.e. need to enable more APIs later, you can always come back and do this then.

Think about billing

For some APIs, you won’t be able to do anything interesting with the credentials hosted in your project unless you have also linked a billing account. This is true, for example, for BigQuery and anything that has to do with Maps. This is NOT true, for example, for Drive or Sheets or Gmail.

If your target API requires a billing account, that obviously raises the stakes for how you manage any API keys, OAuth clients, or service account tokens. Plan accordingly.

If you’re new to Google Cloud Platform, you’ll get to enjoy GCP Free Tier. At the time of writing, this means you get a $300 credit and no additional billing will happen without your express consent. So there is a low-stress way to experiment with APIs, with a billing account enabled, without putting actual money on the line.

API Key

Some APIs accept requests to read public resources, in which case the request can be sent with an API key in lieu of a token. If this is possible, it’s a good idea to expose this workflow in a wrapper package, because then your users can decide to go into a “de-authed” mode. When using the package in a non-interactive or indirect fashion (e.g. a scheduled job on a remote server or via Shiny), it is wonderful to NOT have to manage a token, if the work can be done with an API key instead.

Some APIs aren’t really usable without a token, in which case an API key may not be relevant and you can ignore this section.

  • From the GCP Console, in the target GCP Project, go to APIs & Services > Credentials.
  • Do Create credentials > API key.
  • You can capture the new API key via clipboard right away or close this pop-up and copy it later from the Credentials page.
  • In any case, I suggest you take the opportunity to edit the API key from the Credentials page and give it a nickname.

Package maintainers might want to build an API key in as a fallback, possibly taking some measures to obfuscate the key and limit its use to your package.

What does a user do with an API key?

Package users could register an API key for use with a wrapper package. For example, in googlesheets4, one would use googlesheets4::gs4_auth_configure() to store a key for use in downstream requests, i.e. after a call to googlesheets4::gs4_deauth():

library(googlesheets4)

gs4_auth_configure(api_key = "YOUR_API_KEY_GOES_HERE")
gs4_deauth()

# now you can read public resources, such as official example Sheets,
# without any need for auth
gs4_example("gapminder") %>% 
  read_sheet()

OAuth client ID and secret

Most APIs are used to create and modify resources on behalf of the user and these requests must include the user’s token. A regular user will generally need to send an OAuth2 token, which is obtained under the auspices of an OAuth “app” or “client”. This is called three-legged OAuth, where the 3 legs are the client, the user, and Google.

The basic steps are described in the Prerequisites section for doing Google OAuth 2.0 for Mobile & Desktop Apps:

  • From the GCP Console, in the target GCP Project, go to APIs & Services > Credentials.
  • Do Create credentials > OAuth client ID.
  • Select Application type, either “Desktop app” (the most common type used with gargle) or “Web application” (useful for the pseudo-OOB flow).
  • You can capture the client ID and secret via clipboard at this point or, as we recommend, download the full information as JSON. We recommend using the JSON, as this conveys the client type (desktop vs. web) and any redirect URIs (important for the web type).
  • At any time, you can navigate to a particular client ID and click “Download JSON”.

Two ways to package this info for use with gargle:

  1. Use gargle::gargle_oauth_client_from_json(). This is the preferred workflow, because the JSON conveys the client type (desktop vs. web) and any redirect URIs (important for the web type).
    • Provide the path to the downloaded JSON file.
  2. Use gargle::gargle_oauth_client().

In both cases, I suggest you devise a nickname for each OAuth credential and use it as the credential’s name in GCP Console and as the name argument to gargle::gargle_oauth_client_from_json() or gargle::gargle_oauth_client().

Package maintainers might want to build this client in as a fallback, possibly taking some measures to obfuscate the client ID and secret and limit its use to your package.

  • Note that three-legged OAuth always requires the involvement of a user, so the word “secret” here can be somewhat confusing. It is not a secret in the same sense as a password or token. But you probably still want to store it in an opaque way, so that someone else cannot easily “borrow” it and present an OAuth consent screen that impersonates your package.

What does a user do with an OAuth client?

Package users could register their own client for use with a wrapper package. For example, in googledrive, one would use googledrive::drive_auth_configure() to do this:

library(googledrive)

google_client <- gargle::gargle_oauth_client_from_json(
  path = "/path/to/the/JSON/that/was/downloaded/from/gcp/console.json",
  name = "acme-corp-google-client"
)
drive_auth_configure(app = google_client)

# now any new OAuth tokens are obtained with the configured client

Service account token

For a long time, the recommended way to make authorized requests to an API in a non-interactive context was to use a service account token. As of April 2021, an alternative exists – workload identity federation – which is available to applications running on specific non-Google Cloud platforms, such as AWS. If you can use workload identity federation, you probably should (see the next section). But for those who can’t, here we outline the use of a conventional service account.

An official overview of service accounts is given in this official documentation by Google. But note that it’s not necessary to understand all of that in order to use a service account token.

  • From the GCP Console, in the target GCP Project, go to IAM & Admin > Service accounts.
  • Give it a decent name and description.
    • For example, the service account used to create the googledrive docs has name “googledrive-docs” and description “Used when generating googledrive documentation”.
  • Service account permissions. Whether you need to do anything here depends on the API(s) you are targetting. You can also modify roles later and iteratively sort this out.
    • For example, the service account used to create the googledrive docs does not have any explicit roles.
    • The service account used to test bigrquery has roles BigQuery Admin and Storage Admin.
  • Grant users access to this service account? So far, I have not done this, so feel free to do nothing here. Or if you know this is useful to you, then by all means do so.
  • Do Create key and download as JSON. This file is what we mean when we talk about a “service account token” in the documentation of gargle and packages that use gargle. gargle::credentials_service_account() expects the path to this file.
  • Appreciate that this JSON file holds sensitive information. Treat it like a username & password combo! This file holds credentials that potentially have a lot of power and that don’t expire.
  • Consider storing this file in such a way that it will be automatically discovered by the Application Default Credentials search strategy. See credentials_app_default() for details.
  • You will notice the downloaded JSON file has an awful name, so sometimes I create a symlink that uses the service account’s name, to make it easier to tell what this file is.
  • Remember to grant this service account the necessary permissions on any resources you plan to access, e.g., read or write permission on a specific Google Sheet. The service account has no formal relationship to you as a Google user and won’t automatically inherit permissions.

Authors of wrapper packages can use the symmetric encryption strategy described in Managing tokens securely to use this token on remote servers, such as continuous integration services like GitHub Actions.

What does a user do with a service account token?

You could provide the token’s filepath to a wrapper package’s main auth function, e.g.:

# googledrive
drive_auth(path = "/path/to/your/service-account-token.json")

Alternatively, you could put the token somewhere (or store its location in an environment variable) so that it is auto-discovered by the Application Default Credentials search strategy.

Workload identity federation

Workload identity federation is a new (as of April 2021) keyless authentication mechanism that allows applications running on a non-Google Cloud platform, such as AWS, to access Google Cloud resources without using a conventional service account token. This eliminates the dilemma of how to safely manage service account credential files.

Unlike service accounts, the configuration file for workload identity federation contains no secrets. Instead, it holds non-sensitive metadata. The external application obtains the needed sensitive data “on-the-fly” from the running instance. The combined data is then used for a token exchange that ultimately yields a short-lived GCP access token. This access token allows the external application to impersonate a service account and inherit the permissions of the service account to access GCP resources.

So what’s not to love? Well, first, this auth flow is only available if your code is running on AWS, Azure, or another platform that supports the OpenID Connect protocol. Second, there’s a non-trivial amount of pre-configuration necessary on both ends. But once that is done, you can download a configuration file that makes auth work automagically with gargle.

This feature is still experimental in gargle and currently only supports AWS. For more, see the documentation for credentials_external_account(). Like conventional service account tokens, workload identity federation is a great fit for the Application Default Credentials strategy for discovering credentials. See credentials_app_default() for more about that.

These two links provide, respectively, a high-level overview and step-by-step instructions for this flow:

Further reading

Learn more in Google’s documentation:

gargle/inst/doc/oauth-client-not-app.R0000644000176200001440000000466314456275116017354 0ustar liggesusers## ---- include = FALSE--------------------------------------------------------- knitr::opts_chunk$set( collapse = TRUE, comment = "#>" ) ## ----setup-------------------------------------------------------------------- library(gargle) ## ----------------------------------------------------------------------------- (path_to_installed_client <- system.file( "extdata", "client_secret_installed.googleusercontent.com.json", package = "gargle" )) jsonlite::prettify(scan(path_to_installed_client, what = character())) (client <- gargle_oauth_client_from_json(path_to_installed_client)) class(client) (path_to_web_client <- system.file( "extdata", "client_secret_web.googleusercontent.com.json", package = "gargle" )) jsonlite::prettify(scan(path_to_web_client, what = character())) (client <- gargle_oauth_client_from_json(path_to_web_client)) class(client) ## ---- eval = FALSE------------------------------------------------------------ # # BEFORE # drive_auth_configure <- function(app, path, api_key) { # # not showing this code # .auth$set_app(app) # # more code we're not showing # } # # drive_oauth_app <- function() .auth$app # # # AFTER # drive_auth_configure <- function(client, path, api_key, app = deprecated()) { # if (lifecycle::is_present(app)) { # lifecycle::deprecate_warn( # "2.1.0", # "drive_auth_configure(app)", # "drive_auth_configure(client)" # ) # drive_auth_configure(client = app, path = path, api_key = api_key) # } # # # not showing this code # .auth$set_client(client) # # more code we're not showing # } # # drive_oauth_client <- function() .auth$client # # drive_oauth_app <- function() { # lifecycle::deprecate_warn( # "2.1.0", "drive_oauth_app()", "drive_oauth_client()" # ) # drive_oauth_client() # } ## ----eval = FALSE------------------------------------------------------------- # usethis::use_lifecycle() ## ----eval = FALSE------------------------------------------------------------- # drive_auth <- function(...) { # # code not shown # cred <- gargle::token_fetch( # scopes = scopes, # # app = drive_oauth_client() %||% , # BEFORE # client = drive_oauth_client() %||% , # AFTER # email = email, # path = path, # package = "googledrive", # cache = cache, # use_oob = use_oob, # token = token # ) # # code not shown # } gargle/inst/doc/oauth-client-not-app.html0000644000176200001440000007722114456275117020120 0ustar liggesusers Transition from OAuth app to OAuth client

Transition from OAuth app to OAuth client

library(gargle)

Over the course of several releases (v1.3.0, v1.4.0, and v1.5.0), gargle has shifted to using an OAuth client in the user flow facilitated by gargle::credentials_user_oauth2(), instead the previous OAuth “app”. This is a more than just a vocabulary change (but it is also a vocabulary change). This vignette explains what actually changed and how wrapper packages should adjust.

Why change was needed

In 2022, Google partially deprecated the out-of-band (OOB) OAuth flow. The OOB flow is used by R users who are working with Google APIs and who use R in the browser, such as via RStudio Server, Posit Workbench, Posit Cloud, or Google Colaboratory.

Conventional OOB auth still works under certain conditions, for example, if the OAuth client is associated with a GCP project that is in testing mode or that is internal to a Google Workspace. But conventional OOB is no longer supported for projects that serve external users that are in production mode. In particular, this means that conventional OOB is no longer supported for the GCP project that has historically made auth “just work” for casual users of packages such as googledrive, googlesheets4, and bigrquery. The default OAuth client used by these package no longer works with conventional OOB.

In response, as of v1.3.0, gargle implements a new variant of OOB, called pseudo-OOB, to continue to provide a user-friendly auth flow for googledrive/googlesheets4/bigrquery on RStudio Server/Posit Workbench/Posit Cloud/Google Colaboratory. The pseudo-OOB flow is also available for other developers to use. This flow is triggered when use_oob = TRUE (an existing convention in gargle and gargle-using packages) and the configured OAuth client is of the web type (when creating an OAuth client, this is called the “Web application” type).

use_oob
FALSE TRUE
client type installed use httpuv to spin up
a temporary web server
conventional OOB
web --not possible-- pseudo-OOB

In the past, gargle basically assumed that every OAuth client was of the installed type (when creating an OAuth client, this is called the “Desktop app” type). Therefore, the introduction of pseudo-OOB meant that gargle had to learn about different OAuth client types (web vs. installed). And that didn’t play well with httr::oauth_app(), which gargle had been using to store the client ID and secret.

That’s why there is a new S3 class, "gargle_oauth_client", with a constructor of the same name. Since more information is now necessary to instantiate a client (e.g. its type and, potentially, redirect URIs), the recommended way to create a client is to provide JSON downloaded from the GCP console to gargle_oauth_client_from_json().

Since we had to introduce a new S3 class and supporting functions, we also took this chance to make the vocabulary pivot from “OAuth app” to “OAuth client”. Google’s documentation has always talked about the “OAuth client”, so this is more natural. This vocabulary is also more future-facing, anticipating the day when gargle might shift from httr to httr2, which uses httr2:oauth_client(). As a bridging measure, the "gargle_oauth_client" class currently inherits from httr’s "oauth_app", but this probably won’t be true in the long-term.

How to instantiate an OAuth client in R

If you do auth via gargle, here are some recommended changes:

  1. Stop using httr::oauth_app() or gargle::oauth_app_from_json() to instantiate an OAuth client.
  2. Start using gargle_oauth_client_from_json() (strongly recommended) or gargle_oauth_client() instead.

This advice applies to anything you do inside your package and also to what you encourage and document for your users.

gargle ships with JSON files for two non-functional OAuth clients, just to make this all more concrete:

(path_to_installed_client <- system.file(
  "extdata", "client_secret_installed.googleusercontent.com.json",
  package = "gargle"
))
#> [1] "/private/tmp/RtmpfBi3gI/Rinst89b755898995/gargle/extdata/client_secret_installed.googleusercontent.com.json"
jsonlite::prettify(scan(path_to_installed_client, what = character()))
#> {
#>     "installed": {
#>         "client_id": "abc.apps.googleusercontent.com",
#>         "project_id": "a_project",
#>         "auth_uri": "https://accounts.google.com/o/oauth2/auth",
#>         "token_uri": "https://accounts.google.com/o/oauth2/token",
#>         "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
#>         "client_secret": "ssshh-i-am-a-secret",
#>         "redirect_uris": [
#>             "http://localhost"
#>         ]
#>     }
#> }
#> 
(client <- gargle_oauth_client_from_json(path_to_installed_client))
#> <gargle_oauth_client>
#> name: a_project_d1c5a8066d2cbe48e8d94514dd286163
#> id: abc.apps.googleusercontent.com
#> secret: <REDACTED>
#> type: installed
#> redirect_uris: http://localhost
class(client)
#> [1] "gargle_oauth_client" "oauth_app"

(path_to_web_client <- system.file(
  "extdata", "client_secret_web.googleusercontent.com.json",
  package = "gargle"
))
#> [1] "/private/tmp/RtmpfBi3gI/Rinst89b755898995/gargle/extdata/client_secret_web.googleusercontent.com.json"
jsonlite::prettify(scan(path_to_web_client, what = character()))
#> {
#>     "web": {
#>         "client_id": "abc.apps.googleusercontent.com",
#>         "project_id": "a_project",
#>         "auth_uri": "https://accounts.google.com/o/oauth2/auth",
#>         "token_uri": "https://accounts.google.com/o/oauth2/token",
#>         "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
#>         "client_secret": "ssshh-i-am-a-secret",
#>         "redirect_uris": [
#>             "https://www.tidyverse.org/google-callback/"
#>         ]
#>     }
#> }
#> 
(client <- gargle_oauth_client_from_json(path_to_web_client))
#> <gargle_oauth_client>
#> name: a_project_d1c5a8066d2cbe48e8d94514dd286163
#> id: abc.apps.googleusercontent.com
#> secret: <REDACTED>
#> type: web
#> redirect_uris: https://www.tidyverse.org/google-callback/
class(client)
#> [1] "gargle_oauth_client" "oauth_app"

Notice the difference in the JSON for the installed vs. web client. Note the class of the client object, the new type field, and the redirect_uris.

AuthState class

There are two gargle classes that are impacted by the OAuth-app-to-client switch: AuthState and Gargle2.0. We cover AuthState here and Gargle2.0 in the next section.

If a wrapper package follows the design laid out in vignette("gargle-auth-in-client-package"), it will use an instance of AuthState to manage the package’s auth state. Let’s assume that internal object is named .auth, which it usually is. Here are the changes you need to know about in AuthState:

  • The app field is deprecated, in favor of a new field client. If you request .auth$app, there will be a deprecation message and the client field is returned.
  • The $set_app() method is deprecated, in favor of a new $set_client() method. If you call .auth$set_app(), there will be a deprecation message and the input is used, instead, to set the client field.
  • The app argument of the init_AuthState() constructor is deprecated in favor of the new client argument. If you call init_AuthState(app = x), there will be a deprecation message and the input x is used as the client argument instead.

Here are the changes you probably need to make in your package:

  • The first argument of the user-facing function, PKG_auth_configure(), should become client (which is new). Move the existing app argument to the last position and deprecate it.
  • Deprecate PKG_oauth_app() (the function to reveal the user’s configured OAuth client).
  • Introduce PKG_oauth_client() to replace PKG_oauth_app().

Here’s how googledrive::drive_auth_configure() and googledrive::drive_oauth_client() looked before and after the transition:

# BEFORE
drive_auth_configure <- function(app, path, api_key) {
  # not showing this code
  .auth$set_app(app)
  # more code we're not showing
}

drive_oauth_app <- function() .auth$app

# AFTER
drive_auth_configure <- function(client, path, api_key, app = deprecated()) {
  if (lifecycle::is_present(app)) {
    lifecycle::deprecate_warn(
      "2.1.0",
      "drive_auth_configure(app)",
      "drive_auth_configure(client)"
    )
    drive_auth_configure(client = app, path = path, api_key = api_key)
  } 
  
  # not showing this code
  .auth$set_client(client)
  # more code we're not showing
}

drive_oauth_client <- function() .auth$client

drive_oauth_app <- function() {
  lifecycle::deprecate_warn(
    "2.1.0", "drive_oauth_app()", "drive_oauth_client()"
  )
  drive_oauth_client()
}

The approach above follows various conventions explained in vignette("communicate", package = "lifecycle"). If you also choose to use the lifecycle package to assist in this process, usethis::use_lifecycle() function does some helpful one-time setup in your package:

usethis::use_lifecycle()

The roxygen documentation helpers in gargle assume PKG_auth_configure() is adapted as shown above:

  • PREFIX_auth_configure_description() crosslinks to PREFIX_oauth_client() now, not PREFIX_oauth_app().
  • PREFIX_auth_configure_params() documents the client argument
  • PREFIX_auth_configure_params() uses a lifecycle badge and text to communicate that app is deprecated.
  • PREFIX_auth_configure_params() crosslinks to gargle::gargle_oauth_client_from_json() which requires gargle (>= 1.3.0)

Gargle2.0 class

Gargle2.0 is the second gargle class that is impacted by the OAuth-app-to-client switch.

Here are the changes you probably need to make in your package:

  • Inside PKG_auth(), you presumably call gargle::token_fetch(). If you are passing app = <SOMETHING>, change that to client = <SOMETHING>. Neither app nor client are formal arguments of gargle::token_fetch(), instead, these are intended for eventual use by gargle::credentials_user_oauth2(). Here’s a sketch of how this looks in googledrive::drive_auth():

    drive_auth <- function(...) {
      # code not shown
      cred <- gargle::token_fetch(
        scopes = scopes,
        # app = drive_oauth_client() %||% <BUILT_IN_DEFAULT_CLIENT>,   # BEFORE
        client = drive_oauth_client() %||% <BUILT_IN_DEFAULT_CLIENT>,  # AFTER
        email = email,
        path = path,
        package = "googledrive",
        cache = cache,
        use_oob = use_oob,
        token = token
      )
      # code not shown
    }
  • If you ever call gargle::credentials_user_oauth2() directly, use the new client argument instead of the deprecated app argument.

gargle/inst/doc/troubleshooting.Rmd0000644000176200001440000002525214436207160017141 0ustar liggesusers--- title: "Troubleshooting gargle auth" output: rmarkdown::html_vignette vignette: > %\VignetteIndexEntry{Troubleshooting gargle auth} %\VignetteEngine{knitr::rmarkdown} %\VignetteEncoding{UTF-8} --- ```{r, include = FALSE} knitr::opts_chunk$set( collapse = TRUE, comment = "#>" ) ``` ```{r setup} library(gargle) ``` ## `"gargle_verbosity"` option There is a package-wide option that controls gargle's verbosity: `"gargle_verbosity"`. The function `gargle_verbosity()` reveals the current value: ```{r} gargle_verbosity() ``` It defaults to `"info"`, which is fairly quiet. This is because gargle is designed to try a bunch of auth methods (many of which will fail) and persist doggedly until one succeeds. If none succeeds, gargle tries to guide the user through auth or, in a non-interactive session, it throws an error. If you need to see all those gory details, set the `"gargle_verbosity"` option to `"debug"` and you'll get much more output as gargle works through various auth approaches. It is **normal** to see lots of errors, as gargle tries various auth methods in succession, most of which will often fail. ```{r} # save current value op <- options(gargle_verbosity = "debug") gargle_verbosity() # restore original value options(op) ``` Note there are also withr-style helpers: `with_gargle_verbosity()` and `local_gargle_verbosity()`. ```{r} gargle_verbosity() with_gargle_verbosity( "debug", gargle_verbosity() ) gargle_verbosity() f <- function() { local_gargle_verbosity("debug") gargle_verbosity() } f() gargle_verbosity() ``` ## `gargle_oauth_sitrep()` `gargle_oauth_sitrep()` provides an OAuth2 "situation report". `gargle_oauth_sitrep()` is only relevant to OAuth2 user tokens. If you are using (or struggling to use) a service account token, workload identity federation, Application Default Credentials, or credentials from the GCE metadata service, `gargle_oauth_sitrep()` isn't going to help you figure out what's going on. Here is indicative output of `gargle_oauth_sitrep()`, for someone who has accepted the default OAuth cache location and has played with several APIs via gargle-using packages. ```{r, eval = FALSE} gargle_oauth_sitrep() #' > 14 tokens found in this gargle OAuth cache: #' '~/Library/Caches/gargle' #' #' email app scope hash... #' ----------------------------- ----------- ------------------------------ ---------- #' abcdefghijklm@gmail.com thingy ...bigquery, ...cloud-platform 128f9cc... #' buzzy@example.org gargle-demo 15acf95... #' stella@example.org gargle-demo ...drive 4281945... #' abcdefghijklm@gmail.com gargle-demo ...drive 48e7e76... #' abcdefghijklm@gmail.com tidyverse 69a7353... #' nopqr@ABCDEFG.com tidyverse ...spreadsheets.readonly 86a70b9... #' abcdefghijklm@gmail.com tidyverse ...drive d9443db... #' nopqr@HIJKLMN.com tidyverse ...drive d9443db... #' nopqr@ABCDEFG.com tidyverse ...drive d9443db... #' stuvwzyzabcd@gmail.com tidyverse ...drive d9443db... #' efghijklmnopqrtsuvw@gmail.com tidyverse ...drive d9443db... #' abcdefghijklm@gmail.com tidyverse ...drive.readonly ecd11fa... #' abcdefghijklm@gmail.com tidyverse ...bigquery, ...cloud-platform ece63f4... #' nopqr@ABCDEFG.com tidyverse ...spreadsheets f178dd8... ``` It is relatively harmless to delete the folder serving as the OAuth cache. Or, if you have reason to believe one specific cached token is causing you pain, you could delete a specific token (an `.rds` file) from the cache. OAuth user tokens are meant to be perishable and replaceable. If you choose to delete your cache (or a specific token), here is the fallout you can expect: * You will need to re-auth (usually, meaning the browser dance) in projects that have been using the deleted tokens. * If you have `.R` or `.Rmd` files that you execute or render non-interactively, presumably with code such as `PKG_auth(email = "janedoe@example.com")`, those won't run non-interactively until you've obtained and cached a token for the package and that identity (email) interactively once. ## Why do good tokens go bad? Sometimes it feels like auth was working and then suddenly it stops working. If you've cached a token and used it successfully, why would it stop working? ### Too many tokens An existing token can go bad if you've created too many Google tokens, causing your oldest tokens to "fall off the edge". A specific Google user (email) can only have a certain number of OAuth tokens at a time (something like 50 per OAuth client). So, whenever you get a new token (as opposed to refreshing an existing token), there is the potential for it to invalidate an older token. This is unlikely to be an issue for a casual user, but it can absolutely become noticeable for someone who is developing against a Google API or someone working from many different machines / caches. Another reason that an existing token stops working is if it was obtained with an OAuth client that is in "testing" mode. Refresh tokens obtained that way only last for one week, whereas it's more typical for refresh tokens to last almost indefinitely (or, at least, for several months). ### Credential rolling Many users of packages like googlesheets4 or googledrive tacitly rely on the default OAuth client used by those packages. Periodically the maintainer of such a package will need to roll the client, i.e. create a new OAuth client and disable the old one. This will make it impossible to refresh existing tokens, made with the old, disabled client Those tokens will stop working. *In gargle v1.0.0, in March 2021, we rolled the client used in googlesheets4, googledrive, and bigrquery. We reserve the right to disable the old client at any time. Anyone relying on the default client will have to upgrade.* The solution is to update the package in question, e.g. googlesheets4: ```{r eval = FALSE} install.packages("googlesheets4") ``` **Restart R!** Resume your work. Chances are you'll be prompted to re-auth with the new client and you'll be back in business. What does this problem look like in the wild? With gargle versions up to v1.0.0, you will probably see this: ``` Auto-refreshing stale OAuth token. Error in get("refresh_oauth2.0", asNamespace("httr"))(self$endpoint, self$app, : Unauthorized (HTTP 401). ``` If you're trying to create a token, instead of refreshing one, you might see this in the browser, while R is waiting to receive input: ``` Google Authorization Error Error 401: deleted_client The OAuth client was deleted. ``` It might look something like this: ```{r, echo = FALSE, out.width = "400px"} #| fig-alt: > #| Screenshot with the following text: "Google", #| "Authorization Error", "Error 401: deleted_client", #| "The OAuth client was deleted." knitr::include_graphics("deleted_client.png") ``` As of gargle version v1.1.0, we're trying harder to recognize this specific problem and to provide a more detailed and actionable error message: ``` Auto-refreshing stale OAuth token. Error: Client error: (401) UNAUTHENTICATED * Request not authenticated due to missing, invalid, or expired OAuth token. * Request had invalid authentication credentials. Expected OAuth 2 access token, login cookie or other valid authentication credential. See https://developers.google.com/identity/sign-in/web/devconsole-project. Run `rlang::last_error()` to see where the error occurred. In addition: Warning message: Unable to refresh token, because the associated OAuth app has been deleted * You appear to be relying on the default app used by the googlesheets4 package * Consider re-installing googlesheets4 and gargle, in case the default app has been updated ``` ## How to avoid auth pain If you have rigged some remote mission critical thing (e.g. a Shiny app or cron job) to use a cached user OAuth token, one day, one of the problems described above will happen and your mission critical token will stop working. Your thing (e.g. the Shiny app or cron job) will mysteriously fail because the OAuth token can't be refreshed and a new token can't be obtained in a non-interactive setting. This is why cached user tokens are a poor fit for such applications. If you choose to use a cached user token anyway, be prepared to deal with this headache periodically. (You may not have much of a choice if you are using, for example, the gmailr package to work with the Gmail API, which has limited support for service accounts.) Consider using your own OAuth client to eliminate your exposure to a third-party deciding to roll their client. Be prepared to generate a fresh token interactively and upload it to the token cache consulted by your remote mission critical thing. Better yet, upgrade to a more robust strategy for [non-interactive auth](https://gargle.r-lib.org/articles/non-interactive-auth.html), such as a service account token. ## How to inspect the last response By default, `gargle::response_process()` stores the most recently processed response in an internal environment. You can access this response with the nonexported helper `gargle:::gargle_last_response()`. Prior to storage, a few parts of the response are redacted or deleted, such as the access token and the handle. These are either sensitive (the token) or useless (the handle) and they have more downside than upside for downstream debugging use. Here's an example of accessing the most recent response and writing it to file, which could be shared with someone else for debugging. The response is in this example has HTTP status 200, i.e. it is not an error. But this process works the same even if in the case of an error, e.g. an HTTP status >= 400. ```{r include = FALSE} # only run the chunk below in settings that are known to be safe, i.e. where # occasional, incidental failure is OK can_decrypt <- gargle::secret_has_key("GARGLE_KEY") ``` ```{r eval = can_decrypt, purl = can_decrypt} library(gargle) req <- request_build( method = "GET", path = "webfonts/v1/webfonts", params = list( sort = "popularity" ), key = gargle_api_key(), base_url = "https://www.googleapis.com" ) resp <- request_make(req) out <- response_process(resp) lr <- gargle:::gargle_last_response() tmp <- tempfile("gargle-last-response-") saveRDS(lr, tmp) # you could share this .rds file with a colleague or the gargle maintainer # how it would look to complete the round trip, i.e. load this on the other end rt_lr <- readRDS(tmp) all.equal(lr, rt_lr) # clean up unlink("tmp") ``` gargle/inst/doc/auth-from-web.R0000644000176200001440000000132114456275114016043 0ustar liggesusers## ---- include = FALSE--------------------------------------------------------- knitr::opts_chunk$set( collapse = TRUE, comment = "#>" ) ## ---- echo = FALSE, out.width = "400px"--------------------------------------- knitr::include_graphics("invalid_request.png") ## ----eval = FALSE------------------------------------------------------------- # library(googledrive) # # drive_auth(use_oob = TRUE) # # # now carry on with your work # drive_find(n_max = 5) ## ----eval = FALSE------------------------------------------------------------- # options(gargle_oob_default = TRUE) ## ----eval = FALSE------------------------------------------------------------- # options(gargle_oauth_client_type = "web") gargle/inst/doc/how-gargle-gets-tokens.html0000644000176200001440000013341514456275116020442 0ustar liggesusers How gargle gets tokens

How gargle gets tokens

This vignette explains the purpose and usage of token_fetch() and the functions it subsequently calls. The goal of token_fetch() is to secure a token for use in downstream requests.

The target audience is someone who works directly with a Google API. These people roughly fall into two camps:

  • The author of an R package that wraps a Google API.
  • The useR who is writing a script or app, without using such a wrapper, either because the wrapper does not exist or there’s a reason to avoid the dependency.

This vignette might also be useful to the user of a wrapper package who needs to influence the operations of token_fetch(), e.g. by telling it to try auth methods in a non-default order or to not try certain methods at all.

token_fetch() is aimed at whoever is going to manage the returned token, e.g., incorporate it into downstream requests. It can be very nice for users if wrapper packages assume this responsibility, as opposed to requiring users to explicitly acquire and manage their tokens. We give a few design suggestions here and cover this in more depth in vignette("gargle-auth-in-client-package").

library(gargle)

token_fetch()

token_fetch() is a rather magical function for getting a token. The goal is to make auth relatively painless for users, while allowing developers and power users to take control when and if they need to. Most users will presumably interact with token_fetch() only in an indirect way, mediated through an API wrapper package. That is not because the interface of token_fetch() is unfriendly – it’s very flexible! The objective of token_fetch() is to allow package developers to take responsibility for managing the user’s token, without having to implement all the different ways of obtaining that token in the first place.

The signature of token_fetch() is very simple and, therefore, not very informative:

token_fetch(scopes, ...)

Under the hood, token_fetch() calls a sequence of much more specific credential functions, each wrapped in a tryCatch() and returning NULL if unsuccessful. The only formal argument these functions have in common is scopes, with the rest being passed via ....

This gives a sense of the credential functions and reflects the order in which they are called:

writeLines(names(cred_funs_list()))
#> credentials_byo_oauth2
#> credentials_service_account
#> credentials_external_account
#> credentials_app_default
#> credentials_gce
#> credentials_user_oauth2

It is possible to manipulate this registry of functions. The help for cred_funs_list() is a good place to learn more and we present a concrete example in the last section of this vignette.

For now, however, we assume you’re working with the default registry that ships with gargle.

Note also that these credential functions are exported and can be called directly.

Get verbose output

To see more information about what gargle is up to, set the "gargle_verbosity" option to “debug”. Read more in the docs for gargle_verbosity().

credentials_byo_oauth2()

The first function tried is credentials_byo_oauth2(). Here’s how a call to token_fetch() might work:

token_fetch(token = <TOKEN2.0>)

credentials_byo_oauth2(
  token = <TOKEN2.0>
)

credentials_byo_oauth2() provides a back door for a “bring your own token” workflow. This function accounts for the scenario where an OAuth token has been obtained through external means and it’s convenient to be able to put it into force.

credentials_byo_oauth2() checks that token is of class httr::Token2.0 and that it appears to be associated with Google. A token of class request is also acceptable, in which case the auth_token component is extracted and treated as the input. This is how a Token2.0 object would present, if processed with httr::config(), as functions like googledrive::drive_token() and bigrquery::bq_token() do.

If token is not provided or if it doesn’t satisfy these requirements, we fail and token_fetch()’s execution moves on to the next function in the registry.

credentials_service_account()

The next function tried is credentials_service_account(). Here’s how a call to token_fetch() with service account inputs plays out:

token_fetch(scopes = <SCOPES>, path = "/path/to/your/service-account.json")

# credentials_byo_oauth2() fails because no `token`,
# which leads to this call:
credentials_service_account(
  scopes = <SCOPES>,
  path = "/path/to/your/service-account.json"
)

The scopes are often provided by the API wrapper function that is mediating the calls to token_fetch() and credential_service_account(). The path argument is presumably coming from the user. It is treated as a JSON representation of service account credentials, in any form that is acceptable to jsonlite::fromJSON(). In the above example, that is a file path, but it could also be a JSON string. If there is no named path argument or if it can’t be parsed as a service account credential, we fail and token_fetch()’s execution moves on to the next function in the registry.

Here is some Google documentation about service accounts:

For R users, a service account is a great option for credentials that will be used in a script or application running remotely or in an unattended fashion. In particular, this is a better approach than trying to move OAuth2 credentials from one machine to another. For example, a service account is the preferred method of auth when testing and documenting a package on a continuous integration service.

The JSON key file must be managed securely. In particular, it should not be kept in, e.g., a GitHub repository (unless it is encrypted). The encryption strategy used by gargle and other packages is described in the article Managing tokens securely.

Note that fetching a token for a service account requires a reasonably accurate system clock. This is of particular importance for users running gargle inside a Docker container, as Docker for Windows has intermittently seen problems with clock drift. If your service account token requests fail with “Bad Request” inside a container, but succeed locally, check that the container’s system clock is accurate.

credentials_external_account()

The next function tried is credentials_external_account(). Here’s how a call to token_fetch() with an external account inputs plays out:

token_fetch(scopes = <SCOPES>, path = "/path/to/your/external-account.json")

# credentials_byo_oauth2() fails because no `token`,
# credentials_service_account() fails because the JSON provided via
#   `path` is not of type "service_account",
# which leads to this call:
credentials_external_account(
  scopes = <SCOPES>,
  path = "/path/to/your/external-account.json"
)

credentials_external_account() implements something called workload identity federation and is available to applications running on specific non-Google Cloud platforms. At the time of writing, gargle only supports AWS, but this could be expanded to other providers, such as Azure, if there is a documented need.

Similar to credentials_service_account(), the path is treated as a JSON representation of the account’s configuration and it’s probably a file path. However, in contrast to credentials_service_account(), this JSON only contains non-sensitive metadata, which is, indeed, the main point of this flow. The secrets needed to complete auth are obtained “on-the-fly” from, e.g., the running EC2 instance.

credentials_external_account() will fail for many reasons: there is no named path argument, the JSON at path can’t be parsed as configuration for an external AWS account, we don’t appear to running on AWS, suggested packages for AWS functionality are not installed, or the workload identity pool is misconfigured. If any of that happens, we fail and token_fetch()’s execution moves on to the next function in the registry.

Here is some Google documentation about workload identity federation and the specifics for AWS:

credentials_app_default()

The next function tried is credentials_app_default(). Here’s how a call to token_fetch() might work:

token_fetch(scopes = <SCOPES>)

# credentials_byo_oauth2() fails because no `token`,
# credentials_service_account() fails because no `path`,
# credentials_external_account() fails because no `path`,
# which leads to this call:
credentials_app_default(
  scopes = <SCOPES>
)

credentials_app_default() loads credentials from a file identified via a search strategy known as Application Default Credentials (ADC). The credentials themselves are conventional service account, external account, or user credentials that happen to be stored in a pre-ordained location and format.

The hope is to make auth “just work” for someone working on Google-provided infrastructure or who has used Google tooling to get started, such as the gcloud command line tool. A sequence of paths is consulted, which we describe here, with some abuse of notation. ALL_CAPS represents the value of an environment variable.

${GOOGLE_APPLICATION_CREDENTIALS}
${CLOUDSDK_CONFIG}/application_default_credentials.json

# on Windows:
%APPDATA%\gcloud\application_default_credentials.json
%SystemDrive%\gcloud\application_default_credentials.json
C:\gcloud\application_default_credentials.json

# on not-Windows:
~/.config/gcloud/application_default_credentials.json

If the above search successfully identifies a JSON file, it is parsed and ingested either as a service account token, an external account configuration, or an OAuth2 user credential. In the case of an OAuth2 credential, the requested scopes must also meet certain criteria. Note that this will NOT work for OAuth2 credentials initiated by gargle, which are stored on disk in .rds files. The storage of OAuth2 user credentials as JSON is unique to certain Google tools – possibly just the gcloud CLI – and should probably be regarded as deprecated. It is recommended to use ADC with a service account or workload identity federation. If this quest is unsuccessful, we fail and token_fetch()’s execution moves on to the next function in the registry.

The main takeaway lesson:

  • You can make auth “just work” by storing the JSON for a service account or an external account at one of the filepaths listed above. It will be automagically discovered when token_fetch() is called with only the scopes argument specified.

Again, remember that the JSON key file for a conventional service account must be managed securely and should NOT live in a directory that syncs to the cloud. The JSON configuration for an external account is not actually sensitive and this is one of the benefits of this flow, but it’s only available in a very narrow set of circumstances.

credentials_gce()

The next function tried is credentials_gce(). Here’s how a call to token_fetch() might work:

token_fetch(scopes = <SCOPES>)
# or perhaps
token_fetch(scopes = <SCOPES>, service_account = <SERVICE_ACCOUNT>)

# credentials_byo_oauth2() fails because no `token`,
# credentials_service_account() fails because no `path`,
# credentials_external_account() fails because no `path`,
# credentials_app_default() fails because no ADC found,
# which leads to one of these calls:
credentials_gce(
  scopes = <SCOPES>,
  service_account = "default"
)
# or
credentials_gce(
  scopes = <SCOPES>,
  service_account = <SERVICE_ACCOUNT>
)

credentials_gce() retrieves service account credentials from a metadata service that is specific to virtual machine instances running on Google Cloud Engine (GCE). Basically, if you have to ask what this is about, this is not the auth method for you. Let us move on.

If this seems to happening to you and it’s not what you want, see the last section for how to remove this auth method.

credentials_user_oauth2()

The next and final function tried is credentials_user_oauth2(). Here’s how a call to token_fetch() might work:

token_fetch(scopes = <SCOPES>)

# credentials_byo_oauth2() fails because no `token`,
# credentials_service_account() fails because no `path`,
# credentials_external_account() fails because no `path`,
# credentials_app_default() fails because no ADC found,
# credentials_gce() fails because not on GCE,
# which leads to this call:
credentials_user_oauth2(
  scopes = <SCOPES>,
  app = <OAUTH_APP>,
  package = "<PACKAGE>"
)

credentials_user_oauth2() is where the vast majority of users will end up. This is the function that choreographs the traditional “OAuth dance” in the browser. User credentials are cached locally, at the user level, by default. Therefore, after first use, there are scenarios in which gargle can determine unequivocally that it already has a suitable token on hand and can load (and possibly refresh) it, without additional user intervention.

The scopes, app (likely to be renamed client in a future version of gargle), and package are generally provided by the API wrapper function that is mediating the calls to token_fetch(). Do not “borrow” an OAuth client from gargle or any other package; always use credentials associated with your package or provided by your user. Per the Google User Data Policy https://developers.google.com/terms/api-services-user-data-policy, your application must accurately represent itself when authenticating to Google API services.

The wrapper package would presumably also declare itself as the package requesting a token (this is used in messages). So here’s how a call to token_fetch() and credentials_user_oauth2() might look when initiated from THINGY_auth(), a function in the fictional thingyr wrapper package:

# user initiates auth or does something that triggers it indirectly
THINGY_auth()

# which then calls
gargle::token_fetch(
  scopes  = <SCOPES_NEEDED_FOR_THE_THINGY_API>,
  app     = thingy_app(),
  package = "thingyr"
)

# which leads to this call:
credentials_user_oauth2(
  scopes  = <SCOPES_NEEDED_FOR_THE_THINGY_API>,
  app     = thingy_app(),
  package = "thingyr"
)

See vignette("gargle-auth-in-client-package") for design ideas for a function like THINGY_auth().

What happens tomorrow or next week? Do we make this user go through the browser dance again? How do we get to that happy place where we don’t bug them constantly about auth?

First, we define “suitable”, i.e. what it means to find a matching token in the cache. credentials_user_oauth2() is a thin wrapper around gargle2.0_token() which is the constructor for the gargle::Gargle2.0 class used to hold an OAuth2 token. And that call might look something like this (simplified for communication purposes):

gargle2.0_token(
  email   = gargle_oauth_email(),
  app     = thingy_app(),
  package = "thingyr",
  scope   = <SCOPES_NEEDED_FOR_THE_THINGY_API>,
  cache   = gargle_oauth_cache()
)

gargle looks in the cache specified by gargle_oauth_cache() for a token that has these scopes, this client, and the Google identity specified by email. By default email is NA, so we might find one or more tokens that have the necessary scopes and client. In that case, gargle reveals the email associated with the matching token(s) and asks the user for explicit instructions about how to proceed. That looks something like this:

The thingyr package is requesting access to your Google account.
Enter '1' to start a new auth process or select a pre-authorized account.
1: Send me to the browser for a new auth process.
2: janedoe_personal@gmail.com
3: janedoe@example.com
4: janedoe_work@gmail.com
Selection: 

If none of the tokens has the right scopes and client (or if the user declines to use a pre-existing token), we head to the browser to initiate OAuth2 flow de novo.

A user can reduce the need for interaction by passing the target email to thingy_auth():

thingy_auth(email = "janedoe_work@gmail.com")

or by specifying same in the "gargle_oauth_email" option. A value of email = TRUE, passed directly or via the option, is an alternative strategy: TRUE means that gargle is allowed to use a matching token whenever there is exactly one match.

The elevated status of email for gargle::Gargle2.0 tokens is motivated by the fact that many of us have multiple Google identities and need them to be very prominent when working with Google APIs. This is one of the main motivations for gargle::Gargle2.0, which extends httr::Token2.0. The gargle::Gargle2.0 class also defaults to a user-level token cache, as opposed to project-level. An overview of the current OAuth cache is available via gargle_oauth_cache() and the output looks something like this:

gargle_oauth_sitrep()
#> 14 tokens found in this gargle OAuth cache:
#> ~/Library/Caches/gargle
#' 
#' email                         app         scopes                         hash...   
#' ----------------------------- ----------- ------------------------------ ----------
#' abcdefghijklm@gmail.com       thingy      ...bigquery, ...cloud-platform 128f9cc...
#' buzzy@example.org             gargle-demo                                15acf95...
#' stella@example.org            gargle-demo ...drive                       4281945...
#' abcdefghijklm@gmail.com       gargle-demo ...drive                       48e7e76...
#' abcdefghijklm@gmail.com       tidyverse                                  69a7353...
#' nopqr@ABCDEFG.com             tidyverse   ...spreadsheets.readonly       86a70b9...
#' abcdefghijklm@gmail.com       tidyverse   ...drive                       d9443db...
#' nopqr@HIJKLMN.com             tidyverse   ...drive                       d9443db...
#' nopqr@ABCDEFG.com             tidyverse   ...drive                       d9443db...
#' stuvwzyzabcd@gmail.com        tidyverse   ...drive                       d9443db...
#' efghijklmnopqrtsuvw@gmail.com tidyverse   ...drive                       d9443db...
#' abcdefghijklm@gmail.com       tidyverse   ...drive.readonly              ecd11fa...
#' abcdefghijklm@gmail.com       tidyverse   ...bigquery, ...cloud-platform ece63f4...
#' nopqr@ABCDEFG.com             tidyverse   ...spreadsheets                f178dd8...

Manipulate the credential function registry

Recall that you can get an overview of the credential functions that token_fetch() works through like so:

writeLines(names(cred_funs_list()))
#> credentials_byo_oauth2
#> credentials_service_account
#> credentials_external_account
#> credentials_app_default
#> credentials_gce
#> credentials_user_oauth2

Sometimes more than one of these auth methods “work”, but only one of them actually “works” and, sadly, it’s not the first one. In this case, gargle successfully gets a token, but then you experience token-related failure in downstream work.

The most common example of this is someone who is working on Google Compute Engine (GCE), but they prefer to auth as a normal user, not as the default service account.

Let’s say you want to prevent token_fetch() from even trying one specific auth method, clearing the way for it to automagically use the method you want. You can remove a specific credential function from the registry. Here’s how to do this for the scenario described above, where you want to skip GCE-specific auth:

gargle::cred_funs_add(credentials_gce = NULL)

Learn more in the docs for cred_funs_list(). You can even make narrowly scoped changes to the registry with local_cred_funs() and with_cred_funs().

gargle/inst/extdata/0000755000176200001440000000000014436207160014125 5ustar liggesusersgargle/inst/extdata/client_secret_installed.googleusercontent.com.json0000644000176200001440000000053414431310014026212 0ustar liggesusers{"installed":{"client_id":"abc.apps.googleusercontent.com","project_id":"a_project","auth_uri":"https://accounts.google.com/o/oauth2/auth","token_uri":"https://accounts.google.com/o/oauth2/token","auth_provider_x509_cert_url":"https://www.googleapis.com/oauth2/v1/certs","client_secret":"ssshh-i-am-a-secret","redirect_uris":["http://localhost"]}} gargle/inst/extdata/fake_service_account.json0000644000176200001440000000112714436207160021163 0ustar liggesusers{ "type": "service_account", "project_id":"some-project", "private_key_id": "1234567890", "private_key": "-----BEGIN PRIVATE KEY-----\n\n-----END PRIVATE KEY-----\n", "client_email": "someone@some-project.iam.gserviceaccount.com", "client_id": "0987654321", "auth_uri": "https://accounts.google.com/o/oauth2/auth", "token_uri": "https://oauth2.googleapis.com/token", "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs", "client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/someone%40some-project.iam.gserviceaccount.com" } gargle/inst/extdata/client_secret_web.googleusercontent.com.json0000644000176200001440000000056014433520365025025 0ustar liggesusers{"web":{"client_id":"abc.apps.googleusercontent.com","project_id":"a_project","auth_uri":"https://accounts.google.com/o/oauth2/auth","token_uri":"https://accounts.google.com/o/oauth2/token","auth_provider_x509_cert_url":"https://www.googleapis.com/oauth2/v1/certs","client_secret":"ssshh-i-am-a-secret","redirect_uris":["https://www.tidyverse.org/google-callback/"]}} gargle/inst/pseudo-oob/0000755000176200001440000000000014431310014014533 5ustar liggesusersgargle/inst/pseudo-oob/google-callback/0000755000176200001440000000000014431310014017541 5ustar liggesusersgargle/inst/pseudo-oob/google-callback/index.html0000644000176200001440000000516414431310014021544 0ustar liggesusers

One more step...

To continue signing in, return to your R session, which should be prompting you to provide the code below.

gargle/inst/discovery-doc-ingest/0000755000176200001440000000000014022166555016537 5ustar liggesusersgargle/inst/discovery-doc-ingest/api-wide-parameter-names.txt0000644000176200001440000000030113620077466024055 0ustar liggesusersdescription etagRequired httpMethod id mediaUpload parameterOrder parameters path request response scopes supportsMediaDownload supportsMediaUpload supportsSubscription useMediaDownloadService gargle/inst/discovery-doc-ingest/method-properties.csv0000644000176200001440000000626213620077422022731 0ustar liggesusersproperty,type,description,additionalProperties,items,properties description,string,Description of this method.,NULL,NULL,NULL etagRequired,boolean,Whether this method requires an ETag to be specified. The ETag is sent as an HTTP If-Match or If-None-Match header.,NULL,NULL,NULL httpMethod,string,HTTP method used by this method.,NULL,NULL,NULL id,string,A unique ID for this method. This property can be used to match methods between different versions of Discovery.,NULL,NULL,NULL mediaUpload,object,Media upload parameters.,NULL,NULL,"list(accept = list(type = ""array"", description = ""MIME Media Ranges for acceptable media uploads to this method."", items = list(type = ""string"")), maxSize = list(type = ""string"", description = ""Maximum size of a media upload, such as \""1MB\"", \""2GB\"" or \""3TB\"".""), protocols = list(type = ""object"", description = ""Supported upload protocols."", properties = list(resumable = list(type = ""object"", description = ""Supports the Resumable Media Upload protocol."", properties = list(multipart = list(type = ""boolean"", description = ""True if this endpoint supports uploading multipart media."", default = ""true""), path = list(type = ""string"", description = ""The URI path to be used for upload. Should be used in conjunction with the basePath property at the api-level.""))), simple = list(type = ""object"", description = ""Supports uploading as a single HTTP request."", properties = list(multipart = list(type = ""boolean"", description = ""True if this endpoint supports upload multipart media."", default = ""true""), path = list( type = ""string"", description = ""The URI path to be used for upload. Should be used in conjunction with the basePath property at the api-level.""))))))" parameterOrder,array,"Ordered list of required parameters, serves as a hint to clients on how to structure their method signatures. The array is ordered such that the ""most-significant"" parameter appears first.",NULL,"list(type = ""string"")",NULL parameters,object,Details for all parameters in this method.,"list(`$ref` = ""JsonSchema"", description = ""Details for a single parameter in this method."")",NULL,NULL path,string,The URI path of this REST method. Should be used in conjunction with the basePath property at the api-level.,NULL,NULL,NULL request,object,The schema for the request.,NULL,NULL,"list(`$ref` = list(type = ""string"", description = ""Schema ID for the request schema.""), parameterName = list(type = ""string"", description = ""parameter name.""))" response,object,The schema for the response.,NULL,NULL,"list(`$ref` = list(type = ""string"", description = ""Schema ID for the response schema.""))" scopes,array,OAuth 2.0 scopes applicable to this method.,NULL,"list(type = ""string"")",NULL supportsMediaDownload,boolean,Whether this method supports media downloads.,NULL,NULL,NULL supportsMediaUpload,boolean,Whether this method supports media uploads.,NULL,NULL,NULL supportsSubscription,boolean,Whether this method supports subscriptions.,NULL,NULL,NULL useMediaDownloadService,boolean,"Indicates that downloads from this method should use the download service URL (i.e. ""/download""). Only applies if the method supports media download.",NULL,NULL,NULL gargle/inst/discovery-doc-ingest/api-wide-parameters-humane.txt0000644000176200001440000000135413620077463024420 0ustar liggesusersalt string Data format for the response. fields string Selector specifying which fields to include in a partial response. key string API key. Your API key identifies your project and provides you with API access, quota, and reports. Required unless you provide an OAuth 2.0 token. oauth_token string OAuth 2.0 token for the current user. prettyPrint boolean Returns response with indentations and line breaks. quotaUser string An opaque string that represents a user for quota purposes. Must not exceed 40 characters. userIp string Deprecated. Please use quotaUser instead. gargle/inst/discovery-doc-ingest/parameter-properties.csv0000644000176200001440000000624613620077433023435 0ustar liggesusersproperty,type,description,$ref,additionalProperties,items,properties $ref,string,"A reference to another schema. The value of this property is the ""id"" of another schema.",NULL,NULL,NULL,NULL additionalProperties,NULL,"If this is a schema for an object, this property is the schema for any additional properties with dynamic keys on this object.",JsonSchema,NULL,NULL,NULL annotations,object,Additional information about this property.,NULL,NULL,NULL,"list(required = list(type = ""array"", description = ""A list of methods for which this property is required on requests."", items = list(type = ""string"")))" default,string,The default value of this property (if one exists).,NULL,NULL,NULL,NULL description,string,A description of this object.,NULL,NULL,NULL,NULL enum,array,Values this parameter may take (if it is an enum).,NULL,NULL,"list(type = ""string"")",NULL enumDescriptions,array,"The descriptions for the enums. Each position maps to the corresponding value in the ""enum"" array.",NULL,NULL,"list(type = ""string"")",NULL format,string,An additional regular expression or key that helps constrain the value. For more details see: http://tools.ietf.org/html/draft-zyp-json-schema-03#section-5.23,NULL,NULL,NULL,NULL id,string,Unique identifier for this schema.,NULL,NULL,NULL,NULL items,NULL,"If this is a schema for an array, this property is the schema for each element in the array.",JsonSchema,NULL,NULL,NULL location,string,Whether this parameter goes in the query or the path for REST requests.,NULL,NULL,NULL,NULL maximum,string,The maximum value of this parameter.,NULL,NULL,NULL,NULL minimum,string,The minimum value of this parameter.,NULL,NULL,NULL,NULL pattern,string,The regular expression this parameter must conform to. Uses Java 6 regex format: http://docs.oracle.com/javase/6/docs/api/java/util/regex/Pattern.html,NULL,NULL,NULL,NULL properties,object,"If this is a schema for an object, list the schema for each property of this object.",NULL,"list(`$ref` = ""JsonSchema"", description = ""A single property of this object. The value is itself a JSON Schema object describing this property."")",NULL,NULL readOnly,boolean,"The value is read-only, generated by the service. The value cannot be modified by the client. If the value is included in a POST, PUT, or PATCH request, it is ignored by the service.",NULL,NULL,NULL,NULL repeated,boolean,Whether this parameter may appear multiple times.,NULL,NULL,NULL,NULL required,boolean,Whether the parameter is required.,NULL,NULL,NULL,NULL type,string,The value type for this schema. A list of values can be found here: http://tools.ietf.org/html/draft-zyp-json-schema-03#section-5.1,NULL,NULL,NULL,NULL variant,object,"In a variant data type, the value of one property is used to determine how to interpret the entire entity. Its value must exist in a map of descriminant values to schema names.",NULL,NULL,NULL,"list(discriminant = list(type = ""string"", description = ""The name of the type discriminant property.""), map = list(type = ""array"", description = ""The map of discriminant value to schema to use for parsing.."", items = list(type = ""object"", properties = list(`$ref` = list(type = ""string""), type_value = list(type = ""string"")))))" gargle/inst/discovery-doc-ingest/api-wide-parameters.csv0000644000176200001440000000147713620077376023132 0ustar liggesusersproperty,type,description,default,enum,enumDescriptions,location alt,string,Data format for the response.,json,json,Responses with Content-Type of application/json,query fields,string,Selector specifying which fields to include in a partial response.,NULL,NULL,NULL,query key,string,"API key. Your API key identifies your project and provides you with API access, quota, and reports. Required unless you provide an OAuth 2.0 token.",NULL,NULL,NULL,query oauth_token,string,OAuth 2.0 token for the current user.,NULL,NULL,NULL,query prettyPrint,boolean,Returns response with indentations and line breaks.,true,NULL,NULL,query quotaUser,string,An opaque string that represents a user for quota purposes. Must not exceed 40 characters.,NULL,NULL,NULL,query userIp,string,Deprecated. Please use quotaUser instead.,NULL,NULL,NULL,query gargle/inst/discovery-doc-ingest/drive-example.R0000644000176200001440000000273213434110217021416 0ustar liggesuserslibrary(tidyverse) ## necessary only during gargle development to get devtools' shim for ## system.file() load_all() source( system.file("discovery-doc-ingest", "ingest-functions.R", package = "gargle") ) x <- download_discovery_document("drive:v3") dd <- read_discovery_document(x) methods <- get_raw_methods(dd) methods <- methods %>% map(groom_properties, dd) methods <- methods %>% map(add_schema_params, dd) methods <- methods %>% map(add_global_params, dd) ## duplicate two methods to create a companion for media ## simpler to do this here, in data, than in wrapper functions mediafy <- function(target_id, methods) { new <- target_method <- methods[[target_id]] new$id <- paste0(target_id, ".media") new$path <- pluck(target_method, "mediaUpload", "protocols", "simple", "path") new$parameters <- c( new$parameters, uploadType = list(list(type = "string", required = TRUE, location = "query")) ) methods[[new$id]] <- new methods } methods <- mediafy("drive.files.update", methods) methods <- mediafy("drive.files.create", methods) .endpoints <- methods attr(.endpoints, "base_url") <- dd$rootUrl ## View(.endpoints) # usually you would execute this from *within* the target package, # but I cannot do so in this example # please excuse the shenanigans to temporarily target the googledrive project usethis::with_project( "~/rrr/googledrive", # line below is the important one! usethis::use_data(.endpoints, internal = TRUE, overwrite = TRUE) ) gargle/inst/discovery-doc-ingest/ingest-functions.R0000644000176200001440000002142214022166555022162 0ustar liggesuserslibrary(tidyverse) #' Get versioned IDs from API Discovery Service #' #' @return A character vector. #' @keywords internal #' @examples #' get_discovery_ids() #' grep("drive", get_discovery_ids(), value = TRUE) #' grep("sheets", get_discovery_ids(), value = TRUE) #' grep("gmail", get_discovery_ids(), value = TRUE) #' grep("bigquery", get_discovery_ids(), value = TRUE) get_discovery_ids <- function() { apis <- httr::content( httr::GET("https://www.googleapis.com/discovery/v1/apis") ) map_chr(apis[["items"]], "id") } #' Form the URL for a Discovery Document #' #' @param id Versioned ID string for target API. Use [get_discovery_ids()] to #' see them all and find the one you want. #' @return A URL to a JSON file. #' @keywords internal #' @examples #' make_discovery_url("sheets:v4") make_discovery_url <- function(id) { av <- set_names(as.list(strsplit(id, split =":")[[1]]), c("api", "version")) ## https://developers.google.com/discovery/v1/reference/apis/getRest getRest_url <- "https://www.googleapis.com/discovery/v1/apis/{api}/{version}/rest" glue::glue_data(av, getRest_url) } #' List (likely) Discovery Documents in a local folder #' #' @param id Optional ID string, possibly versioned, for target API. Use #' [get_discovery_ids()] to see them all and find the one you want. #' @param path Optional directory in which to look. Defaults to `data-raw` #' within the current project. #' #' @return Files whose names "look like" a Discovery Document #' @keywords internal #' @examples #' list_discovery_documents() #' list_discovery_documents("sheets") #' list_discovery_documents("sheets:v4") list_discovery_documents <- function(id = NULL, path = NULL) { path <- path %||% rprojroot::find_package_root_file("data-raw") if (!is.null(id)) { if (!grepl(":", id)) { id <- glue::glue("{id}:v[[:digit:]]+") } id <- sub(":", "-", id) } id <- id %||% "[[:alnum:]]+[-][[:alnum:]]+" date <- "[[:digit:]]{4}-[[:digit:]]{2}-[[:digit:]]{2}" regexp <- glue::glue("{id}_{date}[.]json") fs::dir_ls(path = path, regexp = regexp) %>% fs::path_rel(path) } #' Download a Discovery Document #' #' @param id Versioned ID string for target API. Use [get_discovery_ids()] to #' see them all and find the one you want. #' @param path Target filepath. Default filename is formed from the API's #' versioned ID and the Discovery Document's revision date. Default parent #' directory is the current package's `data-raw/` directory, if such exists, #' or current working directory, otherwise. #' #' @return Filepath #' @keywords internal #' @examples #' download_discovery_document("drive:v3") #' download_discovery_document("sheets:v4") #' download_discovery_document("gmail:v1") #' download_discovery_document("bigquery:v2") #' download_discovery_document("docs:v1") #' download_discovery_document("youtube:v3") download_discovery_document <- function(id, path = NULL) { url <- make_discovery_url(id) dd <- httr::GET(url) httr::stop_for_status(dd, glue::glue("find Discovery Document for ID '{id}'")) if (is.null(path)) { dd_content <- httr::content(dd) api_date <- dd_content[c("revision", "id")] api_date <- c( id = sub(":", "-", api_date$id), revision = as.character(as.Date(api_date$revision, format = "%Y%m%d")) ) json_filename <- fs::path(paste(api_date, collapse = "_"), ext = "json") data_raw <- rprojroot::find_package_root_file("data-raw") path <- if (fs::dir_exists(data_raw)) { fs::path(data_raw, json_filename) } else { json_filename } } writeLines(httr::content(dd, as = "text"), path) path } #' Read a Discovery Document #' #' @param path Path to a JSON Discovery Document #' #' @return A list #' @examples #' drive <- "data-raw/drive-v3_2019-02-07.json" #' dd <- read_discovery_document(drive) read_discovery_document <- function(path) { jsonlite::fromJSON(path) } #' Get raw methods #' #' https://developers.google.com/discovery/v1/using#discovery-doc-methods #' #' @param dd List representing a Discovery Document #' #' @return a named list with one element per method #' @examples #' drive <- "data-raw/drive-v3_2019-02-07.json" #' dd <- read_discovery_document(drive) #' ee <- get_raw_methods(dd) get_raw_methods <- function(dd) { dd %>% pluck("resources") %>% map("methods") %>% flatten() %>% set_names(map_chr(., "id")) } #' Groom method properties #' #' Tweak raw method properties to make them more useful to us downstream: #' #' * Prepend the API's `servicePath` to `path`s. #' * Remove the constant stem `"https://www.googleapis.com/auth/"` from #' scopes and collapse multiple scopes into one comma-separated string. #' * Elevate any `$ref` part of `request` or `response` to be the actual #' data for `request` or `response`. #' * Reorder the properties so they appear in a predictable order. However, #' we do not turn missing properties into explicitly missing properties, #' i.e. we don't guarantee all methods have the same properties. #' #' We don't touch the `parameters` list here, because it needs enough work to #' justify having separate functions for that. #' #' @param methods A named list of raw methods, from [get_raw_methods()] #' @param dd A Discovery Document as a list, from [read_discovery_document()] #' #' @return A named list of "less raw" methods groom_properties <- function(method, dd) { method$path <- fs::path(dd$servicePath, method$path) condense_scopes <- function(scopes) { scopes %>% str_remove("https://www.googleapis.com/auth/") %>% str_c(collapse = ", ") } method$scopes <- condense_scopes(method$scopes) ## I am currently ignoring the fact that `request` sometimes has both ## a `$ref` and a `parameterName` part in the original JSON if (has_name(method, "request")) { method$request <- method$request$`$ref` } if (has_name(method, "response")) { method$response <- method$response$`$ref` } # all of the properties in the RestMethod schema, in order of usefulness property_names <- c( "id", "httpMethod", "path", "parameters", "scopes", "description", "request", "response", "mediaUpload", "supportsMediaDownload", "supportsMediaUpload", "useMediaDownloadService", "etagRequired", "parameterOrder", "supportsSubscription" ) method[intersect(property_names, names(method))] } #' Expand schema placeholders #' #' Adds the properties associated with a `request` schema to a method's #' parameter list. #' #' Some methods can send an instance of a API resource in the body of a request. #' This is indicated by the presence of a schema in the method's `request` #' property. For example, the `drive.files.copy` method permits a "Files #' resource" in the request body. This is how you convey the desired `name` of #' the new copy. #' #' In practice, this means you can drop such metadata in the body. That is, you #' don't actually have to label this explicitly as having `kind = drive#file` #' (although that would probably be more proper!), nor do you have to include #' all the possible pieces of metadata that constitute a "Files resource". Just #' specify the bits that you need to. #' #' https://developers.google.com/drive/api/v3/reference/files/copy #' https://developers.google.com/drive/api/v3/reference/files#resource #' #' This function consults the method's `request` and, if it holds a schema, the #' schema metadata is appended to the method's existing parameters. This way our #' request building functions recognize the keys and know that such info belongs #' in the body (vs. the url or the query). #' #' @param method A single method #' @param dd A Discovery Document as a list, from [read_discovery_document()] #' #' @return The input method, but with a potentially expanded parameter list. add_schema_params <- function(method, dd) { req <- pluck(method, "request") if (is.null(req)) { return(method) } id <- method$id schema_params <- dd[[c("schemas", req, "properties")]] schema_params <- modify(schema_params, ~ `[[<-`(.x, "location", "body")) message(glue::glue("{id} gains {req} schema params\n")) method$parameters <- c(method$parameters, schema_params) method } #' Add API-wide parameters #' #' Certain parameters are sensible for any request to a specific API and, #' indeed, are usually common across APIs. Examples are "fields", "key", and #' "oauth_token". This function appends these parameters to a method's parameter #' list. Yes, this means some info is repeated in all methods, but this way our #' methods are more self-contained and our request building functions can be #' simpler. #' #' @param method A single method #' @param dd A Discovery Document as a list, from [read_discovery_document()] #' #' @return The input method, but with an expanded parameter list. add_global_params <- function(method, dd) { method[["parameters"]] <- c(method[["parameters"]], dd[["parameters"]]) method } gargle/inst/discovery-doc-ingest/parameter-properties-humane.txt0000644000176200001440000000613413620077466024736 0ustar liggesusers$ref string A reference to another schema. The value of this property is the "id" of another schema. additionalProperties NULL If this is a schema for an object, this property is the schema for any additional properties with dynamic keys on this object. annotations object Additional information about this property. default string The default value of this property (if one exists). description string A description of this object. enum array Values this parameter may take (if it is an enum). enumDescriptions array The descriptions for the enums. Each position maps to the corresponding value in the "enum" array. format string An additional regular expression or key that helps constrain the value. For more details see: http://tools.ietf.org/html/draft-zyp- json-schema-03#section-5.23 id string Unique identifier for this schema. items NULL If this is a schema for an array, this property is the schema for each element in the array. location string Whether this parameter goes in the query or the path for REST requests. maximum string The maximum value of this parameter. minimum string The minimum value of this parameter. pattern string The regular expression this parameter must conform to. Uses Java 6 regex format: http:// docs.oracle.com/javase/6/docs/api/java/util/ regex/Pattern.html properties object If this is a schema for an object, list the schema for each property of this object. readOnly boolean The value is read-only, generated by the service. The value cannot be modified by the client. If the value is included in a POST, PUT, or PATCH request, it is ignored by the service. repeated boolean Whether this parameter may appear multiple times. required boolean Whether the parameter is required. type string The value type for this schema. A list of values can be found here: http:// tools.ietf.org/html/draft-zyp-json- schema-03#section-5.1 variant object In a variant data type, the value of one property is used to determine how to interpret the entire entity. Its value must exist in a map of descriminant values to schema names. gargle/inst/discovery-doc-ingest/discover-discovery.R0000644000176200001440000000503014022166555022503 0ustar liggesuserslibrary(tidyverse) ## necessary only during gargle development to get devtools' shim for ## system.file() load_all() ddi_dir <- system.file("discovery-doc-ingest", package = "gargle") source(fs::path(ddi_dir, "ingest-functions.R")) x <- download_discovery_document("discovery:v1") ee <- read_discovery_document(x) # api-wide parameters ap <- ee %>% pluck("parameters") %>% enframe(name = "property", value = "info") %>% mutate(info = map(info, enframe)) %>% unnest(cols = info) %>% spread(key = name, value = value, convert = TRUE) %>% select(property, type, description, everything()) %>% write_csv(fs::path(ddi_dir, "api-wide-parameters.csv")) # properties of a method mp <- ee %>% pluck("schemas", "RestMethod", "properties") %>% enframe(name = "property", value = "info") %>% mutate(info = map(info, enframe)) %>% unnest(cols = info) %>% spread(key = name, value = value, convert = TRUE) %>% select(property, type, description, everything()) %>% write_csv(fs::path(ddi_dir, "method-properties.csv")) # properties of a parameter of a method pp <- ee %>% pluck("schemas", "JsonSchema", "properties") %>% enframe(name = "property", value = "info") %>% mutate(info = map(info, enframe)) %>% unnest(cols = info) %>% spread(key = name, value = value, convert = TRUE) %>% select(property, type, description, everything()) %>% write_csv(fs::path(ddi_dir, "parameter-properties.csv")) make_humane_table <- function(df) { pad <- function(.x, .n) { length(.x) <- .n .x } df %>% select(property, type, description) %>% mutate( description = str_wrap(description, width = 45), description = str_split(description, pattern = "\n"), n = lengths(description), property = map2(property, n, pad), type = map2(type, n, pad), n = NULL ) %>% unnest(cols = c(property, type, description)) %>% replace_na(list(property = "", type = "")) %>% modify_at(c("property", "type"), ~ format(.x, justify = "left")) %>% glue::glue_data("{property} {type} {description}") } make_humane_table(ap) %>% write_lines(fs::path(ddi_dir, "api-wide-parameters-humane.txt")) make_humane_table(mp) %>% write_lines(fs::path(ddi_dir, "method-properties-humane.txt")) make_humane_table(pp) %>% write_lines(fs::path(ddi_dir, "parameter-properties-humane.txt")) write_lines(mp$property, fs::path(ddi_dir, "api-wide-parameter-names.txt")) write_lines(mp$property, fs::path(ddi_dir, "method-property-names.txt")) write_lines(pp$property, fs::path(ddi_dir, "parameter-property-names.txt")) gargle/inst/discovery-doc-ingest/parameter-property-names.txt0000644000176200001440000000026413620077471024246 0ustar liggesusers$ref additionalProperties annotations default description enum enumDescriptions format id items location maximum minimum pattern properties readOnly repeated required type variant gargle/inst/discovery-doc-ingest/method-property-names.txt0000644000176200001440000000030113620077470023535 0ustar liggesusersdescription etagRequired httpMethod id mediaUpload parameterOrder parameters path request response scopes supportsMediaDownload supportsMediaUpload supportsSubscription useMediaDownloadService gargle/inst/discovery-doc-ingest/method-properties-humane.txt0000644000176200001440000000362213620077464024233 0ustar liggesusersdescription string Description of this method. etagRequired boolean Whether this method requires an ETag to be specified. The ETag is sent as an HTTP If- Match or If-None-Match header. httpMethod string HTTP method used by this method. id string A unique ID for this method. This property can be used to match methods between different versions of Discovery. mediaUpload object Media upload parameters. parameterOrder array Ordered list of required parameters, serves as a hint to clients on how to structure their method signatures. The array is ordered such that the "most-significant" parameter appears first. parameters object Details for all parameters in this method. path string The URI path of this REST method. Should be used in conjunction with the basePath property at the api-level. request object The schema for the request. response object The schema for the response. scopes array OAuth 2.0 scopes applicable to this method. supportsMediaDownload boolean Whether this method supports media downloads. supportsMediaUpload boolean Whether this method supports media uploads. supportsSubscription boolean Whether this method supports subscriptions. useMediaDownloadService boolean Indicates that downloads from this method should use the download service URL (i.e. "/download"). Only applies if the method supports media download. gargle/inst/WORDLIST0000644000176200001440000000176114444241216013671 0ustar liggesusersAFAICT API's Auth AuthState Behaviour CLI CMD Codecov Colaboratory De Decrypt DropBox FieldMask Filepath GCE GCP GKE GceToken Gmail IAM JSON Jupyter Keyless Kubernetes OAuth OOB ORCID OpenID Opnieuw Opnieuw's PBC PID RStudio SDK Tidyverse URI URIs VM VMs WifToken XDG ambiently auth authed automagic automagically aws backoff backtrace behaviour bigrquery cancelled cli cloneable config cron crosslinks customise datetime dbi de decrypt decrypted deprecations dev discoverable ec env erroring filepath funder funs gargle’ gmailr googleComputeEngineR googleComputeEngineR's googleapis googledrive googledrive's googlesheets httpuv httr httr's inlined inlining io iteratively jitter json keyless lifecycle macOS misconfigured mockr mortem nonexported novo oauth oob pre prepending programmatically rappdirs refreshable repo retryable rlang roxygen rstudioapi shinyapps subclasses subprocess symlink targetted targetting templating testthat thingyr tidyverse urlencoded useR useRs userinfo webserver withr www