R/credential_providers.R

Defines functions no_credentials iam_credentials_provider get_container_credentials_eks get_container_credentials container_credentials_provider get_assume_role_with_web_identity_creds get_assumed_role_creds get_creds_from_sts_resp get_token_code config_file_source_profile sso_credential_process config_file_credential_source config_file_credential_process config_file_provider credentials_file_provider env_provider anonymous_provider

#' @include net.R
#' @include credential_sts.R
#' @include credential_sso.R
#' @include dateutil.R
#' @include iniutil.R
#' @include logging.R
#' @include util.R
NULL

Creds <- struct(
  access_key_id = "",
  secret_access_key = "",
  session_token = "",
  access_token = "",
  expiration = Inf,
  provider_name = ""
)

# Set anonymous credentials
anonymous_provider <- function(anonymous) {
  if (!anonymous) {
    return(NULL)
  }
  return(Creds())
}

# Retrieve credentials stored in R or OS environment variables.
env_provider <- function() {
  access_key_id <- get_env("AWS_ACCESS_KEY_ID")
  secret_access_key <- get_env("AWS_SECRET_ACCESS_KEY")
  session_token <- get_env("AWS_SESSION_TOKEN")
  expiration <- as_timestamp(get_env("AWS_CREDENTIAL_EXPIRATION"), "iso8601")
  if (length(expiration) == 0) expiration <- Inf

  if (access_key_id != "" && secret_access_key != "") {
    creds <- Creds(
      access_key_id = access_key_id,
      secret_access_key = secret_access_key,
      session_token = session_token,
      expiration = expiration
    )
  } else {
    creds <- NULL
  }
  return(creds)
}

# Retrieve credentials stored in credentials file.
credentials_file_provider <- function(profile = "") {
  credentials_path <- get_credentials_file_path()
  if (is.null(credentials_path)) {
    log_info("Unable to locate credentials file")
    return(NULL)
  }

  aws_profile <- get_profile_name(profile)

  credentials <- read_ini(credentials_path)

  if (is.null(credentials[[aws_profile]])) {
    log_info("Profile '%s' not found in '%s'", aws_profile, credentials_path)
    return(NULL)
  }

  access_key_id <- credentials[[aws_profile]]$aws_access_key_id
  secret_access_key <- credentials[[aws_profile]]$aws_secret_access_key
  session_token <- credentials[[aws_profile]]$aws_session_token

  if (is.null(access_key_id) || is.null(secret_access_key)) {
    log_info(
      "Unable to locate aws_access_key_id or aws_secret_access_key in credentials file profile '%s'.",
      aws_profile
    )
    return(NULL)
  }

  if (is.null(session_token)) {
    session_token <- ""
  }

  if (access_key_id != "" && secret_access_key != "") {
    creds <- Creds(
      access_key_id = access_key_id,
      secret_access_key = secret_access_key,
      session_token = session_token,
      expiration = Inf
    )
  } else {
    log_info("Unable to get credentials from credentials file.")
    creds <- NULL
  }
  return(creds)
}

# Get credentials that are specified by an item in the AWS config file.
config_file_provider <- function(profile = "") {
  config_path <- get_config_file_path()
  if (is.null(config_path)) {
    log_info("Unable to locate config file")
    return(NULL)
  }

  config <- read_ini(config_path)

  profile_name <- get_profile_name(profile)
  if (profile_name != "default") profile_name <- paste("profile", profile_name)
  if (is.null(config[[profile_name]])) {
    log_info("Profile '%s' not found in '%s'", profile_name, config_path)
    return(NULL)
  }
  profile <- config[[profile_name]]

  if ("credential_process" %in% names(profile)) {
    creds <- config_file_credential_process(profile$credential_process)
    if (!is.null(creds)) {
      return(creds)
    }
  }

  if ("sso_role_name" %in% names(profile)) {
    sso_role_name <- profile$sso_role_name
    sso_account_id <- profile$sso_account_id
    sso_session <- NULL
    if ("sso_session" %in% names(profile)) {
      sso_session <- profile$sso_session
      sso_session_profile_name <- paste("sso-session", profile$sso_session)
      sso_session_profile <- config[[sso_session_profile_name]]
      sso_start_url <- sso_session_profile$sso_start_url
      sso_region <- sso_session_profile$sso_region
    } else {
      sso_start_url <- profile$sso_start_url
      sso_region <- profile$sso_region
    }
    creds <- sso_credential_process(
      sso_session,
      sso_start_url,
      sso_account_id,
      sso_region,
      sso_role_name
    )
    if (!is.null(creds)) {
      return(creds)
    }
  }
  profile_nms <- names(profile)

  if ("role_arn" %in% profile_nms) {
    role_arn <- profile$role_arn
    role_session_name <- profile$role_session_name
    if (is.null(role_session_name)) {
      sys <- Sys.info()
      role_session_name <- digest::digest(paste0(sys["user"], sys["nodename"]))
    }
    mfa_serial <- profile$mfa_serial

    if ("credential_source" %in% profile_nms) {
      credential_source <- profile$credential_source
      creds <- config_file_credential_source(role_arn, role_session_name, mfa_serial, credential_source)
      if (!is.null(creds)) {
        return(creds)
      }
    }
    if ("source_profile" %in% profile_nms) {
      source_profile <- profile$source_profile
      creds <- config_file_source_profile(role_arn, role_session_name, mfa_serial, source_profile)
      if (!is.null(creds)) {
        return(creds)
      }
    }

    # Get web_identity_token
    # https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-role.html#cli-configure-role-oidc
    if ("web_identity_token_file" %in% profile_nms) {
      web_identity_token_file <- profile$web_identity_token_file
      web_identity_token <- get_web_identity_token(web_identity_token_file)
      creds <- get_assume_role_with_web_identity_creds(role_arn, role_session_name, web_identity_token)
      if (!is.null(creds)) {
        return(creds)
      }
    }
  }
  log_info("Unable to get credentials from config file.")
  return(NULL)
}

# Get credentials by running a process specified in `command`.
config_file_credential_process <- function(command) {
  output <- system(command, intern = TRUE)
  data <- jsonlite::fromJSON(output)

  if (data$Version != 1) {
    return(NULL)
  }

  access_key_id <- data$AccessKeyId
  secret_access_key <- data$SecretAccessKey
  if (is.null(access_key_id) || access_key_id == "" ||
    is.null(secret_access_key) || secret_access_key == "") {
    return(NULL)
  }

  session_token <- data$SessionToken
  if (is.null(session_token)) session_token <- ""

  expiration <- as_timestamp(data$Expiration, "iso8601")
  if (length(expiration) == 0) expiration <- Inf

  creds <- Creds(
    access_key_id = access_key_id,
    secret_access_key = secret_access_key,
    session_token = session_token,
    expiration = expiration
  )
  return(creds)
}

# Get the `role_arn`'s temporary credentials given a `credential_source`,
# either "Environment", "Ec2InstanceMetadata", or "EcsContainer".
# See https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-role.html
config_file_credential_source <- function(role_arn, role_session_name, mfa_serial, credential_source) {
  if (credential_source == "Environment") {
    creds <- env_provider()
  } else if (credential_source == "Ec2InstanceMetadata") {
    creds <- iam_credentials_provider()
  } else if (credential_source == "EcsContainer") {
    creds <- container_credentials_provider()
  }
  if (is.null(creds)) {
    return(NULL)
  }
  role_creds <- get_assumed_role_creds(role_arn, role_session_name, mfa_serial, creds)
  return(role_creds)
}

# Get credentials from profile associated with an SSO login.  Assumes
# the user has already logged in via e.g. the aws cli so that a cached
# access token is available.
sso_credential_process <- function(sso_session,
                                   sso_start_url,
                                   sso_account_id,
                                   sso_region,
                                   sso_role_name) {
  input_str <- sso_session %||% sso_start_url
  cache_key <- digest::digest(enc2utf8(input_str), algo = "sha1", serialize = FALSE)
  json_file <- paste0(cache_key, ".json")
  root <- ifelse(
    Sys.info()[[1]] == "Windows",
    file.path(Sys.getenv("HOMEDRIVE"), Sys.getenv("HOMEPATH")),
    "~"
  )
  sso_cache <- file.path(root, ".aws", "sso", "cache", json_file)
  if (!file.exists(sso_cache)) {
    stopf(
      "Error loading SSO Token: Token for %s does not exist",
      input_str
    )
  }
  cache_creds <- jsonlite::fromJSON(sso_cache)
  if (!("accessToken" %in% names(cache_creds)) || !("expiresAt" %in% names(cache_creds))) {
    stopf(
      "Error loading SSO Token: Token for %s is invalid.",
      sso_start_url
    )
  }
  svc <- sso(
    config = list(
      credentials = list(
        creds = list(
          access_token = cache_creds$accessToken
        )
      ),
      region = sso_region,
      endpoint = "",
      close_connection = FALSE,
      disable_rest_protocol_uri_cleaning = TRUE
    )
  )
  resp <- svc$get_role_credentials(sso_role_name, sso_account_id, cache_creds$accessToken)
  if (is.null(resp)) {
    return(NULL)
  }
  roleCredentials <- resp$roleCredentials
  creds <- Creds(
    access_key_id = roleCredentials$accessKeyId,
    secret_access_key = roleCredentials$secretAccessKey,
    session_token = roleCredentials$sessionToken,
    expiration = roleCredentials$expiration
  )
  return(creds)
}

# Get STS temporary credentials for the role with ARN `role_arn` using
# credentials found in profile named `source_profile`.
# See https://docs.aws.amazon.com/credref/latest/refdocs/setting-global-source_profile.html.
config_file_source_profile <- function(role_arn, role_session_name, mfa_serial, source_profile) {
  creds <- credentials_file_provider(source_profile)
  if (is.null(creds)) creds <- config_file_provider(source_profile)
  if (is.null(creds)) {
    return(NULL)
  }
  role_creds <- get_assumed_role_creds(role_arn, role_session_name, mfa_serial, creds)
  return(role_creds)
}

# Get the user's MFA token code from a prompt.
# Use an RStudio prompt if running in RStudio.
# Otherwise use a text prompt in the console.
get_token_code <- function() {
  if (requireNamespace("rstudioapi", quietly = TRUE) && rstudioapi::isAvailable()) {
    token_code <- rstudioapi::showPrompt("MFA", "Enter MFA token code")
  } else {
    token_code <- readline("Enter MFA token code: ")
  }
  return(token_code)
}

get_creds_from_sts_resp <- function(resp) {
  role_creds <- Creds(
    access_key_id = resp$Credentials$AccessKeyId,
    secret_access_key = resp$Credentials$SecretAccessKey,
    session_token = resp$Credentials$SessionToken,
    expiration = as_timestamp(resp$Credentials$Expiration, "iso8601")
  )
  return(role_creds)
}

# Get STS credentials for AssumeRole `role_arn`, using credentials in `creds`.
# If the role requires MFA, the MFA device's serial number must be provided in
# `mfa_serial`, and the user will be prompted interactively to provide the
# current MFA token code.
get_assumed_role_creds <- function(role_arn, role_session_name, mfa_serial, creds) {
  svc <- sts(config = list(credentials = list(creds = creds)))
  if (is.null(mfa_serial) || mfa_serial == "") {
    resp <- svc$assume_role(
      RoleArn = role_arn,
      RoleSessionName = role_session_name
    )
  } else {
    token_code <- get_token_code()
    resp <- svc$assume_role(
      RoleArn = role_arn,
      RoleSessionName = role_session_name,
      SerialNumber = mfa_serial,
      TokenCode = token_code
    )
  }
  if (is.null(resp)) {
    return(NULL)
  }
  role_creds <- get_creds_from_sts_resp(resp)
  return(role_creds)
}

# Get STS credentials for AssumeRoleWithWebIdentity
get_assume_role_with_web_identity_creds <- function(role_arn, role_session_name, web_identity_token) {
  svc <- sts(config = list(credentials = list(anonymous = TRUE)))

  resp <- svc$assume_role_with_web_identity(
    RoleArn = role_arn,
    RoleSessionName = role_session_name,
    WebIdentityToken = web_identity_token
  )

  if (is.null(resp)) {
    return(NULL)
  }
  role_creds <- get_creds_from_sts_resp(resp)
  return(role_creds)
}

# Retrieve container job role credentials
container_credentials_provider <- function() {
  # Initialize to NULL
  credentials_response <- NULL

  container_credentials_uri <- get_env("AWS_CONTAINER_CREDENTIALS_RELATIVE_URI")
  container_credentials_token <- get_env("AWS_WEB_IDENTITY_TOKEN_FILE")

  # Look for job role credentials first, then web identity token file
  if (container_credentials_uri != "") {
    credentials_response <- get_container_credentials()
  } else if (container_credentials_token != "") {
    credentials_response <- get_container_credentials_eks()
  }

  access_key_id <- credentials_response$access_key_id
  secret_access_key <- credentials_response$secret_access_key
  session_token <- credentials_response$session_token
  expiration <- credentials_response$expiration

  # return credential
  if (is.null(access_key_id) || is.null(secret_access_key) ||
    is.null(session_token)) {
    log_info(
      "Unable to obtain access_key_id, secret_access_key or session_token"
    )
    return(NULL)
  }

  if (access_key_id != "" && secret_access_key != "" &&
    session_token != "") {
    creds <- Creds(
      access_key_id = access_key_id,
      secret_access_key = secret_access_key,
      session_token = session_token,
      expiration = expiration
    )
  } else {
    log_info("Unable to obtain credentials from container.")
    creds <- NULL
  }
  return(creds)
}

# Gets the job role credentials by making an http request
get_container_credentials <- function() {
  credentials_uri <- get_env("AWS_CONTAINER_CREDENTIALS_RELATIVE_URI")
  if (nchar(credentials_uri) == 0) {
    return(NULL)
  }

  metadata_url <- file.path("http://169.254.170.2", credentials_uri)
  metadata_request <-
    new_http_request("GET", metadata_url, timeout = 1)

  metadata_response <- tryCatch(
    {
      issue(metadata_request)
    },
    error = function(e) {
      NULL
    }
  )

  if (is.null(metadata_response) || metadata_response$status_code != 200) {
    return(NULL)
  }

  credentials_response_body <-
    jsonlite::fromJSON(raw_to_utf8(metadata_response$body))

  credentials_list <- list(
    access_key_id = credentials_response_body$AccessKeyId,
    secret_access_key = credentials_response_body$SecretAccessKey,
    session_token = credentials_response_body$Token,
    expiration = as_timestamp(credentials_response_body$Expiration, "iso8601")
  )

  return(credentials_list)
}

get_container_credentials_eks <- function() {
  credentials_list <- get_assume_role_with_web_identity_creds(
    role_arn = get_role_arn(),
    role_session_name = get_role_session_name(),
    web_identity_token = get_web_identity_token()
  )

  return(credentials_list)
}

# Retrieve credentials for EC2 IAM Role
iam_credentials_provider <- function() {
  iam_role <- get_iam_role()
  if (is.null(iam_role)) {
    log_info("Unable to obtain iam role")
    return(NULL)
  }

  credentials_url <- file.path("iam/security-credentials", iam_role)

  credentials_response <- get_instance_metadata(credentials_url)

  if (is.null(credentials_response)) {
    log_info("Unable credentials from iam role")
    return(NULL)
  }

  credentials_response_body <- jsonlite::fromJSON(raw_to_utf8(credentials_response$body))

  access_key_id <- credentials_response_body$AccessKeyId
  secret_access_key <- credentials_response_body$SecretAccessKey
  session_token <- credentials_response_body$Token
  expiration <- as_timestamp(credentials_response_body$Expiration, "iso8601")

  if (is.null(access_key_id) || is.null(secret_access_key) ||
    is.null(session_token)) {
    log_info(
      "Unable to obtain access_key_id, secret_access_key or session_token"
    )
    return(NULL)
  }

  if (access_key_id != "" && secret_access_key != "" &&
    session_token != "") {
    creds <- Creds(
      access_key_id = access_key_id,
      secret_access_key = secret_access_key,
      session_token = session_token,
      expiration = expiration
    )
  } else {
    log_info("Unable to obtain credentials from iam role.")
    creds <- NULL
  }
  return(creds)
}

no_credentials <- function() {
  message <- (
    if (isTRUE(getOption("paws.log_level") <= 2L)) {
      'No compatible credentials provided. Use `options("paws.log_level" = 3L)` for more information.'
    } else {
      "No compatible credentials provided."
    })
  stop(message, call. = FALSE)
}

Try the paws.common package in your browser

Any scripts or data that you put into this service are public.

paws.common documentation built on Nov. 12, 2023, 1:08 a.m.