diff --git a/DESCRIPTION b/DESCRIPTION index 43c351b..da30f8f 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -14,7 +14,7 @@ Description: Platform independent 'API' to access the operating system's License: MIT + file LICENSE URL: https://r-lib.github.io/keyring/index.html, https://github.com/r-lib/keyring#readme BugReports: https://github.com/r-lib/keyring/issues -RoxygenNote: 7.1.2 +RoxygenNote: 7.2.1 Roxygen: list(markdown = TRUE, r6 = FALSE) Imports: assertthat, @@ -31,6 +31,7 @@ Suggests: callr, covr, mockery, + paws, testthat, withr Encoding: UTF-8 @@ -40,6 +41,7 @@ Collate: 'api.R' 'assertions.R' 'backend-class.R' + 'backend-awssecretsmanager.R' 'backend-env.R' 'backend-file.R' 'backend-macos.R' diff --git a/NAMESPACE b/NAMESPACE index 2145751..15d85dc 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -1,6 +1,7 @@ # Generated by roxygen2: do not edit by hand export(backend) +export(backend_awssecretsmanager) export(backend_env) export(backend_file) export(backend_keyrings) diff --git a/R/backend-awssecretsmanager.R b/R/backend-awssecretsmanager.R new file mode 100644 index 0000000..811c2b4 --- /dev/null +++ b/R/backend-awssecretsmanager.R @@ -0,0 +1,260 @@ + + +#' AWS Secrets Manager keyring backend +#' +#' This backend must be selected explicitly. It makes calls to the AWS +#' secretsmanager service. +#' +#' This backend does not support keyrings or user names. The call to the +#' AWS service is authenticated by either the user's credentials or the IAM +#' user associated with the process, for example in a docker container. +#' +#' Note that the AWS APIs provide enventual consistency, it can take +#' a noticeable amount of time, up to five minutes, for updates and deletes +#' to propagate and so code that updates, deletes and lists needs to be +#' written to tolerate that. +#' +#' +#' @family keyring backends +#' @export +#' @include backend-class.R +#' @examples +#' \dontrun{ +#' ## +#' kb <- backend_awssecretsmanager$new() +#' kb$set_with_value("service", password = "secret") +#' kb$get("service") +#' kb$delete("service") +#' } + +backend_awssecretsmanager <- R6Class( + "backend_awssecretsmanager", + inherit = backend_keyrings, + public = list( + name = "aws", + initialize = function(keyring = NULL) + b_aws_init(self, private, keyring), + + get = function(service, + username = NULL, + keyring = NULL) + b_aws_get(self, private, service, username, keyring), + get_raw = function(service, + username = NULL, + keyring = NULL) + b_aws_get_raw(self, private, service, username, keyring), + set = function(service, + username = NULL, + keyring = NULL, + prompt = "Password: ") + b_aws_set(self, private, service, username, keyring, prompt), + set_with_value = function(service, + username = NULL, + password = NULL, + keyring = NULL) + b_aws_set_with_value(self, private, service, username, password, + keyring), + set_with_raw_value = function(service, + username = NULL, + password = NULL, + keyring = NULL) + b_aws_set_with_raw_value(self, private, service, username, password, + keyring), + delete = function(service, + username = NULL, + keyring = NULL) + b_aws_delete(self, private, service, username, keyring), + list = function(service = NULL, keyring = NULL) + b_aws_list(self, private, service, keyring), + is_available = function(report_error = FALSE) + b_aws_is_available(self, private, report_error), + + has_keyring_support = function() + { + return(FALSE) + }, + + docs = function() { + modifyList(super$docs(), + list(. = "Store secrets using the AWS Secrets manager.")) + } + ), + + private = list( + keyring = NULL, + sservice = NULL, + requestToken = paste("123456789012345678901234567890", as.character(Sys.time())), + keyring_create_direct = function(keyring, password = NULL) + b_aws_keyring_create_direct(self, private, keyring, password) + ) +) + +b_aws_init <- function(self, private, keyring) { + if (!is.null(keyring)) + stop("keyring parameter is not supported by the aws secrets manager backend") + private$sservice <- paws::secretsmanager() + invisible(self) +} + +b_aws_get <- function(self, private, service, username, keyring) { + if (!is.null(username)) + stop("username parameter is not supported by the aws secrets manager backend") + if (!is.null(keyring)) + stop("keyring parameter is not supported by the aws secrets manager backend") + return(private$sservice$get_secret_value(SecretId = service,)$SecretString) +} + +b_aws_get_raw <- + function(self, private, service, username, keyring) { + if (!is.null(username)) + stop("username parameter is not supported by the aws secrets manager backend") + if (!is.null(keyring)) + stop("keyring parameter is not supported by the aws secrets manager backend") + return(private$sservice$svc$get_secret_value(SecretId = service,)$SecretBinary) + } + +b_aws_set <- + function(self, + private, + service, + username, + keyring, + prompt) { + if (!is.null(username)) + stop("username parameter is not supported by the aws secrets manager backend") + if (!is.null(keyring)) + stop("keyring parameter is not supported by the aws secrets manager backend") + username <- username %||% getOption("keyring_username") + password <- get_pass(prompt) + if (is.null(password)) + stop("No secret provided") + private$sservice$create_secret( + ClientRequestToken = private$requestToken, + Description = "", + Name = service, + SecretString = password + ) + invisible(self) + } + +b_aws_set_with_value <- + function(self, + private, + service, + username, + password, + keyring) { + if (!is.null(username)) + stop("username parameter is not supported by the aws secrets manager backend") + if (!is.null(keyring)) + stop("keyring parameter is not supported by the aws secrets manager backend") + username <- username %||% getOption("keyring_username") + keyring <- keyring %||% private$keyring + private$sservice$create_secret( + ClientRequestToken = private$requestToken, + Description = "", + Name = service, + SecretString = password + ) + invisible(self) + } + +b_aws_set_with_raw_value <- + function(self, + private, + service, + username, + password, + keyring) { + if (!is.null(username)) + stop("username parameter is not supported by the aws secrets manager backend") + if (!is.null(keyring)) + stop("keyring parameter is not supported by the aws secrets manager backend") + username <- username %||% getOption("keyring_username") + keyring <- keyring %||% private$keyring + private$sservice$create_secret( + ClientRequestToken = private$requestToken, + Description = "", + Name = service, + SecretBinaryString = password + ) + invisible(self) + } + +b_aws_delete <- + function(self, private, service, username, keyring) { + if (!is.null(username)) + stop("username parameter is not supported by the aws secrets manager backend") + if (!is.null(keyring)) + stop("keyring parameter is not supported by the aws secrets manager backend") + username <- username %||% getOption("keyring_username") + keyring <- keyring %||% private$keyring + private$sservice$delete_secret(ForceDeleteWithoutRecovery = TRUE, + SecretId = service) + invisible(self) + } + +b_aws_list <- function(self, private, service, keyring) { + if (!is.null(keyring)) + stop("keyring parameter is not supported by the aws secrets manager backend") + keyring <- keyring %||% private$keyring + if (is.null(service) || + service == "") + # missing defaults to null in calling routine + { + res = private$sservice$list_secrets() + secretList = res$SecretList + while(length(res$NextToken)>0) + { + res = private$sservice$list_secrets(NextToken=res$NextToken) + secretList = append(secretList, res$SecretList) + } + } else + { + res = private$sservice$list_secrets(Filter = list(list( + Key = "name", Values = c(service) + ))) + secretList = res$SecretList + while(length(res$NextToken)>0) + { + res = private$sservice$list_secrets(NextToken=res$NextToken, Filter = list(list( + Key = "name", Values = c(service) + ))) + secretList = append(secretList, res$SecretList) + } + } + nameList = c() + if (length(secretList) > 0) + { + for (i in 1:length(secretList)) + { + nameList = c(nameList, secretList[[i]]$Name) + } + } + df = data.frame(service = nameList, + stringsAsFactors = FALSE) + df$username = NULL + + return(df) +} + +b_aws_is_available <- function(self, private, report_error) { + if(!requireNamespace("paws")) + { + if(report_error) + { + signalCondition("Paws library not available. It is required for AWS access") + } + return(FALSE) + } + callerID = try(paws::sts()$get_caller_identity()) + if (inherits(callerID, "try-error")) { + if(report_error) + { + signalCondition("No AWS credentials or AWS not reachable") + } + return(FALSE) + } + return(TRUE) + } + diff --git a/R/default_backend.R b/R/default_backend.R index af885c4..4f849f3 100644 --- a/R/default_backend.R +++ b/R/default_backend.R @@ -24,8 +24,8 @@ #' 1. the `keyring_keyring` option. #' - You can change this by using `options(keyring_keyring = "NEWVALUE")` #' 1. If this is not set, the `R_KEYRING_KEYRING` environment variable. -#' - Change this value with `Sys.setenv(R_KEYRING_KEYRING = "NEWVALUE")`, -#' either in your script or in your `.Renviron` file. +#' - Change this value with `Sys.setenv(R_KEYRING_KEYRING = "NEWVALUE")`, +#' either in your script or in your `.Renviron` file. #' See [base::Startup] for information about using `.Renviron` #' 1. Finally, if neither of these are set, the OS default keyring is used. #' - Usually the keyring is automatically unlocked when the user logs in. @@ -33,10 +33,10 @@ #' @param keyring Character string, the name of the keyring to use, #' or `NULL` for the default keyring. #' @return The backend object itself. -#' -#' +#' +#' #' @seealso [backend_env], [backend_file], [backend_macos], -#' [backend_secret_service], [backend_wincred] +#' [backend_secret_service], [backend_wincred], [backend_awssecretsmanager] #' #' @export #' @name backends @@ -84,7 +84,7 @@ default_backend_auto <- function() { } else if (sysname == "linux" && "secret_service" %in% names(known_backends) && backend_secret_service$new()$is_available()) { backend_secret_service - + } else if ("file" %in% names(known_backends)) { backend_file @@ -111,5 +111,6 @@ known_backends <- list( "macos" = backend_macos, "secret_service" = backend_secret_service, "env" = backend_env, - "file" = backend_file + "file" = backend_file, + "awssecretsmanager" = backend_awssecretsmanager ) diff --git a/R/package.R b/R/package.R index 901ec3e..07d8ca0 100644 --- a/R/package.R +++ b/R/package.R @@ -5,7 +5,8 @@ #' implementations. Currently supported: #' * Keychain on macOS, #' * Credential Store on Windows, -#' * the Secret Service API on Linux, and +#' * the Secret Service API on Linux +#' * the AWS Secrets Manager, and #' * environment variables on other platforms. #' #' @section Configuring an OS-specific backend: @@ -15,8 +16,10 @@ #' - MacOS: [backend_macos] #' - Linux: [backend_secret_service] #' - Windows: [backend_wincred] -#' - Or store the secrets in environment variables on other operating +#' - Store the secrets in environment variables on other operating #' systems: [backend_env] +#' - Or in the AWS secrets Manager: +#' [backend_awssecretsmanager] #' #' @section Query secret keys in a keyring: #' diff --git a/README.Rmd b/README.Rmd index 42e0f81..69d42f7 100644 --- a/README.Rmd +++ b/README.Rmd @@ -28,6 +28,7 @@ credential store. Currently supports: * Keychain on macOS (`backend_macos`), * Credential Store on Windows (`backend_wincred`), * the Secret Service API on Linux (`backend_secret_service`), +* AWS Secrets Manager (`backend_awssecretsmanager`), * encrypted files (`backend_file`), and * environment variables (`backend_env`). @@ -55,6 +56,15 @@ This backend works best on Linux servers. Install these packages: No additional software is needed. +### AWS Secrets Manager + +The paws package must be installed from CRAN to access the AWS services +along with the AWS CLI and credentials. + +```{r eval = FALSE} +install.packages("paws") +``` + ### R package Install the package from CRAN: diff --git a/README.md b/README.md index 549b971..2555280 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,7 @@ store. Currently supports: - Keychain on macOS (`backend_macos`), - Credential Store on Windows (`backend_wincred`), - the Secret Service API on Linux (`backend_secret_service`), +- AWS Secrets Manager (`backend_awssecretsmanager`), - encrypted files (`backend_file`), and - environment variables (`backend_env`). @@ -47,6 +48,15 @@ packages: No additional software is needed. +### AWS Secrets Manager + +The paws package must be installed from CRAN to access the AWS services +along with the AWS CLI, and AWS credentials. + +``` r +install.packages("paws") +``` + ### R package Install the package from CRAN: diff --git a/_pkgdown.yml b/_pkgdown.yml index 8ab5040..3974eaa 100644 --- a/_pkgdown.yml +++ b/_pkgdown.yml @@ -20,6 +20,7 @@ reference: - backend_secret_service - backend_file - backend_env + - backend_awssecretsmanager - title: Implementing new backends contents: diff --git a/keyring.Rproj b/keyring.Rproj index dde2c3a..6fccb25 100644 --- a/keyring.Rproj +++ b/keyring.Rproj @@ -18,3 +18,4 @@ StripTrailingWhitespace: Yes BuildType: Package PackageUseDevtools: Yes PackageInstallArgs: --no-multiarch --with-keep.source +PackageRoxygenize: rd,collate,namespace,vignette diff --git a/man/backend.Rd b/man/backend.Rd index a3095af..48ee2e1 100644 --- a/man/backend.Rd +++ b/man/backend.Rd @@ -10,7 +10,9 @@ methods. Implementing the \code{list} method is optional. Additional methods can be defined as well. } \details{ -These are the semantics of the various methods:\preformatted{get(service, username = NULL, keyring = NULL) +These are the semantics of the various methods: + +\if{html}{\out{