gargle/0000755000176200001440000000000014067641672011530 5ustar liggesusersgargle/NAMESPACE0000644000176200001440000000257514067635251012754 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_dat) export(AuthState) export(Gargle2.0) export(GceToken) export(WifToken) export(bulletize) export(cred_funs_add) export(cred_funs_clear) export(cred_funs_list) 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_error_message) export(gargle_map_cli) export(gargle_oauth_cache) export(gargle_oauth_email) export(gargle_oauth_sitrep) export(gargle_oob_default) export(gargle_verbosity) export(init_AuthState) 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(tidyverse_api_key) export(tidyverse_app) export(token_email) export(token_fetch) export(token_tokeninfo) export(token_userinfo) export(with_gargle_verbosity) import(fs) import(rlang) importFrom(glue,glue) importFrom(glue,glue_collapse) importFrom(glue,glue_data) gargle/LICENSE0000644000176200001440000000005514022166555012526 0ustar liggesusersYEAR: 2020 COPYRIGHT HOLDER: RStudio, Google gargle/README.md0000644000176200001440000001046114067403717013005 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/master/graph/badge.svg)](https://codecov.io/gh/r-lib/gargle?branch=master) [![R-CMD-check](https://github.com/r-lib/gargle/workflows/R-CMD-check/badge.svg)](https://github.com/r-lib/gargle/actions) 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("devtools") devtools::install_github("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. Select a #> pre-authorised account or enter '0' to obtain a new token. Press #> Esc/Ctrl + C to abort. #> 1: janedoe_personal@gmail.com #> 2: janedoe@example.com #> Selection: 1 token #> #> google #> gargle-demo #> janedoe_personal@gmail.com #> ...userinfo.email #> access_token, expires_in, refresh_token, scope, ... ``` 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" "Montserrat" "Noto Sans JP" "Open Sans" #> [5] "Poppins" "Roboto" "Roboto Condensed" "Source Sans Pro" ``` 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/0000755000176200001440000000000014067403717012277 5ustar liggesusersgargle/man/credentials_service_account.Rd0000644000176200001440000000464514067403577020334 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. Use for a service account which has been granted domain-wide authority by an administrator. Such delegation of domain-wide authority means that the service account is permitted to act on behalf of users, without their consent. Identify the user to impersonate via their email, e.g. \code{subject = "user@example.com"}.} } \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 vignette \href{https://gargle.r-lib.org/articles/how-gargle-gets-tokens.html}{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.Rd0000644000176200001440000000050114067372466015515 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.Rd0000644000176200001440000000626614067372466017447 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. Use for a service account which has been granted domain-wide authority by an administrator. Such delegation of domain-wide authority means that the service account is permitted to act on behalf of users, without their consent. Identify the user to impersonate via their email, e.g. \code{subject = "user@example.com"}.} } \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}.\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 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/production#providing_credentials_to_your_application} \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.Rd0000644000176200001440000000051314067372466014572 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.Rd0000644000176200001440000000324214022166555014662 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://developers.google.com/protocol-buffers/docs/reference/google.protobuf#json-encoding-of-field-masks}{JSON encoding of a Protocol Buffers FieldMask}. } gargle/man/credentials_gce.Rd0000644000176200001440000000300414067372466015704 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 for Google Compute Engine} \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{ Uses the metadata service available on GCE VMs to fetch an access token. } \examples{ \dontrun{ credentials_gce() } } \seealso{ \url{https://cloud.google.com/compute/docs/storing-retrieving-metadata} 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.Rd0000644000176200001440000001175714067372466014306 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-print}{\code{GceToken$print()}} \item \href{#method-init_credentials}{\code{GceToken$init_credentials()}} \item \href{#method-cache}{\code{GceToken$cache()}} \item \href{#method-load_from_cache}{\code{GceToken$load_from_cache()}} \item \href{#method-can_refresh}{\code{GceToken$can_refresh()}} \item \href{#method-refresh}{\code{GceToken$refresh()}} \item \href{#method-revoke}{\code{GceToken$revoke()}} \item \href{#method-clone}{\code{GceToken$clone()}} } } \if{html}{ \out{
Inherited methods} \itemize{ \item \out{}\href{../../httr/html/Token.html#method-hash}{\code{httr::Token$hash()}}\out{} \item \out{}\href{../../httr/html/Token.html#method-initialize}{\code{httr::Token$initialize()}}\out{} \item \out{}\href{../../httr/html/Token2.0.html#method-sign}{\code{httr::Token2.0$sign()}}\out{} \item \out{}\href{../../httr/html/Token2.0.html#method-validate}{\code{httr::Token2.0$validate()}}\out{} } \out{
} } \if{html}{\out{
}} \if{html}{\out{}} \if{latex}{\out{\hypertarget{method-print}{}}} \subsection{Method \code{print()}}{ Print token \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-init_credentials}{}}} \subsection{Method \code{init_credentials()}}{ Placeholder implementation of required method \subsection{Usage}{ \if{html}{\out{
}}\preformatted{GceToken$init_credentials()}\if{html}{\out{
}} } } \if{html}{\out{
}} \if{html}{\out{}} \if{latex}{\out{\hypertarget{method-cache}{}}} \subsection{Method \code{cache()}}{ Placeholder implementation of required method \subsection{Usage}{ \if{html}{\out{
}}\preformatted{GceToken$cache(...)}\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-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{
}} } \subsection{Arguments}{ \if{html}{\out{
}} \describe{ \item{\code{...}}{Not used.} } \if{html}{\out{
}} } } \if{html}{\out{
}} \if{html}{\out{}} \if{latex}{\out{\hypertarget{method-can_refresh}{}}} \subsection{Method \code{can_refresh()}}{ Placeholder implementation of required method \subsection{Usage}{ \if{html}{\out{
}}\preformatted{GceToken$can_refresh()}\if{html}{\out{
}} } } \if{html}{\out{
}} \if{html}{\out{}} \if{latex}{\out{\hypertarget{method-refresh}{}}} \subsection{Method \code{refresh()}}{ Refresh a GCE token \subsection{Usage}{ \if{html}{\out{
}}\preformatted{GceToken$refresh()}\if{html}{\out{
}} } } \if{html}{\out{
}} \if{html}{\out{}} \if{latex}{\out{\hypertarget{method-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-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.Rd0000644000176200001440000000161714017730626015532 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.Rd0000644000176200001440000000335114067372466015067 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}} 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. Use \code{\link[=cred_funs_list]{cred_funs_list()}} to see the current registry, in order. See the vignette \href{https://gargle.r-lib.org/articles/how-gargle-gets-tokens.html}{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{ 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.Rd0000644000176200001440000000062014017730626015675 0ustar liggesusers% Generated by roxygen2: do not edit by hand % Please edit documentation in R/gargle-api-key.R, R/gargle-oauth-app.R \name{internal-assets} \alias{internal-assets} \alias{tidyverse_api_key} \alias{tidyverse_app} \title{Assets for internal use} \usage{ tidyverse_api_key() tidyverse_app() } \description{ Assets for use inside specific packages maintained by the tidyverse team. } \keyword{internal} gargle/man/gargle2.0_token.Rd0000644000176200001440000000506214067372466015460 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(), app = gargle_app(), package = "gargle", scope = NULL, user_params = NULL, type = NULL, use_oob = gargle_oob_default(), credentials = NULL, cache = if (is.null(credentials)) gargle_oauth_cache() else FALSE, ... ) } \arguments{ \item{email}{Optional. Allows 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 targetted 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. Use \code{NA} or \code{FALSE} to match nothing and force the OAuth dance in the browser. Use \code{TRUE} to allow email auto-discovery, if exactly one matching token is found in the cache. Specify just the domain with a glob pattern, e.g. \code{"*@example.com"}, to create code that "just works" for both \code{alice@example.com} and \code{bob@example.com}. Defaults to the option named "gargle_oauth_email", retrieved by \code{\link[=gargle_oauth_email]{gargle_oauth_email()}}.} \item{app}{An OAuth consumer application, created by \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{user_params}{Named list holding endpoint specific parameters to pass to the server when posting the request for obtaining or refreshing the access token.} \item{type}{content type used to override incorrect server response} \item{use_oob}{Whether to prefer "out of band" authentication. Defaults to the option named "gargle_oob_default", retrieved via \code{\link[=gargle_oob_default]{gargle_oob_default()}}.} \item{credentials}{Advanced use only: allows you to completely customise token generation.} \item{cache}{Specifies the OAuth token cache. Defaults to the option named "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.} } \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/gargle-package.Rd0000644000176200001440000000213214022166555015413 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 . 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@rstudio.com} (\href{https://orcid.org/0000-0002-6983-2759}{ORCID}) Authors: \itemize{ \item Craig Citro \email{craigcitro@google.com} \item Hadley Wickham \email{hadley@rstudio.com} (\href{https://orcid.org/0000-0003-4757-117X}{ORCID}) } Other contributors: \itemize{ \item Google Inc [copyright holder] \item RStudio [copyright holder, funder] } } \keyword{internal} gargle/man/gargle_oauth_sitrep.Rd0000644000176200001440000000203514067372466016623 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 "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 app (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.Rd0000644000176200001440000001361614067372466014331 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-new}{\code{WifToken$new()}} \item \href{#method-init_credentials}{\code{WifToken$init_credentials()}} \item \href{#method-refresh}{\code{WifToken$refresh()}} \item \href{#method-format}{\code{WifToken$format()}} \item \href{#method-print}{\code{WifToken$print()}} \item \href{#method-can_refresh}{\code{WifToken$can_refresh()}} \item \href{#method-cache}{\code{WifToken$cache()}} \item \href{#method-load_from_cache}{\code{WifToken$load_from_cache()}} \item \href{#method-validate}{\code{WifToken$validate()}} \item \href{#method-revoke}{\code{WifToken$revoke()}} \item \href{#method-clone}{\code{WifToken$clone()}} } } \if{html}{ \out{
Inherited methods} \itemize{ \item \out{}\href{../../httr/html/Token.html#method-hash}{\code{httr::Token$hash()}}\out{} \item \out{}\href{../../httr/html/Token2.0.html#method-sign}{\code{httr::Token2.0$sign()}}\out{} } \out{
} } \if{html}{\out{
}} \if{html}{\out{}} \if{latex}{\out{\hypertarget{method-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-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-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-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-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-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-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-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-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-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-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.Rd0000644000176200001440000000204014017730626016613 0ustar liggesusers% Generated by roxygen2: do not edit by hand % Please edit documentation in R/oauth-app.R \name{oauth_app_from_json} \alias{oauth_app_from_json} \title{Create an OAuth app from JSON} \usage{ oauth_app_from_json(path, appname = NULL) } \arguments{ \item{path}{JSON downloaded from Google Cloud Platform Console, containing a client id (aka key) 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{ Essentially a wrapper around \code{\link[httr:oauth_app]{httr::oauth_app()}} that extracts the necessary info from JSON obtained from \href{https://console.cloud.google.com}{Google Cloud Platform Console}. If no \code{appname} is given, the \code{"project_id"} from the JSON is used. } \examples{ \dontrun{ oauth_app( path = "/path/to/the/JSON/you/downloaded/from/gcp/console.json" ) } } gargle/man/AuthState-class.Rd0000644000176200001440000001673314067372466015613 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 client package that makes requests to a Google API. The \href{https://gargle.r-lib.org/articles/gargle-auth-in-client-package.html}{How to use gargle for auth in a client package} vignette 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{app} is an OAuth app 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 app 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{app}}{An OAuth consumer application.} \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-new}{\code{AuthState$new()}} \item \href{#method-format}{\code{AuthState$format()}} \item \href{#method-set_app}{\code{AuthState$set_app()}} \item \href{#method-set_api_key}{\code{AuthState$set_api_key()}} \item \href{#method-set_auth_active}{\code{AuthState$set_auth_active()}} \item \href{#method-set_cred}{\code{AuthState$set_cred()}} \item \href{#method-clear_cred}{\code{AuthState$clear_cred()}} \item \href{#method-get_cred}{\code{AuthState$get_cred()}} \item \href{#method-has_cred}{\code{AuthState$has_cred()}} \item \href{#method-clone}{\code{AuthState$clone()}} } } \if{html}{\out{
}} \if{html}{\out{}} \if{latex}{\out{\hypertarget{method-new}{}}} \subsection{Method \code{new()}}{ Create a new AuthState \subsection{Usage}{ \if{html}{\out{
}}\preformatted{AuthState$new( package = NA_character_, app = NULL, api_key = NULL, auth_active = TRUE, cred = NULL )}\if{html}{\out{
}} } \subsection{Arguments}{ \if{html}{\out{
}} \describe{ \item{\code{package}}{Package name.} \item{\code{app}}{An OAuth consumer application.} \item{\code{api_key}}{An API key.} \item{\code{auth_active}}{Logical, indicating whether auth is active.} \item{\code{cred}}{Credentials.} } \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-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-set_app}{}}} \subsection{Method \code{set_app()}}{ Set the OAuth app \subsection{Usage}{ \if{html}{\out{
}}\preformatted{AuthState$set_app(app)}\if{html}{\out{
}} } \subsection{Arguments}{ \if{html}{\out{
}} \describe{ \item{\code{app}}{An OAuth consumer application.} } \if{html}{\out{
}} } } \if{html}{\out{
}} \if{html}{\out{}} \if{latex}{\out{\hypertarget{method-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-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-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-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-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-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-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/request_develop.Rd0000644000176200001440000001406414022166555015776 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 \href{https://support.google.com/googleapi/answer/6158857?hl=en&ref_topic=7013279}{Credentials, access, security, and identity}. 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 vignette \href{https://gargle.r-lib.org/articles/request-helper-functions.html}{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 \href{https://www.googleapis.com/discovery/v1/apis/drive/v3/rest}{Drive API Discovery Document}, 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/credentials_external_account.Rd0000644000176200001440000001036614067372466020515 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/access-resources-aws#generate}{Automatically generate credentials}. 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/access-resources-aws} } 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.Rd0000644000176200001440000001144414067403717015507 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. } \details{ Consider an example where we are willing to make a request up to 5 times.\preformatted{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. \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:\preformatted{b , 2b, 4b, 8b, ... = b * 2^0, b * 2^1, b * 2^2, b * 2^3, ... } 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}:\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) } } \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. The Sheets API v4 has "a limit of 500 requests per 100 seconds per project and 100 requests per 100 seconds per user. Limits for reads and writes are tracked separately." In our experience, the "100 (read or write) requests per 100 seconds 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 100 seconds, 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://tools.ietf.org/html/rfc7231#section-7.1.3} \item \url{https://developers.google.com/sheets/api/reference/limits} \item \url{https://googleapis.dev/python/google-api-core/latest/retry.html} } } gargle/man/figures/0000755000176200001440000000000014067372466013751 5ustar liggesusersgargle/man/figures/lifecycle-defunct.svg0000644000176200001440000000170414067372466020061 0ustar liggesuserslifecyclelifecycledefunctdefunct gargle/man/figures/lifecycle-maturing.svg0000644000176200001440000000170614067372466020261 0ustar liggesuserslifecyclelifecyclematuringmaturing gargle/man/figures/lifecycle-archived.svg0000644000176200001440000000170714067372466020221 0ustar liggesusers lifecyclelifecyclearchivedarchived gargle/man/figures/lifecycle-questioning.svg0000644000176200001440000000171414067372466020777 0ustar liggesuserslifecyclelifecyclequestioningquestioning gargle/man/figures/lifecycle-superseded.svg0000644000176200001440000000171314067372466020574 0ustar liggesusers lifecyclelifecyclesupersededsuperseded gargle/man/figures/lifecycle-stable.svg0000644000176200001440000000167414067372466017711 0ustar liggesuserslifecyclelifecyclestablestable gargle/man/figures/lifecycle-experimental.svg0000644000176200001440000000171614067372466021131 0ustar liggesuserslifecyclelifecycleexperimentalexperimental gargle/man/figures/lifecycle-deprecated.svg0000644000176200001440000000171214067372466020530 0ustar liggesuserslifecyclelifecycledeprecateddeprecated gargle/man/oauth_external_token.Rd0000644000176200001440000000321014067372466017012 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/access-resources-aws#generate}{Automatically generate credentials}. 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.Rd0000644000176200001440000000574414067372466017236 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 does very little when called directly with a token: \itemize{ \item If input 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 produced by \code{googledrive::drive_token()} or \code{bigrquery::bq_token()}. \item Checks that the input appears to be a Google OAuth token, based on the embedded \code{oauth_endpoint}. \item Refreshes the token, if it's refreshable. \item Returns its input. } There is no point 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:\preformatted{library(googledrive) library(googlesheets4) drive_auth(email = "jane_doe@example.com") sheets_auth(token = drive_token()) # work with both packages freely now } } \examples{ \dontrun{ # assume `my_token` is a Token2.0 object returned by a function such as # httr::oauth2.0_token() or gargle::gargle2.0_token() 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.Rd0000644000176200001440000000701114067372466016167 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) response_as_json(resp) gargle_error_message(resp) } \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.} } \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.Rd0000644000176200001440000000354414022166555014642 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. 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.Rd0000644000176200001440000000750714067372466015621 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_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_verbosity() local_gargle_verbosity(level, env = parent.frame()) 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:\preformatted{options( gargle_oauth_email = "jane@example.com", gargle_oauth_cache = "/path/to/folder/that/does/not/sync/to/cloud" ) } } \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 the option named "gargle_oob_default", falls back to the option named "httr_oob_default", and eventually defaults to \code{FALSE}. This controls whether to prefer "out of band" authentication. We also return \code{FALSE} unconditionally on RStudio Server or Cloud. This value is ultimately passed to \code{\link[httr:init_oauth2.0]{httr::init_oauth2.0()}} as \code{use_oob}. If \code{FALSE} (and httpuv is installed), a local webserver is used for the OAuth dance. Otherwise, user gets a URL and prompt for a validation code. Read more about "out of band" authentication in the vignette \href{https://gargle.r-lib.org/articles/auth-from-web.html}{Auth when using R in the browser}. } \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_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_verbosity() } gargle/man/gargle_app.Rd0000644000176200001440000000164414067372466014702 0ustar liggesusers% Generated by roxygen2: do not edit by hand % Please edit documentation in R/gargle-oauth-app.R \name{gargle_app} \alias{gargle_app} \title{OAuth app for demonstration purposes} \usage{ gargle_app() } \value{ An OAuth consumer application, produced by \code{\link[httr:oauth_app]{httr::oauth_app()}}, invisibly. } \description{ Invisibly returns an OAuth app that can be used to test drive gargle before obtaining your own client ID and secret. This OAuth app 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 app (client ID and secret), 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{ \dontrun{ gargle_app() } } \keyword{internal} gargle/man/request_make.Rd0000644000176200001440000000510114022166555015245 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.Rd0000644000176200001440000000272414022166555015515 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_, app = NULL, api_key = NULL, auth_active = TRUE, cred = NULL ) } \arguments{ \item{package}{Package name, an optional string. The associated package will generally by implied by the namespace within which the \code{AuthState} is defined. But it's possible to record the package name explicitly and seems like a good practice.} \item{app}{Optional. An OAuth consumer application, as produced by \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()}}.} } \value{ An object of class \link{AuthState}. } \description{ Constructor function for objects of class \link{AuthState}. } \examples{ my_app <- httr::oauth_app( appname = "my_package", key = "keykeykeykeykeykey", secret = "secretsecretsecret" ) init_AuthState( package = "my_package", app = my_app, api_key = "api_key_api_key_api_key", ) } gargle/man/credentials_user_oauth2.Rd0000644000176200001440000001056414067372466017417 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, app = gargle_app(), package = "gargle", ... ) } \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{app}{An OAuth consumer application, created by \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. Allows 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 targetted 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. Use \code{NA} or \code{FALSE} to match nothing and force the OAuth dance in the browser. Use \code{TRUE} to allow email auto-discovery, if exactly one matching token is found in the cache. Specify just the domain with a glob pattern, e.g. \code{"*@example.com"}, to create code that "just works" for both \code{alice@example.com} and \code{bob@example.com}. Defaults to the option named "gargle_oauth_email", retrieved by \code{\link[=gargle_oauth_email]{gargle_oauth_email()}}.} \item{\code{use_oob}}{Whether to prefer "out of band" authentication. Defaults to the option named "gargle_oob_default", retrieved via \code{\link[=gargle_oob_default]{gargle_oob_default()}}.} \item{\code{cache}}{Specifies the OAuth token cache. Defaults to the option named "gargle_oauth_cache", retrieved via \code{\link[=gargle_oauth_cache]{gargle_oauth_cache()}}.} \item{\code{user_params}}{Named list holding endpoint specific parameters to pass to the server when posting the request for obtaining or refreshing the access token.} \item{\code{type}}{content type used to override incorrect server response} \item{\code{credentials}}{Advanced use only: allows you to completely customise token generation.} }} } \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 app 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 app scopes <- "https://www.googleapis.com/auth/drive" credentials_user_oauth2(scopes, app = gargle_app()) ## bring your own app app <- httr::oauth_app( appname = "my_awesome_app", key = "keykeykeykeykeykey", secret = "secretsecretsecret" ) credentials_user_oauth2(scopes, app) } } \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.Rd0000644000176200001440000000336213660264223014535 0ustar liggesusers% Generated by roxygen2: do not edit by hand % Please edit documentation in R/credential-function-registry.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_set_default} \title{Credential function registry} \usage{ cred_funs_list() cred_funs_add(...) cred_funs_set(ls) cred_funs_clear() cred_funs_set_default() } \arguments{ \item{...}{One or more functions with the right signature: its first argument is named \code{scopes}, and it includes \code{...} as an argument.} \item{ls}{A list of credential functions.} } \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:\preformatted{* "First registered, last tried." * "Last registered, first tried." } \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_set_default}: Reset the registry to the gargle default. }} \examples{ names(cred_funs_list()) creds_one <- function(scopes, ...) {} cred_funs_add(creds_one) cred_funs_add(one = creds_one) cred_funs_add(one = creds_one, two = creds_one) cred_funs_add(one = creds_one, creds_one) # undo all of the above and return to default cred_funs_set_default() } \seealso{ \code{\link[=token_fetch]{token_fetch()}}, which is where the registry is actually used. } gargle/man/Gargle-class.Rd0000644000176200001440000001657214067372466015113 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, app, 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, below \code{"~/.R/gargle/gargle-oauth"}, i.e. at the user level. 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.} } \if{html}{\out{
}} } \section{Methods}{ \subsection{Public methods}{ \itemize{ \item \href{#method-new}{\code{Gargle2.0$new()}} \item \href{#method-format}{\code{Gargle2.0$format()}} \item \href{#method-print}{\code{Gargle2.0$print()}} \item \href{#method-hash}{\code{Gargle2.0$hash()}} \item \href{#method-cache}{\code{Gargle2.0$cache()}} \item \href{#method-load_from_cache}{\code{Gargle2.0$load_from_cache()}} \item \href{#method-refresh}{\code{Gargle2.0$refresh()}} \item \href{#method-init_credentials}{\code{Gargle2.0$init_credentials()}} \item \href{#method-clone}{\code{Gargle2.0$clone()}} } } \if{html}{ \out{
Inherited methods} \itemize{ \item \out{}\href{../../httr/html/Token2.0.html#method-can_refresh}{\code{httr::Token2.0$can_refresh()}}\out{} \item \out{}\href{../../httr/html/Token2.0.html#method-revoke}{\code{httr::Token2.0$revoke()}}\out{} \item \out{}\href{../../httr/html/Token2.0.html#method-sign}{\code{httr::Token2.0$sign()}}\out{} \item \out{}\href{../../httr/html/Token2.0.html#method-validate}{\code{httr::Token2.0$validate()}}\out{} } \out{
} } \if{html}{\out{
}} \if{html}{\out{}} \if{latex}{\out{\hypertarget{method-new}{}}} \subsection{Method \code{new()}}{ Create a Gargle2.0 token \subsection{Usage}{ \if{html}{\out{
}}\preformatted{Gargle2.0$new( email = gargle_oauth_email(), app = gargle_app(), package = "gargle", credentials = NULL, params = list(), cache_path = gargle_oauth_cache() )}\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{app}}{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 \code{\link[httr:init_oauth2.0]{httr::init_oauth2.0()}}. Some we actively use in gargle: \code{scope}, \code{use_oob}. Most we do not: \code{user_params}, \code{type}, \code{as_header}, \code{use_basic_auth}, \code{config_init}, \code{client_credentials}.} \item{\code{cache_path}}{Specifies the OAuth token cache. Read more in \code{\link[=gargle_oauth_cache]{gargle_oauth_cache()}}.} } \if{html}{\out{
}} } \subsection{Returns}{ A Gargle2.0 token. } } \if{html}{\out{
}} \if{html}{\out{}} \if{latex}{\out{\hypertarget{method-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-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-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-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-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-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-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-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/DESCRIPTION0000644000176200001440000000361414067641672013242 0ustar liggesusersPackage: gargle Title: Utilities for Working with Google APIs Version: 1.2.0 Authors@R: c(person(given = "Jennifer", family = "Bryan", role = c("aut", "cre"), email = "jenny@rstudio.com", comment = c(ORCID = "0000-0002-6983-2759")), person(given = "Craig", family = "Citro", role = "aut", email = "craigcitro@google.com"), person(given = "Hadley", family = "Wickham", role = "aut", email = "hadley@rstudio.com", comment = c(ORCID = "0000-0003-4757-117X")), person(given = "Google Inc", role = "cph"), person(given = "RStudio", 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.3) Imports: cli (>= 3.0.0), fs (>= 1.3.1), glue (>= 1.3.0), httr (>= 1.4.0), jsonlite, rappdirs, rlang (>= 0.4.9), rstudioapi, stats, utils, withr Suggests: aws.ec2metadata, aws.signature, covr, httpuv, knitr, mockr, rmarkdown, sodium, spelling, testthat (>= 3.0.0) VignetteBuilder: knitr Config/testthat/edition: 3 Encoding: UTF-8 Language: en-US RoxygenNote: 7.1.1.9001 NeedsCompilation: no Packaged: 2021-07-02 16:28:58 UTC; jenny Author: Jennifer Bryan [aut, cre] (), Craig Citro [aut], Hadley Wickham [aut] (), Google Inc [cph], RStudio [cph, fnd] Maintainer: Jennifer Bryan Repository: CRAN Date/Publication: 2021-07-02 16:50:02 UTC gargle/build/0000755000176200001440000000000014067637312012623 5ustar liggesusersgargle/build/vignette.rds0000644000176200001440000000074114067637312015164 0ustar liggesusersSKO0>xJ(b? !ԪRVzfg8N#nm.,dn+;o^ߌnGQԍ{;mmw_.k`83pHd(j\ig 3?3%bc(%5`_̅3CƱ6"82vW2O->;b6C+&&i8p tRCJB"' Aip)Ț2G˼WS3DBh򆉱P%UHhV؊J'SX0oԏ=W@?jԁRF hw%?o.j|h=uӌ?7>`/|h}@lebk] _ƜiGKj}× ۿ߆i״b:_oU_ڽr-v|78C=JcE/u>6_33X:9Xⱉ gargle/tests/0000755000176200001440000000000014067637312012666 5ustar liggesusersgargle/tests/spelling.R0000644000176200001440000000024114022166555014620 0ustar liggesusersif(requireNamespace('spelling', quietly = TRUE)) spelling::spell_check_test(vignettes = TRUE, error = FALSE, skip_on_cran = TRUE) gargle/tests/testthat/0000755000176200001440000000000014067641672014532 5ustar liggesusersgargle/tests/testthat/test-credentials_app_default.R0000644000176200001440000000431414067372466022477 0ustar liggesuserstest_that("credentials_app_default_path(), default, non-Windows", { withr::local_envvar(c( GOOGLE_APPLICATION_CREDENTIALS = NA, CLOUDSDK_CONFIG = NA, APPDATA = NA, SystemDrive = NA )) with_mock( is_windows = function() FALSE, { expect_equal( credentials_app_default_path(), path_home(".config", "gcloud", "application_default_credentials.json") ) } ) }) test_that("credentials_app_default_path(), default, Windows", { withr::local_envvar(c( GOOGLE_APPLICATION_CREDENTIALS = NA, CLOUDSDK_CONFIG = NA, APPDATA = NA, SystemDrive = NA )) with_mock( is_windows = function() TRUE, { expect_equal( credentials_app_default_path(), path("C:", "gcloud", "application_default_credentials.json") ) } ) }) test_that("credentials_app_default_path(), system drive, Windows", { withr::local_envvar(c( GOOGLE_APPLICATION_CREDENTIALS = NA, CLOUDSDK_CONFIG = NA, APPDATA = NA, SystemDrive = "D:" )) with_mock( is_windows = function() TRUE, { expect_equal( credentials_app_default_path(), path("D:", "gcloud", "application_default_credentials.json") ) } ) }) test_that("credentials_app_default_path(), APPDATA env var, Windows", { withr::local_envvar(c( GOOGLE_APPLICATION_CREDENTIALS = NA, CLOUDSDK_CONFIG = NA, APPDATA = path("D:", "AppData"), SystemDrive = "D:" )) with_mock( is_windows = function() TRUE, { expect_equal( credentials_app_default_path(), path("D:", "AppData", "gcloud", "application_default_credentials.json") ) } ) }) test_that("credentials_app_default_path(), CLOUDSDK_CONFIG env var", { withr::local_envvar(c( GOOGLE_APPLICATION_CREDENTIALS = NA, CLOUDSDK_CONFIG = path("CLOUDSDK", "path") )) expect_equal( credentials_app_default_path(), path("CLOUDSDK", "path", "application_default_credentials.json") ) }) test_that("credentials_app_default_path(), GOOGLE_APPLICATION_CREDENTIALS env var", { withr::local_envvar(c( GOOGLE_APPLICATION_CREDENTIALS = path("GAC", "path"), CLOUDSDK_CONFIG = path("CLOUDSDK", "path") )) expect_equal( credentials_app_default_path(), path("GAC", "path") ) }) gargle/tests/testthat/test-credentials-byo-oauth2.R0000644000176200001440000000251214067372466022120 0ustar liggesuserstest_that("credentials_byo_oauth2() demands a Token2.0", { expect_error( credentials_byo_oauth2(token = "a_naked_access_token"), 'inherits(token, "Token2.0") is not TRUE', fixed = TRUE ) }) test_that("credentials_byo_oauth2() rejects a token that obviously not Google", { token <- httr::Token2.0$new( app = httr::oauth_app("x", "y", "z"), endpoint = httr::oauth_endpoints("github"), credentials = list(access_token = "ACCESS_TOKEN"), cache_path = FALSE ) expect_error( credentials_byo_oauth2(token = token), "doesn't use Google's OAuth endpoint" ) }) 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-aaa.R0000644000176200001440000000057614017730626016355 0ustar liggesuserstest_that("token works", { skip_if_offline() skip_if_no_auth() expect_error_free( token <- credentials_service_account( scopes = "https://www.googleapis.com/auth/userinfo.email", path = rawToChar(secret_read("gargle", "gargle-testing.json")) ) ) email <- token_email(token) expect_match(email, "^gargle-testing@.*[.]iam[.]gserviceaccount[.]com") }) gargle/tests/testthat/fixtures/0000755000176200001440000000000014067637312016377 5ustar liggesusersgargle/tests/testthat/fixtures/tokeninfo-stale_400.rds0000644000176200001440000000220514067372466022603 0ustar liggesusersVoEWb$MQEXk?R5N4&$ʥ2ݱ=d=u p@BUq~ڞ^z̾z=.RL*-a _<qiX!nd C*zEL 1رN?N\SN|uђVi{k5VՌj~^v:&2h:C:#d ~ANTj"U76 0/AאD0ZetS3X>o)9jr'󇸇98?'"nSDiCwwvQR)A]mF^d[a%9SܩSG1ڨUl,Xx[TfJ@5ex5dTpfK9HpJT̑F܇,B2;Z5Ǯ; lz-:s|TXVG#[Ȍ_Qki#W˄DQlM\h}v6gNR-W/I  _ ,ϧ7>{%UB^xڛ49gka$ppwi@kbBXzz3#7[9*/fC2Eʩٷ}O?{pͧ?~ݾ_?\Ƿ×zbQWQfs|fN,;Pi Dž"\L [ Y(VPAIx,*]Ҧ i[owᆬ$O^2MHV@ya6/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.rds0000644000176200001440000000266214067372466031144 0ustar liggesusersWoEw$n@HXPC;i7uS棶Ӻ5ۓlff];3gn B  8~?[ D̛73ާP(4r9"wx)]Ou(YTj{4F>h=yH&>{Żg͎=\^3ٍ;Gmuo]PծVzꃛ'Dyb[xجX6A ⭸_xHfr*=Zm0 F B 榢vXl7n'7sbhoՑXUre1rY%^J~#o%!L+^ u7B!T [+2*l42k[3k[[i[g6Q7V j6O &20y%=3_V))b`avYҰRyNscV+*R8cT}ΓW6qr\jt=_^ o$E@eUG\KDt^'\^x12.q8JgSe?^ K-YL /|$^ u (y7p!fK8UaT|]h\4!pTШpM,$#6 p" ?WwzHq ]diJ) # <\+Z\S8fxIqaXrUsqh6H}c kJ2%X0*Ba\LMщAD`G0S bS3ж8)Q$imx xj˴'3Z6K8*nUi)az wslj43;/ob88V6ouCO)}ܨU xR5|uqq@9˧6h@/5#]ar ]ڃ90NqgۓGNْ~?9g1?lʿ;:|' 8fEvZC=ǿ_rW?,~~Ol_|74efX# %91Szj[@Msİ͸̧2e#3 .XdI*-7d&ث*EoK.}VUޅ?`ybYU%^)ۢK9!_ul턆xV<= L*ʤ(juakn**ĀS0XR9y*5Y.:):X"0-h)#Ctx\hmTG M/~]I>ͯ>-rs!0IKS,U]l1Ek!^3Mf BcUj+0-nNs p 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/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.R0000644000176200001440000000155014025417246030536 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 <- googlesheets4::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.rds0000644000176200001440000000220714067372466023161 0ustar liggesusersVoEG'i*^VT^{m'U`LD|*RUE;uDs\7J~9qIξyo{7{Rd2\/Ȃ|@kV*>c}#xe9|1U`g8}olXL79% f`f騏[L}OM,$bV3ZҌZ04h~7UVjF zYpIPՏ=U@ coiqSEoI\EṾSށrǑuDMi=l*Z+-an}i3LYvvՠF}LQ٨QYi\x2{'f|K7@g|z-67%JHMFgLQAr;'BwMLyHyr&<̇  ]zOO2;3agURQႴpg#9O_P M=H\|0"geym4v:TK!A`~Ԋn_/1.k@\22i>@89cig+~m;/o~ݾ'ΟWo_=)$L-hG 370tr, U0p^0l~4r]h0* I%mz-<<xlγ ۦ] 688 -*83K}!* msjv[mihr-nl4Rz9@4΋> TX_4rρ6;SsEz;;@d}n}'l[cJn1(0RŌ-pǒ2$gHf (> j8%⿃ZXǹl2!>cgO7Lʭ7/3ѓAƈH?SCn\4ntgargle/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-oauth-app.R0000644000176200001440000000155514067372466017540 0ustar liggesuserstest_that("oauth app from JSON", { oa <- oauth_app_from_json( test_path( "fixtures", "client_secret_123.googleusercontent.com.json" ) ) expect_s3_class(oa, "oauth_app") expect_equal(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( test_path( "fixtures", "client_secret_456.googleusercontent.com.json" ) ) expect_s3_class(oa, "oauth_app") expect_equal(oa$appname, "a_project") expect_equal(oa$secret, "ssshh-i-am-a-secret") expect_equal(oa$key, "abc.apps.googleusercontent.com") }) test_that("JSON that is apparently not an oauth app triggers error", { nope <- jsonlite::toJSON(test_path("fixtures", "service-account-token.json")) expect_error( oauth_app_from_json(nope), "Can't find .* in the JSON" ) }) gargle/tests/testthat/test-Gargle-class.R0000644000176200001440000000446314067372466020147 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", { rlang::local_interactive(FALSE) expect_error(gargle2.0_token(cache = FALSE), "requires an interactive session") }) 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_error( gargle2.0_token(email = NA, cache = cache_folder), "OAuth2 flow requires an interactive session" ) expect_error( gargle2.0_token(email = FALSE, cache = cache_folder), "OAuth2 flow requires an interactive session" ) }) test_that("Gargle2.0 prints nicely", { fauxen <- gargle2.0_token( email = "a@example.org", app = httr::oauth_app("APPNAME", key = "KEY", secret = "SECRET"), credentials = list(a = 1), cache = FALSE ) expect_snapshot(print(fauxen)) }) gargle/tests/testthat/test-inside-the-house.R0000644000176200001440000000164014067372466021007 0ustar liggesuserstest_that("gargle is 'inside the house'", { expect_true(from_permitted_package()) expect_error_free(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", { oa <- gargle_app() expect_s3_class(oa, "oauth_app") expect_match(oa$appname, "^gargle") }) test_that("tidyverse oauth app", { oa <- tidyverse_app() expect_s3_class(oa, "oauth_app") expect_match(oa$appname, "^tidyverse") }) gargle/tests/testthat/test-request-make.R0000644000176200001440000000043514067372466020241 0ustar liggesuserstest_that("request_make() errors for invalid HTTP methods", { expect_error( request_make(list(method = httr::GET)), "is.character(x$method) is not TRUE", fixed = TRUE ) expect_error( request_make(list(method = "PETCH")), "Not a recognized HTTP method" ) }) gargle/tests/testthat/test-registry.R0000644000176200001440000000303514067372466017505 0ustar liggesusers# These are used in several tests below. creds_one <- function(scopes, ...) {} creds_two <- function(scopes, arg1, arg2 = "optional", ...) {} test_that("We recognize the right credential functions", { expect_true(is_cred_fun(creds_one)) expect_true(is_cred_fun(creds_two)) invalid_one <- function(scope, ...) {} invalid_two <- function(scopes, arg1, arg2 = "optional") {} invalid_three <- 17 expect_false(is_cred_fun(invalid_one)) expect_false(is_cred_fun(invalid_two)) expect_false(is_cred_fun(invalid_three)) }) test_that("We can register new credential functions", { withr::defer(cred_funs_clear()) cred_funs_clear() cred_funs_add(creds_one) expect_equal(1, length(cred_funs_list())) cred_funs_add(creds_two) expect_equal(2, length(cred_funs_list())) cred_funs_clear() expect_equal(0, length(cred_funs_list())) for (i in 1:5) { cred_funs_add(creds_one) expect_equal(i, length(cred_funs_list())) } cred_funs_set(list(creds_two)) expect_equal(1, length(cred_funs_list())) }) test_that("We capture credential function names when possible", { withr::defer(cred_funs_clear()) cred_funs_clear() cred_funs_add(a = creds_one) cred_funs_add(b = function(scopes, ...) {}) cred_funs_add(creds_one) cred_funs_add(function(scopes, ...) {}) expect_equal(names(cred_funs_list()), c("", "", "b", "a")) cred_funs_clear() cred_funs_add( function(scopes, ...) {}, creds_one, b = function(scopes, ...) {}, a = creds_one ) expect_equal(names(cred_funs_list()), c("", "", "b", "a")) }) gargle/tests/testthat/test-oauth-refresh.R0000644000176200001440000000167014067372466020414 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.R0000644000176200001440000002550314067372466020022 0ustar liggesusers# cache_establish ------------------------------------------------------------ test_that("cache_establish() insists on sensible input", { expect_error( cache_establish(letters[1:2]), "must have length 1" ) expect_error( cache_establish(1), class = "gargle_error_bad_class" ) expect_error( cache_establish(list(1)), class = "gargle_error_bad_class" ) }) test_that("`cache = TRUE` uses default cache path", { with_mock( ## 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", { with_mock( # we want no existing cache to be found, be it current or legacy gargle_default_oauth_cache_path = function() file_temp(), gargle_legacy_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::with_options( list(gargle_oauth_cache = NA), with_mock( # we want no existing cache to be found, be it current or legacy gargle_default_oauth_cache_path = function() file_temp(), gargle_legacy_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)", { 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", app = httr::oauth_app("apple", key = "KEY", secret = "SECRET"), credentials = list(a = 1), cache = cache_folder ) fauxen_b <- gargle2.0_token( email = "b@example.org", app = httr::oauth_app("banana", key = "KEY", secret = "SECRET"), 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$app, "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) }) # 1 short hash match, interactive # >1 short hash match, interactive # 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", "app", "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-gce-token.R0000644000176200001440000000267214067372466017517 0ustar liggesuserstest_that("Can list service accounts", { service_accounts <- c("account1@project.gserviceaccount.com", "default") request_mock <- function(path, ...) { stopifnot(path == "instance/service-accounts") httr:::response( url = path, status_code = 200, header = list(`metadata-flavor` = "Google"), content = charToRaw(paste0(c(service_accounts, ""), collapse = "/\n")) ) } with_mock( gce_metadata_request = request_mock, { expect_equal(service_accounts, list_service_accounts()) } ) }) test_that("GCE metadata env vars are respected", { # TODO: use withr here tryCatch({ expect_equal("http://metadata.google.internal/", gce_metadata_url()) Sys.setenv(GCE_METADATA_URL = "fake.url") expect_equal("http://fake.url/", gce_metadata_url()) options(gargle.gce.use_ip = TRUE) expect_equal("http://169.254.169.254/", gce_metadata_url()) Sys.setenv(GCE_METADATA_IP = "1.2.3.4") expect_equal("http://1.2.3.4/", gce_metadata_url()) }, finally = { # We could save and restore these values, but there's no reason they should # be set in tests. Sys.unsetenv("GCE_METADATA_IP") Sys.unsetenv("GCE_METADATA_URL") options(gargle.gce.use_ip = NULL) }) }) test_that("GCE metadata detection fails not on GCE", { tryCatch({ Sys.setenv(GCE_METADATA_URL = "some.fake.address") expect_false(detect_gce()) }, finally = { Sys.unsetenv("GCE_METADATA_URL") }) }) gargle/tests/testthat/test-token-info.R0000644000176200001440000000117214067372466017706 0ustar liggesuserstest_that("token_*() functions work", { skip_if_offline() skip_if_no_auth() token <- credentials_service_account( scopes = "https://www.googleapis.com/auth/userinfo.email", path = rawToChar(secret_read("gargle", "gargle-testing.json")) ) expect_error_free( # this implies a call to token_userinfo() email <- token_email(token) ) expect_error_free( 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.R0000644000176200001440000000461014067372466020642 0ustar liggesuserstest_that("inputs are checked when creating AuthState", { app <- httr::oauth_app("APPNAME", key = "KEY", secret = "SECRET") expect_error( init_AuthState( package = NULL, app = app, api_key = "API_KEY", auth_active = TRUE ), 'is_scalar_character(package) is not TRUE', fixed = TRUE ) expect_error(init_AuthState(app = "not_an_oauth_app"), 'is not TRUE') expect_error(init_AuthState(app = app, api_key = 1234), 'is not TRUE') expect_error( init_AuthState(app = app, api_key = "API_KEY", auth_active = NULL), 'is not TRUE' ) a <- init_AuthState( package = "PACKAGE", app = app, api_key = "API_KEY", auth_active = TRUE ) expect_s3_class(a, "AuthState") }) test_that("AuthState app can be modified and cleared", { app <- httr::oauth_app("AAA", key = "KEY", secret = "SECRET") a <- init_AuthState(app = app, api_key = "API_KEY", auth_active = TRUE) expect_equal(a$app$appname, "AAA") app2 <- httr::oauth_app("BBB", key = "KEY", secret = "SECRET") a$set_app(app2) expect_equal(a$app$appname, "BBB") a$set_app(NULL) expect_null(a$app) }) test_that("AuthState api_key can be modified and cleared", { app <- httr::oauth_app("AAA", key = "KEY", secret = "SECRET") a <- init_AuthState(app = app, 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", { app <- httr::oauth_app("AAA", key = "KEY", secret = "SECRET") a <- init_AuthState(app = app, 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", { app <- httr::oauth_app("AAA", key = "KEY", secret = "SECRET") a <- init_AuthState(app = app, api_key = "A", 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", { app <- httr::oauth_app("APPNAME", key = "KEY", secret = "SECRET") a <- init_AuthState( package = "PKG", app = app, api_key = "API_KEY", auth_active = TRUE ) a$set_cred(structure("TOKEN", class = "some_sort_of_token")) expect_snapshot(print(a)) }) gargle/tests/testthat/helper.R0000644000176200001440000000141214067372466016134 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) } with_mock <- function(..., .parent = parent.frame()) { mockr::with_mock(..., .parent = .parent, .env = "gargle") } skip_if_no_auth <- function() { testthat::skip_if_not( secret_can_decrypt("gargle"), "Authentication not available" ) } expect_error_free <- function(...) { expect_error(..., regexp = NA) } expect_info <- function(...) { if (is_interactive()) { expect_output(...) } else { expect_message(...) } } gargle/tests/testthat/_snaps/0000755000176200001440000000000014067403717016011 5ustar liggesusersgargle/tests/testthat/_snaps/oauth-refresh.md0000644000176200001440000000343014067403371021103 0ustar liggesusers# 'deleted_client' causes extra special feedback Code gargle_refresh_failure(err, httr::oauth_app(appname = NULL, key = "KEY", secret = "SECRET")) Warning Unable to refresh token, because the associated OAuth app has been deleted. --- Code gargle_refresh_failure(err, httr::oauth_app(appname = "APPNAME", key = "KEY", secret = "SECRET")) Warning Unable to refresh token, because the associated OAuth app has been deleted. * App name: 'APPNAME' --- Code gargle_refresh_failure(err, httr::oauth_app(appname = "APPNAME", key = "KEY", secret = "SECRET"), package = "PACKAGE") Warning Unable to refresh token, because the associated OAuth app has been deleted. * App name: 'APPNAME' i If you did not configure this OAuth app, it may be built into the PACKAGE package. If so, consider re-installing PACKAGE to get an updated app. --- Code gargle_refresh_failure(err, httr::oauth_app(appname = "fake-calliope", key = "KEY", secret = "SECRET")) Warning Unable to refresh token, because the associated OAuth app has been deleted. i You appear to be relying on the default app used by the gargle package. Consider re-installing gargle, in case the default app has been updated. --- Code gargle_refresh_failure(err, httr::oauth_app(appname = "fake-calliope", key = "KEY", secret = "SECRET"), package = "PACKAGE") Warning Unable to refresh token, because the associated OAuth app has been deleted. i You appear to be relying on the default app used by the PACKAGE package. Consider re-installing PACKAGE and gargle, in case the default app has been updated. gargle/tests/testthat/_snaps/AuthState-class.md0000644000176200001440000000045214067403367021342 0ustar liggesusers# AuthState prints nicely Code print(a) Output -- ---------------------------------------------------- package: PKG app: APPNAME api_key: API_KEY auth_active: TRUE credentials: gargle/tests/testthat/_snaps/Gargle-class.md0000644000176200001440000000050314067403367020636 0ustar liggesusers# Gargle2.0 prints nicely Code print(fauxen) Output -- -------------------------------------------------------- oauth_endpoint: google app: APPNAME email: 'a@example.org' scopes: ...userinfo.email credentials: a gargle/tests/testthat/_snaps/oauth-cache.md0000644000176200001440000001025714067403717020521 0ustar liggesusers# cache_clean() works Code cache_clean(cache_folder, "apple") Message v Deleting 1 token obtained with an old tidyverse OAuth app. i Expect interactive prompts to re-auth with the new app. ! Is this rolling of credentials highly disruptive to your workflow? That means you should rely on your own OAuth app (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 ! 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'. --- 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 app 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 app scopes hash... _____________ ___________ ______ __________ a@example.org gargle-clio {hash...} gargle/tests/testthat/_snaps/request-develop.md0000644000176200001440000000033114067403401021442 0ustar liggesusers# request_develop() errors for unrecognized parameters These parameters are unknown: * 'b' * 'c' # request_develop() errors if required parameter is missing These parameters are missing: * 'a' gargle/tests/testthat/_snaps/response_process.md0000644000176200001440000000660714067403402021727 0ustar liggesusers# Resource exhausted (Sheets, ReadGroup) 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_limit: ReadRequestsPerMinutePerUser * metadata.consumer: projects/603366585132 * metadata.service: sheets.googleapis.com * metadata.quota_metric: sheets.googleapis.com/read_requests # Request for non-existent resource (Drive) 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 # Request for which we don't have scope (Fitness) 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) 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) 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) 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) 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) Client error: (400) Bad Request * Invalid Value # Request to bad URL (tokeninfo, HTML content) Expected content type 'application/json', not 'text/html'. * Not Found gargle/tests/testthat/_snaps/request_retry.md0000644000176200001440000000155314067403401021242 0ustar liggesusers# request_retry() logic works as advertised Code writeLines(fix_strategy(fix_wait_time(msg_fail_once))) Output x Request failed [429] oops i Retry 1 happens in {WAIT_TIME} seconds ... (strategy: exponential backoff, full jitter) --- Code writeLines(fix_strategy(fix_wait_time(msg_retry_after))) Output x Request failed [429] oops i Retry 1 happens in {WAIT_TIME} seconds ... (strategy: 'Retry-After' header) --- Code writeLines(fix_strategy(fix_wait_time(msg_max_tries))) Output 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/inside-the-house.md0000644000176200001440000000244214067403367021510 0ustar liggesusers# it is possible to be 'outside the house' Code local(gargle:::check_permitted_package(), envir = globalenv()) 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()) 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 gargle/tests/testthat/_snaps/utils-ui.md0000644000176200001440000000220414067403403020074 0ustar liggesusers# gargle_verbosity() validates the value it finds Option "gargle_verbosity" must be one of: 'debug', 'info', and 'silent' # gargle_verbosity() accomodates people using the old option Code out <- gargle_verbosity() Message ! Option "gargle_quiet" is deprecated in favor of "gargle_verbosity" i Instead of: `options(gargle_quiet = FALSE)` Now do: `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")) gargle/tests/testthat/test-utils-ui.R0000644000176200001440000000257614067372466017421 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"))) }) gargle/tests/testthat/test-request_retry.R0000644000176200001440000001214614067372466020555 0ustar liggesuserstest_that("request_retry() logic works as advertised", { # TODO: I'm testing too much re: retry logic via messages. # Classed errors should simplify things, in due course. 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 fix_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) fix_strategy <- function(x) { sub( ", clipped to (floor|ceiling) of [[:digit:]]+([.][[:digit:]]+)? seconds", "", x, perl = TRUE ) } local_gargle_verbosity("debug") # 2021-03-18 # The switch to mockr::with_mock() has caused trouble interactively # executing this test. The closures used to mock request_make() seem to # use a common counter (i) and responses. I plan to open a mockr issue. # Workaround in the meantime: load_all() before each interactive call to # with_mock(). # succeed on first try out <- with_mock( request_make = faux_request_make(), { request_retry() } ) expect_equal(httr::status_code(out), 200) # fail, then succeed (exponential backoff) r <- list(faux_response(429), faux_response()) with_mock( request_make = faux_request_make(r), gargle_error_message = function(...) "oops", { msg_fail_once <- capture.output( out <- request_retry(max_total_wait_time_in_seconds = 5), type = "message" ) } ) expect_snapshot( writeLines(fix_strategy(fix_wait_time(msg_fail_once))) ) expect_equal(httr::status_code(out), 200) # fail, then succeed (Retry-After header) r <- list( faux_response(429, h = list(`Retry-After` = 1.4)), faux_response() ) with_mock( request_make = faux_request_make(r), gargle_error_message = function(...) "oops", { msg_retry_after <- capture.output( out <- request_retry(), type = "message" ) } ) expect_snapshot( writeLines(fix_strategy(fix_wait_time(msg_retry_after))) ) expect_equal(httr::status_code(out), 200) # make sure max_tries_total is adjustable) r <- list( faux_response(429), faux_response(429), faux_response(429), faux_response() ) with_mock( request_make = faux_request_make(r[1:3]), gargle_error_message = function(...) "oops", { msg_max_tries <- capture.output( out <- request_retry(max_tries_total = 3, max_total_wait_time_in_seconds = 6), type = "message" ) } ) expect_snapshot( writeLines(fix_strategy(fix_wait_time(msg_max_tries))) ) expect_equal(httr::status_code(out), 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] with_mock( gargle_error_message = function(...) "oops", { wait_times <- vapply( rep.int(1, 100), backoff, FUN.VALUE = numeric(1), resp = faux_error(), min_wait = 3 ) } ) %>% suppressMessages() 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] with_mock( gargle_error_message = function(...) "oops", { wait_times <- vapply( rep.int(1, 100), backoff, FUN.VALUE = numeric(1), resp = faux_error(), base = 6, max_wait = 3 ) } ) %>% suppressMessages() 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" ) } # play with capitalization and character vs numeric out <- with_mock( gargle_error_message = function(...) "oops", { backoff(1, faux_429(list(`Retry-After` = "1.2"))) } ) %>% suppressMessages() expect_equal(out, 1.2) out <- with_mock( gargle_error_message = function(...) "oops", { backoff(1, faux_429(list(`retry-after` = 2.4))) } ) %>% suppressMessages() expect_equal(out, 2.4) # should work even when tries_made > 1 out <- with_mock( gargle_error_message = function(...) "oops", { backoff(3, faux_429(list(`reTry-aFteR` = 3.6))) } ) %>% suppressMessages() expect_equal(out, 3.6) }) gargle/tests/testthat/test-request-develop.R0000644000176200001440000000500514067372466020760 0ustar liggesuserstest_that("request_develop() errors for unrecognized parameters", { expect_snapshot_error( request_develop( endpoint = list(parameters = list(a = list())), 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))), 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-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-fetch.R0000644000176200001440000000237314022166555016721 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_clear()) cred_funs_add(creds_always) expect_equal(1, token_fetch(c())) cred_funs_add(creds_never) expect_equal(1, token_fetch(c())) }) test_that("We fetch tokens in order", { withr::defer(cred_funs_clear()) cred_funs_add(creds_always) cred_funs_add(creds_maybe) expect_equal(1, token_fetch(c())) expect_equal(2, token_fetch(c(), arg1 = "abc")) cred_funs_set(list(creds_always, 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_clear()) cred_funs_add(creds_never) expect_null(token_fetch(c())) }) test_that("We don't need any registered functions", { expect_null(token_fetch(c())) }) test_that("We keep looking for credentials on error", { withr::defer(cred_funs_clear()) cred_funs_add(creds_always) cred_funs_add(creds_failure) expect_equal(1, token_fetch(c())) }) gargle/tests/testthat/test-assets.R0000644000176200001440000000157714067372466017150 0ustar liggesuserstest_that("default options", { withr::local_options(list( gargle_oauth_cache = NULL, gargle_oob_default = NULL, httr_oob_default = NULL, gargle_oauth_email = NULL, gargle_verbosity = NULL, gargle_quiet = NULL )) expect_equal(gargle_oauth_cache(), NA) expect_false(gargle_oob_default()) 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 API key", { key <- gargle_api_key() expect_true(is_string(key)) }) gargle/tests/testthat/test-response_process.R0000644000176200001440000000355514067372466021240 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}")) expect_snapshot_error(response_process(resp)) } 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 ) }) 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-field-mask.R0000644000176200001440000000103614022166555017637 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/0000755000176200001440000000000014067637312013534 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.Rmd0000644000176200001440000001411014067372466016660 0ustar liggesusers--- title: "Auth when using R in the browser" output: rmarkdown::html_vignette vignette: > %\VignetteIndexEntry{Auth when using R in 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://www.rstudio.com/products/rstudio/download-server/), [RStudio Cloud](https://rstudio.cloud), or [RStudio Workbench](https://www.rstudio.com/products/workbench/), 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". 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. The need to use oob auth can sometimes be detected automatically. For example, oob auth is always used when the httpuv package is not installed. gargle also tries to detect usage via RStudio Server, Cloud, or Workbench, but this still may not catch 100% of situations where oob auth is necessary. Therefore, some users may still need to recognize this situation and explicitly request oob auth. Here's a typical presentation of this problem: 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. ``` This is a sign that you need to explicitly request oob auth. This article describes how to do so in a package that uses gargle for auth, which includes: * [bigrquery](https://bigrquery.r-dbi.org) (>= v1.2.0) * [googledrive](https://googledrive.tidyverse.org) (>= v1.0.0) * [gmailr](https://gmailr.r-lib.org) (>= v1.0.0) * [googlesheets4](https://googlesheets4.tidyverse.org) * [gcalendr](https://github.com/andrie/gcalendr) *GitHub only* ## Request oob auth in the `PKG_auth()` call These packages 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 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) ``` This code is tailored to an interactive session and assumes that a user is present to respond. If you *also* need to setup a token for non-interactive use, see the article [Non-interactive auth](https://gargle.r-lib.org/articles/non-interactive-auth.html). A key point is that oob auth is relevant to how you *initially* obtain a token. It is orthogonal to downstream use and refreshing. So it is possible that you need to attend to both! ## Set the `gargle_oob_default` option If you know that you *always* want to use oob auth, 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 that option has been set, it is honoured by downstream calls to `PKG_auth()`, explicit or implicit, because the default behaviour of `use_oob` is to consult the option: ```{r, eval = FALSE} 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) {...} ``` ## 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 httr 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 if you don't have to? Here are ways to fix. * 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 [Generating OAuth tokens for a server using httr](https://support.rstudio.com/hc/en-us/articles/217952868-Generating-OAuth-tokens-from-a-server) covers some of the same ground, although for the httr package. gargle provides a Google-specific interface to httr. gargle first consults the `gargle_oob_default` option and, if that is undefined, also consults the `httr_oob_default` option. If you're creating content to be deployed (for example on [shinyapps.io](https://www.shinyapps.io) or [RStudio Connect](https://www.rstudio.com/products/connect/)), you will also need to consider how the [deployed content will authenticate non-interactively](https://gargle.r-lib.org/articles/non-interactive-auth.html). gargle/vignettes/non-interactive-auth.Rmd0000644000176200001440000003643714067372466020267 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 [How to use gargle for auth in a client package](https://gargle.r-lib.org/articles/gargle-auth-in-client-package.html). Examples include: * [bigrquery](https://bigrquery.r-dbi.org) (>= v1.2.0) * [googledrive](https://googledrive.tidyverse.org) (>= v1.0.0) * [gmailr](https://gmailr.r-lib.org) (>= v1.0.0) * [googlesheets4](https://googlesheets4.tidyverse.org) * [gcalendr](https://github.com/andrie/gcalendr) *GitHub only* Full details on [`gargle::token_fetch()`](https://gargle.r-lib.org/reference/token_fetch.html), which powers this strategy, are given in [How gargle gets tokens](https://gargle.r-lib.org/articles/how-gargle-gets-tokens.html). ## 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 [RStudio Connect](https://rstudio.com/products/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 they are no more secure or hidden than the other source files in the project. The vignette [Managing tokens securely](https://gargle.r-lib.org/articles/articles/managing-tokens-securely.html) describes a method for embedding an encrypted token in the project, which is an extra level of care needed to work with, e.g., continuous integration services, such as GitHub Actions, Travis-CI, or AppVeyor. ## 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 don't use multiple Google identities, you can be more vague: options(gargle_oauth_email = TRUE) # Approach #2: call PACKAGE_auth() proactively. library(googledrive) # Either specify the user: drive_auth(email = "jenny@example.com") # Or, if you don't use multiple Google identities, you can be more vague: drive_auth(email = TRUE) ``` Keep reading if you want to actually understand this. ## 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. This requires some advance preparation, but that tends to pay off pretty quickly, in terms of having a much more robust auth setup. **Step 1**: Get a service account and then download a token. Described in the gargle article [How to get your own API credentials](https://gargle.r-lib.org/articles/get-api-credentials.html), specifically in the [Service account token](https://gargle.r-lib.org/articles/get-api-credentials.html#service-account-token) section. **Step 2**: Call the wrapper package's main auth function proactively and provide the path to your service account token. 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 the gargle article [Managing tokens securely](https://gargle.r-lib.org/articles/articles/managing-tokens-securely.html). If the code is running on AWS, a special auth flow is available called workload identity federation. Learn more in the documentation for `credentials_external_account()`. 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, there's a decent chance you 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 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. 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](https://gargle.r-lib.org/articles/how-gargle-gets-tokens.html#credentials_app_default) of the gargle article [How gargle gets tokens](https://gargle.r-lib.org/articles/how-gargle-gets-tokens.html). 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](https://gargle.r-lib.org/articles/how-gargle-gets-tokens.html#credentials_byo_oauth2) of the gargle article [How gargle gets tokens](https://gargle.r-lib.org/articles/how-gargle-gets-tokens.html). ## Arrange for an OAuth 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") ``` If you are doing setup in a web-based environment, such as RStudio Server, you may also need to request out-of-band auth, whenever you are first acquiring a token. That is a separate issue, which is explained in [Auth when using R in the browser](https://gargle.r-lib.org/articles/auth-from-web.html). **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_auth(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 app (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/articles/0000755000176200001440000000000014067372466015350 5ustar liggesusersgargle/vignettes/articles/managing-tokens-securely.Rmd0000644000176200001440000003714214067372466022736 0ustar liggesusers--- title: "Managing tokens securely" output: rmarkdown::html_vignette vignette: > %\VignetteIndexEntry{Managing tokens securely} %\VignetteEngine{knitr::rmarkdown} %\VignetteEncoding{UTF-8} --- ```{r, include = FALSE} knitr::opts_chunk$set( collapse = TRUE, comment = "#>" ) ``` Testing presents special challenges for packages that wrap an API. Here we tackle one of those problems: how to deal with auth in a non-interactive setting on a remote machine. This affects gargle itself and will affect any client package that relies on gargle for auth. This article documents the token management approach taken in gargle. We wanted it to be relatively easy to have a secret, such as an auth token, that we can: * Use locally * Use with continuous integration (CI) services, such as GitHub Actions * Use with [R-hub](https://docs.r-hub.io) all while keeping the secret secure. The approach uses symmetric encryption, where the shared key is stored in an environment variable. Why? This works well with existing conventions for local R usage. Most CI services offer support for secure environment variables. And R-hub accepts environment variables via the `env_vars` argument of [`rhub::check()`](https://r-hub.github.io/rhub/reference/check.html). This is based on an approach originally worked out in [bigrquery](https://bigrquery.r-dbi.org). ## Accessing the `secret_*()` functions gargle's approach to managing test tokens is implemented through several functions that all start with the `secret_` prefix. These functions are not (currently?) exported. This may seem odd, since others might want to use these functions. But note they are only needed during setup or at test time. This sort of usage is compatible with others calling internal gargle functions and possibly inlining a version of a couple test helpers. One way to make the `secret_*()` functions available for local experimentation is to call `devtools::load_all()`, which exposes all internal objects in a package: ```{r eval = FALSE} devtools::load_all("path/to/source/of/gargle/") ``` The approach I'll take in this article is to call these functions via `:::`. ## Overview of the approach 1. Add the [sodium package](https://cran.r-project.org/package=sodium) to Suggests in your DESCRIPTION, via `usethis::use_package("sodium", "Suggests")` if you like. 1. Generate a random PASSWORD and give it a self-documenting name, e.g. `GARGLE_PASSWORD`. Store as an environment variable. 1. Identify a secret file of interest, such as the JSON representing a service account token. This is presumably stored *outside* your package. 1. Use the PASSWORD to apply a method for symmetric encryption to the target file. Store the resulting encrypted file in a designated location *within* your package. 1. Store or pass the PASSWORD as an environment variable everywhere you'll need to decrypt the secret. - Check that the platform has support for keeping the PASSWORD concealed. - Make sure you don't do anything in your own code that would dump it to, e.g., a log file. 1. Rig your tests to determine if the key is available and, therefore, whether decryption is going to be possible. - If "no", carry on gracefully with any tests that don't require auth. - If "yes", decrypt the secret and put the associated token into force globally for the test run or on an "as needed" basis in individual tests. ## Annotated code-through ### Generate a name for the PASSWORD `secret_pw_name()` creates a name of the form "PACKAGE_PASSWORD", a convention baked into the `secret_*()` family of functions. ```{r} (pw_name <- gargle:::secret_pw_name("gargle")) ``` ### Generate a random PASSWORD In real life, you should keep the output of `secret_pw_gen()` to yourself! We reveal it here as part of the exposition. ```{r} (pw <- gargle:::secret_pw_gen()) ``` ### Define environment variable in `.Renviron` Combine the name and value to form a line like this in your user-level `.Renviron` file: ```{r, echo = FALSE, comment = NA} cat(paste0(pw_name, "=", pw), sep = "\n") ``` [`usethis::edit_r_environ()`](https://usethis.r-lib.org/reference/edit.html) can help create or open this file. We **strongly recommend** using the user-level `.Renviron`, as opposed to project-level, because this makes it less likely you will share sensitive information by mistake. If you don't take our advice and choose to store the PASSWORD in a file inside a Git repo, you must make sure that file is listed in `.gitignore`. This still would not prevent leaking your secret if, for example, that project is in a directory that syncs to DropBox. Make sure `.Renviron` ends in a newline; the lack of this is a notorious cause of silent failure. Remember you'll need to restart R or call `readRenviron("~/.Renviron")` for the newly defined environment variable to take effect. ### Provide environment variable to other services #### GitHub Actions: Define the environment variable as an encrypted secret in your repo: Use the secrets context to expose a secret as an environment variable in your workflows. That will look like like so, in some appropriate place in your workflow file: ``` env: PACKAGE_PASSWORD: ${{ secrets.PACKAGE_PASSWORD }} ``` Remember that the secret, and therefore the associated environment variable, is not available when workflows are triggered via an external pull request. This is another reason to carry on gracefully when token decryption is not possible (see below). #### Travis-CI Define the environment variable in your repo settings via the browser UI: Alternatively, you can use the Travis command line interface to configure the environment variable or even define an encrypted environment variable in `.travis.yml`. Regardless of how you define it, remember that private environment variables are not available to external pull requests, which is another reason to carry on gracefully when token decryption is not possible (see below). You may also need something like this in `.travis.yml` so that the sodium R package can be installed: ``` yaml addons: apt: sources: - sourceline: 'ppa:chris-lea/libsodium' packages: - libsodium-dev ``` #### AppVeyor Define the environment variable in the Environment page of your repo's Settings. Make sure to request variable encryption and to click "Save" at the bottom. In the General page, you probably want to check "Enable secure variables in Pull Requests from the same repository only" and, again, explicitly "Save". As with Travis, it is also possible to encrypt the password using your AppVeyor account's public key and inline the value in `appveyor.yml`. There is a helpful web UI for that does the encryption and generates the lines to add to your config: This can also be found via *Settings > Encrypt YAML*. #### R-hub Send the environment variable in your calls to [`rhub::check()`](https://r-hub.github.io/rhub/reference/check.html) and [friends](https://r-hub.github.io/rhub/reference/index.html#section-check-shortcuts): ``` rhub::check(env_vars = Sys.getenv(gargle:::secret_pw_name("gargle"), names = TRUE)) ``` ### Encrypt the secret file `secret_write()` takes 3 arguments: * `package` name. Processed through `secret_pw_name()` in order to retrieve the PASSWORD from an appropriately named environment variable. * `name` of the encrypted file to write. The location is below `inst/secret` in the source of `package`. * `data`, either a file path to the unencrypted secret file or the data to be encrypted as a raw vector. In the case of a secret file, we **strongly recommend** that its primary home on your local computer is outside your package and, generally, outside of any folder that syncs regularly to a remote, e.g. GitHub or DropBox. This decreases the chance of accidental leakage. Example of a call to `secret_write()`, where `gargle-testing.json` is a JSON file downloaded for a service account managed via the [Google API / Cloud Platform console](https://console.cloud.google.com/project): ```{r eval = FALSE} gargle:::secret_write( package = "gargle", name = "gargle-testing.json", input = "a/very/private/local/folder/gargle-testing.json" ) ``` This writes an encrypted version of `gargle-testing.json` to `inst/secret/gargle-testing.json` relative to the current working directory, which is presumably the top-level directory of gargle's source. This encrypted file *should* be committed and pushed. ### Test setup Now you need to rig your tests or their setup around this encrypted token. You need to plan for two scenarios: * Decryption is going to work. This is where you actually get to test package functionality against the target API, with auth. * Decryption is not going to work. Either because the Suggested [sodium](https://cran.r-project.org/package=sodium) package is not available or (much more likely) because the environment variable that represents the key is not available. - This will be the case on CRAN, by definition, because there is no way to share an encrypted secret. - This will be the case for external contributors, on their personal machines and when their GitHub pull requests are checked via CI services, such as GitHub Actions, Travis-CI, or AppVeyor. #### CI configuration We recommend that you actively check your package under the "no decryption, no token" scenario, so that you discover problems before CRAN or your contributors do. In fact, this should probably be the default situation for your CI jobs and you only supply the secret to a single, flagship job -- probably the check with the current R release and your favorite operating system. Here's the simplified build matrix from the `R CMD check` GitHub Actions workflow file used by gargle (note: we redacted a very long `rspm` URL that's irrelevant here): ``` yaml strategy: matrix: config: - {os: macOS-latest, r: 'devel', gargle_auth: GARGLE_NOAUTH} - {os: macOS-latest, r: '4.0', gargle_auth: GARGLE_PASSWORD} - {os: windows-latest, r: '4.0', gargle_auth: GARGLE_NOAUTH} - {os: ubuntu-16.04, r: '4.0', gargle_auth: GARGLE_NOAUTH, rspm: "..."} - {os: ubuntu-16.04, r: '3.6', gargle_auth: GARGLE_NOAUTH, rspm: "..."} - {os: ubuntu-16.04, r: '3.5', gargle_auth: GARGLE_NOAUTH, rspm: "..."} - {os: ubuntu-16.04, r: '3.4', gargle_auth: GARGLE_NOAUTH, rspm: "..."} - {os: ubuntu-16.04, r: '3.3', gargle_auth: GARGLE_NOAUTH, rspm: "..."} env: GARGLE_PASSWORD: ${{ secrets[matrix.config.gargle_auth] }} ``` Notice how `GARGLE_PASSWORD` will only be available when checking against the released version of R on macOS-latest. bigrquery implements the same idea with a different approach: it does not provide `BIGRQUERY_PASSWORD` to the main `R CMD check` GitHub Actions workflow at all. Instead there is a separate "live api" workflow that only has one job and that accesses the secret. The tidyverse / r-lib team is transitioning from Travis/AppVeyor to GitHub Actions. But in case it is helpful, here's a simplified excerpt from a `.travis.yml` file used by gargle in the past. The main `r: release` build accesses `GARGLE_PASSWORD` implicitly as an encrypted environment variable, but `R CMD check` runs for the other builds with `GARGLE_PASSWORD` explicitly unset: ``` yaml matrix: include: - r: release # - r: release env: GARGLE_PASSWORD='' - r: devel env: GARGLE_PASSWORD='' - r: oldrel env: GARGLE_PASSWORD='' ``` Regardless of your CI platform, your absolute best bet for writing configuration files is to look at what other R package developers are doing in their public source repos. All of the above is static, simplified, and (probably) stale and will never reflect the current state-of-the-art. #### Testthat configuration In a wrapper package, you could determine decrypt-ability at the start of the test run. Here's representative code from googledrive's `tests/testthat/helper.R` file, but something similar can be seen in bigrquery and googlesheets4: ```{r eval = FALSE} if (gargle:::secret_can_decrypt("googledrive")) { json <- gargle:::secret_read("googledrive", "googledrive-testing.json") drive_auth(path = rawToChar(json)) } ``` Versions of `secret_can_decrypt()` and `secret_read()` are defined here in gargle. `drive_auth()` is a function specific to googledrive that loads a token for use downstream (in multiple tests, in this case). Note that it can clearly accept a JSON string, as an alternative to a filepath, and that's very favorable for our workflow. We'll come back to this below. But what if `secret_can_decrypt()` returns `FALSE` and no token is loaded? That's where you rely on a custom test skipper. Here is the [test skipper from googledrive](https://github.com/tidyverse/googledrive/blob/master/tests/testthat/helper.R): ```{r eval = FALSE} # googledrive skip_if_no_token <- function() { testthat::skip_if_not(drive_has_token(), "No Drive token") } ``` `googledrive::drive_has_token()` returns `TRUE` if a token is available and `FALSE` otherwise. By calling the skipper at the start of tests that require auth, you arrange for your package to cope gracefully when the token cannot be decrypted, e.g., on CRAN and in pull requests. It is typical to define such a skipper in `tests/testthat/helper.R` or similar. *gargle's usage of the testing token is a bit different, still evolving, and less relevant to the maintainers of wrapper packages. Therefore it's not featured here.* ### Known sources of friction Once you dig into the `secret_*()` family, you will notice there are two recurring sources of friction: * File or object? You almost certainly store your secrets in files. But the sodium functions for data encrypt and decrypt work with R objects. So, for example, it is convenient if token ingest can accept an R object as opposed to only a file path. * Raw vectors. You might think of the PASSWORD or even the secret file itself (e.g., JSON) in terms of plain text. But the sodium functions for data encrypt and decrypt work with *raw vectors*, not character vectors. Be prepared to see related conversions in the `secret_*()` functions. Functions useful for these conversions: * `writeBin()` / `readBin()` * `charToRaw()` / `rawToChar()` * `sodium::data_encrypt()` / `sodium::data_decrypt` * `sodium::bin2hex()` / `sodium::hex2bin()` ## Resources bigrquery and googledrive, which both use this approach. * [`bigrquery/tests/testthat/helper-auth.R`](https://github.com/r-dbi/bigrquery/blob/master/tests/testthat/helper-auth.R) * [`googledrive/tests/testthat/helper.R`](https://github.com/tidyverse/googledrive/blob/master/tests/testthat/helper.R) * Setup chunk of a pkgdown article that is rendered and deployed via CI: [`googledrive/vignettes/articles/multiple-files.Rmd#L10-L29`](https://github.com/tidyverse/googledrive/blob/f08c545284eadd4e69a14d1e843d228e8166e896/vignettes/articles/multiple-files.Rmd#L10-L29) "Managing secrets" vignette of httr: * Vignettes of the sodium package, especially the parts relating to symmetric encryption: * * The [cyphr](https://ropensci.github.io/cyphr/) package, which smooths over frictions like those identified above relating to "file vs. object?" and "character vs. raw?": * gargle/vignettes/get-api-credentials.Rmd0000644000176200001440000003332614067372466020036 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 $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 Developers 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 app or 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 Developers Console, in the target GCP Project, go to *APIs & Services > Credentials*. * Do *Create credentials > OAuth client ID*. * Select Application type "Desktop app". * You can capture the client ID and secret via clipboard right away. * At any time, you can navigate to a particular client ID and click "Download JSON". Two ways to package this info for use with httr or gargle, both of which require an object of class `httr::oauth_app`: 1. Use `httr::oauth_app()`. - The client ID goes in the `key` argument. - The client secret goes in the `secret` argument. 1. Use `gargle::oauth_app_from_json()`. - Provide the path to the downloaded JSON file. 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 `appname` argument to `httr::oauth_app()` or `gargle::oauth_app_from_json()`. Package maintainers might want to build this app 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 app (client ID and secret)? Package users could register this app 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) # method 1: direct provision client ID and secret google_app <- httr::oauth_app( "my-very-own-google-app", key = "123456789.apps.googleusercontent.com", secret = "abcdefghijklmnopqrstuvwxyz" ) drive_auth_configure(app = google_app) # method 2: provide filepath to JSON containing client ID and secret drive_auth_configure( path = "/path/to/the/JSON/you/downloaded/from/gcp/console.json" ) # now any new OAuth tokens are obtained with the configured app ``` ## 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 Developers 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: [How to use identity federation to access Google Cloud resources from Amazon Web Services (AWS)](https://cloud.google.com/iam/docs/access-resources-aws) ## 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.Rmd0000644000176200001440000004171314067372466021665 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 app 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) { cred <- gargle::token_fetch( scopes = scopes, app = drive_oauth_app() %||% , 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. `token_fetch()` is described in the vignette [How gargle gets tokens](https://gargle.r-lib.org/articles/how-gargle-gets-tokens.html). 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::AuthClass` to hold the auth state. Here's how it is initialized in googledrive: ```{r eval = FALSE} .auth <- gargle::init_AuthState( package = "googledrive", auth_active = TRUE # app = NULL, # api_key = NULL, # cred = NULL ) ``` The OAuth `app` 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 app Most users should present OAuth user credentials to Google APIs. However, most users can also be spared the fiddly details surrounding this. The OAuth app is one example. The app is a component that most users do not even know about and they are content to use the same app for all work through a client package: possibly, the app built into the package. There is a field in the `.auth` auth state to hold the OAuth `app`. Exported auth helpers, `drive_oauth_app()` and `drive_auth_configure()`, retrieve and modify the current app to support users ready to take that level of control. ```{r, eval = FALSE} library(googledrive) google_app <- httr::oauth_app( appname = "acme-corp", key = "123456789.apps.googleusercontent.com", secret = "abcdefghijklmnopqrstuvwxyz" ) drive_auth_configure(app = google_app) drive_oauth_app() #> acme-corp #> key: 123456789.apps.googleusercontent.com #> secret: ``` Do not "borrow" an OAuth app (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. ### 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()` and retrieve that value with `drive_api_key()`, just like the OAuth app. 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 app, 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 app 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()` pre-emptively 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 behaviour. 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()` for more about these options. ## 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. `R/YOURPKG_auth.R` is a good place to do this. 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/master/R/drive_auth.R) and [`r-dbi/bigrquery/R/bq_auth.R`](https://github.com/r-dbi/bigrquery/blob/master/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 <- .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 ) } ``` `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](https://gargle.r-lib.org/articles/request-helper-functions.html) 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 Application Default Credentials or a Google Cloud Engine flow. 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 sheets_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 app (client ID and secret) 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/master/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_app()` returns `.auth$app`. * `drive_api_key()` returns `.auth$key`. * `drive_auth_configure()` can be used to configure auth. This is how an advanced user would enter their own OAuth app and API key into 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 -- then 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. ## BYOAK = Bring Your Own App and Key Advanced users can use their own OAuth app and API key. `drive_auth_configure()` lives in `R/drive_auth()` and it provides the ability to modify the current `app` and `api_key`. Recall that `drive_oauth_app()` and `drive_api_key()` also exist for targeted, read-only access. The vignette [How to get your own API credentials](https://gargle.r-lib.org/articles/get-api-credentials.html)" describes how to an API key and OAuth app. Packages that always send 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.Rmd0000644000176200001440000004471514067403577020514 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. `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 [How to use gargle for auth in a client package](https://gargle.r-lib.org/articles/gargle-auth-in-client-package.html). ```{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} 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. From now on, 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 option named "gargle_verbosity" to "debug". Read more in the docs for `gargle_verbosity()`. ## `credentials_service_account()` The first 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") # 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 second 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") # 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_service_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: [How to use identity federation to access Google Cloud resources from Amazon Web Services (AWS)](https://cloud.google.com/iam/docs/access-resources-aws) ## `credentials_app_default()` The third function tried is `credentials_app_default()`. Here's how a call to `token_fetch()` might work: ```{r, eval = FALSE} token_fetch(scopes = ) # credentials_service_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/production#providing_credentials_to_your_application). 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_service_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. ## `credentials_byo_oauth2()` The next function tried is `credentials_byo_oauth2()`. Here's how a call to `token_fetch()` might work: ```{r, eval = FALSE} token_fetch(token = ) # credentials_service_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_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_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_service_account() fails because no `path`, # credentials_app_default() fails because no ADC found, # credentials_gce() fails because not on GCE, # credentials_byo_oauth2() fails because no `token`, # 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`, and `package` are generally provided by the API wrapper function that is mediating the calls to `token_fetch()`. Do not "borrow" an OAuth app (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. 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 [How to use gargle for auth in a client package](https://gargle.r-lib.org/articles/gargle-auth-in-client-package.html) 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 app, 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 app. 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. Select a pre-authorised account or enter '0' to obtain a new token. Press Esc/Ctrl + C to abort. 1: janedoe_personal@gmail.com 2: janedoe@example.com 3: janedoe_work@gmail.com Selection: 3 ``` If none of the tokens has the right scopes and app (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() #' gargle OAuth cache path: #' /Users/janedoe/.R/gargle/gargle-oauth #' #' 14 tokens found #' #' 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... ``` gargle/vignettes/troubleshooting.Rmd0000644000176200001440000002052014067372466017434 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. ```{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 app or 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. ### Credential rolling Many users of packages like googlesheets4 or googledrive tacitly rely on the default OAuth app used by those packages. Periodically the maintainer of such a package will need to roll the app, i.e. create a new OAuth app and disable the old one. This will make it impossible to refresh existing tokens, made with the old, disabled app. Those tokens will stop working. *In gargle v1.0.0, in March 2021, we rolled the app used in googlesheets4, googledrive, and bigrquery. At some point in 2021, we plan to disable the old app. Anyone relying on the default app 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 app 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"} 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. Consider using your own OAuth app to eliminate your exposure to a third-party deciding to roll their app. 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. gargle/R/0000755000176200001440000000000014067403717011725 5ustar liggesusersgargle/R/token-info.R0000644000176200001440000001110714067372466014127 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. #' 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 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 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.rda0000644000176200001440000000224114067372466014072 0ustar liggesusersBZh91AY&SYS_P?@@@H S>UP*Rmj4a @=@i  5C82@@@i@h 4RHM4Ѝ14ɪ?=xQ)3S'eP(m&4 {R"22\X뗬&>d\RR-J\領ϡfdea1DɎ05tb*D6vLxRRjzu7luPeYee %BU*F[rҡVKMBY[7O%MlS$ڊ&خE%ꯞ[E1|]Ui7 TU)^ӉIs҇-tPT%J*B * RKeʔ_*$ԋ (p>T,!|0AФ$lDJU)%*P\LTK*$|iAQZ{RMԞdmķ)cvυȼ(=eqGa{y<2i>*ca<3=,?1y@P > JƵɢV$N;5Xb=n'+p>>imoijt4v.j j3)ri8%WeåuwݿYn\\l<It±kIEe).{0bvqez#!W16:נ3Lun #PƖ%Nc2|W ^xW4OIM1?Z,Kf`o% =-FiLVtIt]''oy>c #uYI5qq#i9[+U\DuϖˋT&I4NA;#y_g(e8n!Ua.Nìq矟1\(S)FyUw#)eLRjyű:cY6G]>?ӆta%UْR^b8 !q_,\~ 6#[?FfCJϳ4Sx&>~ #' #' @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 } } gargle/R/utils.R0000644000176200001440000000271014067372466013216 0ustar liggesusersempty_string <- function(x) { stopifnot(is.character(x)) !nzchar(x) } is_windows <- function() { tolower(Sys.info()[["sysname"]]) == "windows" } file_is_empty <- function(path) { stopifnot(is_string(path)) file.info(path)$size == 0 } isFALSE <- function(x) identical(x, FALSE) is.oauth_app <- function(x) inherits(x, "oauth_app") is.oauth_endpoint <- function(x) inherits(x, "oauth_endpoint") is_rstudio_server <- function() { if (rstudioapi::hasFun("versionInfo")) { rstudioapi::versionInfo()$mode == "server" } else { FALSE } } 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.R0000644000176200001440000000014414025225563012677 0ustar liggesusers.onLoad <- function(lib, pkg) { # nocov start cred_funs_set_default() invisible() } # nocov end gargle/R/secret.R0000644000176200001440000000512514067372466013346 0ustar liggesusers# Setup support for the NAME=PASSWORD envvar ---------------------------------- # secret_pw_name("gargle") --> "GARGLE_PASSWORD" secret_pw_name <- function(package) { paste0(toupper(package), "_PASSWORD") } # secret_pw_gen() --> "9AkKLa50wf1zHNCnHiQWeFLDoch9MYJHmPNnIVYZgSUt0Emwgi" secret_pw_gen <- function() { 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) { requireNamespace("sodium", quietly = TRUE) && secret_pw_exists(package) } # input should either be a filepath or a raw vector secret_write <- function(package, name, input) { if (is.character(input)) { input <- readBin(input, "raw", file.size(input)) } else if (!is.raw(input)) { gargle_abort_bad_class(input, 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) { 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) { gargle_abort( class = "gargle_error_secret", message = message, package = package ) } gargle/R/credentials_external_account.R0000644000176200001440000003477314067372466020007 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 #' [Automatically generate #' credentials](https://cloud.google.com/iam/docs/access-resources-aws#generate). #' #' 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/gargle-oauth-app.R0000644000176200001440000000174214067372466015217 0ustar liggesusers#' OAuth app for demonstration purposes #' #' @description Invisibly returns an OAuth app that can be used to test drive #' gargle before obtaining your own client ID and secret. This OAuth app 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 app (client ID and secret), 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. #' #' @return An OAuth consumer application, produced by [httr::oauth_app()], #' invisibly. #' @export #' @keywords internal #' @examples #' \dontrun{ #' gargle_app() #' } gargle_app <- function() { goa() } #' @export #' @keywords internal #' @rdname internal-assets tidyverse_app <- function() { check_permitted_package(parent.frame()) toa() } gargle/R/gargle-oauth-endpoint.R0000644000176200001440000000101114067372466016244 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() { httr::oauth_endpoint( base_url = "https://accounts.google.com/o/oauth2", authorize = "auth", access = "token", validate = "https://www.googleapis.com/oauth2/v1/tokeninfo", revoke = "revoke" ) } gargle/R/request_retry.R0000644000176200001440000001716114067403717014773 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. #' #' 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. The #' Sheets API v4 has "a limit of 500 requests per 100 seconds per project and #' 100 requests per 100 seconds per user. Limits for reads and writes are #' tracked separately." In our experience, the "100 (read or write) requests #' per 100 seconds 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 100 seconds, #' 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) } we_should_retry <- function(tries_made, max_tries_total, resp) { if (tries_made >= max_tries_total) { FALSE } else if (httr::status_code(resp) == "429") { 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 <- 100 + stats::runif(1) wait_rationale <- "fixed 100 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 100 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/gargle-package.R0000644000176200001440000000643714067372466014722 0ustar liggesusers#' @keywords internal #' @import fs #' @importFrom glue glue glue_data glue_collapse #' @import rlang "_PACKAGE" # The following block is used by usethis to automatically manage # roxygen namespace tags. Modify with care! ## usethis namespace: start ## 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_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 the option named "gargle_oob_default", falls #' back to the option named "httr_oob_default", and eventually defaults to #' `FALSE`. This controls whether to prefer "out of band" authentication. We #' also return `FALSE` unconditionally on RStudio Server or Cloud. This value is #' ultimately passed to [httr::init_oauth2.0()] as `use_oob`. If `FALSE` (and #' httpuv is installed), a local webserver is used for the OAuth dance. #' Otherwise, user gets a URL and prompt for a validation code. #' #' Read more about "out of band" authentication in the vignette [Auth when using #' R in the browser](https://gargle.r-lib.org/articles/auth-from-web.html). gargle_oob_default <- function() { if (is_rstudio_server()) { # TODO: Is there a better, more general condition we could use to detect # whether OOB is necessary? # Idea from @jcheng: check if it's an SSH session? # e.g. https://unix.stackexchange.com/questions/9605/how-can-i-detect-if-the-shell-is-controlled-from-ssh/9607#9607 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) } gargle/R/inside-the-house.R0000644000176200001440000000206514067372466015233 0ustar liggesusersfrom_permitted_package <- function(env = parent.frame()) { 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 = parent.frame()) { 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) } invisible(env) } gargle/R/AuthState-class.R0000644000176200001440000001470714067372466015074 0ustar liggesusers#' Create an AuthState #' #' Constructor function for objects of class [AuthState]. #' #' @param package Package name, an optional string. The associated package will #' generally by implied by the namespace within which the `AuthState` is #' defined. But it's possible to record the package name explicitly and seems #' like a good practice. #' @param app Optional. An OAuth consumer application, as produced by #' [httr::oauth_app()]. #' @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()]. #' #' @return An object of class [AuthState]. #' @export #' @examples #' my_app <- httr::oauth_app( #' appname = "my_package", #' key = "keykeykeykeykeykey", #' secret = "secretsecretsecret" #' ) #' #' init_AuthState( #' package = "my_package", #' app = my_app, #' api_key = "api_key_api_key_api_key", #' ) init_AuthState <- function(package = NA_character_, app = NULL, api_key = NULL, auth_active = TRUE, cred = NULL) { AuthState$new( package = package, app = app, 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 client package that makes requests to a Google API. #' #' The [How to use gargle for auth in a client #' package](https://gargle.r-lib.org/articles/gargle-auth-in-client-package.html) #' vignette 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. #' * `app` is an OAuth app 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 app 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 app An OAuth consumer application. #' @param api_key An API key. #' @param auth_active Logical, indicating whether auth is active. #' @param cred Credentials. #' #' @export #' @name AuthState-class AuthState <- R6::R6Class("AuthState", list( #' @field package Package name. package = NULL, #' @field app An OAuth consumer application. 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_, app = NULL, api_key = NULL, auth_active = TRUE, cred = NULL) { gargle_debug("initializing AuthState") stopifnot( is_scalar_character(package), is.null(app) || is.oauth_app(app), is.null(api_key) || is_string(api_key), is_bool(auth_active), is.null(cred) || inherits(cred, "Token2.0") ) self$package <- package self$app <- app 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_this("{.pkg {self$package}}"), app = self$app$appname, api_key = obfuscate(self$api_key), auth_active = self$auth_active, credentials = cli_this("{.cls {class(self$cred)[[1]]}}") ) c( cli::cli_format_method( cli::cli_h1("") ), glue("{fr(names(x))}: {fl(x)}") ) }, #' @description Set the OAuth app set_app = function(app) { stopifnot(is.null(app) || is.oauth_app(app)) self$app <- app invisible(self) }, #' @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) } )) gargle/R/utils-ui.R0000644000176200001440000001733714067372466013644 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 ".memo .memo-item-*" = 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 (is_false(gq)) { options(gargle_verbosity = "debug") with_gargle_verbosity( "debug", gargle_debug(c( "!" = "Option {.val gargle_quiet} is deprecated in favor of \\ {.val gargle_verbosity}", "i" = "Instead of: {.code options(gargle_quiet = FALSE)}", " " = 'Now do: {.code options(gargle_verbosity = "debug")}' )) ) } } gv <- getOption("gargle_verbosity", "info") vals <- c("debug", "info", "silent") if (!is_string(gv) || !(gv %in% vals)) { # ideally this would collapse with 'or' not 'and' but I'm going with it gargle_abort('Option "gargle_verbosity" must be one of: {.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 = parent.frame()) { 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 = parent.frame()) { if (gargle_verbosity() == "debug") { cli::cli_div(theme = gargle_theme()) cli::cli_bullets(text, .envir = .envir) } } gargle_info <- function(text, .envir = parent.frame()) { if (gargle_verbosity() %in% c("debug", "info")) { cli::cli_div(theme = gargle_theme()) cli::cli_bullets(text, .envir = .envir) } } # inspired by # https://github.com/rundel/ghclass/blob/6ed836c0e3750b4bfd1386c21b28b91fd7e24b4a/R/util_cli.R#L1-L7 # more discussion at # https://github.com/r-lib/cli/issues/222 cli_this = function(..., .envir = parent.frame()) { txt <- cli::cli_format_method(cli::cli_text(..., .envir = .envir)) # @rundel does this to undo wrapping done by cli_format_method() # I haven't had this need yet # paste(txt, collapse = " ") txt } 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 = parent.frame()) { cli::cli_div(theme = gargle_theme()) cli::cli_abort( message, class = c(class, "gargle_error"), .envir = .envir, ... ) } # 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 = parent.frame()) { cli::cli_div(theme = gargle_theme()) cli::cli_warn(message, .envir = .envir, ...) } gargle_abort_bad_class <- function(object, expected_class) { nm <- as_name(ensym(object)) actual_class <- class(object) expected <- glue_collapse( gargle_map_cli(expected_class, template = "{.cls <>}"), sep = ", ", last = " or " ) msg <- glue(" {.arg {nm}} must be <>, not of class {.cls {actual_class}}.", .open = "<<", .close =">>") gargle_abort( msg, class = "gargle_error_bad_class", object_name = nm, actual_class = actual_class, expected_class = expected_class ) } gargle_abort_bad_params <- function(names, reason) { gargle_abort( c( "These parameters are {reason}:", bulletize(gargle_map_cli(names)) ), class = "gargle_error_bad_params", 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 } } gargle/R/aaa.R0000644000176200001440000000153514067372466012604 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/credentials_user_oauth2.R0000644000176200001440000000503114067372466016672 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 app 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. #' @param app An OAuth consumer application, created by [httr::oauth_app()]. #' @param package Name of the package requesting a token. Used in messages. #' @inheritDotParams gargle2.0_token -scope -app -package #' #' @return A [Gargle2.0] token. #' @family credential functions #' @export #' @examples #' \dontrun{ #' ## Drive scope, built-in gargle demo app #' scopes <- "https://www.googleapis.com/auth/drive" #' credentials_user_oauth2(scopes, app = gargle_app()) #' #' ## bring your own app #' app <- httr::oauth_app( #' appname = "my_awesome_app", #' key = "keykeykeykeykeykey", #' secret = "secretsecretsecret" #' ) #' credentials_user_oauth2(scopes, app) #' } credentials_user_oauth2 <- function(scopes = NULL, app = gargle_app(), package = "gargle", ...) { gargle_debug("trying {.fun credentials_user_oauth2}") gargle2.0_token( app = app, scope = scopes, package = package, ... ) } gargle/R/request-develop.R0000644000176200001440000001752514067372466015214 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](https://gargle.r-lib.org/articles/request-helper-functions.html) #' 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) 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) { required <- Filter(function(x) isTRUE(x$required), spec) missing <- setdiff(names(required), names(provided)) if (length(missing)) { gargle_abort_bad_params(missing, reason = "missing") } unknown <- setdiff(names(provided), names(spec)) if (length(unknown)) { gargle_abort_bad_params(unknown, reason = "unknown") } 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_gce.R0000644000176200001440000001265514067372466015202 0ustar liggesusers#' Get a token for Google Compute Engine #' #' Uses the metadata service available on GCE VMs to fetch an access token. #' #' @inheritParams token_fetch #' @param service_account Name of the GCE service account to use. #' #' @seealso #' #' @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 (!detect_gce() || is.null(scopes)) { return(NULL) } instance_scopes <- get_instance_scopes(service_account = service_account) # We add a special case for the cloud-platform -> bigquery scope implication. if ("https://www.googleapis.com/auth/cloud-platform" %in% instance_scopes) { instance_scopes <- c( "https://www.googleapis.com/auth/bigquery", instance_scopes ) } if (!all(scopes %in% instance_scopes)) { return(NULL) } gce_token <- fetch_gce_access_token(scopes, service_account = service_account) params <- list( as_header = TRUE, scope = scopes, service_account = service_account ) token <- GceToken$new( credentials = gce_token$access_token, params = params, # The underlying Token2 class appears to *require* an endpoint and an app, # though it doesn't use them for anything in this case. endpoint = httr::oauth_endpoints("google"), app = httr::oauth_app("google", key = "KEY", secret = "SECRET") ) token$refresh() if (is.null(token$credentials$access_token) || !nzchar(token$credentials$access_token)) { NULL } else { token } } #' 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 Print token print = function(...) { cat("") }, #' @description Placeholder implementation of required method init_credentials = function() { self$credentials <- list(access_token = NULL) }, #' @description Placeholder implementation of required method cache = function(...) {}, #' @description Placeholder implementation of required method load_from_cache = function(...) {}, #' @description Placeholder implementation of required method can_refresh = function() { TRUE }, #' @description Refresh a GCE token refresh = function() { # The access_token can only include the token itself, not the expiration and # type. Otherwise, the httr code will create extra header lines that bust # the POST request: gce_token <- fetch_gce_access_token( self$params$scope, service_account = self$params$service_account ) self$credentials <- list(access_token = NULL) self$credentials$access_token <- gce_token$access_token }, #' @description Placeholder implementation of required method revoke = function() {} )) gce_metadata_url <- function() { use_ip <- getOption("gargle.gce.use_ip", FALSE) root_url <- Sys.getenv("GCE_METADATA_URL", "metadata.google.internal") if (use_ip) { root_url <- Sys.getenv("GCE_METADATA_IP", "169.254.169.254") } paste0("http://", root_url, "/") } gce_metadata_request <- function(path, stop_on_error = TRUE) { root_url <- gce_metadata_url() # TODO(craigcitro): Add options to ignore proxies. if (grepl("^/", path)) { path <- substring(path, 2) } url <- paste0(root_url, "computeMetadata/v1/", path) timeout <- getOption("gargle.gce.timeout", default = 0.8) response <- try({ httr::with_config(httr::timeout(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 } detect_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. # # @return A list of service account names. list_service_accounts <- function() { accounts <- gce_metadata_request("instance/service-accounts") ct <- httr::content(accounts, as = "text", encoding = "UTF-8") strsplit(ct, split = "/\n", fixed = TRUE)[[1]] } get_instance_scopes <- function(service_account) { path <- glue("instance/service-accounts/{service_account}/scopes") scopes <- gce_metadata_request(path) ct <- httr::content(scopes, as = "text", encoding = "UTF-8") strsplit(ct, split = "\n", fixed = TRUE)[[1]] } # TODO: why isn't scopes used here at all? fetch_gce_access_token <- function(scopes, service_account) { path <- glue("instance/service-accounts/{service_account}/token") response <- gce_metadata_request(path) httr::content(response, as = "parsed", type = "application/json") } gargle/R/oauth-refresh.R0000644000176200001440000000626714067372466014645 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 app # 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, app, 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 = app$key, client_secret = app$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, app, 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, app, 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' app_name <- app$appname %||% "" is_legacy_app <- grepl(gargle_legacy_app_pattern(), app_name) # app 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 app \\ has been deleted.", "i" = "You appear to be relying on the default app used by the \\ {.pkg {main_pkg}} package.", " " = "Consider re-installing {.pkg {all_pkgs}}, \\ in case the default app has been updated." )) return(invisible()) } # deleted app doesn't seem to be one of "ours" gargle_warn(c( "Unable to refresh token, because the associated OAuth app \\ has been deleted.", "*" = if (nzchar(app_name)) "App name: {.field {app_name}}", if (!is.null(package)) { c( "i" = "If you did not configure this OAuth app, it may be built into \\ the {.pkg {package}} package.", " " = "If so, consider re-installing {.pkg {package}} to get an updated \\ app." ) } )) invisible() } gargle/R/credential-function-registry.R0000644000176200001440000000574714067372466017676 0ustar liggesusers#' 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 } #' 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(creds_one) #' cred_funs_add(one = creds_one) #' cred_funs_add(one = creds_one, two = creds_one) #' cred_funs_add(one = creds_one, creds_one) #' #' # undo all of the above and return to default #' cred_funs_set_default() 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." #' #' @param ... One or more functions with the right signature: its first argument #' is named `scopes`, and it includes `...` as an argument. #' @export cred_funs_add <- function(...) { dots <- list(...) stopifnot(all(map_lgl(dots, is_cred_fun))) gargle_env$cred_funs <- c(dots, gargle_env$cred_funs) invisible() } #' @describeIn cred_funs Register a list of credential fetching functions. #' #' @param ls A list of credential functions. #' @export cred_funs_set <- function(ls) { stopifnot(all(map_lgl(ls, is_cred_fun))) gargle_env$cred_funs <- ls invisible() } #' @describeIn cred_funs Clear the credential function registry. #' @export cred_funs_clear <- function() { gargle_env$cred_funs <- list() invisible() } #' @describeIn cred_funs Reset the registry to the gargle default. #' @export cred_funs_set_default <- function() { cred_funs_clear() cred_funs_add(credentials_user_oauth2 = credentials_user_oauth2) cred_funs_add(credentials_byo_oauth2 = credentials_byo_oauth2) cred_funs_add(credentials_gce = credentials_gce) cred_funs_add(credentials_app_default = credentials_app_default) cred_funs_add(credentials_external_account = credentials_external_account) cred_funs_add(credentials_service_account = credentials_service_account) } gargle/R/Gargle-class.R0000644000176200001440000002342214067372466014365 0ustar liggesusers#' Generate a gargle token #' #' Constructor function for objects of class [Gargle2.0]. #' #' @param email Optional. Allows 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 targetted 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. Use `NA` #' or `FALSE` to match nothing and force the OAuth dance in the browser. Use #' `TRUE` to allow email auto-discovery, if exactly one matching token is #' found in the cache. Specify just the domain with a glob pattern, e.g. #' `"*@example.com"`, to create code that "just works" for both #' `alice@example.com` and `bob@example.com`. Defaults to the option named #' "gargle_oauth_email", retrieved by [gargle::gargle_oauth_email()]. #' @param app An OAuth consumer application, created by [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 prefer "out of band" authentication. Defaults to #' the option named "gargle_oob_default", retrieved via #' [gargle::gargle_oob_default()]. #' @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. #' @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(), app = gargle_app(), package = "gargle", ## params start scope = NULL, user_params = NULL, type = NULL, use_oob = gargle_oob_default(), ## params end credentials = NULL, cache = if (is.null(credentials)) gargle_oauth_cache() else FALSE, ...) { params <- list( scope = scope, user_params = user_params, type = type, use_oob = use_oob, as_header = TRUE, use_basic_auth = FALSE, config_init = list(), client_credentials = FALSE ) Gargle2.0$new( email = email, app = app, 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, app, 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, below #' `"~/.R/gargle/gargle-oauth"`, i.e. at the user level. 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 package Name of the package requesting a token. Used in messages. #' #' @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, #' @description Create a Gargle2.0 token #' @param app An OAuth consumer application. #' @param credentials Exists largely for testing purposes. #' @param params A list of parameters for [httr::init_oauth2.0()]. Some we #' actively use in gargle: `scope`, `use_oob`. Most we do not: #' `user_params`, `type`, `as_header`, `use_basic_auth`, `config_init`, #' `client_credentials`. #' @param cache_path Specifies the OAuth token cache. Read more in #' [gargle::gargle_oauth_cache()]. #' @return A Gargle2.0 token. initialize = function(email = gargle_oauth_email(), app = gargle_app(), package = "gargle", credentials = NULL, params = list(), cache_path = gargle_oauth_cache()) { gargle_debug("Gargle2.0 initialize") stopifnot( is.null(email) || is_scalar_character(email) || isTRUE(email) || isFALSE(email) || is_na(email), is.oauth_app(app), is_string(package), is.list(params) ) if (identical(email, "")) { gargle_abort(" {.arg email} must not be \"\" (the empty string). 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$app <- app 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", app = self$app$appname, email = cli_this("{.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) 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$app <- cached$app 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$app, self$credentials, package = self$package ) if (is.null(cred)) { token_remove_from_cache(self) # TODO: why do we return the current, invalid, unrefreshed token? # at the very least, let's clear the refresh_token, to prevent # future refresh attempts 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_rstudio_server()) { encourage_httpuv() } super$init_credentials() } 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()) } local_gargle_verbosity("info") gargle_info(c( "The {.pkg httpuv} package enables a nicer Google auth experience, \\ in many cases.", "It doesn't seem to be installed.", "Would you like to install it now?")) if (utils::menu(c("Yes", "No")) == 1) { utils::install.packages("httpuv") } invisible() } gargle/R/response_process.R0000644000176200001440000002577014067372466015465 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. #' #' @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) { 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) } } else { gargle_abort_request_failed(error_message(resp), resp) } } #' @export #' @rdname response_process response_as_json <- function(resp) { check_for_json(resp) content <- httr::content(resp, type = "raw") content <- rawToChar(content) Encoding(content) <- "UTF-8" jsonlite::fromJSON(content, simplifyVector = FALSE) } check_for_json <- function(resp) { type <- httr::http_type(resp) if (grepl("^application/json", type)) { return(invisible(resp)) } content <- httr::content(resp, as = "text") gargle_abort_request_failed( c( gargle_map_cli( type, template = "Expected content type {.field application/json}, not \\ {.field <>}." ), "*" = obfuscate(content, first = 197, last = 0) ), resp = resp ) } # personal policy: a wrapper around a wrapper around cli_abort() should not # capture/pass an environment # if you really want cli styling, you have to pre-interpolate gargle_abort_request_failed <- function(message, resp) { gargle_abort( message, class = c( "gargle_error_request_failed", glue("http_error_{httr::status_code(resp)}") ), resp = redact_response(resp) ) } #' @export #' @rdname response_process gargle_error_message <- function(resp) { content <- response_as_json(resp) 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 } 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/R/field-mask.R0000644000176200001440000000424514067372466014077 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://developers.google.com/protocol-buffers/docs/reference/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/gargle-api-key.R0000644000176200001440000000216214017730626014644 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(parent.frame()) tak() } gargle/R/roxygen-templates.R0000644000176200001440000002335514067372466015555 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", # ) glue_data_lines <- function(.data, lines, ..., .envir = parent.frame()) { # 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.", "", "If you are interacting with R within a browser (applies to RStudio Server,", "RStudio Workbench, and RStudio Cloud), you need a variant of this flow,", "known as out-of-band auth (\"oob\"). If this does not happen", "automatically, you can request it yourself with `use_oob = TRUE` or,", "more persistently, by setting an option via", "`options(gargle_oob_default = TRUE)`." ), .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, this function allows the user to explicitly:", " * Declare which Google identity to use, via an email address. If there", " are multiple cached tokens, this can clarify which one to use. It can", " also force {PACKAGE} to switch from one identity to another. If", " there's no cached token for the email, this triggers a return to the", " browser to choose the identity and give consent. You can specify just", " the domain by using a glob pattern. This means that a script", " containing `email = \"*@example.com\"` can be run without further", " tweaks on the machine of either `alice@example.com` or", " `bob@example.com`.", " * Use a service account token or workload identity federation.", " * Bring their own [Token2.0][httr::Token-class].", " * Specify non-default behavior re: token caching and out-of-bound", " authentication.", " * Customize scopes.", "", "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 app or API key.", "Read 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 app, 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", "[How to get your own API credentials](https://gargle.r-lib.org/articles/get-api-credentials.html)", "for more.", if (.fallbacks) { c( "If the user does not configure these settings, internal defaults", "are used." ) }, if (.has_api_key) { c( "`{PREFIX}_oauth_app()` and `{PREFIX}_api_key()` retrieve the", "currently configured OAuth app and API key, respectively." ) } else { "`{PREFIX}_oauth_app()` retrieves the currently configured OAuth app." } ) glue_data_lines(lines, .data = .data) } PREFIX_auth_configure_params <- function(.has_api_key = TRUE) { c( "@param app OAuth app, in the sense of [httr::oauth_app()].", "@inheritParams gargle::oauth_app_from_json", if (.has_api_key) { "@param api_key API key." } ) } 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_app()`: the current user-configured", " [httr::oauth_app()].", 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.R0000644000176200001440000004016314067403717014235 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")) { gargle_abort_bad_class(cache, c("logical", "character")) } # takes care of the re-location of the default cache, implemented in v1.1.0 # once we consider the transition done, this if(){...} can go away # the persistent solution for cleaning out legacy tokens is cache_clean() below if (isTRUE(cache) || is_na(cache)) { close_out_legacy_cache() } # 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) } local_gargle_verbosity("info") gargle_info(" Is it OK to cache OAuth access credentials in the folder \\ {.path {path}} between R sessions?") utils::menu(c("Yes", "No")) == 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()) { # deletes an empty directory at the legacy cache location # new location implemented in v1.1.0 # once we consider the transition done, this defer() can go away withr::defer(delete_empty_legacy_cache(cache)) dat_tokens <- gargle_oauth_dat(cache) dat_tokens$legacy <- grepl(pattern, dat_tokens$app) n <- sum(dat_tokens$legacy) if (n == 0) { return(FALSE) } gargle_info(c( "v" = "Deleting {n} token{?s} obtained with an old tidyverse OAuth app.", "i" = "Expect interactive prompts to re-auth with the new app.", "!" = "Is this rolling of credentials highly disruptive to your \\ workflow?", " " = "That means you should rely on your own OAuth app \\ (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) 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 (length(existing) == 1) { gargle_info(c( "i" = "The {.pkg {package}} package is using a cached token for \\ {.email {extract_email(existing)}}.")) return(existing) } } 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") candidate_email <- "*" if (length(existing) > 1) { emails <- extract_email(existing) emails_fmt <- lapply( emails, function(x) cli_this("{.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) } if (length(existing) == 1 && candidate_email == "*") { 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 emails <- extract_email(existing) local_gargle_verbosity("info") gargle_info(c( "The {.pkg {package}} package is requesting access to your Google account.", "Select a pre-authorised account or enter '0' to obtain a new token.", "Press Esc/Ctrl + C to cancel.")) choice <- utils::menu(emails) if (choice == 0) { NULL } else { existing[[choice]] } } ## 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 app (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) app <- map_chr(tokens, function(t) 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, app, 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} {app} {scopes} {hash...}", .transformer = format_transformer ) } #' @export print.gargle_oauth_dat <- function(x, ...) { cli::cat_line(format(x)) invisible(x) } # cache relocation, implemented in v1.1.0 -------------------------------------- gargle_legacy_default_oauth_cache_path <- function() { path_home(".R", "gargle", "gargle-oauth") } # 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() if (dir_exists(default_cache)) { return(default_cache) } cache <- gargle_legacy_default_oauth_cache_path() if (dir_exists(cache)) { gargle_info(c( "!" = "Legacy OAuth cache found.", "!" = "Expect cache to be cleaned and relocated upon first use." )) return(cache) } default_cache } is_legacy_cache <- function(cache) { legacy_cache <- gargle_legacy_default_oauth_cache_path() dir_exists(legacy_cache) && path_real(cache) == path_real(legacy_cache) } delete_empty_legacy_cache <- function(cache) { if (!is_legacy_cache(cache)) { return() } # use dir_ls() or gargle_oauth_dat()? # dir_ls() captures all files # gargle_oauth_dat() just captures files that "look" like our tokens # this should rarely matter, but I'll err on the side of not deleting files # that I don't recognize as ours cache_contents <- dir_ls(cache, all = TRUE) if (length(cache_contents) == 0) { gargle_debug(" Legacy cache {.path {cache}} is empty, deleting the directory") dir_delete(cache) TRUE } else { FALSE } } # gets rid of an existing cache at the legacy location, whatever it takes # # ideally, it's empty and we can delete it and start over # that's why we first delete legacy tokens # # if there are still tokens remaining, move them to new default cache # # once we consider the transition done, this function can go away # the persistent solution for cleaning out legacy tokens is cache_clean() close_out_legacy_cache <- function() { default_cache <- gargle_default_oauth_cache_path() if (dir_exists(default_cache)) { # cache already established at current default path # even if a legacy cache still exists, we shall not speak of it return() } cache <- gargle_legacy_default_oauth_cache_path() if (dir_exists(cache)) { cache_clean(cache) } if (!dir_exists(cache)) { return() } # we have a non-empty legacy cache cache_relocate(from = cache, to = default_cache) } cache_relocate <- function(from, to) { gargle_info(c( "The default location for caching gargle OAuth tokens has changed.", "Previously: {.path {from}}", "As of gargle v1.1.0: {.path {to}}" )) if (!dir_exists(to)) { cache_create(to) } dat_tokens <- gargle_oauth_dat(from) file_move(dat_tokens$filepath, to) gargle_info("Relocating {nrow(dat_tokens)} existing token{?s} to new cache.") delete_empty_legacy_cache(from) } gargle/R/request-make.R0000644000176200001440000000426514067372466014470 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()) { stopifnot(is.character(x$method)) 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/credentials_byo_oauth2.R0000644000176200001440000000504214067372466016507 0ustar liggesusers#' Load a user-provided token #' #' @description #' This function does very little when called directly with a token: #' * If input 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 produced by `googledrive::drive_token()` #' or `bigrquery::bq_token()`. #' * Checks that the input appears to be a Google OAuth token, based on #' the embedded `oauth_endpoint`. #' * Refreshes the token, if it's refreshable. #' * Returns its input. #' #' There is no point 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") #' sheets_auth(token = drive_token()) #' # work with both packages freely now #' ``` #' #' @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 #' # httr::oauth2.0_token() or gargle::gargle2.0_token() #' 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( "{.arg scopes} cannot be specified when user brings their own OAuth token", "{.arg scopes} are already implicit in the token" )) } check_endpoint(token$endpoint) if (token$can_refresh()) { token$refresh() } token } check_endpoint <- function(endpoint) { 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.") } endpoint } gargle/R/credentials_app_default.R0000644000176200001440000001032414067372466016717 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}") endpoint <- httr::oauth_endpoints("google") 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 = 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/token-fetch.R0000644000176200001440000000245614067372466014274 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. Use [cred_funs_list()] to see the current registry, in order. See the #' vignette [How gargle gets #' tokens](https://gargle.r-lib.org/articles/how-gargle-gets-tokens.html) for a #' full description of `token_fetch()`. #' #' @inheritParams credentials_user_oauth2 #' @param ... Additional arguments passed to all credential functions. #' #' @return An [`httr::Token`][httr::Token-class] 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( f(scopes, ...), warning = function(e) { gargle_debug(c("Warning caught by {.fun token_fetch}:", e$message)) NULL }, error = function(e) { gargle_debug(c("Error caught by {.fun token_fetch}:", e$message)) NULL } ) if (!is.null(token)) { return(token) } } NULL } gargle/R/compat-purrr.R0000644000176200001440000000234714067372466014517 0ustar liggesusers# nocov start - compat-purrr (last updated: rlang 0.3.2.9000) # This file serves as a reference for compatibility functions for # purrr. They are not drop-in replacements but allow a similar style # of programming. This is useful in cases where purrr is too heavy a # package to depend on. Please find the most recent version in rlang's # repository. map <- function(.x, .f, ...) { lapply(.x, .f, ...) } map_mold <- function(.x, .f, .mold, ...) { out <- vapply(.x, .f, .mold, ..., USE.NAMES = FALSE) names(out) <- names(.x) out } map_lgl <- function(.x, .f, ...) { map_mold(.x, .f, logical(1), ...) } map_int <- function(.x, .f, ...) { map_mold(.x, .f, integer(1), ...) } map_dbl <- function(.x, .f, ...) { map_mold(.x, .f, double(1), ...) } map_chr <- function(.x, .f, ...) { map_mold(.x, .f, character(1), ...) } walk <- function(.x, .f, ...) { map(.x, .f, ...) invisible(.x) } map2 <- function(.x, .y, .f, ...) { out <- mapply(.f, .x, .y, MoreArgs = list(...), SIMPLIFY = FALSE) if (length(out) == length(.x)) { set_names(out, names(.x)) } else { set_names(out, NULL) } } imap <- function(.x, .f, ...) { map2(.x, vec_index(.x), .f, ...) } vec_index <- function(x) { names(x) %||% seq_along(x) } # nocov end gargle/R/oauth-app.R0000644000176200001440000000235514067372466013761 0ustar liggesusers#' Create an OAuth app from JSON #' #' Essentially a wrapper around [httr::oauth_app()] that extracts the necessary #' info from JSON obtained from [Google Cloud Platform #' Console](https://console.cloud.google.com). If no `appname` is given, #' the `"project_id"` from the JSON is used. #' #' @param path JSON downloaded from Google Cloud Platform Console, containing a #' client id (aka key) and secret, in one of the forms supported for the `txt` #' argument of [jsonlite::fromJSON()] (typically, a file path or JSON string). #' #' @inheritParams httr::oauth_app #' @export #' @examples #' \dontrun{ #' oauth_app( #' path = "/path/to/the/JSON/you/downloaded/from/gcp/console.json" #' ) #' } oauth_app_from_json <- function(path, appname = NULL) { stopifnot(is_string(path), is.null(appname) || is_string(appname)) json <- jsonlite::fromJSON(path, simplifyVector = FALSE) info <- json[["installed"]] %||% json[["web"]] if (!all(c("client_id", "client_secret") %in% names(info))) { gargle_abort(" Can't find {.field client_id} and {.field client_secret} in the JSON.") } httr::oauth_app( appname = appname %||% info$project_id, key = info$client_id, secret = info$client_secret ) } gargle/NEWS.md0000644000176200001440000002762714067637023012637 0ustar liggesusers# 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](https://support.google.com/cloud/answer/7454865?hl=en), 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/MD50000644000176200001440000002666514067641672012057 0ustar liggesusers4c30d2b6ed8b2d07c5f58a40fc5f3e47 *DESCRIPTION 77c27f6830f4a03ce6da86c1d5576510 *LICENSE 9cf597771ea91786d446f939f1705bf9 *NAMESPACE 232da4ef6047699d206729b2e2ea526f *NEWS.md a4d943eb08b2d4d055b2414d6c15a458 *R/AuthState-class.R 401577eb9cd52c258ea972aa7b3318af *R/Gargle-class.R 24698131ab19f67c36e4b602352bcb12 *R/aaa.R 78b46429052179ff24a2a834c92d4ac0 *R/compat-purrr.R a8a24b38eba2b9d749968c85a12709e3 *R/credential-function-registry.R d618301c07f642a3505aa3fcb7510076 *R/credentials_app_default.R decb1ec827026734d67cd372da0b2dca *R/credentials_byo_oauth2.R d34486e0f935aa3d2936f0b310d1f34a *R/credentials_external_account.R 24076e18e109d633e35a9f192981f63b *R/credentials_gce.R af6c205b49bae94ef0f559d5a463f0ae *R/credentials_service_account.R 26c03d06aaea6b385057599bf0d0e0fa *R/credentials_user_oauth2.R 5b51b7dbc52a16bb7fcaaed83f126c4c *R/field-mask.R f6f15c32a05339e6906605c48d8d1dc2 *R/gargle-api-key.R cfad23314b82182b09e4e34cae2e3bae *R/gargle-oauth-app.R e2344c9978a318c84a3e19eae4dddd54 *R/gargle-oauth-endpoint.R 776a9c2b92bef48c73b6695ecdb664f8 *R/gargle-package.R 2382c9be419ce9382a4a96b0580a18fc *R/inside-the-house.R 131218a80f8240cc2db7e06fea788bed *R/oauth-app.R a9714008f7d1541bdd3dfc06a9561682 *R/oauth-cache.R db91b5c3468d92daaec3864fc76d7091 *R/oauth-refresh.R 8340aaae1d1d23032a6de8bae9ce2c82 *R/request-develop.R 26494fda13a67defaaba6ccb24ce6bfb *R/request-make.R 7cbd8c40fac21fe1aa07d07c4fc2d06d *R/request_retry.R 709f4fd0643c5837f715e8a36c692d27 *R/response_process.R ff1f5174c67992382029451cfe79788a *R/roxygen-templates.R 38048e5b54749d2a9fc0a6293a16081c *R/secret.R 28a8f2ecddceeee37f1f836f8724bcf2 *R/sysdata.rda a4109871a23a90556e0300afad788f27 *R/token-fetch.R 0cf516d845a4b4053cc99054ec66511e *R/token-info.R dfc4d9a16754af88762af665ce208108 *R/utils-ui.R 8047be38c1dc21fdc4e8715325b5afc8 *R/utils.R 6b2fc88bd96e5bb0a64c603199ee329d *R/zzz.R d94370bd1349df4e8838bf53fc7b98bc *README.md 746905a4b8c98e9638feac824cc94988 *build/vignette.rds e2bc6bd9e00ad3a8cd30231f0a6b1954 *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 77e5a4a806b0f82060520b0101b31b6b *inst/doc/auth-from-web.R dd3d75bc4242bbf6e6bcaa5f2b1757c7 *inst/doc/auth-from-web.Rmd 20a98e45f339b743fb6aa1e7d730215e *inst/doc/auth-from-web.html c7a6014a47b3937045abe8ac0165034c *inst/doc/gargle-auth-in-client-package.R 98887d2dbf1b2831b818eefd0670450b *inst/doc/gargle-auth-in-client-package.Rmd 494d63f2321447a37209c10db4e526ac *inst/doc/gargle-auth-in-client-package.html d7de0d0f6286a6142065f0ed5731fef6 *inst/doc/get-api-credentials.R c36860c9678267ebb3b102ca9b6cd106 *inst/doc/get-api-credentials.Rmd 782b0495648a83f1fa113f2dfe543dd5 *inst/doc/get-api-credentials.html 400d46277e012db193a42bf8c89cc919 *inst/doc/how-gargle-gets-tokens.R f5d7059c732fd2ac2cae118433f3b9b1 *inst/doc/how-gargle-gets-tokens.Rmd 8db43d9d324a767f03d9ca24cdbba24f *inst/doc/how-gargle-gets-tokens.html 0744cea5617767869ab89f0d17075976 *inst/doc/non-interactive-auth.R 1d44570a2e0a79f924838364834a7bf9 *inst/doc/non-interactive-auth.Rmd bdedbebbf4e6bd244f7cd4aac4b6d891 *inst/doc/non-interactive-auth.html 22fd3602f7b66785cb19618c51285013 *inst/doc/request-helper-functions.R e7c5f90a2d47fb7ad1761dfb4e6b9732 *inst/doc/request-helper-functions.Rmd f63f7c1842cb8d07938459a9782294bf *inst/doc/request-helper-functions.html 4d5999140a1c1e4b13ab4ffbdd460c03 *inst/doc/troubleshooting.R 0e60c89beabc04db600792adc54acb00 *inst/doc/troubleshooting.Rmd b13dce931e65863d0e72b70cc699dc22 *inst/doc/troubleshooting.html e27c1d920d32833028be422d8a32ea00 *inst/secret/gargle-testing.json 95128c4e73881850d40a0481d008de66 *man/AuthState-class.Rd 75a79295ee2b713a3adcf3fb361bd329 *man/Gargle-class.Rd d7ff7b8317e393bf557001ae92a6f582 *man/GceToken.Rd d7be2b360d254b088425fb709d85fb11 *man/WifToken.Rd 7c6eed21b606f1a6b9290abb24698f77 *man/bulletize.Rd dddd36bc564049cde890dc3a469ad880 *man/cred_funs.Rd 435bb7ca0fee4282f979616c47133736 *man/credentials_app_default.Rd b6f69cb2ccc8f65673d9ad24afa10ba2 *man/credentials_byo_oauth2.Rd 44e520668a2a8fbd64ee59e14116c880 *man/credentials_external_account.Rd 85e695e5f6830300c9c7d3a71179dca2 *man/credentials_gce.Rd 45ea47b11cc34c52c1c486ed1f890f53 *man/credentials_service_account.Rd 154ad8ea76d77c6dbb47a459ec30c03c *man/credentials_user_oauth2.Rd 447798f354c79deb8318944619aa915a *man/field_mask.Rd cb1e46f469cfbbbde29c8b5113e1d789 *man/figures/lifecycle-archived.svg c0d2e5a54f1fa4ff02bf9533079dd1f7 *man/figures/lifecycle-defunct.svg a1b8c987c676c16af790f563f96cbb1f *man/figures/lifecycle-deprecated.svg c3978703d8f40f2679795335715e98f4 *man/figures/lifecycle-experimental.svg 952b59dc07b171b97d5d982924244f61 *man/figures/lifecycle-maturing.svg 27b879bf3677ea76e3991d56ab324081 *man/figures/lifecycle-questioning.svg 53b3f893324260b737b3c46ed2a0e643 *man/figures/lifecycle-stable.svg 1c1fe7a759b86dc6dbcbe7797ab8246c *man/figures/lifecycle-superseded.svg a529dfd86134b47c4742af1fa0bd951a *man/gargle-package.Rd f5a909fd9d4705b21d9e04b606790800 *man/gargle2.0_token.Rd 874b283c81c4d1cbbb1b6e1a1aebadf6 *man/gargle_api_key.Rd a78beade96d784187f6d8511e5cc04a4 *man/gargle_app.Rd c6efd4d574f082f577134e994f6430e3 *man/gargle_map_cli.Rd 851582b3462f4553337200f3c35e8921 *man/gargle_oauth_sitrep.Rd f7f41264ee3e7c1441c0f946b0f5f719 *man/gargle_options.Rd 34c496433ebe919e30ab8ccebdf64e2b *man/init_AuthState.Rd 3c82999d5891863850acd5ce946fe52c *man/internal-assets.Rd 7f9ea84006142cb39a9ec0779dba2018 *man/oauth_app_from_json.Rd f859c45d207cc8ae148a097693a136c1 *man/oauth_external_token.Rd 15cb15cc786ab4ab343b4493590f5901 *man/request_develop.Rd d60cc7b04b017558cab52696e2d20d83 *man/request_make.Rd 89038e194ee5ee7300d992256242016e *man/request_retry.Rd 4d08093d20c3c562a75d023f98fab189 *man/response_process.Rd 94bc419246ae8806a459db47b069657e *man/token-info.Rd 483e3df56cafc0fe0f26bd37dee552ab *man/token_fetch.Rd 0622a97a2aaa3c342f09636052c2d7f5 *tests/spelling.R 786cfb0c126a9cf1f08a94ce90325209 *tests/testthat.R 9b5c52265953b933cd7de2af8769dd5a *tests/testthat/_snaps/AuthState-class.md dbbfb7ff3c5c09bdeb07cf0dffa515b1 *tests/testthat/_snaps/Gargle-class.md b550c9ff342486e88f14e7e3861d65aa *tests/testthat/_snaps/inside-the-house.md 3a597174eb7063af0c827a60283231e9 *tests/testthat/_snaps/oauth-cache.md 6bb8c5dff5c3b224f81d3800d3c17427 *tests/testthat/_snaps/oauth-refresh.md 0eb0221013b9063e95c7ba764a141f3b *tests/testthat/_snaps/request-develop.md 8e4d69b3835a3c1425f2a4e72037ce7f *tests/testthat/_snaps/request_retry.md 11d7d42031d272cbc1947ccc30cfe1f1 *tests/testthat/_snaps/response_process.md 8a072efed1dc51306041e29b571793c4 *tests/testthat/_snaps/utils-ui.md fdf50c6e6a5881cd0406cc37ae101a1a *tests/testthat/fixtures/client_secret_123.googleusercontent.com.json 6b456bb6092a828821996fdeaa3efb62 *tests/testthat/fixtures/client_secret_456.googleusercontent.com.json 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 c1b129cbb7c614987130af80ee411829 *tests/testthat/fixtures/sheets-spreadsheets-get-quota-exceeded-readgroup_429.R 203cd79e8a286dc8aaae4e157d1f710d *tests/testthat/fixtures/sheets-spreadsheets-get-quota-exceeded-readgroup_429.rds 666475d65990f78bd0a9daeaf1df5e82 *tests/testthat/fixtures/tokeninfo-bad-path_404.rds b1440c8aa590fcdd25941f43d9253d1c *tests/testthat/fixtures/tokeninfo-stale_400.rds bcf8ca6fcfbcbd17e314ee63d54d3f20 *tests/testthat/fixtures/tokeninfo_40X.R ca8eccb23a4273c9a7cc1c536ae35144 *tests/testthat/helper.R a8fede6fa611ec3f49c5b0ca616f84f8 *tests/testthat/test-AuthState-class.R 0607d6869d45d8ef27012679b21c4787 *tests/testthat/test-Gargle-class.R fc7ded516d8a71be78fccb0eecbbcacd *tests/testthat/test-aaa.R 2120d004fd7640a9ff20f539a06361e6 *tests/testthat/test-assets.R ae4447083545ba0419fb7629b0b252ac *tests/testthat/test-credentials-byo-oauth2.R 62cf5830bd1341b5e495a271c9b71213 *tests/testthat/test-credentials_app_default.R df70d622dfc9075568720f967c58374d *tests/testthat/test-fetch.R cbf82e656938ff7f826786de8b0417b8 *tests/testthat/test-field-mask.R 90f58c0f146ff0899f18e1ad9b8c8a7c *tests/testthat/test-gce-token.R 9b23ed7e3ec3e472fb25dabf36c1e390 *tests/testthat/test-inside-the-house.R c932c421df065dc9609626528d0c4715 *tests/testthat/test-oauth-app.R 37acad5cd4e54e0cc64df83363c48319 *tests/testthat/test-oauth-cache.R 579816a53a7cb67d3994c3d163cab3a3 *tests/testthat/test-oauth-refresh.R 0e26296058c6d626c0e4673a964c73bd *tests/testthat/test-registry.R 4748daa71f9c204e3235396f1ef021e4 *tests/testthat/test-request-develop.R 3080916e78545be17b11c779d3ec13d3 *tests/testthat/test-request-make.R 13864c87a82f6aeed03a83f0134fa66b *tests/testthat/test-request_retry.R 1fd90f5e6338a9840e360a196b6a8d06 *tests/testthat/test-response_process.R 8a036a2b72f41b024ef9a3fa8cb13de4 *tests/testthat/test-token-info.R a8203501e0f8845306f0c961c91a036e *tests/testthat/test-utils-ui.R cb377fe21bf4a461903c9eaad67367dc *tests/testthat/test-utils.R 850c53dc7f725b14d084e3ee27a9cc4e *vignettes/articles/managing-tokens-securely.Rmd dd3d75bc4242bbf6e6bcaa5f2b1757c7 *vignettes/auth-from-web.Rmd f69dd93a4f263fa467ab61fc6ca787b3 *vignettes/deleted_client.png 98887d2dbf1b2831b818eefd0670450b *vignettes/gargle-auth-in-client-package.Rmd c36860c9678267ebb3b102ca9b6cd106 *vignettes/get-api-credentials.Rmd f5d7059c732fd2ac2cae118433f3b9b1 *vignettes/how-gargle-gets-tokens.Rmd 1d44570a2e0a79f924838364834a7bf9 *vignettes/non-interactive-auth.Rmd e7c5f90a2d47fb7ad1761dfb4e6b9732 *vignettes/request-helper-functions.Rmd 0e60c89beabc04db600792adc54acb00 *vignettes/troubleshooting.Rmd gargle/inst/0000755000176200001440000000000014067637312012501 5ustar liggesusersgargle/inst/secret/0000755000176200001440000000000013512541641013757 5ustar liggesusersgargle/inst/secret/gargle-testing.json0000644000176200001440000000444213512541641017572 0ustar liggesusersPk$|Ycbb2zQ)Ba*[ռ N8vzFj Q! ƃ3G8tv\1ަY7d)ˢuG db_]?kc;* n:BƪSоn,ɂ G 3 J[ǠKvdVv}[}gpqg##wJ!>w㕅#lHwLja*`ګHs3껵@sTd_sJ~"&*=z6 1:1N[zf΃J"u5 A:׆B+$񮬜~SD7'JK4ma*$L(e׃ ½ 2.Dzcu \ =W]_?`D<~Br&6.p~ $> θ͏0I?uYH)2YdPO^}!rHc:~jO,Z2"z↗d%HI3VLtu\c=whVM%blᤓ$ߐdw{2}PTT,MΦ$ љ?ਸ਼U jD:}? Mi)UcQ ^Ӭ Hz8")|yyAޥʼn^#<y tuQ2#E#{Bpw!(͓;+T ug1QX NW:ݪF],64 Zdq4I￉a9s6Iq]SolN><=W\t<ҿ]2R-A$0aKS%r$M"x5Yns ܹKRpcECU!PÕ7'=%Kk`#m WYhQ %YYq&ot>C "+sqO=0b?;}c~k.B.1MQPÿM|3IƗj(s^SFcY~*|Z<q yݬ<>`Ŵ #dk?HU#?Ѯl-"R;zzU9NhQL$jlNXӬ(բ/0tBq¼B~`- f|Q}w3#軁8).Oh3R(tD3#p=%3b" &Eq_VG(l(1zZ%Ƴ89 v Ȕ60,!rYd]ǝvkW?Ib dg:%u;."ׅ߈$ 7qހt_wVwvȮ 9}40бBO{Vpt1R%%DM)|-ۏ}+6/z8XL /u ~8ZI'c\{Вi\:BJp8.֌5垰: Y0.#yl^5:q/2> -~{u74rmMiV X Zu w&^AiFu_¾}HzP}7vkf>K1L puq?Ǝ]qn^"$2;:` jBK] ȇy͘2P>Ժ.]`LT5+Rz%<[BHd<->YqJz/-Kݕjប@/]|~࿋4F`I{'wq d6IU ݸH?gargle/inst/doc/0000755000176200001440000000000014067637312013246 5ustar liggesusersgargle/inst/doc/request-helper-functions.R0000644000176200001440000000520214067637311020342 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.html0000644000176200001440000011544614067637311020212 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 How to use gargle for auth in a client package. Examples include:

Full details on gargle::token_fetch(), which powers this strategy, are given in 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.

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.

This requires some advance preparation, but that tends to pay off pretty quickly, in terms of having a much more robust auth setup.

Step 1: Get a service account and then download a token. Described in the gargle article How to get your own API credentials, specifically in the Service account token section.

Step 2: Call the wrapper package’s main auth function proactively and provide the path to your service account token. 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 the gargle article Managing tokens securely.

If the code is running on AWS, a special auth flow is available called workload identity federation. Learn more in the documentation for credentials_external_account().

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, there’s a decent chance you 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 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.

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 the gargle article 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 the gargle article How gargle gets tokens.

Arrange for an OAuth 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")

If you are doing setup in a web-based environment, such as RStudio Server, you may also need to request out-of-band auth, whenever you are first acquiring a token. That is a separate issue, which is explained in Auth when using R in the browser.

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_auth(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 app (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.R0000644000176200001440000000236214067637310017213 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) # # # method 1: direct provision client ID and secret # google_app <- httr::oauth_app( # "my-very-own-google-app", # key = "123456789.apps.googleusercontent.com", # secret = "abcdefghijklmnopqrstuvwxyz" # ) # drive_auth_configure(app = google_app) # # # method 2: provide filepath to JSON containing client ID and secret # drive_auth_configure( # path = "/path/to/the/JSON/you/downloaded/from/gcp/console.json" # ) # # # now any new OAuth tokens are obtained with the configured app ## ----eval = FALSE------------------------------------------------------------- # # googledrive # drive_auth(path = "/path/to/your/service-account-token.json") gargle/inst/doc/troubleshooting.html0000644000176200001440000020152114067637312017364 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.

# 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 app or 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.

Credential rolling

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

In gargle v1.0.0, in March 2021, we rolled the app used in googlesheets4, googledrive, and bigrquery. At some point in 2021, we plan to disable the old app. Anyone relying on the default app 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 app 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:

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. Consider using your own OAuth app to eliminate your exposure to a third-party deciding to roll their app. 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.

gargle/inst/doc/non-interactive-auth.R0000644000176200001440000001013014067637311017427 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 don't use multiple Google identities, you can be more vague: # options(gargle_oauth_email = TRUE) # # # Approach #2: call PACKAGE_auth() proactively. # library(googledrive) # # Either specify the user: # drive_auth(email = "jenny@example.com") # # Or, if you don't use multiple Google identities, you can be more vague: # drive_auth(email = TRUE) ## ----------------------------------------------------------------------------- # library(googledrive) # # drive_auth(path = "/path/to/your/service-account-token.json") ## ----------------------------------------------------------------------------- # 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_auth(n_max = 5) ## ----------------------------------------------------------------------------- # options(gargle_verbosity = "debug") gargle/inst/doc/auth-from-web.Rmd0000644000176200001440000001411014067372466016372 0ustar liggesusers--- title: "Auth when using R in the browser" output: rmarkdown::html_vignette vignette: > %\VignetteIndexEntry{Auth when using R in 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://www.rstudio.com/products/rstudio/download-server/), [RStudio Cloud](https://rstudio.cloud), or [RStudio Workbench](https://www.rstudio.com/products/workbench/), 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". 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. The need to use oob auth can sometimes be detected automatically. For example, oob auth is always used when the httpuv package is not installed. gargle also tries to detect usage via RStudio Server, Cloud, or Workbench, but this still may not catch 100% of situations where oob auth is necessary. Therefore, some users may still need to recognize this situation and explicitly request oob auth. Here's a typical presentation of this problem: 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. ``` This is a sign that you need to explicitly request oob auth. This article describes how to do so in a package that uses gargle for auth, which includes: * [bigrquery](https://bigrquery.r-dbi.org) (>= v1.2.0) * [googledrive](https://googledrive.tidyverse.org) (>= v1.0.0) * [gmailr](https://gmailr.r-lib.org) (>= v1.0.0) * [googlesheets4](https://googlesheets4.tidyverse.org) * [gcalendr](https://github.com/andrie/gcalendr) *GitHub only* ## Request oob auth in the `PKG_auth()` call These packages 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 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) ``` This code is tailored to an interactive session and assumes that a user is present to respond. If you *also* need to setup a token for non-interactive use, see the article [Non-interactive auth](https://gargle.r-lib.org/articles/non-interactive-auth.html). A key point is that oob auth is relevant to how you *initially* obtain a token. It is orthogonal to downstream use and refreshing. So it is possible that you need to attend to both! ## Set the `gargle_oob_default` option If you know that you *always* want to use oob auth, 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 that option has been set, it is honoured by downstream calls to `PKG_auth()`, explicit or implicit, because the default behaviour of `use_oob` is to consult the option: ```{r, eval = FALSE} 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) {...} ``` ## 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 httr 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 if you don't have to? Here are ways to fix. * 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 [Generating OAuth tokens for a server using httr](https://support.rstudio.com/hc/en-us/articles/217952868-Generating-OAuth-tokens-from-a-server) covers some of the same ground, although for the httr package. gargle provides a Google-specific interface to httr. gargle first consults the `gargle_oob_default` option and, if that is undefined, also consults the `httr_oob_default` option. If you're creating content to be deployed (for example on [shinyapps.io](https://www.shinyapps.io) or [RStudio Connect](https://www.rstudio.com/products/connect/)), you will also need to consider how the [deployed content will authenticate non-interactively](https://gargle.r-lib.org/articles/non-interactive-auth.html). gargle/inst/doc/non-interactive-auth.Rmd0000644000176200001440000003643714067372466020001 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 [How to use gargle for auth in a client package](https://gargle.r-lib.org/articles/gargle-auth-in-client-package.html). Examples include: * [bigrquery](https://bigrquery.r-dbi.org) (>= v1.2.0) * [googledrive](https://googledrive.tidyverse.org) (>= v1.0.0) * [gmailr](https://gmailr.r-lib.org) (>= v1.0.0) * [googlesheets4](https://googlesheets4.tidyverse.org) * [gcalendr](https://github.com/andrie/gcalendr) *GitHub only* Full details on [`gargle::token_fetch()`](https://gargle.r-lib.org/reference/token_fetch.html), which powers this strategy, are given in [How gargle gets tokens](https://gargle.r-lib.org/articles/how-gargle-gets-tokens.html). ## 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 [RStudio Connect](https://rstudio.com/products/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 they are no more secure or hidden than the other source files in the project. The vignette [Managing tokens securely](https://gargle.r-lib.org/articles/articles/managing-tokens-securely.html) describes a method for embedding an encrypted token in the project, which is an extra level of care needed to work with, e.g., continuous integration services, such as GitHub Actions, Travis-CI, or AppVeyor. ## 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 don't use multiple Google identities, you can be more vague: options(gargle_oauth_email = TRUE) # Approach #2: call PACKAGE_auth() proactively. library(googledrive) # Either specify the user: drive_auth(email = "jenny@example.com") # Or, if you don't use multiple Google identities, you can be more vague: drive_auth(email = TRUE) ``` Keep reading if you want to actually understand this. ## 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. This requires some advance preparation, but that tends to pay off pretty quickly, in terms of having a much more robust auth setup. **Step 1**: Get a service account and then download a token. Described in the gargle article [How to get your own API credentials](https://gargle.r-lib.org/articles/get-api-credentials.html), specifically in the [Service account token](https://gargle.r-lib.org/articles/get-api-credentials.html#service-account-token) section. **Step 2**: Call the wrapper package's main auth function proactively and provide the path to your service account token. 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 the gargle article [Managing tokens securely](https://gargle.r-lib.org/articles/articles/managing-tokens-securely.html). If the code is running on AWS, a special auth flow is available called workload identity federation. Learn more in the documentation for `credentials_external_account()`. 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, there's a decent chance you 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 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. 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](https://gargle.r-lib.org/articles/how-gargle-gets-tokens.html#credentials_app_default) of the gargle article [How gargle gets tokens](https://gargle.r-lib.org/articles/how-gargle-gets-tokens.html). 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](https://gargle.r-lib.org/articles/how-gargle-gets-tokens.html#credentials_byo_oauth2) of the gargle article [How gargle gets tokens](https://gargle.r-lib.org/articles/how-gargle-gets-tokens.html). ## Arrange for an OAuth 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") ``` If you are doing setup in a web-based environment, such as RStudio Server, you may also need to request out-of-band auth, whenever you are first acquiring a token. That is a separate issue, which is explained in [Auth when using R in the browser](https://gargle.r-lib.org/articles/auth-from-web.html). **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_auth(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 app (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.R0000644000176200001440000001374714067637310017700 0ustar liggesusers## ---- include = FALSE--------------------------------------------------------- knitr::opts_chunk$set( collapse = TRUE, comment = "#>" ) ## ----setup-------------------------------------------------------------------- library(gargle) ## ---- eval = FALSE------------------------------------------------------------ # token_fetch(scopes, ...) ## ----------------------------------------------------------------------------- names(cred_funs_list()) ## ---- eval = FALSE------------------------------------------------------------ # token_fetch(scopes = , path = "/path/to/your/service-account.json") # # # 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") # # # leads to this call: # credentials_external_account( # scopes = , # path = "/path/to/your/external-account.json" # ) ## ---- eval = FALSE------------------------------------------------------------ # token_fetch(scopes = ) # # # credentials_service_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_service_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(token = ) # # # credentials_service_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_byo_oauth2( # token = # ) ## ---- eval = FALSE------------------------------------------------------------ # token_fetch(scopes = ) # # # credentials_service_account() fails because no `path`, # # credentials_app_default() fails because no ADC found, # # credentials_gce() fails because not on GCE, # # credentials_byo_oauth2() fails because no `token`, # # 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. Select a # pre-authorised account or enter '0' to obtain a new token. Press Esc/Ctrl + C to # abort. # # 1: janedoe_personal@gmail.com # 2: janedoe@example.com # 3: janedoe_work@gmail.com # # Selection: 3 ## ---- eval = FALSE------------------------------------------------------------ # thingy_auth(email = "janedoe_work@gmail.com") ## ---- eval = FALSE------------------------------------------------------------ # gargle_oauth_sitrep() # #' gargle OAuth cache path: # #' /Users/janedoe/.R/gargle/gargle-oauth # #' # #' 14 tokens found # #' # #' 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... gargle/inst/doc/gargle-auth-in-client-package.html0000644000176200001440000013221414067637310021606 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 app 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) {
  cred <- gargle::token_fetch(
    scopes = scopes,
    app = drive_oauth_app() %||% <BUILT_IN_DEFAULT_APP>,
    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. 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::AuthClass to hold the auth state. Here’s how it is initialized in googledrive:

.auth <- gargle::init_AuthState(
  package     = "googledrive",
  auth_active = TRUE
  # app = NULL,
  # api_key = NULL,
  # cred = NULL
)

The OAuth app 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 app

Most users should present OAuth user credentials to Google APIs. However, most users can also be spared the fiddly details surrounding this. The OAuth app is one example. The app is a component that most users do not even know about and they are content to use the same app for all work through a client package: possibly, the app built into the package.

There is a field in the .auth auth state to hold the OAuth app. Exported auth helpers, drive_oauth_app() and drive_auth_configure(), retrieve and modify the current app to support users ready to take that level of control.

library(googledrive)

google_app <- httr::oauth_app(
  appname = "acme-corp",
  key     = "123456789.apps.googleusercontent.com",
  secret  = "abcdefghijklmnopqrstuvwxyz"
)
drive_auth_configure(app = google_app)

drive_oauth_app()
#> <oauth_app> acme-corp
#>   key:    123456789.apps.googleusercontent.com
#>   secret: <hidden>

Do not “borrow” an OAuth app (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.

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() and retrieve that value with drive_api_key(), just like the OAuth app. 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 app, 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 app 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() pre-emptively 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 behaviour. 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() for more about these options.

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. R/YOURPKG_auth.R is a good place to do this.
  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 <- .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() %||% <BUILT_IN_DEFAULT_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
  )
}

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 Application Default Credentials or a Google Cloud Engine flow. 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

sheets_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 app (client ID and secret) 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_app() returns .auth$app.
  • drive_api_key() returns .auth$key.
  • drive_auth_configure() can be used to configure auth. This is how an advanced user would enter their own OAuth app and API key into 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 – then 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.

BYOAK = Bring Your Own App and Key

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

The vignette How to get your own API credentials" describes how to an API key and OAuth app.

Packages that always send 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.html0000644000176200001440000004647614067637307016636 0ustar liggesusers Auth when using R in the browser

Auth when using R in the browser

If you are working with R in a web-based context, such as RStudio Server, RStudio Cloud, or RStudio Workbench, 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”. 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.

The need to use oob auth can sometimes be detected automatically. For example, oob auth is always used when the httpuv package is not installed. gargle also tries to detect usage via RStudio Server, Cloud, or Workbench, but this still may not catch 100% of situations where oob auth is necessary.

Therefore, some users may still need to recognize this situation and explicitly request oob auth.

Here’s a typical presentation of this problem: 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.

This is a sign that you need to explicitly request oob auth.

This article describes how to do so in a package that uses gargle for auth, which includes:

Request oob auth in the PKG_auth() call

These packages 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 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)

This code is tailored to an interactive session and assumes that a user is present to respond. If you also need to setup a token for non-interactive use, see the article Non-interactive auth. A key point is that oob auth is relevant to how you initially obtain a token. It is orthogonal to downstream use and refreshing. So it is possible that you need to attend to both!

Set the gargle_oob_default option

If you know that you always want to use oob auth, 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 that option has been set, it is honoured by downstream calls to PKG_auth(), explicit or implicit, because the default behaviour of use_oob is to consult the option:

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) {...}

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 httr 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 if you don’t have to? Here are ways to fix.

  • 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

Generating OAuth tokens for a server using httr covers some of the same ground, although for the httr package. gargle provides a Google-specific interface to httr. gargle first consults the gargle_oob_default option and, if that is undefined, also consults the httr_oob_default option.

If you’re creating content to be deployed (for example on shinyapps.io or RStudio Connect), you will also need to consider how the deployed content will authenticate non-interactively.

gargle/inst/doc/gargle-auth-in-client-package.R0000644000176200001440000001165614067637307021057 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) { # cred <- gargle::token_fetch( # scopes = scopes, # app = drive_oauth_app() %||% , # 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 <- gargle::init_AuthState( # package = "googledrive", # auth_active = TRUE # # app = NULL, # # api_key = NULL, # # cred = NULL # ) ## ---- eval = FALSE------------------------------------------------------------ # library(googledrive) # # google_app <- httr::oauth_app( # appname = "acme-corp", # key = "123456789.apps.googleusercontent.com", # secret = "abcdefghijklmnopqrstuvwxyz" # ) # drive_auth_configure(app = google_app) # # drive_oauth_app() # #> acme-corp # #> key: 123456789.apps.googleusercontent.com # #> secret: ## ---- 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 <- .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------------------------------------------------------------- # # 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 # # sheets_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.R0000644000176200001440000000522214067637311016620 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") gargle/inst/doc/get-api-credentials.Rmd0000644000176200001440000003332614067372466017550 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 $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 Developers 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 app or 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 Developers Console, in the target GCP Project, go to *APIs & Services > Credentials*. * Do *Create credentials > OAuth client ID*. * Select Application type "Desktop app". * You can capture the client ID and secret via clipboard right away. * At any time, you can navigate to a particular client ID and click "Download JSON". Two ways to package this info for use with httr or gargle, both of which require an object of class `httr::oauth_app`: 1. Use `httr::oauth_app()`. - The client ID goes in the `key` argument. - The client secret goes in the `secret` argument. 1. Use `gargle::oauth_app_from_json()`. - Provide the path to the downloaded JSON file. 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 `appname` argument to `httr::oauth_app()` or `gargle::oauth_app_from_json()`. Package maintainers might want to build this app 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 app (client ID and secret)? Package users could register this app 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) # method 1: direct provision client ID and secret google_app <- httr::oauth_app( "my-very-own-google-app", key = "123456789.apps.googleusercontent.com", secret = "abcdefghijklmnopqrstuvwxyz" ) drive_auth_configure(app = google_app) # method 2: provide filepath to JSON containing client ID and secret drive_auth_configure( path = "/path/to/the/JSON/you/downloaded/from/gcp/console.json" ) # now any new OAuth tokens are obtained with the configured app ``` ## 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 Developers 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: [How to use identity federation to access Google Cloud resources from Amazon Web Services (AWS)](https://cloud.google.com/iam/docs/access-resources-aws) ## 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.html0000644000176200001440000011121614067637311021110 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.Rmd0000644000176200001440000004171314067372466021377 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 app 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) { cred <- gargle::token_fetch( scopes = scopes, app = drive_oauth_app() %||% , 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. `token_fetch()` is described in the vignette [How gargle gets tokens](https://gargle.r-lib.org/articles/how-gargle-gets-tokens.html). 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::AuthClass` to hold the auth state. Here's how it is initialized in googledrive: ```{r eval = FALSE} .auth <- gargle::init_AuthState( package = "googledrive", auth_active = TRUE # app = NULL, # api_key = NULL, # cred = NULL ) ``` The OAuth `app` 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 app Most users should present OAuth user credentials to Google APIs. However, most users can also be spared the fiddly details surrounding this. The OAuth app is one example. The app is a component that most users do not even know about and they are content to use the same app for all work through a client package: possibly, the app built into the package. There is a field in the `.auth` auth state to hold the OAuth `app`. Exported auth helpers, `drive_oauth_app()` and `drive_auth_configure()`, retrieve and modify the current app to support users ready to take that level of control. ```{r, eval = FALSE} library(googledrive) google_app <- httr::oauth_app( appname = "acme-corp", key = "123456789.apps.googleusercontent.com", secret = "abcdefghijklmnopqrstuvwxyz" ) drive_auth_configure(app = google_app) drive_oauth_app() #> acme-corp #> key: 123456789.apps.googleusercontent.com #> secret: ``` Do not "borrow" an OAuth app (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. ### 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()` and retrieve that value with `drive_api_key()`, just like the OAuth app. 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 app, 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 app 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()` pre-emptively 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 behaviour. 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()` for more about these options. ## 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. `R/YOURPKG_auth.R` is a good place to do this. 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/master/R/drive_auth.R) and [`r-dbi/bigrquery/R/bq_auth.R`](https://github.com/r-dbi/bigrquery/blob/master/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 <- .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 ) } ``` `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](https://gargle.r-lib.org/articles/request-helper-functions.html) 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 Application Default Credentials or a Google Cloud Engine flow. 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 sheets_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 app (client ID and secret) 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/master/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_app()` returns `.auth$app`. * `drive_api_key()` returns `.auth$key`. * `drive_auth_configure()` can be used to configure auth. This is how an advanced user would enter their own OAuth app and API key into 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 -- then 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. ## BYOAK = Bring Your Own App and Key Advanced users can use their own OAuth app and API key. `drive_auth_configure()` lives in `R/drive_auth()` and it provides the ability to modify the current `app` and `api_key`. Recall that `drive_oauth_app()` and `drive_api_key()` also exist for targeted, read-only access. The vignette [How to get your own API credentials](https://gargle.r-lib.org/articles/get-api-credentials.html)" describes how to an API key and OAuth app. Packages that always send 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.Rmd0000644000176200001440000004471514067403577020226 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. `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 [How to use gargle for auth in a client package](https://gargle.r-lib.org/articles/gargle-auth-in-client-package.html). ```{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} 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. From now on, 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 option named "gargle_verbosity" to "debug". Read more in the docs for `gargle_verbosity()`. ## `credentials_service_account()` The first 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") # 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 second 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") # 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_service_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: [How to use identity federation to access Google Cloud resources from Amazon Web Services (AWS)](https://cloud.google.com/iam/docs/access-resources-aws) ## `credentials_app_default()` The third function tried is `credentials_app_default()`. Here's how a call to `token_fetch()` might work: ```{r, eval = FALSE} token_fetch(scopes = ) # credentials_service_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/production#providing_credentials_to_your_application). 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_service_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. ## `credentials_byo_oauth2()` The next function tried is `credentials_byo_oauth2()`. Here's how a call to `token_fetch()` might work: ```{r, eval = FALSE} token_fetch(token = ) # credentials_service_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_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_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_service_account() fails because no `path`, # credentials_app_default() fails because no ADC found, # credentials_gce() fails because not on GCE, # credentials_byo_oauth2() fails because no `token`, # 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`, and `package` are generally provided by the API wrapper function that is mediating the calls to `token_fetch()`. Do not "borrow" an OAuth app (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. 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 [How to use gargle for auth in a client package](https://gargle.r-lib.org/articles/gargle-auth-in-client-package.html) 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 app, 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 app. 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. Select a pre-authorised account or enter '0' to obtain a new token. Press Esc/Ctrl + C to abort. 1: janedoe_personal@gmail.com 2: janedoe@example.com 3: janedoe_work@gmail.com Selection: 3 ``` If none of the tokens has the right scopes and app (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() #' gargle OAuth cache path: #' /Users/janedoe/.R/gargle/gargle-oauth #' #' 14 tokens found #' #' 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... ``` gargle/inst/doc/get-api-credentials.html0000644000176200001440000007212114067637310017756 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 $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 Developers 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 app or 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 Developers Console, in the target GCP Project, go to APIs & Services > Credentials.
  • Do Create credentials > OAuth client ID.
  • Select Application type “Desktop app”.
  • You can capture the client ID and secret via clipboard right away.
  • At any time, you can navigate to a particular client ID and click “Download JSON”.

Two ways to package this info for use with httr or gargle, both of which require an object of class httr::oauth_app:

  1. Use httr::oauth_app().
    • The client ID goes in the key argument.
    • The client secret goes in the secret argument.
  2. Use gargle::oauth_app_from_json().
    • Provide the path to the downloaded JSON file.

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 appname argument to httr::oauth_app() or gargle::oauth_app_from_json().

Package maintainers might want to build this app 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 app (client ID and secret)?

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

library(googledrive)

# method 1: direct provision client ID and secret
google_app <- httr::oauth_app(
  "my-very-own-google-app",
  key = "123456789.apps.googleusercontent.com",
  secret = "abcdefghijklmnopqrstuvwxyz"
)
drive_auth_configure(app = google_app)

# method 2: provide filepath to JSON containing client ID and secret
drive_auth_configure(
  path = "/path/to/the/JSON/you/downloaded/from/gcp/console.json"
)

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

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 Developers 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/troubleshooting.Rmd0000644000176200001440000002052014067372466017146 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. ```{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 app or 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. ### Credential rolling Many users of packages like googlesheets4 or googledrive tacitly rely on the default OAuth app used by those packages. Periodically the maintainer of such a package will need to roll the app, i.e. create a new OAuth app and disable the old one. This will make it impossible to refresh existing tokens, made with the old, disabled app. Those tokens will stop working. *In gargle v1.0.0, in March 2021, we rolled the app used in googlesheets4, googledrive, and bigrquery. At some point in 2021, we plan to disable the old app. Anyone relying on the default app 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 app 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"} 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. Consider using your own OAuth app to eliminate your exposure to a third-party deciding to roll their app. 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. gargle/inst/doc/auth-from-web.R0000644000176200001440000000160614067637307016055 0ustar liggesusers## ---- include = FALSE--------------------------------------------------------- knitr::opts_chunk$set( collapse = TRUE, comment = "#>" ) ## ----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------------------------------------------------------------ # 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) {...} gargle/inst/doc/how-gargle-gets-tokens.html0000644000176200001440000013653414067637310020443 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.

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 How to use gargle for auth in a 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:

names(cred_funs_list())
#> [1] "credentials_service_account"  "credentials_external_account"
#> [3] "credentials_app_default"      "credentials_gce"             
#> [5] "credentials_byo_oauth2"       "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.

From now on, 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 option named “gargle_verbosity” to “debug”. Read more in the docs for gargle_verbosity().

credentials_service_account()

The first 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")

# 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 second 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")

# 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_service_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 third function tried is credentials_app_default(). Here’s how a call to token_fetch() might work:

token_fetch(scopes = <SCOPES>)

# credentials_service_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_service_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.

credentials_byo_oauth2()

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

token_fetch(token = <TOKEN2.0>)

# credentials_service_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_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_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_service_account() fails because no `path`,
# credentials_app_default() fails because no ADC found,
# credentials_gce() fails because not on GCE,
# credentials_byo_oauth2() fails because no `token`,
# 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, and package are generally provided by the API wrapper function that is mediating the calls to token_fetch(). Do not “borrow” an OAuth app (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.

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 How to use gargle for auth in a 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 app, 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 app. 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. Select a
pre-authorised account or enter '0' to obtain a new token. Press Esc/Ctrl + C to
abort.

1: janedoe_personal@gmail.com
2: janedoe@example.com
3: janedoe_work@gmail.com

Selection: 3

If none of the tokens has the right scopes and app (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()
#' gargle OAuth cache path:
#' /Users/janedoe/.R/gargle/gargle-oauth
#' 
#' 14 tokens found
#' 
#' 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...
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/WORDLIST0000644000176200001440000000155014067405264013673 0ustar liggesusersAPI's AppVeyor Auth AuthState BYOAK CLI CMD Codecov De DropBox FieldMask Filepath GCE GCP Gmail IAM JSON Keyless OAuth ORCID OpenID Opnieuw Opnieuw's PID SDK Testthat Tidyverse UI VMs WifToken YAML api apis appveyor auth authed automagically aws backoff backtrace behaviour bigrquery cancelled ci cli cloneable config cran cron crypto customise cyphr datetime dbi de decrypt decrypted ec emptively filepath funder gargle’ gcalendr github gmailr googledrive googledrive's googlesheets honoured https httpuv httr httr's inlining io iteratively jitter json keyless macOS misconfigured mockr mortem novo oauth oob pkgdown pre prepending programmatically rappdirs refreshable repo repo's repos rlang ropensci roxygen rstudioapi shinyapps subclasses symlink targetted targetting testthat thingyr tidyverse travis unencrypted urlencoded useR useRs userinfo webserver withr www