diff --git a/DESCRIPTION b/DESCRIPTION index f96bbab2..908a9b26 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -49,4 +49,4 @@ Config/testthat/edition: 3 Encoding: UTF-8 Language: en-US Roxygen: list(markdown = TRUE) -RoxygenNote: 7.2.3 +RoxygenNote: 7.3.2 diff --git a/NAMESPACE b/NAMESPACE index 02ac4e4f..66ca8be6 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -22,6 +22,7 @@ export(credentials_app_default) export(credentials_byo_oauth2) export(credentials_external_account) export(credentials_gce) +export(credentials_github_actions) export(credentials_service_account) export(credentials_user_oauth2) export(field_mask) @@ -46,6 +47,7 @@ export(local_cred_funs) export(local_gargle_verbosity) export(oauth_app_from_json) export(oauth_external_token) +export(oauth_gha_token) export(request_build) export(request_develop) export(request_make) diff --git a/R/credentials_external_account.R b/R/credentials_external_account.R index 89cdf84e..5e846426 100644 --- a/R/credentials_external_account.R +++ b/R/credentials_external_account.R @@ -228,17 +228,21 @@ detect_aws_ec2 <- function() { } 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.") + if (params$github_actions) { + serialized_subject_token <- gha_subject_token(params) + } else { + 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) } - 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, @@ -248,7 +252,8 @@ init_oauth_external_account <- function(params) { fetch_wif_access_token( federated_access_token, impersonation_url = params[["service_account_impersonation_url"]], - scope = params[["scope"]] + scope = params[["scope"]], + lifetime = params[["lifetime"]] ) } @@ -378,13 +383,14 @@ fetch_federated_access_token <- function(params, # 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") { + scope = "https://www.googleapis.com/auth/cloud-platform", + lifetime = "3600s") { 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), + body = list(scope = scope, lifetime = lifetime), token = httr::add_headers( Authorization = paste("Bearer", federated_access_token$access_token) ) diff --git a/R/credentials_github_actions.R b/R/credentials_github_actions.R new file mode 100644 index 00000000..def4cff0 --- /dev/null +++ b/R/credentials_github_actions.R @@ -0,0 +1,130 @@ +#' Get a token using Github Actions +#' +#' @description + +#' `r lifecycle::badge('experimental')` +#' +#' @inheritParams token_fetch + +#' @param project_id The google cloud project id +#' @param workload_identity_provider The workload identity provider +#' @param service_account The service account email address +#' @param lifetime Lifespan of token in seconds as a string `"300s"` +#' @param scopes Requested scopes for the access token +#' + +#' @seealso There is some setup required in GCP to enable this auth flow. +#' This function reimplements the `google-github-actions/auth`. The +#' documentation for that workflow provides instructions on the setup steps. + +#' * + +#' @return A [WifToken()] or `NULL`. +#' @family credential functions +#' @export +#' @examples +#' \dontrun{ +#' credentials_github_actions( +#' project_id = "project-id-12345", +#' workload_identity_provider = "projects/123456789/locations/global/workloadIdentityPools/my-pool/providers/my-provider" +#' service_account = "my-service-account@my-project.iam.gserviceaccount.com", +#' scopes = "https://www.googleapis.com/auth/drive.file" +#' ) +#' } +credentials_github_actions <- function( + project_id, + workload_identity_provider, + service_account, + lifetime = "300s", + scopes = "https://www.googleapis.com/auth/drive.file", + ...) { + gargle_debug("trying {.fun credentials_github_actions}") + if (!detect_github_actions() || is.null(scopes)) { + return(NULL) + } + + scopes <- normalize_scopes(add_email_scope(scopes)) + + token <- oauth_gha_token( + project_id = project_id, + workload_identity_provider = workload_identity_provider, + service_account = service_account, + lifetime = lifetime, + 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 on Github Actions +#' +#' @inheritParams credentials_github_actions +#' @param universe Set the domain for the endpoints +#' +#' @keywords internal +#' @export +oauth_gha_token <- function(project_id, + workload_identity_provider, + service_account, + lifetime, + scopes = "https://www.googleapis.com/auth/drive.file", + id_token_url = Sys.getenv("ACTIONS_ID_TOKEN_REQUEST_URL"), + id_token_request_token = Sys.getenv("ACTIONS_ID_TOKEN_REQUEST_TOKEN")) { + if (id_token_url == "" || id_token_request_token == "") { + gargle_abort(paste0( + "GitHub Actions did not inject $ACTIONS_ID_TOKEN_REQUEST_TOKEN or ", + "$ACTIONS_ID_TOKEN_REQUEST_URL into this job. This most likely means the ", + "GitHub Actions workflow permissions are incorrect, or this job is being ", + "run from a fork. For more information, please see ", + "https://docs.github.com/en/actions/security-guides/automatic-token-authentication#permissions-for-the-github_token" + )) + } + + params <- list( + scopes = scopes, # this is $scopes but WifToken$new() copies it to $scope + lifetime = lifetime, + id_token_url = id_token_url, + id_token_request_token = id_token_request_token, + github_actions = TRUE, + token_url = "https://sts.googleapis.com/v1/token", + audience = paste0("//iam.googleapis.com/", workload_identity_provider), + oidc_token_audience = paste0("https://iam.googleapis.com/", workload_identity_provider), + subject_token_type = "urn:ietf:params:oauth:token-type:jwt", + service_account_impersonation_url = paste0( + "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/", + service_account, + ":generateAccessToken" + ), + as_header = TRUE + ) + WifToken$new(params = params) +} + + +detect_github_actions <- function() { + if (Sys.getenv("GITHUB_ACTIONS") == "true") { + return(TRUE) + } + gargle_debug("Environment variable GITHUB_ACTIONS is not 'true'") + FALSE +} + +gha_subject_token <- function(params) { + gargle_debug("gha_subject_token") + + req <- list( + method = "GET", + url = params[["id_token_url"]], + token = httr::add_headers(Authorization = paste("Bearer", params$id_token_request_token)) + ) + query_audience <- list(audience = params$oidc_token_audience) + resp <- request_make(req, query = query_audience) + response_process(resp)$value +} diff --git a/man/credentials_app_default.Rd b/man/credentials_app_default.Rd index 3e180bfc..fb731cf9 100644 --- a/man/credentials_app_default.Rd +++ b/man/credentials_app_default.Rd @@ -71,6 +71,7 @@ Other credential functions: \code{\link{credentials_byo_oauth2}()}, \code{\link{credentials_external_account}()}, \code{\link{credentials_gce}()}, +\code{\link{credentials_github_actions}()}, \code{\link{credentials_service_account}()}, \code{\link{credentials_user_oauth2}()}, \code{\link{token_fetch}()} diff --git a/man/credentials_byo_oauth2.Rd b/man/credentials_byo_oauth2.Rd index 77d93e61..0eab0dad 100644 --- a/man/credentials_byo_oauth2.Rd +++ b/man/credentials_byo_oauth2.Rd @@ -71,6 +71,7 @@ Other credential functions: \code{\link{credentials_app_default}()}, \code{\link{credentials_external_account}()}, \code{\link{credentials_gce}()}, +\code{\link{credentials_github_actions}()}, \code{\link{credentials_service_account}()}, \code{\link{credentials_user_oauth2}()}, \code{\link{token_fetch}()} diff --git a/man/credentials_external_account.Rd b/man/credentials_external_account.Rd index 0c454e6c..596369d0 100644 --- a/man/credentials_external_account.Rd +++ b/man/credentials_external_account.Rd @@ -82,6 +82,7 @@ Other credential functions: \code{\link{credentials_app_default}()}, \code{\link{credentials_byo_oauth2}()}, \code{\link{credentials_gce}()}, +\code{\link{credentials_github_actions}()}, \code{\link{credentials_service_account}()}, \code{\link{credentials_user_oauth2}()}, \code{\link{token_fetch}()} diff --git a/man/credentials_gce.Rd b/man/credentials_gce.Rd index db8db8d3..fa20171c 100644 --- a/man/credentials_gce.Rd +++ b/man/credentials_gce.Rd @@ -97,6 +97,7 @@ Other credential functions: \code{\link{credentials_app_default}()}, \code{\link{credentials_byo_oauth2}()}, \code{\link{credentials_external_account}()}, +\code{\link{credentials_github_actions}()}, \code{\link{credentials_service_account}()}, \code{\link{credentials_user_oauth2}()}, \code{\link{token_fetch}()} diff --git a/man/credentials_github_actions.Rd b/man/credentials_github_actions.Rd new file mode 100644 index 00000000..37c5cdc5 --- /dev/null +++ b/man/credentials_github_actions.Rd @@ -0,0 +1,62 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/credentials_github_actions.R +\name{credentials_github_actions} +\alias{credentials_github_actions} +\title{Get a token using Github Actions} +\usage{ +credentials_github_actions( + project_id, + workload_identity_provider, + service_account, + lifetime = "300s", + scopes = "https://www.googleapis.com/auth/drive.file", + ... +) +} +\arguments{ +\item{project_id}{The google cloud project id} + +\item{workload_identity_provider}{The workload identity provider} + +\item{service_account}{The service account email address} + +\item{lifetime}{Lifespan of token in seconds as a string \code{"300s"}} + +\item{scopes}{Requested scopes for the access token} + +\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]}} +} +\examples{ +\dontrun{ +credentials_github_actions( + project_id = "project-id-12345", + workload_identity_provider = "projects/123456789/locations/global/workloadIdentityPools/my-pool/providers/my-provider" + service_account = "my-service-account@my-project.iam.gserviceaccount.com", + scopes = "https://www.googleapis.com/auth/drive.file" +) +} +} +\seealso{ +There is some setup required in GCP to enable this auth flow. +This function reimplements the \code{google-github-actions/auth}. The +documentation for that workflow provides instructions on the setup steps. +\itemize{ +\item \url{https://github.com/google-github-actions/auth?tab=readme-ov-file#indirect-wif} +} + +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}()}, +\code{\link{token_fetch}()} +} +\concept{credential functions} diff --git a/man/credentials_service_account.Rd b/man/credentials_service_account.Rd index ce766b2a..1f673ec4 100644 --- a/man/credentials_service_account.Rd +++ b/man/credentials_service_account.Rd @@ -62,6 +62,7 @@ Other credential functions: \code{\link{credentials_byo_oauth2}()}, \code{\link{credentials_external_account}()}, \code{\link{credentials_gce}()}, +\code{\link{credentials_github_actions}()}, \code{\link{credentials_user_oauth2}()}, \code{\link{token_fetch}()} } diff --git a/man/credentials_user_oauth2.Rd b/man/credentials_user_oauth2.Rd index 8dd068fe..0c3509c6 100644 --- a/man/credentials_user_oauth2.Rd +++ b/man/credentials_user_oauth2.Rd @@ -117,6 +117,7 @@ Other credential functions: \code{\link{credentials_byo_oauth2}()}, \code{\link{credentials_external_account}()}, \code{\link{credentials_gce}()}, +\code{\link{credentials_github_actions}()}, \code{\link{credentials_service_account}()}, \code{\link{token_fetch}()} } diff --git a/man/oauth_gha_token.Rd b/man/oauth_gha_token.Rd new file mode 100644 index 00000000..9e3d90e8 --- /dev/null +++ b/man/oauth_gha_token.Rd @@ -0,0 +1,33 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/credentials_github_actions.R +\name{oauth_gha_token} +\alias{oauth_gha_token} +\title{Generate OAuth token for an external account on Github Actions} +\usage{ +oauth_gha_token( + project_id, + workload_identity_provider, + service_account, + lifetime, + scopes = "https://www.googleapis.com/auth/drive.file", + id_token_url = Sys.getenv("ACTIONS_ID_TOKEN_REQUEST_URL"), + id_token_request_token = Sys.getenv("ACTIONS_ID_TOKEN_REQUEST_TOKEN") +) +} +\arguments{ +\item{project_id}{The google cloud project id} + +\item{workload_identity_provider}{The workload identity provider} + +\item{service_account}{The service account email address} + +\item{lifetime}{Lifespan of token in seconds as a string \code{"300s"}} + +\item{scopes}{Requested scopes for the access token} + +\item{universe}{Set the domain for the endpoints} +} +\description{ +Generate OAuth token for an external account on Github Actions +} +\keyword{internal} diff --git a/man/token_fetch.Rd b/man/token_fetch.Rd index eb5cc073..1dbb60ed 100644 --- a/man/token_fetch.Rd +++ b/man/token_fetch.Rd @@ -44,6 +44,7 @@ Other credential functions: \code{\link{credentials_byo_oauth2}()}, \code{\link{credentials_external_account}()}, \code{\link{credentials_gce}()}, +\code{\link{credentials_github_actions}()}, \code{\link{credentials_service_account}()}, \code{\link{credentials_user_oauth2}()} }