Skip to content

Auth in github actions #290

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion DESCRIPTION
Original file line number Diff line number Diff line change
Expand Up @@ -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
2 changes: 2 additions & 0 deletions NAMESPACE
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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)
Expand Down
32 changes: 19 additions & 13 deletions R/credentials_external_account.R
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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"]]
)
}

Expand Down Expand Up @@ -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)
)
Expand Down
130 changes: 130 additions & 0 deletions R/credentials_github_actions.R
Original file line number Diff line number Diff line change
@@ -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.

#' * <https://github.com/google-github-actions/auth?tab=readme-ov-file#indirect-wif>

#' @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 = "[email protected]",
#' 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
}
1 change: 1 addition & 0 deletions man/credentials_app_default.Rd

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions man/credentials_byo_oauth2.Rd

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions man/credentials_external_account.Rd

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions man/credentials_gce.Rd

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

62 changes: 62 additions & 0 deletions man/credentials_github_actions.Rd

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions man/credentials_service_account.Rd

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions man/credentials_user_oauth2.Rd

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

33 changes: 33 additions & 0 deletions man/oauth_gha_token.Rd

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions man/token_fetch.Rd

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading