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{
}}\preformatted{get(service, username = NULL, keyring = NULL) get_raw(service, username = NULL, keyring = NULL) set(service, username = NULL, keyring = NULL, prompt = "Password: ") set_with_value(service, username = NULL, password = NULL, @@ -19,7 +21,7 @@ set_with_raw_value(service, username = NULL, password = NULL, keyring = NULL) delete(service, username = NULL, keyring = NULL) list(service = NULL, keyring = NULL) -} +}\if{html}{\out{
}} What these functions do: \itemize{ diff --git a/man/backend_awssecretsmanager.Rd b/man/backend_awssecretsmanager.Rd new file mode 100644 index 0000000..914d5d5 --- /dev/null +++ b/man/backend_awssecretsmanager.Rd @@ -0,0 +1,37 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/backend-awssecretsmanager.R +\name{backend_awssecretsmanager} +\alias{backend_awssecretsmanager} +\title{AWS Secrets Manager keyring backend} +\description{ +This backend must be selected explicitly. It makes calls to the AWS +secretsmanager service. +} +\details{ +This backend does not support keyrings or user names. The call to the +AWS service is authenticated by either the user's ceedentials 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. +} +\examples{ +\dontrun{ +## +kb <- backend_awssecretsmanager$new() +kb$set_with_value("service", password = "secret") +kb$get("service") +kb$delete("service") +} +} +\seealso{ +Other keyring backends: +\code{\link{backend_env}}, +\code{\link{backend_file}}, +\code{\link{backend_macos}}, +\code{\link{backend_secret_service}}, +\code{\link{backend_wincred}} +} +\concept{keyring backends} diff --git a/man/backend_env.Rd b/man/backend_env.Rd index 329a0e2..a28a678 100644 --- a/man/backend_env.Rd +++ b/man/backend_env.Rd @@ -36,6 +36,7 @@ env$delete("r-keyring-test", username = "donaldduck") } \seealso{ Other keyring backends: +\code{\link{backend_awssecretsmanager}}, \code{\link{backend_file}}, \code{\link{backend_macos}}, \code{\link{backend_secret_service}}, diff --git a/man/backend_file.Rd b/man/backend_file.Rd index b3af6b0..37b8cca 100644 --- a/man/backend_file.Rd +++ b/man/backend_file.Rd @@ -19,6 +19,7 @@ kb <- backend_file$new() } \seealso{ Other keyring backends: +\code{\link{backend_awssecretsmanager}}, \code{\link{backend_env}}, \code{\link{backend_macos}}, \code{\link{backend_secret_service}}, diff --git a/man/backend_keyrings.Rd b/man/backend_keyrings.Rd index 056b903..cde10c4 100644 --- a/man/backend_keyrings.Rd +++ b/man/backend_keyrings.Rd @@ -13,7 +13,9 @@ inherit from this class and redefine the \code{get}, \code{set}, \code{set_with_ } \details{ See \link{backend} for the first set of methods. This is the semantics of the -keyring management methods:\preformatted{keyring_create(keyring) +keyring management methods: + +\if{html}{\out{
}}\preformatted{keyring_create(keyring) keyring_list() keyring_delete(keyring = NULL) keyring_lock(keyring = NULL) @@ -21,7 +23,7 @@ keyring_unlock(keyring = NULL, password = NULL) keyring_is_locked(keyring = NULL) keyring_default() keyring_set_default(keyring = NULL) -} +}\if{html}{\out{
}} \itemize{ \item \code{keyring_create()} creates a new keyring. \item \code{keyring_list()} lists all keyrings. diff --git a/man/backend_macos.Rd b/man/backend_macos.Rd index 3e5a5da..50162b2 100644 --- a/man/backend_macos.Rd +++ b/man/backend_macos.Rd @@ -26,6 +26,7 @@ kb$delete_keyring("foobar") } \seealso{ Other keyring backends: +\code{\link{backend_awssecretsmanager}}, \code{\link{backend_env}}, \code{\link{backend_file}}, \code{\link{backend_secret_service}}, diff --git a/man/backend_secret_service.Rd b/man/backend_secret_service.Rd index 2b228c3..ad5542f 100644 --- a/man/backend_secret_service.Rd +++ b/man/backend_secret_service.Rd @@ -14,8 +14,10 @@ This backend supports multiple keyrings. See \link{backend} for the documentation of the individual methods. The \code{is_available()} method checks is a Secret Service daemon is running on the system, by trying to connect to it. It returns a logical -scalar, or throws an error, depending on its argument:\preformatted{is_available = function(report_error = FALSE) -} +scalar, or throws an error, depending on its argument: + +\if{html}{\out{
}}\preformatted{is_available = function(report_error = FALSE) +}\if{html}{\out{
}} Argument: \itemize{ @@ -37,6 +39,7 @@ kb$delete_keyring("foobar") } \seealso{ Other keyring backends: +\code{\link{backend_awssecretsmanager}}, \code{\link{backend_env}}, \code{\link{backend_file}}, \code{\link{backend_macos}}, diff --git a/man/backend_wincred.Rd b/man/backend_wincred.Rd index dcbc4ca..3246526 100644 --- a/man/backend_wincred.Rd +++ b/man/backend_wincred.Rd @@ -28,6 +28,7 @@ kb$delete_keyring("foobar") } \seealso{ Other keyring backends: +\code{\link{backend_awssecretsmanager}}, \code{\link{backend_env}}, \code{\link{backend_file}}, \code{\link{backend_macos}}, diff --git a/man/backends.Rd b/man/backends.Rd index 1d540d2..f096093 100644 --- a/man/backends.Rd +++ b/man/backends.Rd @@ -59,5 +59,5 @@ See \link[base:Startup]{base::Startup} for information about using \code{.Renvir } \seealso{ \link{backend_env}, \link{backend_file}, \link{backend_macos}, -\link{backend_secret_service}, \link{backend_wincred} +\link{backend_secret_service}, \link{backend_wincred}, \link{backend_awssecretsmanager} } diff --git a/tests/testthat/test-awssecretsmanager.R b/tests/testthat/test-awssecretsmanager.R new file mode 100644 index 0000000..e3556f8 --- /dev/null +++ b/tests/testthat/test-awssecretsmanager.R @@ -0,0 +1,111 @@ +# +# the SecretsManager API is described as eventually consistent and +# immediately listing a just-created secret is not guaranteed to work, +# re-creating one that has just been deleted may not work either. +# In the list case we cannot tell the difference betwen not found and not +# propagated. This should not be an issue in the real world where +# hopefully the delays between creating and referencing a secret are +# more than the five minutes that Amazon recommends waiting becore giving up. +# +# All this means that these tests can appear somewhat flakey when delays +# appear and disappear. The list test can both work and not work in the +# same run and so have had loops added to protect them from failing. +# + +context("AWS Secrets Manager") + +Sys.setenv(R_KEYRING_TEST_USE_AWS=1) + +test_that("set, list, get, delete", { + + skip_on_cran() + # AWS secret creation costs money, don't do it by accident + if(Sys.getenv("R_KEYRING_TEST_USE_AWS")[[1]] == "") { + skip("AWS backend not tested, environment variable R_KEYRING_TEST_USE_AWS not set") + } + callerID = try(paws::sts()$get_caller_identity()) + if (inherits(callerID, "try-error")) { + skip("No AWS credentials to use for testing") + } + + service <- random_service() + service2 <- random_service() + username <- random_username() + password <- random_password() + + kb <- backend_awssecretsmanager$new() + + expect_true(kb$is_available()) + + expect_error(kb$set_with_value(service, username, password)) + expect_silent(kb$set_with_value(service, password = password)) + expect_silent(kb$set_with_value(service2, password = password)) + sleepTime = 1 + sleepCount = 0 + repeat { + serviceName = kb$list(service)$service + if (!is.null(serviceName)) { + break + } + sleepCount = sleepCount + 1 + if (sleepCount > 6) + { + fail(message = "gave up waiting for AWS secret create to propagate while testing listing a named secret") + } + Sys.sleep(sleepTime) + sleepTime = sleepTime * 2 + } + + expect_equal(serviceName, c(service)) + + repeat { + serviceName = kb$list()$service + if (length(serviceName) >= 2) { + break + } + sleepCount = sleepCount + 1 + if (sleepCount > 6) + { + fail(message = "gave up waiting for AWS secret create to propagate while testing listing multiple secrets") + } + Sys.sleep(sleepTime) + sleepTime = sleepTime * 2 + } + + expect_gte(length(serviceName),2) + + expect_error(kb$get(service, username)) + + expect_error(kb$list(service, username)) + + expect_error(kb$delete(service, username)) + + expect_silent(kb$delete(service)) + expect_silent(kb$delete(service2)) +}) + +test_that("set, get, delete, without username", { + + skip_on_cran() + # AWS secret creation costs money, don't do it by accident + if(Sys.getenv("R_KEYRING_TEST_USE_AWS")[[1]] == "") { + skip("AWS backend not tested, environment variable R_KEYRING_TEST_USE_AWS not set") + } + callerID = try(paws::sts()$get_caller_identity()) + if (inherits(callerID, "try-error")) { + skip("No AWS credentials to use for testing") + } + + service <- random_service() + password <- random_password() + + kb <- backend_awssecretsmanager$new() + + expect_silent(kb$set_with_value(service, password = password)) + + expect_equal(kb$get(service), password) + + #expect_snapshot(kb$list(),"list") + +# expect_silent(kb$delete(service)) +}) diff --git a/tests/testthat/testthat-problems.rds b/tests/testthat/testthat-problems.rds new file mode 100644 index 0000000..230e486 Binary files /dev/null and b/tests/testthat/testthat-problems.rds differ