From 6fb28d7300eac799d3def7e5a69e6c3c19127cda Mon Sep 17 00:00:00 2001 From: Scott Chamberlain Date: Thu, 11 Jan 2024 10:02:47 -0800 Subject: [PATCH 01/28] fix string interpolation in wait fxn generator --- R/wait.R | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/R/wait.R b/R/wait.R index 2a49cd2..5461a92 100644 --- a/R/wait.R +++ b/R/wait.R @@ -1,7 +1,7 @@ # wait fxn generator wait_until <- function(fun, message) { function(id, sleep = 2, status_target = "available") { - cli::cli_alert_info("Waiting for instance status: {.emph status_target}") + cli::cli_alert_info("Waiting for instance status: {.emph {status_target}}") options(cli.spinner = "simpleDots") on.exit(options(cli.spinner = NULL), add = TRUE) cli::cli_progress_bar(format = "{cli::pb_spin} {message}") # nolint From eedc6e605d151c6008c48e0d7b22c6b563ceb914 Mon Sep 17 00:00:00 2001 From: Scott Chamberlain Date: Fri, 12 Jan 2024 14:44:37 -0800 Subject: [PATCH 02/28] gitignore and skip tests for redshift and rds for now until have a nice clean vcr secrets hiding workflow --- .gitignore | 4 ++ tests/fixtures/aws_db_rds_create.yml | 82 +++++++++++++++++++++++++--- tests/testthat/test-db-rds.R | 6 +- tests/testthat/test-db-redshift.R | 2 + 4 files changed, 83 insertions(+), 11 deletions(-) diff --git a/.gitignore b/.gitignore index ecf9281..a427176 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,7 @@ .DS_Store docs inst/doc + +# tests fixtures +tests/fixtures/aws_db_rds_create.yml +tests/fixtures/aws_db_redshift_create.yml diff --git a/tests/fixtures/aws_db_rds_create.yml b/tests/fixtures/aws_db_rds_create.yml index d9622ba..21c603d 100644 --- a/tests/fixtures/aws_db_rds_create.yml +++ b/tests/fixtures/aws_db_rds_create.yml @@ -4,13 +4,13 @@ http_interactions: uri: https://rds.<>.amazonaws.com/ body: encoding: '' - string: Action=CreateDBInstance&AllocatedStorage=20&DBInstanceClass=db.t3.micro&DBInstanceIdentifier=aninstance&DBName=dev&Engine=mariadb&MasterUserPassword=zzzzzz&MasterUsername=xxxx&StorageEncrypted=true&Version=2014-10-31&VpcSecurityGroupIds.VpcSecurityGroupId.1=sg-xxxxxxxxx + string: Action=CreateDBInstance&AllocatedStorage=20&DBInstanceClass=db.t3.micro&DBInstanceIdentifier=xxxxxx&DBName=xxxxx&Engine=mariadb&MasterUserPassword=xxxx&MasterUsername=xxxxxx&StorageEncrypted=true&Version=2014-10-31&VpcSecurityGroupIds.VpcSecurityGroupId.1=sg-0e48eebcf03cb88fa headers: User-Agent: paws/0.6.1 (R4.3.2; darwin20; aarch64) Accept: application/xml Content-Type: application/x-www-form-urlencoded; charset=utf-8 - Content-Length: '289' - X-Amz-Date: 20231211T164845Z + Content-Length: '284' + X-Amz-Date: 20240112T210858Z Authorization: redacted response: status: @@ -19,14 +19,78 @@ http_interactions: reason: OK message: 'Success: (200) OK' headers: - x-amzn-requestid: 5b60abc6-a56d-4360-910d-977576fc5397 + x-amzn-requestid: d3abb957-dbcf-4d94-b2cb-831f16bc55fb + content-encoding: deflate strict-transport-security: max-age=31536000 content-type: text/xml - content-length: '3725' - date: Mon, 11 Dec 2023 16:48:46 GMT + content-length: '1280' + date: Fri, 12 Jan 2024 21:08:58 GMT body: encoding: '' file: no - string: 20default.mariadb10.6in-sync10.6.14xxxxrds-ca-2019db.t3.micro0falsefalse0creating1arn:aws:kms:<>:510628056329:key/84537062-3947-4e47-b1d5-f67516deda17default:mariadb-10-6in-syncfalseregionrds-ca-20190db-OXSB5TXJ6DCCZXYXRTKLGZIDAM07:12-07:42falsearn:aws:rds:<>:510628056329:db:aninstanceaninstancemariadbtruefalseIPV4falsedevfalsetruevpc-0f0202006f3a3a7cesubnet-0e982356909366ab8Active<>bsubnet-0e930392c710ce58dActive<>asubnet-09d489f5ea970be38Active<>csubnet-0826d6353dff96cfeActive<>dCompletedefaultdefaultsg-xxxxxxxxxactivegeneral-public-license****tue:12:45-tue:13:15gp2truefalse5b60abc6-a56d-4360-910d-977576fc5397 - recorded_at: 2023-12-11 16:48:47 GMT - recorded_with: vcr/1.2.2, webmockr/0.9.0 + string: 20default.mariadb10.6in-sync10.6.14xxxxxxxxxdb.t3.micro0falsefalse0creating1arn:aws:kms:<>:744061095407:key/760bf2ac-8827-433e-acf0-4b552d79fc0edefault:mariadb-10-6in-syncfalseregionxxx0db-LDEB7WUTQ4TCPLVMSJYFHO2JHU09:44-10:14falsearn:aws:rds:<>:744061095407:db:xxxxxxxxxxxxmariadbtruefalseIPV4falsexxxxxfalsetruevpc-0e5be60895b3f4ecbsubnet-0130e9da2b710d93eActive<>dsubnet-0cb7fc32b2ee6f854Active<>csubnet-03fd7f44bd2d67abfActive<>asubnet-08f773156ffb2009aActive<>bCompletedefaultdefaultsg-0e48eebcf03cb88faactivegeneral-public-license****sat:07:39-sat:08:09gp2truefalsed3abb957-dbcf-4d94-b2cb-831f16bc55fb + recorded_at: 2024-01-12 21:08:59 GMT + recorded_with: vcr/1.2.2.91, webmockr/0.9.0 +- request: + method: post + uri: https://rds.<>.amazonaws.com/ + body: + encoding: '' + string: Action=DescribeDBInstances&Version=2014-10-31 + headers: + User-Agent: paws/0.6.1 (R4.3.2; darwin20; aarch64) + Accept: application/xml + Content-Type: application/x-www-form-urlencoded; charset=utf-8 + Content-Length: '45' + X-Amz-Date: 20240112T210859Z + Authorization: redacted + response: + status: + status_code: 200 + category: Success + reason: OK + message: 'Success: (200) OK' + headers: + x-amzn-requestid: xxxx + content-encoding: deflate + strict-transport-security: max-age=31536000 + content-type: text/xml + content-length: '1325' + date: Fri, 12 Jan 2024 21:08:58 GMT + body: + encoding: '' + file: no + string: 20default.mariadb10.6in-syncfalse10.6.14xxxxxxxxxdb.t3.micro0falsefalse0creating1arn:aws:kms:<>:744061095407:key/760bf2ac-8827-433e-acf0-4b552d79fc0edefault:mariadb-10-6in-syncfalseregionxxx0db-LDEB7WUTQ4TCPLVMSJYFHO2JHU09:44-10:14falsearn:aws:rds:<>:744061095407:db:xxxxxxxxxxxxmariadbtruefalseIPV4stoppedfalsexxxxxfalsetruevpc-0e5be60895b3f4ecbsubnet-0130e9da2b710d93eActive<>dsubnet-0cb7fc32b2ee6f854Active<>csubnet-03fd7f44bd2d67abfActive<>asubnet-08f773156ffb2009aActive<>bCompletedefaultdefaultsg-0e48eebcf03cb88faactivegeneral-public-license****sat:07:39-sat:08:09gp2truefalsexxxx + recorded_at: 2024-01-12 21:08:59 GMT + recorded_with: vcr/1.2.2.91, webmockr/0.9.0 +- request: + method: post + uri: https://secretsmanager.<>.amazonaws.com/ + body: + encoding: '' + string: '{"Name":"xxxx","ClientRequestToken":"xxxx","SecretString":"{\"engine\":\"mariadb\",\"host\":{},\"username\":\"xxxxxx\",\"password\":\"xxxx\",\"dbname\":\"xxxxx\",\"port\":{}}"}' + headers: + Accept: application/json, text/xml, application/xml, */* + User-Agent: paws/0.6.1 (R4.3.2; darwin20; aarch64) + X-Amz-Target: secretsmanager.CreateSecret + Content-Type: application/x-amz-json-1.1 + Content-Length: '225' + X-Amz-Date: 20240112T210859Z + Authorization: redacted + response: + status: + status_code: 200 + category: Success + reason: OK + message: 'Success: (200) OK' + headers: + x-amzn-requestid: 809ee892-fa87-4e0b-9387-2a5462522d8b + content-type: application/x-amz-json-1.1 + content-length: '165' + date: Fri, 12 Jan 2024 21:08:59 GMT + body: + encoding: '' + file: no + string: '{"ARN":"arn:aws:secretsmanager:<>:744061095407:secret:xxx","Name":"xxx","VersionId":"xxxx"}' + recorded_at: 2024-01-12 21:08:59 GMT + recorded_with: vcr/1.2.2.91, webmockr/0.9.0 diff --git a/tests/testthat/test-db-rds.R b/tests/testthat/test-db-rds.R index 24ed234..6df06c9 100644 --- a/tests/testthat/test-db-rds.R +++ b/tests/testthat/test-db-rds.R @@ -1,9 +1,11 @@ +skip("skipping until secrets stuff done") + test_that("aws_db_rds_create", { vcr::use_cassette("aws_db_rds_create", { z <- aws_db_rds_create( id = "aninstance", class = "db.t3.micro", - user = "xxxx", pwd = "zzzzzz", - security_group_ids = list("sg-xxxxxxxxx"), + user = "xxx", pwd = "xxx", + security_group_ids = list("sg-xxxxxx"), wait = FALSE, verbose = FALSE ) }) diff --git a/tests/testthat/test-db-redshift.R b/tests/testthat/test-db-redshift.R index 5d38c57..7fa4fae 100644 --- a/tests/testthat/test-db-redshift.R +++ b/tests/testthat/test-db-redshift.R @@ -1,3 +1,5 @@ +skip("skipping until secrets stuff done") + test_that("aws_db_redshift_create", { vcr::use_cassette("aws_db_redshift_create", { z <- aws_db_redshift_create( From ac222ffc06ff6af9da11feae0221066b449fe8bb Mon Sep 17 00:00:00 2001 From: Scott Chamberlain Date: Fri, 12 Jan 2024 21:59:47 -0800 Subject: [PATCH 03/28] - added family of fxns for working with aws secrets manager - added two new pkg imports: uuid, jsonlite - rds create now creates username and password for the user, storing in aws secrets manager --- DESCRIPTION | 4 +- NAMESPACE | 10 + R/database-misc.R | 14 +- R/database-rds.R | 55 +++- R/database-redshift.R | 2 +- R/globals.R | 2 + R/onload.R | 2 +- R/random_user.R | 18 ++ R/secrets_manager.R | 258 +++++++++++++++++ R/sixtyfour-package.R | 2 +- R/ui_fetch_secret.R | 79 ++++++ R/utils.R | 25 +- R/words.R | 392 ++++++++++++++++++++++++++ _pkgdown.yml | 4 + man/aws_db_instance_status.Rd | 2 +- man/aws_db_rds_con.Rd | 4 +- man/aws_db_rds_create.Rd | 16 +- man/aws_secrets_all.Rd | 19 ++ man/aws_secrets_create.Rd | 61 ++++ man/aws_secrets_delete.Rd | 36 +++ man/aws_secrets_get.Rd | 40 +++ man/aws_secrets_list.Rd | 19 ++ man/aws_secrets_pwd.Rd | 21 ++ man/aws_secrets_rotate.Rd | 46 +++ man/aws_secrets_update.Rd | 55 ++++ man/construct_db_secret.Rd | 36 +++ man/random_user.Rd | 20 ++ man/ui_fetch_secret.Rd | 41 +++ tests/testthat/helper-vcr.R | 9 + tests/testthat/test-secrets_manager.R | 82 ++++++ 30 files changed, 1341 insertions(+), 33 deletions(-) create mode 100644 R/random_user.R create mode 100644 R/secrets_manager.R create mode 100644 R/ui_fetch_secret.R create mode 100644 R/words.R create mode 100644 man/aws_secrets_all.Rd create mode 100644 man/aws_secrets_create.Rd create mode 100644 man/aws_secrets_delete.Rd create mode 100644 man/aws_secrets_get.Rd create mode 100644 man/aws_secrets_list.Rd create mode 100644 man/aws_secrets_pwd.Rd create mode 100644 man/aws_secrets_rotate.Rd create mode 100644 man/aws_secrets_update.Rd create mode 100644 man/construct_db_secret.Rd create mode 100644 man/random_user.Rd create mode 100644 man/ui_fetch_secret.Rd create mode 100644 tests/testthat/test-secrets_manager.R diff --git a/DESCRIPTION b/DESCRIPTION index 6ffe1ef..951b467 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -28,7 +28,9 @@ Imports: s3fs (>= 0.1.3), cli, glue, - memoise + memoise, + uuid, + jsonlite Suggests: knitr, rmarkdown, diff --git a/NAMESPACE b/NAMESPACE index 7a0b70e..788d193 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -39,6 +39,14 @@ export(aws_role) export(aws_role_create) export(aws_role_delete) export(aws_roles) +export(aws_secrets_all) +export(aws_secrets_create) +export(aws_secrets_delete) +export(aws_secrets_get) +export(aws_secrets_list) +export(aws_secrets_pwd) +export(aws_secrets_rotate) +export(aws_secrets_update) export(aws_user) export(aws_user_access_key) export(aws_user_add_to_group) @@ -48,6 +56,7 @@ export(aws_user_delete) export(aws_user_exists) export(aws_users) export(billing) +export(random_user) export(s3_path) export(set_s3_interface) importFrom(cli,cli_inform) @@ -62,6 +71,7 @@ importFrom(fs,file_exists) importFrom(fs,fs_bytes) importFrom(glue,glue) importFrom(lubridate,as_datetime) +importFrom(magrittr,"%<>%") importFrom(magrittr,"%>%") importFrom(paws,costexplorer) importFrom(paws,iam) diff --git a/R/database-misc.R b/R/database-misc.R index da5e0af..50fddb0 100644 --- a/R/database-misc.R +++ b/R/database-misc.R @@ -1,12 +1,14 @@ #' internal helper function -#' @param id an RDS instance ID or Redshift cluster ID -#' @param fun a function that takes an ID for an AWS RDS instance +#' @param id (function) an RDS instance ID or Redshift cluster ID +#' @param fun (function) a function that takes an ID for an AWS RDS instance #' or Redshift cluster, and returns a single boolean +#' @param see_fun (character) the function to point users to in the +#' message for database connection #' @noRd #' @keywords internal -info <- function(id, fun) { +info <- function(id, fun, see_fun = "") { cli::cli_alert_success("Instance is up!") - cli::cli_alert_info("See `aws_db_rds_con` for connection info") + cli::cli_alert_info("See `{see_fun}` for connection info") cli::cli_alert_info("Instance details:") con_info <- fun(id) for (i in seq_along(con_info)) { @@ -32,3 +34,7 @@ which_driver <- function(engine) { stop(glue::glue("{engine} not currently supported")) ) } + +random_str <- function(prefix = "-") { + paste0(prefix, sub("-.+", "", uuid::UUIDgenerate())) +} diff --git a/R/database-rds.R b/R/database-rds.R index acd8ba6..c6eb603 100644 --- a/R/database-rds.R +++ b/R/database-rds.R @@ -25,12 +25,12 @@ #' library(dplyr) #' tbl(con_rds, "mtcars") #' } -aws_db_rds_con <- function(user, pwd, id = NULL, host = NULL, port = NULL, - dbname = NULL, engine = NULL, ...) { - check_for_pkg("DBI") +aws_db_rds_con <- function( + user = NULL, pwd = NULL, id = NULL, host = NULL, + port = NULL, dbname = NULL, engine = NULL, ...) { - stopifnot("user is required" = !missing(user)) - stopifnot("pwd is required" = !missing(pwd)) + check_for_pkg("DBI") + is_class(engine, "character") if (!is.null(id)) { con_info <- instance_con_info(id) @@ -45,13 +45,15 @@ aws_db_rds_con <- function(user, pwd, id = NULL, host = NULL, port = NULL, ) } + creds <- ui_fetch_secret(user, pwd, engine) + DBI::dbConnect( which_driver(engine), host = host, port = port, dbname = dbname, - user = user, - password = pwd, + user = creds$user, + password = creds$password, ... ) } @@ -63,14 +65,16 @@ aws_db_rds_con <- function(user, pwd, id = NULL, host = NULL, port = NULL, #' @param id (character) required. instance identifier. The identifier for #' this DB instance. This parameter is stored as a lowercase string. #' Constraints: must contain from 1 to 63 letters, numbers, or hyphens; first -#' character must be a letter; cn't end with a hyphen or contain two +#' character must be a letter; can't end with a hyphen or contain two #' consecutive hyphens. required. #' @param class (character) required. The compute and memory capacity of the #' DB instance, for example `db.m5.large`. #' @param user (character) User name associated with the admin user account for -#' the cluster that is being created. +#' the cluster that is being created. If `NULL`, we generate a random user +#' name, see [random_user()] #' @param pwd (character) Password associated with the admin user account for -#' the cluster that is being created. +#' the cluster that is being created. If `NULL`, we generate a random password +#' with [aws_secrets_pwd()] (which uses the AWS Secrets Manager service) #' @param dbname (character) The name of the first database to be created when #' the cluster is created. default: "dev". additional databases can be created #' within the cluster @@ -87,6 +91,8 @@ aws_db_rds_con <- function(user, pwd, id = NULL, host = NULL, port = NULL, #' until the cluster is available. If `wait=FALSE` use #' `aws_db_instance_status()` to check on the cluster status. #' @param verbose (logical) verbose informational output? default: `TRUE` +#' @param aws_secrets (logical) should we manage your database credentials +#' in AWS Secrets Manager? default: `TRUE` #' @param ... named parameters passed on to #' [create_db_instance](https://www.paws-r-sdk.com/docs/rds_create_db_instance/) #' @details See above link to `create_cluster` docs for details on requirements @@ -104,11 +110,19 @@ aws_db_rds_con <- function(user, pwd, id = NULL, host = NULL, port = NULL, #' see . also prints useful #' connection information after instance is available. aws_db_rds_create <- - function(id, class, user, pwd, dbname = "dev", + function(id, class, user = NULL, pwd = NULL, dbname = "dev", engine = "mariadb", storage = 20, storage_encrypted = TRUE, security_group_ids = NULL, - wait = TRUE, verbose = TRUE, ...) { + wait = TRUE, verbose = TRUE, aws_secrets = TRUE, ...) { aws_db_rds_client() + if (is.null(user)) { + user <- random_user() + if (verbose) cli::cli_alert_info("`user` is NULL; created user: {.strong {user}}") + } + if (is.null(pwd)) { + pwd <- aws_secrets_pwd() + if (verbose) cli::cli_alert_info("`pwd` is NULL; created password: *******") + } env64$rds$create_db_instance( DBName = dbname, DBInstanceIdentifier = id, Engine = engine, DBInstanceClass = class, @@ -121,7 +135,22 @@ aws_db_rds_create <- if (wait) { wait_for_instance(id) } - if (verbose) info(id, instance_con_info) + if (aws_secrets) { + if (verbose) cli::cli_alert_info("Uploading user/pwd to secrets manager") + x <- instance_con_info(id) + aws_secrets_create( + name = paste0(id, random_str()), + secret = construct_db_secret( + engine = x$engine, + host = x$host, + username = user, + password = pwd, + dbname = x$dbname, + port = x$port + ) + ) + } + if (verbose) info(id, instance_con_info, "aws_db_rds_con") return(env64$rds) } diff --git a/R/database-redshift.R b/R/database-redshift.R index 0bd74d2..59b04f2 100644 --- a/R/database-redshift.R +++ b/R/database-redshift.R @@ -112,7 +112,7 @@ aws_db_redshift_create <- if (wait) { wait_for_cluster(id) } - if (verbose) info(id, cluster_con_info) + if (verbose) info(id, cluster_con_info, "aws_db_redshift_con") return(env64$redshift) } diff --git a/R/globals.R b/R/globals.R index a570ec6..2dd242f 100644 --- a/R/globals.R +++ b/R/globals.R @@ -6,6 +6,8 @@ utils::globalVariables(c( "PolicyName", # "Arn", # ".", # + "secret_raw", # + "secret_str", # "PasswordLastUsed", # "CreateDate", # NULL diff --git a/R/onload.R b/R/onload.R index bbb4987..217f15d 100644 --- a/R/onload.R +++ b/R/onload.R @@ -5,7 +5,7 @@ env64 <- new.env() # sets creds for paws and s3fs for the S3 service env64$s3 <- set_s3_interface("aws") - # iam and costexplorer services env64$iam <- paws::iam() env64$costexplorer <- paws::costexplorer() + env64$secretsmanager <- paws::secretsmanager() } diff --git a/R/random_user.R b/R/random_user.R new file mode 100644 index 0000000..4154013 --- /dev/null +++ b/R/random_user.R @@ -0,0 +1,18 @@ +capfirst <- function(x) { + substr(x, 1, 1) <- toupper(substr(x, 1, 1)) + x +} + +#' Get a random user +#' +#' @export +#' @return (character) a username with a random adjective plus a +#' random noun combined into one string, shortened to no longer than 16 +#' characters if longer than 16 +#' @examples +#' random_user() +#' replicate(10, random_user()) +random_user <- function() { + sample_upcase <- function(x) capfirst(sample(x, size = 1)) + substring(paste0(sample_upcase(adjectives), sample_upcase(nouns)), 1, 16) +} diff --git a/R/secrets_manager.R b/R/secrets_manager.R new file mode 100644 index 0000000..dcd75fe --- /dev/null +++ b/R/secrets_manager.R @@ -0,0 +1,258 @@ +#' List secrets +#' @export +#' @return (list) list with secrets +#' @examples \dontrun{ +#' aws_secrets_list() +#' } +aws_secrets_list <- function() { + env64$secretsmanager$list_secrets() +} + +#' Get all secret values +#' @export +#' @return (list) list with secrets +#' @examples \dontrun{ +#' aws_secrets_list() +#' } +aws_secrets_all <- function() { + env64$secretsmanager$list_secrets() %>% + .$SecretList %>% + purrr::map(function(x) aws_secrets_get(x$Name)) +} + +check_secret <- function(secret) { + if (!inherits(secret, c("raw", "character"))) { + stop("`secret` must be of class character or raw", call. = FALSE) + } +} + +#' Get a random password +#' +#' @export +#' @param ... named parameters passed on to `get_random_password` +#' +#' @examples \dontrun{ +#' aws_secrets_pwd() +#' aws_secrets_pwd(ExcludeNumbers = TRUE) +#' } +aws_secrets_pwd <- function(...) { + env64$secretsmanager$get_random_password( + PasswordLength = 40L, + ExcludePunctuation = TRUE, + ... + )$RandomPassword +} + +#' Create a secret +#' +#' This function does not create your database username and/or password. +#' Instead, it creates a "secret", which is typically a combination +#' of credentials (username + password + other metadata) +#' +#' @export +#' @param name (character) The name of the new secret. required +#' @param secret (character/raw) The text or raw data to encrypt and store +#' in this new version of the secret. AWS recommends for text to use a JSON +#' structure of key/value pairs for your secret value (see examples below). +#' required +#' @param description (character) The description of the secret. optional +#' @param ... further named parameters passed on to `create_secret` +#' +#' @return (list) with fields: +#' - ARN +#' - Name +#' - VersionId +#' - ReplicationStatus +#' @details Note that we autogenerate a random UUID to pass to the +#' `ClientRequestToken` parameter of the `paws` function `create_secret` +#' used internally in this function. +#' +#' This function creates a new secret. See [aws_secrets_update()] to +#' update an existing secret. This function fails if you call it with +#' an existing secret with the same name or ARN +#' @examples \dontrun{ +#' # Text secret +#' x <- aws_secrets_create( +#' name = "MyTestDatabaseSecret", +#' secret = '{"username":"david","password":"EXAMPLE-PASSWORD"}', +#' description = "My test database secret as a string" +#' ) +#' +#' # Raw secret +#' x <- aws_secrets_create( +#' name = "MyRawDatabaseSecret", +#' secret = charToRaw('{"username":"david","password":"EXAMPLE-PASSWORD"}'), +#' description = "My test database secret as raw" +#' ) +#' } +aws_secrets_create <- function(name, secret, description = NULL, ...) { + check_secret(secret) + secret_str <- secret_raw <- NULL + if (rlang::is_raw(secret)) secret_raw <- secret + if (rlang::is_character(secret)) secret_str <- secret + env64$secretsmanager$create_secret( + Name = name, + ClientRequestToken = uuid::UUIDgenerate(), + Description = description, + SecretBinary = secret_raw, + SecretString = secret_str, ... + ) +} + +#' Update a secret +#' @export +#' @inheritParams aws_secrets_create +#' @param id (character) The name or ARN of the secret. required +#' @param ... further named parameters passed on to `put_secret_value` +#' +#' @return (list) with fields: +#' - ARN +#' - Name +#' - VersionId +#' - VersionStages +#' @autoglobal +#' @details Note that we autogenerate a random UUID to pass to the +#' `ClientRequestToken` parameter of the `paws` function used internally +#' @examples \dontrun{ +#' # Create a secret +#' aws_secrets_create( +#' name = "TheSecret", +#' secret = '{"username":"jane","password":"cat"}', +#' description = "A string" +#' ) +#' +#' aws_secrets_get("TheSecret") +#' +#' # Update the secret +#' aws_secrets_update( +#' id = "TheSecret", +#' secret = '{"username":"jane","password":"kitten"}' +#' ) +#' +#' aws_secrets_get("TheSecret") +#' } +aws_secrets_update <- function(id, secret, ...) { + check_secret(secret) + secret_str <- secret_raw <- NULL + if (rlang::is_raw(secret)) secret_raw <- secret + if (rlang::is_character(secret)) secret_str <- secret + env64$secretsmanager$put_secret_value( + SecretId = id, + ClientRequestToken = uuid::UUIDgenerate(), + SecretBinary = secret_raw, + SecretString = secret_str, ... + ) +} + +#' Get a secret +#' @export +#' @inheritParams aws_secrets_update +#' @param ... further named parameters passed on to `get_secret_value` +#' +#' @return (list) with fields: +#' - ARN +#' - Name +#' - VersionId +#' - SecretBinary +#' - SecretString +#' - VersionStages +#' - CreatedDate +#' @examples \dontrun{ +#' # Does exist +#' aws_secrets_get(id = "MyTestDatabaseSecret") +#' +#' # Does not exist +#' # aws_secrets_get(id = "DoesntExist") +#' #> Error: ResourceNotFoundException (HTTP 400). Secrets Manager +#' #> can't find the specified secret. +#' } +aws_secrets_get <- function(id, ...) { + env64$secretsmanager$get_secret_value(SecretId = id, ...) +} + +#' Delete a secret +#' @export +#' @inheritParams aws_secrets_update +#' @param ... further named parameters passed on to `delete_secret` +#' +#' @return (list) with fields: +#' - ARN +#' - Name +#' - DeletionDate +#' @examples \dontrun{ +#' # Does exist +#' aws_secrets_delete(id = "MyTestDatabaseSecret") +#' +#' # Does not exist +#' # aws_secrets_get(id = "DoesntExist") +#' #> Error: ResourceNotFoundException (HTTP 400). Secrets Manager +#' #> can't find the specified secret. +#' } +aws_secrets_delete <- function(id, ...) { + env64$secretsmanager$delete_secret(SecretId = id, ...) +} + +#' Rotate a secret +#' @export +#' @inheritParams aws_secrets_update +#' @inherit aws_secrets_update details +#' @param lambda_arn (character) The ARN of the Lambda rotation function. +#' Only supply for secrets that use a Lambda rotation function to rotate +#' @param rules (list) asdfadf +#' @param immediately (logical) whether to rotate the secret immediately or not. +#' default: `TRUE` +#' @references +#' @autoglobal +#' @return (list) with fields: +#' - ARN +#' - Name +#' - VersionId +#' @examples \dontrun{ +#' aws_secrets_rotate(id = "MyTestDatabaseSecret") +#' aws_secrets_rotate(id = "MyTestDatabaseSecret", rules = list( +#' Duration = "2h", +#' ScheduleExpression = "cron(0 16 1,15 * ? *)" +#' ) +#' } +aws_secrets_rotate <- function(id, lambda_arn = NULL, rules = NULL, immediately = TRUE) { + env64$secretsmanager$rotate_secret( + SecretId = id, + ClientRequestToken = uuid::UUIDgenerate(), + RotationLambdaARN = secret_raw, + RotationRules = secret_str, + RotateImmediately = immediately + ) +} + +#' Construct a database secret string or raw version of it +#' +#' @param engine,host,username,password,dbname,port supply parameters to +#' go into either a json string or raw version of the json string +#' @param as (character) one of "string" or "raw" +#' @keywords internal +#' @references +#' @examples \dontrun{ +#' construct_db_secret("redshift", dbname = "hello", port = 5439) +#' construct_db_secret("mariadb", dbname = "world", port = 3306) +#' construct_db_secret("postgresql", dbname = "bears", port = 5432, as = "raw") +#' } +construct_db_secret <- function( + engine, host = "", username = "", + password = "", dbname = "", port = "", as = "string") { + + + dat <- list( + "engine" = engine, + "host" = host, + "username" = username, + "password" = password, + "dbname" = dbname, + "port" = port + ) + json_dat <- jsonlite::toJSON(dat, auto_unbox = TRUE) + switch(as, + string = as.character(json_dat), + raw = charToRaw(json_dat), + stop("`as` must be one of 'string' or 'raw'", call. = FALSE) + ) +} diff --git a/R/sixtyfour-package.R b/R/sixtyfour-package.R index 3dbf5d6..53cb19f 100644 --- a/R/sixtyfour-package.R +++ b/R/sixtyfour-package.R @@ -8,7 +8,7 @@ "_PACKAGE" ## usethis namespace: start -#' @importFrom magrittr %>% +#' @importFrom magrittr %>% %<>% #' @importFrom paws s3 iam costexplorer #' @importFrom s3fs s3_file_system #' @importFrom glue glue diff --git a/R/ui_fetch_secret.R b/R/ui_fetch_secret.R new file mode 100644 index 0000000..de397bd --- /dev/null +++ b/R/ui_fetch_secret.R @@ -0,0 +1,79 @@ +#' Fetch secrets +#' +#' @param user (character) xx +#' @param password (character) password +#' @param engine (character) engine to filter secrets with. if not supplied +#' (`NULL`) all secrets are considered +#' @section How the function works: +#' - If user and password are supplied they are returned immediately +#' - If user and password are not supplied, we fetch all secrets in +#' your secrets manager service, and then ask you which one you'd like +#' to use. If you choose none of them this function returns NULL for +#' both user and password +#' @keywords internal +#' @examples \dontrun{ +#' # user,pwd supplied, return them right away at top of fxn +#' ui_fetch_secret(engine = "mariadb", user = "jane", password = "apple") +#' +#' # user,pwd null +#' ui_fetch_secret(engine = "redshift") +#' ui_fetch_secret(engine = "mariadb") +#' } +ui_fetch_secret <- function(user = NULL, password = NULL, engine = NULL) { + # if user and password supplied return them + if (!is.null(user) && !is.null(password)) { + return(list(user = user, password = password)) + } + + # get all secrets data + secrets <- aws_secrets_all() + + # organize secrets + new_secrets <- list() + for (i in seq_along(secrets)) { + new_secrets[[i]] <- c( + list(name = secrets[[i]]$Name), + jsonlite::fromJSON(secrets[[i]]$SecretString) + ) + } + new_secrets_df <- + Filter(function(x) length(x$host) > 0, new_secrets) %>% + bind_rows() + if (!is.null(engine)) { + new_secrets_df %<>% filter(engine == !!engine) + } + if (NROW(new_secrets_df) == 0) { + stop("No secrets found", call. = FALSE) + } + dboptions <- + new_secrets_df %>% + glue::glue_data( + "Secret name: {name}\n", + " Engine: {engine}\n", + " Host: {host}", + .trim = FALSE + ) %>% + as.character() + + # if any db secrets found in their secrets manager, prompt user + picked <- picker(c( + "No credentials were supplied", + glue("We found {length(dboptions)} in your AWS secrets manager"), + "Which set of database credentials do you want to use?" + ), dboptions) + + if (picked == 0) { + user <- password <- NULL + } else { + selected_secret <- new_secrets_df[picked, ] + user <- selected_secret$username + password <- selected_secret$password + } + + list(user = user, password = password) +} + +picker <- function(msg, choices, .envir = parent.frame()) { + cli::cli_inform(msg, .envir = .envir) + utils::menu(choices) +} diff --git a/R/utils.R b/R/utils.R index 83e5f61..6a643e5 100644 --- a/R/utils.R +++ b/R/utils.R @@ -113,8 +113,10 @@ path_s3_build <- function(x) { #' @examplesIf interactive() #' path_as_s3("http://s64-test-3.s3.amazonaws.com/") #' path_as_s3("https://s64-test-3.s3.amazonaws.com/") -#' path_as_s3(c("https://s64-test-3.s3.amazonaws.com/", -#' "https://mybucket.s3.amazonaws.com/")) +#' path_as_s3(c( +#' "https://s64-test-3.s3.amazonaws.com/", +#' "https://mybucket.s3.amazonaws.com/" +#' )) #' path_as_s3(c("apple", "banana", "pear", "pineapple")) path_as_s3 <- function(paths) { paths <- gsub("https?://", "", paths) @@ -138,8 +140,12 @@ path_as_s3 <- function(paths) { #' } paginate_aws <- function(fun, target, ...) { res <- fun(...) - if (!rlang::has_name(res, "IsTruncated")) return(res[[target]]) - if (!res$IsTruncated) return(res[[target]]) + if (!rlang::has_name(res, "IsTruncated")) { + return(res[[target]]) + } + if (!res$IsTruncated) { + return(res[[target]]) + } all_results <- list(res) more_results <- TRUE @@ -173,3 +179,14 @@ tidy_generator <- function(vars) { mutate(CreateDate = as_datetime(CreateDate)) } } + +is_class <- function(x, class) { + if (is.null(x)) { + return(invisible()) + } + if (!inherits(x, class)) { + stop(glue("`{substitute(x)}` should be class {class}"), + call. = FALSE + ) + } +} diff --git a/R/words.R b/R/words.R new file mode 100644 index 0000000..ccea5b8 --- /dev/null +++ b/R/words.R @@ -0,0 +1,392 @@ +adjectives <- c( + "absorbing", "abstract", "academic", "accelerated", "accented", + "accountant", "acquainted", "acute", "addicting", "addictive", + "adjustable", "admired", "adult", "adverse", "advised", + "aerosol", "afraid", "aggravated", "aggressive", "agreeable", + "alienate", "aligned", "alleged", "almond", + "alright", "altruistic", "ambient", "ambivalent", "amiable", + "amino", "amorphous", "amused", "anatomical", "ancestral", + "angrier", "answerable", "antiquarian", + "antiretroviral", "appellate", "applicable", "apportioned", "approachable", + "appropriated", "arabic", "archer", "aroused", "arrested", + "assertive", "assigned", "athletic", "atrocious", "attained", + "authoritarian", "autobiographical", "avaricious", "avocado", "awake", + "awsome", "backstage", "backwoods", "balding", "bandaged", + "banded", "banned", "barreled", "battle", "beaten", + "begotten", "beguiled", "bellied", "belted", "beneficent", + "besieged", "betting", "biggest", "biochemical", + "bipolar", "blackened", "blame", "blessed", "blindfolded", + "bloat", "blocked", "blooded", "blushing", + "boastful", "bolder", "bolstered", "bonnie", "bored", + "boundary", "bounded", "bounding", "branched", "brawling", + "brazen", "breeding", "bridged", "brimming", "brimstone", + "broadest", "broiled", "broker", "bronze", "bruising", + "buffy", "bullied", "bungling", "burial", "buttery", + "cancerous", "candied", "canonical", "cantankerous", "cardinal", + "carefree", "caretaker", "casual", "cathartic", "causal", + "chapel", "characterized", "charcoal", "cheeky", "cherished", + "chipotle", "chirping", "chivalrous", "circumstantial", "civic", + "civil", "civilised", "clanking", "clapping", "claptrap", + "classless", "cleansed", "cleric", "cloistered", "codified", + "colloquial", "colour", "combat", "combined", "comely", + "commissioned", "commonplace", "commuter", "commuting", "comparable", + "complementary", "compromising", "conceding", "concentrated", "conceptual", + "conditioned", "confederate", "confident", "confidential", "confining", + "confuse", "congressional", "consequential", "conservative", "constituent", + "contaminated", "contemporaneous", "convertible", "convex", + "cooked", "coronary", "corporatist", "correlated", "corroborated", + "cosmic", "cover", "crash", + "culminate", "cushioned", "dandy", "dashing", + "dazzled", "decreased", "decrepit", "dedicated", "defaced", + "defective", "defenseless", "deluded", "deodorant", "departed", + "depress", "designing", "despairing", "destitute", "detective", + "determined", "devastating", "deviant", "devilish", "devoted", + "diabetic", "diagonal", "dictated", "didactic", "differentiated", + "diffused", "dirtier", "disabling", "disconnected", "discovered", + "disdainful", "diseased", "disfigured", "disheartened", "disheveled", + "disillusioned", "disparate", "dissident", "doable", "doctrinal", + "doing", "dotted", "downbeat", "dozen", + "draining", "draught", "dread", "dried", "dropped", + "drowned", "dulled", "duplicate", "eaten", "echoing", + "economical", "elaborated", "elastic", "elective", "electoral", + "elven", "embryo", "emerald", "emergency", "emissary", + "emotional", "employed", "enamel", "encased", "encrusted", + "endangered", "engraved", "engrossing", "enlarged", "enlisted", + "enlivened", "ensconced", "entangled", "enthralling", "entire", + "envious", "eradicated", "eroded", "esoteric", "essential", + "evaporated", "evergreen", "everlasting", "exacting", + "exasperated", "excess", "exciting", "executable", "existent", + "exonerated", "exorbitant", "exotic", "exponential", "export", + "extraordinary", "exultant", "exulting", "facsimile", "fading", + "fainter", "fallacious", "faltering", "famous", + "fancier", "fated", "favourable", "fearless", + "feathered", "fellow", "fermented", "ferocious", "fiddling", + "filling", "firmer", "fitted", "flammable", "flawed", + "fledgling", "fleshy", "flexible", "flickering", "floral", + "flowering", "flowing", "foggy", "folic", "foolhardy", + "foolish", "footy", "forehand", "forked", "formative", + "formulaic", "fractional", "fragrant", + "freakish", "freckled", "freelance", "freight", "fresh", + "fretted", "frugal", "fulfilling", "fuming", "funded", + "funny", "furry", "garbled", "gathered", "gendered", + "geologic", "geometric", "gibberish", "gilded", "ginger", + "glare", "glaring", "gleaming", "glorified", "glorious", + "goalless", "goody", "grammatical", "grande", + "grateful", "gratuitous", "graven", "greener", "grinding", + "grizzly", "groaning", "grudging", "guaranteed", "gusty", + "gypsy", "handheld", + "harlot", "healing", "healthier", "healthiest", + "heart", "heathen", "hedonistic", "heralded", + "hissy", "hitless", "holiness", "homesick", "homosexual", + "honorable", "hooded", "hopeless", "horrendous", "horrible", + "huddled", "human", "humbling", "humid", + "humiliating", "hypnotized", "idealistic", "idiosyncratic", "ignited", + "illustrated", "illustrative", "imitated", "immense", "immersive", + "impassive", "impressionable", "improbable", + "impulsive", "inattentive", "inbound", + "inbounds", "incalculable", "incomprehensible", "indefatigable", "indigenous", + "indigo", "indiscriminate", "indomitable", "inert", "inflate", + "inform", "inheriting", "injured", "injurious", "inking", + "inoffensive", "insane", "insensible", "insidious", "insincere", + "insistent", "insolent", "insufferable", "intemperate", "interdependent", + "interesting", "interfering", "intern", "interpreted", "intersecting", + "intuitive", "irresolute", "irritate", + "jerking", "joining", "joint", + "journalistic", "joyful", "keyed", "knowing", "lacklustre", + "laden", "lagging", "lamented", "laughable", "layered", + "leather", "leathern", "leery", "legible", + "leisure", "lessening", "liberating", "lifted", + "lightest", "limitless", "listening", "literary", "liver", + "livid", "lobster", "locked", + "loudest", "loveliest", + "lowering", "lucid", "luckless", + "luxurious", "magazine", "maniac", "manmade", "maroon", + "mastered", "mated", "material", "materialistic", "meaningful", + "measuring", "mediaeval", "medical", "meditated", "medley", + "melodic", "memorable", "memorial", "metabolic", "metallic", + "metallurgical", "metering", "midair", "midterm", "midway", + "mighty", "migrating", "minor", + "mirrored", "misguided", "misshapen", "mitigated", "mixed", + "modernized", "molecular", "monarch", "monastic", "morbid", + "motley", "motorized", "mounted", "multidisciplinary", + "muscled", "muscular", "muted", "mysterious", "mythic", + "natural", "nauseous", "negative", "networked", + "neurological", "neutered", "newest", "night", "nitrous", + "noncommercial", "nonsense", "north", "nuanced", + "occurring", "offensive", "oldest", "oncoming", + "onstage", "onward", "opaque", + "operating", "opportunist", "opposing", "ordinate", + "outdone", "outlaw", "outsized", "overboard", "overheated", + "oversize", "overworked", "oyster", "paced", "panting", + "paralyzed", "paramount", "parental", "parted", "partisan", + "passive", "pastel", "patriot", "peacekeeping", "pedestrian", + "peevish", "penal", "penned", "pensive", "perceptual", + "perky", "permissible", "pernicious", "perpetuate", "perplexed", + "pervasive", "philosophical", "picturesque", + "pillaged", "piped", "piquant", "pitching", "plausible", + "pliable", "plumb", "politician", "polygamous", + "pornographic", "portmanteau", "posed", "positive", "possible", + "postpartum", "prank", "precocious", "predicted", + "premium", "preparatory", "prerequisite", "prescient", "preserved", + "presidential", "pressed", "pressurized", "presumed", + "priced", "pricier", "primal", "primer", "primetime", + "printed", "private", "problem", "procedural", + "process", "prodigious", "professional", "programmed", "progressive", + "prolific", "promising", "promulgated", "pronged", "proportionate", + "protracted", "pulled", "pulsed", "quick", + "readable", "realizing", "recognised", "recovering", "recurrent", + "recycled", "redeemable", "reflecting", "regal", "registering", + "reliable", "reminiscent", "remorseless", "removable", "renewable", + "repeating", "repellent", "reserve", "resigned", "respectful", + "rested", "restrict", "resultant", "retaliatory", "retiring", + "revelatory", "reverend", "reversing", "revolving", "ridiculous", + "ringed", "risque", "robust", "roomful", + "rotating", "roused", "rubber", "running", + "runtime", "rustling", "safest", "salient", "sanctioned", + "saute", "saved", "scandalized", "scarlet", "scattering", + "sceptical", "scheming", "schizophrenic", "scoundrel", "scratched", + "scratchy", "scrolled", "seated", + "semiautomatic", "senior", "sensed", "sentient", + "sexier", "shadowy", "shaken", "shaker", "shameless", + "shaped", "shiny", "shipped", "shivering", "shoestring", + "short", "signed", "simplest", "simplistic", + "sizable", "skeleton", "skinny", "skirting", "skyrocketed", + "slamming", "slanting", "slapstick", "sleek", "sleepless", + "sleepy", "slender", "slimmer", "smacking", "smokeless", + "smothered", "smouldering", "snuff", "socialized", + "sometime", "sought", "spanking", "sparing", "spattered", + "specialized", "specific", "speedy", "spherical", "spiky", + "spineless", "sprung", "squint", "stainless", "standing", + "starlight", "startled", "stately", "statewide", "stereoscopic", + "sticky", "stimulant", "stinky", "stoked", "stolen", + "storied", "strained", "strapping", "strengthened", "stubborn", + "stylized", "suave", "subjective", "subjugated", "subordinate", + "succeeding", "suffering", "summary", "sunset", "sunshine", + "supernatural", "supervisory", "surrogate", "suspended", + "suspenseful", "swarthy", "sweating", "sweeping", "swinging", + "swooning", "sympathize", "synchronized", "synonymous", "synthetic", + "tailed", "tallest", "tangible", "tanked", "tarry", + "technical", "tectonic", "telepathic", "tenderest", "territorial", + "testimonial", "thicker", "threatening", + "timed", "timely", "timid", "torrent", + "totalled", "tougher", "traditional", "transformed", "transgendered", + "trapped", "traumatic", "traveled", "traverse", "treated", + "trial", "trunk", "trusting", "trying", "twisted", + "tyrannical", "unaided", "unassisted", "unassuming", + "unattractive", "uncapped", "uncomfortable", "uncontrolled", "uncooked", + "uncooperative", "underground", "undersea", "undisturbed", "unearthly", + "uneasy", "unequal", "unfazed", "unfinished", "unforeseen", + "unforgivable", "unidentified", "unimaginative", "uninspired", "unintended", + "uninvited", "universal", "unmasked", "unorthodox", "unparalleled", + "unpleasant", "unprincipled", "unread", "unreasonable", "unregulated", + "unreliable", "unremitting", "unsafe", "unsanitary", "unsealed", + "unsuccessful", "unsupervised", "untimely", "unwary", "unwrapped", + "uppity", "upstart", "useless", "utter", "valiant", + "valid", "valued", "vanilla", "vaulting", "vaunted", + "veering", "vegetative", "vented", "verbal", "verifying", + "veritable", "versed", "vinyl", "virgin", "visceral", + "visual", "voluptuous", "wanton", "warlike", + "washed", "waterproof", "waved", "weakest", + "wetting", "wheeled", "whirlwind", + "widen", "widening", "widow", "willful", "willing", + "winnable", "winningest", "wireless", "wistful", "woeful", + "wooded", "woodland", "wordless", "workable", "worldly", + "worldwide", "worsted", "worthless" +) + +nouns <- c( + "abbey", "absence", "absorption", + "absurdity", "abundance", "acceptance", + "accessibility", "accommodation", "accountability", "accounting", + "accreditation", "accuracy", "acquiescence", "acreage", "actress", + "actuality", "adage", "adaptation", "adherence", "adjustment", + "advancement", "advert", "advertisement", + "advertising", "advice", "aesthetics", "affinity", "aggression", + "agriculture", "aircraft", "airtime", "allegation", "allegiance", + "allegory", "allergy", "allies", "alligator", "allocation", + "allotment", "ammonia", "anatomy", + "ankle", "announcement", "annoyance", "annuity", + "anomaly", "anthropology", "anxiety", "apologise", + "apparatus", "appendix", + "applause", "appointment", "appraisal", "archery", "archipelago", + "architecture", "ardor", "arrears", "arrow", "artisan", + "artistry", "ascent", "assembly", "assignment", "association", + "auspices", "authority", "aversion", "aviation", + "babbling", "backlash", "baker", "ballet", + "banjo", "baron", "barrier", "barrister", "bases", + "basin", "basis", "battery", "battling", "bedtime", + "beginner", "begun", "bending", "bicycle", "billing", + "bingo", "biography", "biology", "birthplace", "blackberry", + "blather", "blossom", "boardroom", "boasting", "bodyguard", + "boldness", "bonding", "bones", + "bonus", "bookmark", "boomer", "booty", "bounds", + "bowling", "brainstorming", "breadth", "breaker", "brewer", + "brightness", "broccoli", "broth", "brotherhood", "browsing", + "brunch", "brunt", "building", "bullion", "bureaucracy", + "buyout", "cabal", "cabbage", + "calamity", "campaign", "canonization", "captaincy", + "carrier", "cartridge", "cassette", "catfish", "caught", + "celebrity", "cemetery", "certainty", "certification", "charade", + "chasm", "cheerleader", "cheesecake", + "chili", "chivalry", "cilantro", + "circus", "civilisation", "civility", "clearance", "clearing", + "clerk", "climber", "closeness", "clothing", "clutches", + "coaster", "coconut", "coding", "collaborator", "colleague", + "college", "collision", "colors", "combustion", "comedian", + "commander", "commemoration", "commenter", "commissioner", + "commune", "competition", "completeness", "complexity", "computing", + "comrade", "concur", "condominium", "conduit", "confidant", + "configuration", "conflagration", "conflict", "consist", + "consistency", "consolidation", "conspiracy", "constable", + "consultancy", "contentment", "contents", "contractor", "conversation", + "cornerstone", "corpus", "correlation", "counselor", + "countdown", "countryman", "coverage", "covering", "coyote", + "cracker", "criminality", "crocodile", "cropping", + "crossover", "crossroads", "culprit", "cumin", + "curator", "curfew", "cursor", "custard", "cutter", + "cyclist", "cyclone", "cylinder", "cynicism", "daddy", + "darkness", "dawning", "daybreak", "dealing", + "dedication", "deduction", "defection", "deference", "deficiency", + "definition", "deflation", "degeneration", "delegation", "delicacy", + "delirium", "demeanor", "demonstration", + "denomination", "dentist", "departure", "depletion", + "designation", "despotism", "detention", "developer", + "dexterity", "diagnosis", "dialect", "differentiation", + "digress", "dioxide", "diploma", + "discord", "discovery", "dismissal", + "dispatcher", "disservice", "distribution", "distributor", "diver", + "diversity", "docking", "dollar", + "dominion", "donkey", "doorstep", "doorway", + "downside", "drafting", "drank", "drilling", "driver", + "drumming", "duchess", "ducking", "dugout", + "dumps", "dwelling", "dynamics", "eagerness", "earnestness", + "earnings", "eater", "editor", "effectiveness", "electricity", + "elements", "eloquence", "emancipation", "embodiment", "embroidery", + "emperor", "employment", "encampment", "enclosure", "encouragement", + "endangerment", "enlightenment", "enthusiasm", "environment", "environs", + "envoy", "epilepsy", "equation", "equator", "error", + "espionage", "estimation", "evacuation", "exaggeration", + "examination", "exclamation", "expediency", "exploitation", "extinction", + "eyewitness", "falls", "fastball", + "feedback", "ferocity", "fertilization", "finale", + "firing", "fixing", "flashing", "flask", "flora", + "fluke", "folklore", "follower", "foothold", "footing", + "forefinger", "forefront", "forgiveness", "formality", "formation", + "formula", "foyer", "fragmentation", "framework", "fraud", + "freestyle", "frequency", "friendliness", "fries", "frigate", + "fulfillment", "function", "functionality", "fundraiser", "fusion", + "futility", "gallantry", "gallery", "genesis", "genitals", + "girlfriend", "glamour", "glitter", "glucose", "google", + "grandeur", "grappling", "greens", "gridlock", "grocer", + "groundwork", "grouping", "gunman", "gusto", "habitation", + "hacker", "hallway", "hamburger", "hammock", "handling", + "hands", "handshake", "happiness", "hardship", "headcount", + "header", "headquarters", "heads", "headset", "hearth", + "hearts", "heath", "hegemony", "height", "hello", + "helper", "helping", "hierarchy", "hoarding", + "hockey", "homeland", "homer", "honesty", + "horseman", "hostility", "housing", "humility", "hurricane", + "iceberg", "ignition", "illness", "illustration", + "illustrator", "immunity", "immunization", + "inaccuracy", "inaction", "inactivity", "inauguration", + "indicator", "inevitability", "infamy", "infiltration", "influx", + "iniquity", "innocence", "innovation", "inspiration", + "instruction", "instructor", "insurer", "interact", "intercession", + "intermission", "interpretation", "intersection", "interval", + "invasion", "investment", "involvement", + "irrigation", "iteration", "jogging", + "jones", "joseph", "juggernaut", "juncture", "jurisprudence", + "juror", "kangaroo", "kingdom", "knocking", "laborer", + "larceny", "laurels", "layout", "leadership", "leasing", + "legislation", "leopard", "liberation", "licence", "lifeblood", + "lifeline", "ligament", "lighting", "likeness", + "lineage", "liner", "lineup", "liquidation", "listener", + "literature", "litigation", "litre", "loathing", "locality", + "lodging", "logic", "longevity", "lookout", "lordship", + "lustre", "machinery", "madness", "magnificence", + "mahogany", "mailing", "mainframe", "maintenance", "majority", + "manga", "mango", "manifesto", "mantra", "manufacturer", + "maple", "martin", "martyrdom", "mathematician", "matrix", + "matron", "mayhem", "mayor", "means", "meantime", + "measurement", "mechanics", "mediator", "medics", "melodrama", + "memory", "mentality", "metaphysics", "method", "metre", + "miner", "mirth", "misconception", "misery", "mishap", + "misunderstanding", "mobility", "molasses", "momentum", "monarchy", + "monument", "morale", "mortality", "motto", "mouthful", + "mouthpiece", "mover", "movie", "mowing", "murderer", + "musician", "mutation", "mythology", "narration", "narrator", + "nationality", "negligence", "neighborhood", "neighbour", "nervousness", + "networking", "nexus", "nightmare", "nobility", "nobody", + "noodle", "normalcy", "notification", "nourishment", "novella", + "nucleus", "nuisance", "nursery", "nutrition", "nylon", + "oasis", "obscenity", "obscurity", "observer", "offense", + "onslaught", "operation", "opportunity", "opposition", "oracle", + "orchestra", "organisation", "organizer", "orientation", "originality", + "ounce", "outage", "outcome", "outdoors", "outfield", + "outing", "outpost", "outset", "overseer", "owner", + "oxygen", "pairing", "panther", "paradox", "parliament", + "parsley", "parson", "passenger", "pasta", "patchwork", + "pathos", "patriotism", "pendulum", "penguin", "permission", + "persona", "perusal", "pessimism", "peter", "philosopher", + "phosphorus", "phrasing", "physique", "piles", "plateau", + "playing", "plaza", "plethora", "plurality", "pneumonia", + "pointer", "poker", "policeman", "polling", "polygamy", + "poster", "posterity", "posting", "postponement", "potassium", + "pottery", "poultry", "pounding", "pragmatism", "precedence", + "precinct", "preoccupation", "pretense", "priesthood", "prisoner", + "privacy", "probation", "proceeding", "proceedings", "processing", + "processor", "progression", "projection", "prominence", "propensity", + "prophecy", "prorogation", "prospectus", "protein", "prototype", + "providence", "provider", "provocation", "proximity", "puberty", + "publicist", "publicity", "publisher", "pundit", "putting", + "quantity", "quart", "quilting", "quorum", "racism", + "radiance", "ralph", "rancher", "ranger", "rapidity", + "rapport", "ratification", "rationality", "reaction", "reader", + "reassurance", "receptor", "recipe", "recognition", + "recourse", "rector", "recurrence", + "redistribution", "redundancy", "refinery", "reformer", "refrigerator", + "regularity", "regulator", "reinforcement", "reinstatement", + "relativism", "relaxation", "repayment", + "repertoire", "repository", "republic", "reputation", + "residency", "resignation", "restaurant", "resurgence", "retailer", + "retention", "retirement", "reviewer", "riches", + "roadblock", "robber", "rocks", "rubbing", "runoff", + "sarcasm", "saucer", + "scarcity", "scenario", "scenery", "schism", "scholarship", + "schooner", "scissors", "scolding", "scooter", + "scouring", "scrimmage", "scrum", "seating", "sediment", + "semicolon", "semiconductor", "semifinal", "senator", + "sending", "serenity", "seriousness", "sesame", + "setup", "sewing", "sharpness", "shaving", "shoplifting", + "shopping", "siding", "simplicity", "simulation", "sinking", + "skate", "sloth", "slugger", "snack", "snail", + "snapshot", "snark", "soccer", "solemnity", "solicitation", + "solitude", "somewhere", "sophistication", "sorcery", "souvenir", + "spaghetti", "specification", "specimen", "specs", "spectacle", + "spectre", "speculation", "sperm", "spoiler", "squad", + "squid", "staging", "stagnation", "staircase", "stairway", + "stamina", "standpoint", "standstill", "stanza", "statement", + "stillness", "stimulus", "stocks", "stole", "stoppage", + "storey", "storyteller", "stylus", "subcommittee", "subscription", + "subsidy", "suburb", "success", "sufferer", "supposition", + "suspension", "sweater", "sweepstakes", "swimmer", "syndrome", + "synopsis", "syntax", "system", "tablespoon", "taker", + "tavern", "technology", "telephony", "template", "tempo", + "tendency", "tendon", "terrier", + "theater", "thicket", "thoroughfare", + "threshold", "thriller", "thunderstorm", "ticker", "tiger", + "tights", "tossing", "touchdown", "tourist", + "tourney", "tracing", "tractor", "translation", + "transmission", "transmitter", "traveler", "treadmill", + "trilogy", "trout", "tuning", "twenties", + "tycoon", "ultimatum", "underdog", + "unhappiness", "unification", "university", "uprising", + "validity", "vampire", "vanguard", "variation", "vegetation", + "verification", "viability", "vicinity", "victory", "viewpoint", + "villa", "vindication", "violation", "vista", "vocalist", + "vogue", "volcano", "voltage", "vulnerability", + "waistcoat", "waitress", "wardrobe", "warmth", "watchdog", + "wealth", "weariness", "whereabouts", + "widget", "width", "windfall", "wiring", + "withholding", "words", "workman" +) diff --git a/_pkgdown.yml b/_pkgdown.yml index 247d366..12b876b 100644 --- a/_pkgdown.yml +++ b/_pkgdown.yml @@ -39,3 +39,7 @@ reference: - title: IAM contents: - starts_with("aws_iam") + - title: Secrets manager + contents: + - starts_with("aws_secret") + - random_user diff --git a/man/aws_db_instance_status.Rd b/man/aws_db_instance_status.Rd index df79d14..3f8b6e6 100644 --- a/man/aws_db_instance_status.Rd +++ b/man/aws_db_instance_status.Rd @@ -10,7 +10,7 @@ aws_db_instance_status(id) \item{id}{(character) required. instance identifier. The identifier for this DB instance. This parameter is stored as a lowercase string. Constraints: must contain from 1 to 63 letters, numbers, or hyphens; first -character must be a letter; cn't end with a hyphen or contain two +character must be a letter; can't end with a hyphen or contain two consecutive hyphens. required.} } \value{ diff --git a/man/aws_db_rds_con.Rd b/man/aws_db_rds_con.Rd index 79ec2b8..d969b9e 100644 --- a/man/aws_db_rds_con.Rd +++ b/man/aws_db_rds_con.Rd @@ -5,8 +5,8 @@ \title{Get a database connection to Amazon RDS} \usage{ aws_db_rds_con( - user, - pwd, + user = NULL, + pwd = NULL, id = NULL, host = NULL, port = NULL, diff --git a/man/aws_db_rds_create.Rd b/man/aws_db_rds_create.Rd index ec5d885..0324bb7 100644 --- a/man/aws_db_rds_create.Rd +++ b/man/aws_db_rds_create.Rd @@ -7,8 +7,8 @@ aws_db_rds_create( id, class, - user, - pwd, + user = NULL, + pwd = NULL, dbname = "dev", engine = "mariadb", storage = 20, @@ -16,6 +16,7 @@ aws_db_rds_create( security_group_ids = NULL, wait = TRUE, verbose = TRUE, + aws_secrets = TRUE, ... ) } @@ -23,17 +24,19 @@ aws_db_rds_create( \item{id}{(character) required. instance identifier. The identifier for this DB instance. This parameter is stored as a lowercase string. Constraints: must contain from 1 to 63 letters, numbers, or hyphens; first -character must be a letter; cn't end with a hyphen or contain two +character must be a letter; can't end with a hyphen or contain two consecutive hyphens. required.} \item{class}{(character) required. The compute and memory capacity of the DB instance, for example \code{db.m5.large}.} \item{user}{(character) User name associated with the admin user account for -the cluster that is being created.} +the cluster that is being created. If \code{NULL}, we generate a random user +name, see \code{\link[=random_user]{random_user()}}} \item{pwd}{(character) Password associated with the admin user account for -the cluster that is being created.} +the cluster that is being created. If \code{NULL}, we generate a random password +with \code{\link[=aws_secrets_pwd]{aws_secrets_pwd()}} (which uses the AWS Secrets Manager service)} \item{dbname}{(character) The name of the first database to be created when the cluster is created. default: "dev". additional databases can be created @@ -58,6 +61,9 @@ until the cluster is available. If \code{wait=FALSE} use \item{verbose}{(logical) verbose informational output? default: \code{TRUE}} +\item{aws_secrets}{(logical) should we manage your database credentials +in AWS Secrets Manager? default: \code{TRUE}} + \item{...}{named parameters passed on to \href{https://www.paws-r-sdk.com/docs/rds_create_db_instance/}{create_db_instance}} } diff --git a/man/aws_secrets_all.Rd b/man/aws_secrets_all.Rd new file mode 100644 index 0000000..59955c7 --- /dev/null +++ b/man/aws_secrets_all.Rd @@ -0,0 +1,19 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/secrets_manager.R +\name{aws_secrets_all} +\alias{aws_secrets_all} +\title{Get all secret values} +\usage{ +aws_secrets_all() +} +\value{ +(list) list with secrets +} +\description{ +Get all secret values +} +\examples{ +\dontrun{ +aws_secrets_list() +} +} diff --git a/man/aws_secrets_create.Rd b/man/aws_secrets_create.Rd new file mode 100644 index 0000000..cc79151 --- /dev/null +++ b/man/aws_secrets_create.Rd @@ -0,0 +1,61 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/secrets_manager.R +\name{aws_secrets_create} +\alias{aws_secrets_create} +\title{Create a secret} +\usage{ +aws_secrets_create(name, secret, description = NULL, ...) +} +\arguments{ +\item{name}{(character) The name of the new secret. required} + +\item{secret}{(character/raw) The text or raw data to encrypt and store +in this new version of the secret. AWS recommends for text to use a JSON +structure of key/value pairs for your secret value (see examples below). +required} + +\item{description}{(character) The description of the secret. optional} + +\item{...}{further named parameters passed on to \code{create_secret} +\url{https://www.paws-r-sdk.com/docs/secretsmanager_create_secret/}} +} +\value{ +(list) with fields: +\itemize{ +\item ARN +\item Name +\item VersionId +\item ReplicationStatus +} +} +\description{ +This function does not create your database username and/or password. +Instead, it creates a "secret", which is typically a combination +of credentials (username + password + other metadata) +} +\details{ +Note that we autogenerate a random UUID to pass to the +\code{ClientRequestToken} parameter of the \code{paws} function \code{create_secret} +used internally in this function. + +This function creates a new secret. See \code{\link[=aws_secrets_update]{aws_secrets_update()}} to +update an existing secret. This function fails if you call it with +an existing secret with the same name or ARN +} +\examples{ +\dontrun{ +# Text secret +x <- aws_secrets_create( + name = "MyTestDatabaseSecret", + secret = '{"username":"david","password":"EXAMPLE-PASSWORD"}', + description = "My test database secret as a string" +) + +# Raw secret +x <- aws_secrets_create( + name = "MyRawDatabaseSecret", + secret = charToRaw('{"username":"david","password":"EXAMPLE-PASSWORD"}'), + description = "My test database secret as raw" +) +} +} diff --git a/man/aws_secrets_delete.Rd b/man/aws_secrets_delete.Rd new file mode 100644 index 0000000..f831bcf --- /dev/null +++ b/man/aws_secrets_delete.Rd @@ -0,0 +1,36 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/secrets_manager.R +\name{aws_secrets_delete} +\alias{aws_secrets_delete} +\title{Delete a secret} +\usage{ +aws_secrets_delete(id, ...) +} +\arguments{ +\item{id}{(character) The name or ARN of the secret. required} + +\item{...}{further named parameters passed on to \code{delete_secret} +\url{https://www.paws-r-sdk.com/docs/secretsmanager_delete_secret/}} +} +\value{ +(list) with fields: +\itemize{ +\item ARN +\item Name +\item DeletionDate +} +} +\description{ +Delete a secret +} +\examples{ +\dontrun{ +# Does exist +aws_secrets_delete(id = "MyTestDatabaseSecret") + +# Does not exist +# aws_secrets_get(id = "DoesntExist") +#> Error: ResourceNotFoundException (HTTP 400). Secrets Manager +#> can't find the specified secret. +} +} diff --git a/man/aws_secrets_get.Rd b/man/aws_secrets_get.Rd new file mode 100644 index 0000000..a9a341f --- /dev/null +++ b/man/aws_secrets_get.Rd @@ -0,0 +1,40 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/secrets_manager.R +\name{aws_secrets_get} +\alias{aws_secrets_get} +\title{Get a secret} +\usage{ +aws_secrets_get(id, ...) +} +\arguments{ +\item{id}{(character) The name or ARN of the secret. required} + +\item{...}{further named parameters passed on to \code{get_secret_value} +\url{https://www.paws-r-sdk.com/docs/secretsmanager_get_secret_value/}} +} +\value{ +(list) with fields: +\itemize{ +\item ARN +\item Name +\item VersionId +\item SecretBinary +\item SecretString +\item VersionStages +\item CreatedDate +} +} +\description{ +Get a secret +} +\examples{ +\dontrun{ +# Does exist +aws_secrets_get(id = "MyTestDatabaseSecret") + +# Does not exist +# aws_secrets_get(id = "DoesntExist") +#> Error: ResourceNotFoundException (HTTP 400). Secrets Manager +#> can't find the specified secret. +} +} diff --git a/man/aws_secrets_list.Rd b/man/aws_secrets_list.Rd new file mode 100644 index 0000000..f5b4491 --- /dev/null +++ b/man/aws_secrets_list.Rd @@ -0,0 +1,19 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/secrets_manager.R +\name{aws_secrets_list} +\alias{aws_secrets_list} +\title{List secrets} +\usage{ +aws_secrets_list() +} +\value{ +(list) list with secrets +} +\description{ +List secrets +} +\examples{ +\dontrun{ +aws_secrets_list() +} +} diff --git a/man/aws_secrets_pwd.Rd b/man/aws_secrets_pwd.Rd new file mode 100644 index 0000000..ad11e8c --- /dev/null +++ b/man/aws_secrets_pwd.Rd @@ -0,0 +1,21 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/secrets_manager.R +\name{aws_secrets_pwd} +\alias{aws_secrets_pwd} +\title{Get a random password} +\usage{ +aws_secrets_pwd(...) +} +\arguments{ +\item{...}{named parameters passed on to \code{get_random_password} +\url{https://www.paws-r-sdk.com/docs/secretsmanager_get_random_password/}} +} +\description{ +Get a random password +} +\examples{ +\dontrun{ +aws_secrets_pwd() +aws_secrets_pwd(ExcludeNumbers = TRUE) +} +} diff --git a/man/aws_secrets_rotate.Rd b/man/aws_secrets_rotate.Rd new file mode 100644 index 0000000..61c4f73 --- /dev/null +++ b/man/aws_secrets_rotate.Rd @@ -0,0 +1,46 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/secrets_manager.R +\name{aws_secrets_rotate} +\alias{aws_secrets_rotate} +\title{Rotate a secret} +\usage{ +aws_secrets_rotate(id, lambda_arn = NULL, rules = NULL, immediately = TRUE) +} +\arguments{ +\item{id}{(character) The name or ARN of the secret. required} + +\item{lambda_arn}{(character) The ARN of the Lambda rotation function. +Only supply for secrets that use a Lambda rotation function to rotate} + +\item{rules}{(list) asdfadf} + +\item{immediately}{(logical) whether to rotate the secret immediately or not. +default: \code{TRUE}} +} +\value{ +(list) with fields: +\itemize{ +\item ARN +\item Name +\item VersionId +} +} +\description{ +Rotate a secret +} +\details{ +Note that we autogenerate a random UUID to pass to the +\code{ClientRequestToken} parameter of the \code{paws} function used internally +} +\examples{ +\dontrun{ +aws_secrets_rotate(id = "MyTestDatabaseSecret") +aws_secrets_rotate(id = "MyTestDatabaseSecret", rules = list( + Duration = "2h", + ScheduleExpression = "cron(0 16 1,15 * ? *)" + ) +} +} +\references{ +\url{https://www.paws-r-sdk.com/docs/secretsmanager_rotate_secret/} +} diff --git a/man/aws_secrets_update.Rd b/man/aws_secrets_update.Rd new file mode 100644 index 0000000..587f26f --- /dev/null +++ b/man/aws_secrets_update.Rd @@ -0,0 +1,55 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/secrets_manager.R +\name{aws_secrets_update} +\alias{aws_secrets_update} +\title{Update a secret} +\usage{ +aws_secrets_update(id, secret, ...) +} +\arguments{ +\item{id}{(character) The name or ARN of the secret. required} + +\item{secret}{(character/raw) The text or raw data to encrypt and store +in this new version of the secret. AWS recommends for text to use a JSON +structure of key/value pairs for your secret value (see examples below). +required} + +\item{...}{further named parameters passed on to \code{put_secret_value} +\url{https://www.paws-r-sdk.com/docs/secretsmanager_put_secret_value/}} +} +\value{ +(list) with fields: +\itemize{ +\item ARN +\item Name +\item VersionId +\item VersionStages +} +} +\description{ +Update a secret +} +\details{ +Note that we autogenerate a random UUID to pass to the +\code{ClientRequestToken} parameter of the \code{paws} function used internally +} +\examples{ +\dontrun{ +# Create a secret +aws_secrets_create( + name = "TheSecret", + secret = '{"username":"jane","password":"cat"}', + description = "A string" +) + +aws_secrets_get("TheSecret") + +# Update the secret +aws_secrets_update( + id = "TheSecret", + secret = '{"username":"jane","password":"kitten"}' +) + +aws_secrets_get("TheSecret") +} +} diff --git a/man/construct_db_secret.Rd b/man/construct_db_secret.Rd new file mode 100644 index 0000000..b8826fe --- /dev/null +++ b/man/construct_db_secret.Rd @@ -0,0 +1,36 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/secrets_manager.R +\name{construct_db_secret} +\alias{construct_db_secret} +\title{Construct a database secret string or raw version of it} +\usage{ +construct_db_secret( + engine, + host = "", + username = "", + password = "", + dbname = "", + port = "", + as = "string" +) +} +\arguments{ +\item{engine, host, username, password, dbname, port}{supply parameters to +go into either a json string or raw version of the json string} + +\item{as}{(character) one of "string" or "raw"} +} +\description{ +Construct a database secret string or raw version of it +} +\examples{ +\dontrun{ +construct_db_secret("redshift", dbname = "hello", port = 5439) +construct_db_secret("mariadb", dbname = "world", port = 3306) +construct_db_secret("postgresql", dbname = "bears", port = 5432, as = "raw") +} +} +\references{ +\url{https://docs.aws.amazon.com/secretsmanager/latest/userguide/reference_secret_json_structure.html} +} +\keyword{internal} diff --git a/man/random_user.Rd b/man/random_user.Rd new file mode 100644 index 0000000..fcae495 --- /dev/null +++ b/man/random_user.Rd @@ -0,0 +1,20 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/random_user.R +\name{random_user} +\alias{random_user} +\title{Get a random user} +\usage{ +random_user() +} +\value{ +(character) a username with a random adjective plus a +random noun combined into one string, shortened to no longer than 16 +characters if longer than 16 +} +\description{ +Get a random user +} +\examples{ +random_user() +replicate(10, random_user()) +} diff --git a/man/ui_fetch_secret.Rd b/man/ui_fetch_secret.Rd new file mode 100644 index 0000000..36e470b --- /dev/null +++ b/man/ui_fetch_secret.Rd @@ -0,0 +1,41 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/ui_fetch_secret.R +\name{ui_fetch_secret} +\alias{ui_fetch_secret} +\title{Fetch secrets} +\usage{ +ui_fetch_secret(user = NULL, password = NULL, engine = NULL) +} +\arguments{ +\item{user}{(character) xx} + +\item{password}{(character) password} + +\item{engine}{(character) engine to filter secrets with. if not supplied +(\code{NULL}) all secrets are considered} +} +\description{ +Fetch secrets +} +\section{How the function works}{ + +\itemize{ +\item If user and password are supplied they are returned immediately +\item If user and password are not supplied, we fetch all secrets in +your secrets manager service, and then ask you which one you'd like +to use. If you choose none of them this function returns NULL for +both user and password +} +} + +\examples{ +\dontrun{ +# user,pwd supplied, return them right away at top of fxn +ui_fetch_secret(engine="mariadb", user="jane", password="apple") + +# user,pwd null +ui_fetch_secret(engine="redshift") +ui_fetch_secret(engine="mariadb") +} +} +\keyword{internal} diff --git a/tests/testthat/helper-vcr.R b/tests/testthat/helper-vcr.R index 17b12c0..19b3b98 100644 --- a/tests/testthat/helper-vcr.R +++ b/tests/testthat/helper-vcr.R @@ -14,3 +14,12 @@ invisible(vcr::vcr_configure( ) )) vcr::check_cassette_names() + +purge_secrets <- function(x) { + x <- aws_secrets_list() + if (length(x$SecretList) > 0) { + x$SecretList %>% + purrr::map_vec(purrr::pluck, "Name") %>% + purrr::map(aws_secrets_delete) + } +} diff --git a/tests/testthat/test-secrets_manager.R b/tests/testthat/test-secrets_manager.R new file mode 100644 index 0000000..842c2e4 --- /dev/null +++ b/tests/testthat/test-secrets_manager.R @@ -0,0 +1,82 @@ +test_that("aws_secrets_list", { + vcr::use_cassette("aws_secrets_list", { + purge_secrets() + res <- aws_secrets_list() + }) + + expect_type(res, "list") + expect_named(res, c("SecretList", "NextToken")) + expect_equal(length(res$SecretList), 0) +}) + +# Sys.sleep(5) # sleep to allow purge_secrets to finish aws side of deletion + +test_that("aws_secrets_create", { + secret_name <- "Testing6789" + + vcr::use_cassette("aws_secrets_create", { + x <- aws_secrets_create( + name = secret_name, + secret = '{"username":"bear","password":"apple"}', + description = "A note about the secret" + ) + }) + + expect_type(x, "list") + expect_named(x, c("ARN", "Name", "VersionId", "ReplicationStatus")) + expect_equal(x$Name, secret_name) + expect_match(x$ARN, "arn:aws") + expect_type(x$VersionId, "character") + + # cleanup + aws_secrets_delete(secret_name, ForceDeleteWithoutRecovery = TRUE) +}) + + +test_that("aws_secrets_get", { + vcr::use_cassette("aws_secrets_get_by_name", { + secret_name_thename <- "TestingTheThing1" + the_secret <- '{"username":"bear","password":"apple"}' + aws_secrets_create( + name = secret_name_thename, + secret = the_secret, + description = "A note about the secret" + ) + + res <- aws_secrets_get(secret_name_thename) + }) + + expect_type(res, "list") + expect_named(res, c( + "ARN", "Name", "VersionId", "SecretBinary", + "SecretString", "VersionStages", "CreatedDate" + )) + expect_equal(res$Name, secret_name_thename) + expect_match(res$ARN, "arn:aws") + expect_equal(res$SecretString, the_secret) + + vcr::use_cassette("aws_secrets_get_by_arn", { + secret_name_arn <- "TestingAnotherThing1" + the_secret <- '{"username":"deer","password":"bananas"}' + x <- aws_secrets_create( + name = secret_name_arn, + secret = the_secret, + description = "A quick note about the secret" + ) + + res <- aws_secrets_get(x$ARN) + }) + + expect_type(res, "list") + expect_named(res, c( + "ARN", "Name", "VersionId", "SecretBinary", + "SecretString", "VersionStages", "CreatedDate" + )) + expect_equal(res$Name, secret_name_arn) + expect_match(res$ARN, "arn:aws") + expect_equal(res$SecretString, the_secret) + + # cleanup + aws_secrets_delete(secret_name_thename, ForceDeleteWithoutRecovery = TRUE) + aws_secrets_delete(secret_name_arn, ForceDeleteWithoutRecovery = TRUE) +}) From cf40fb90222df7327fe938cd15594dd89842142a Mon Sep 17 00:00:00 2001 From: Scott Chamberlain Date: Fri, 12 Jan 2024 20:13:21 -0800 Subject: [PATCH 04/28] remove unused param from test helper fxn --- tests/testthat/helper-vcr.R | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/testthat/helper-vcr.R b/tests/testthat/helper-vcr.R index 19b3b98..86f74b9 100644 --- a/tests/testthat/helper-vcr.R +++ b/tests/testthat/helper-vcr.R @@ -15,7 +15,7 @@ invisible(vcr::vcr_configure( )) vcr::check_cassette_names() -purge_secrets <- function(x) { +purge_secrets <- function() { x <- aws_secrets_list() if (length(x$SecretList) > 0) { x$SecretList %>% From ba7b21fa1b62856c1a6b041735c86f7d148857ae Mon Sep 17 00:00:00 2001 From: Scott Chamberlain Date: Fri, 12 Jan 2024 21:53:22 -0800 Subject: [PATCH 05/28] gitignore fixtures --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index a427176..14e538f 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,4 @@ inst/doc # tests fixtures tests/fixtures/aws_db_rds_create.yml tests/fixtures/aws_db_redshift_create.yml +tests/fixtures/aws_secret*.yml From 55cb01ead3ad20ad1689744c43118921c45f97ec Mon Sep 17 00:00:00 2001 From: Scott Chamberlain Date: Fri, 12 Jan 2024 22:03:20 -0800 Subject: [PATCH 06/28] update man files --- man/path_as_s3.Rd | 6 ++++-- man/ui_fetch_secret.Rd | 6 +++--- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/man/path_as_s3.Rd b/man/path_as_s3.Rd index 8aae49a..3cbf479 100644 --- a/man/path_as_s3.Rd +++ b/man/path_as_s3.Rd @@ -20,8 +20,10 @@ Convert a s3 like path to a single format \dontshow{if (interactive()) (if (getRversion() >= "3.4") withAutoprint else force)(\{ # examplesIf} path_as_s3("http://s64-test-3.s3.amazonaws.com/") path_as_s3("https://s64-test-3.s3.amazonaws.com/") -path_as_s3(c("https://s64-test-3.s3.amazonaws.com/", - "https://mybucket.s3.amazonaws.com/")) +path_as_s3(c( + "https://s64-test-3.s3.amazonaws.com/", + "https://mybucket.s3.amazonaws.com/" +)) path_as_s3(c("apple", "banana", "pear", "pineapple")) \dontshow{\}) # examplesIf} } diff --git a/man/ui_fetch_secret.Rd b/man/ui_fetch_secret.Rd index 36e470b..e96af77 100644 --- a/man/ui_fetch_secret.Rd +++ b/man/ui_fetch_secret.Rd @@ -31,11 +31,11 @@ both user and password \examples{ \dontrun{ # user,pwd supplied, return them right away at top of fxn -ui_fetch_secret(engine="mariadb", user="jane", password="apple") +ui_fetch_secret(engine = "mariadb", user = "jane", password = "apple") # user,pwd null -ui_fetch_secret(engine="redshift") -ui_fetch_secret(engine="mariadb") +ui_fetch_secret(engine = "redshift") +ui_fetch_secret(engine = "mariadb") } } \keyword{internal} From 994abeefce53abb3c3ff82674e1cc7b0ee77739e Mon Sep 17 00:00:00 2001 From: Scott Chamberlain Date: Fri, 12 Jan 2024 22:03:26 -0800 Subject: [PATCH 07/28] skip some tests for now --- tests/testthat/test-db-rds.R | 3 ++- tests/testthat/test-db-redshift.R | 2 +- tests/testthat/test-secrets_manager.R | 2 ++ 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/tests/testthat/test-db-rds.R b/tests/testthat/test-db-rds.R index 6df06c9..f2dae3f 100644 --- a/tests/testthat/test-db-rds.R +++ b/tests/testthat/test-db-rds.R @@ -1,4 +1,5 @@ -skip("skipping until secrets stuff done") +skip() +skip_on_ci() test_that("aws_db_rds_create", { vcr::use_cassette("aws_db_rds_create", { diff --git a/tests/testthat/test-db-redshift.R b/tests/testthat/test-db-redshift.R index 7fa4fae..3755065 100644 --- a/tests/testthat/test-db-redshift.R +++ b/tests/testthat/test-db-redshift.R @@ -1,4 +1,4 @@ -skip("skipping until secrets stuff done") +skip_on_ci() test_that("aws_db_redshift_create", { vcr::use_cassette("aws_db_redshift_create", { diff --git a/tests/testthat/test-secrets_manager.R b/tests/testthat/test-secrets_manager.R index 842c2e4..0df50cf 100644 --- a/tests/testthat/test-secrets_manager.R +++ b/tests/testthat/test-secrets_manager.R @@ -1,3 +1,5 @@ +skip_on_ci() + test_that("aws_secrets_list", { vcr::use_cassette("aws_secrets_list", { purge_secrets() From 9397b2589311a76be033f901ba9bdeb74d9bf8d9 Mon Sep 17 00:00:00 2001 From: Scott Chamberlain Date: Fri, 12 Jan 2024 22:34:24 -0800 Subject: [PATCH 08/28] add make target that uses gitleaks for scanning for secrets --- Makefile | 7 +++++++ README.Rmd | 4 ++++ README.md | 4 ++++ 3 files changed, 15 insertions(+) diff --git a/Makefile b/Makefile index 69252a9..988a172 100644 --- a/Makefile +++ b/Makefile @@ -40,3 +40,10 @@ style_file: style_package: ${RSCRIPT} -e "styler::style_pkg()" + +scan_secrets: + @echo "scanning for leaks in commits\n" + gitleaks detect --source . -v + @echo "\n\n\n" + @echo "scanning for leaks in uncommitted files\n" + gitleaks protect --source . -v diff --git a/README.Rmd b/README.Rmd index b1d7b86..9d94896 100644 --- a/README.Rmd +++ b/README.Rmd @@ -16,6 +16,10 @@ Development version pak::pkg_install("getwilds/sixtyfour") ``` +## Scanning for secrets + +See the make target `scan_secrets` in the Makefile to scan for secrets. + ## Code of Conduct Please note that the sixtyfour project is released with a [Contributor Code of Conduct](https://contributor-covenant.org/version/2/1/CODE_OF_CONDUCT.html). By contributing to this project, you agree to abide by its terms. diff --git a/README.md b/README.md index a5b7286..ec624e0 100644 --- a/README.md +++ b/README.md @@ -17,6 +17,10 @@ Development version pak::pkg_install("getwilds/sixtyfour") ``` +## Scanning for secrets + +See the make target `scan_secrets` in the Makefile to scan for secrets. + ## Code of Conduct Please note that the sixtyfour project is released with a [Contributor Code of Conduct](https://contributor-covenant.org/version/2/1/CODE_OF_CONDUCT.html). By contributing to this project, you agree to abide by its terms. From 610d0cb14bb38e334999f4f3306b4e4ae3d0fcbb Mon Sep 17 00:00:00 2001 From: Scott Chamberlain Date: Fri, 12 Jan 2024 22:35:46 -0800 Subject: [PATCH 09/28] tweak --- tests/fixtures/aws_db_rds_create.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/fixtures/aws_db_rds_create.yml b/tests/fixtures/aws_db_rds_create.yml index 21c603d..2011c2e 100644 --- a/tests/fixtures/aws_db_rds_create.yml +++ b/tests/fixtures/aws_db_rds_create.yml @@ -68,7 +68,7 @@ http_interactions: uri: https://secretsmanager.<>.amazonaws.com/ body: encoding: '' - string: '{"Name":"xxxx","ClientRequestToken":"xxxx","SecretString":"{\"engine\":\"mariadb\",\"host\":{},\"username\":\"xxxxxx\",\"password\":\"xxxx\",\"dbname\":\"xxxxx\",\"port\":{}}"}' + string: '{"Name":"xxxx","zzz":"xxxx","SecretString":"{\"engine\":\"mariadb\",\"host\":{},\"username\":\"xxxxxx\",\"password\":\"xxxx\",\"dbname\":\"xxxxx\",\"port\":{}}"}' headers: Accept: application/json, text/xml, application/xml, */* User-Agent: paws/0.6.1 (R4.3.2; darwin20; aarch64) From 0c3ec259d15a1283b5cf1273a7283b3a0951c3c1 Mon Sep 17 00:00:00 2001 From: Scott Chamberlain Date: Tue, 16 Jan 2024 06:21:33 -0800 Subject: [PATCH 10/28] db-rds tests, skip on ci only --- tests/testthat/test-db-rds.R | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/testthat/test-db-rds.R b/tests/testthat/test-db-rds.R index f2dae3f..e891102 100644 --- a/tests/testthat/test-db-rds.R +++ b/tests/testthat/test-db-rds.R @@ -1,4 +1,3 @@ -skip() skip_on_ci() test_that("aws_db_rds_create", { From 7e903781fe589bb7cac267d447991310853bc924 Mon Sep 17 00:00:00 2001 From: Scott Chamberlain Date: Tue, 16 Jan 2024 08:28:13 -0800 Subject: [PATCH 11/28] gitignore aws_db_rds_create fixture --- .gitignore | 1 + tests/fixtures/aws_db_rds_create.yml | 96 ---------------------------- tests/testthat/test-db-rds.R | 12 ++-- 3 files changed, 7 insertions(+), 102 deletions(-) delete mode 100644 tests/fixtures/aws_db_rds_create.yml diff --git a/.gitignore b/.gitignore index 14e538f..2f16d77 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,4 @@ inst/doc tests/fixtures/aws_db_rds_create.yml tests/fixtures/aws_db_redshift_create.yml tests/fixtures/aws_secret*.yml +tests/fixtures/aws_db_rds_create.yml diff --git a/tests/fixtures/aws_db_rds_create.yml b/tests/fixtures/aws_db_rds_create.yml deleted file mode 100644 index 2011c2e..0000000 --- a/tests/fixtures/aws_db_rds_create.yml +++ /dev/null @@ -1,96 +0,0 @@ -http_interactions: -- request: - method: post - uri: https://rds.<>.amazonaws.com/ - body: - encoding: '' - string: Action=CreateDBInstance&AllocatedStorage=20&DBInstanceClass=db.t3.micro&DBInstanceIdentifier=xxxxxx&DBName=xxxxx&Engine=mariadb&MasterUserPassword=xxxx&MasterUsername=xxxxxx&StorageEncrypted=true&Version=2014-10-31&VpcSecurityGroupIds.VpcSecurityGroupId.1=sg-0e48eebcf03cb88fa - headers: - User-Agent: paws/0.6.1 (R4.3.2; darwin20; aarch64) - Accept: application/xml - Content-Type: application/x-www-form-urlencoded; charset=utf-8 - Content-Length: '284' - X-Amz-Date: 20240112T210858Z - Authorization: redacted - response: - status: - status_code: 200 - category: Success - reason: OK - message: 'Success: (200) OK' - headers: - x-amzn-requestid: d3abb957-dbcf-4d94-b2cb-831f16bc55fb - content-encoding: deflate - strict-transport-security: max-age=31536000 - content-type: text/xml - content-length: '1280' - date: Fri, 12 Jan 2024 21:08:58 GMT - body: - encoding: '' - file: no - string: 20default.mariadb10.6in-sync10.6.14xxxxxxxxxdb.t3.micro0falsefalse0creating1arn:aws:kms:<>:744061095407:key/760bf2ac-8827-433e-acf0-4b552d79fc0edefault:mariadb-10-6in-syncfalseregionxxx0db-LDEB7WUTQ4TCPLVMSJYFHO2JHU09:44-10:14falsearn:aws:rds:<>:744061095407:db:xxxxxxxxxxxxmariadbtruefalseIPV4falsexxxxxfalsetruevpc-0e5be60895b3f4ecbsubnet-0130e9da2b710d93eActive<>dsubnet-0cb7fc32b2ee6f854Active<>csubnet-03fd7f44bd2d67abfActive<>asubnet-08f773156ffb2009aActive<>bCompletedefaultdefaultsg-0e48eebcf03cb88faactivegeneral-public-license****sat:07:39-sat:08:09gp2truefalsed3abb957-dbcf-4d94-b2cb-831f16bc55fb - recorded_at: 2024-01-12 21:08:59 GMT - recorded_with: vcr/1.2.2.91, webmockr/0.9.0 -- request: - method: post - uri: https://rds.<>.amazonaws.com/ - body: - encoding: '' - string: Action=DescribeDBInstances&Version=2014-10-31 - headers: - User-Agent: paws/0.6.1 (R4.3.2; darwin20; aarch64) - Accept: application/xml - Content-Type: application/x-www-form-urlencoded; charset=utf-8 - Content-Length: '45' - X-Amz-Date: 20240112T210859Z - Authorization: redacted - response: - status: - status_code: 200 - category: Success - reason: OK - message: 'Success: (200) OK' - headers: - x-amzn-requestid: xxxx - content-encoding: deflate - strict-transport-security: max-age=31536000 - content-type: text/xml - content-length: '1325' - date: Fri, 12 Jan 2024 21:08:58 GMT - body: - encoding: '' - file: no - string: 20default.mariadb10.6in-syncfalse10.6.14xxxxxxxxxdb.t3.micro0falsefalse0creating1arn:aws:kms:<>:744061095407:key/760bf2ac-8827-433e-acf0-4b552d79fc0edefault:mariadb-10-6in-syncfalseregionxxx0db-LDEB7WUTQ4TCPLVMSJYFHO2JHU09:44-10:14falsearn:aws:rds:<>:744061095407:db:xxxxxxxxxxxxmariadbtruefalseIPV4stoppedfalsexxxxxfalsetruevpc-0e5be60895b3f4ecbsubnet-0130e9da2b710d93eActive<>dsubnet-0cb7fc32b2ee6f854Active<>csubnet-03fd7f44bd2d67abfActive<>asubnet-08f773156ffb2009aActive<>bCompletedefaultdefaultsg-0e48eebcf03cb88faactivegeneral-public-license****sat:07:39-sat:08:09gp2truefalsexxxx - recorded_at: 2024-01-12 21:08:59 GMT - recorded_with: vcr/1.2.2.91, webmockr/0.9.0 -- request: - method: post - uri: https://secretsmanager.<>.amazonaws.com/ - body: - encoding: '' - string: '{"Name":"xxxx","zzz":"xxxx","SecretString":"{\"engine\":\"mariadb\",\"host\":{},\"username\":\"xxxxxx\",\"password\":\"xxxx\",\"dbname\":\"xxxxx\",\"port\":{}}"}' - headers: - Accept: application/json, text/xml, application/xml, */* - User-Agent: paws/0.6.1 (R4.3.2; darwin20; aarch64) - X-Amz-Target: secretsmanager.CreateSecret - Content-Type: application/x-amz-json-1.1 - Content-Length: '225' - X-Amz-Date: 20240112T210859Z - Authorization: redacted - response: - status: - status_code: 200 - category: Success - reason: OK - message: 'Success: (200) OK' - headers: - x-amzn-requestid: 809ee892-fa87-4e0b-9387-2a5462522d8b - content-type: application/x-amz-json-1.1 - content-length: '165' - date: Fri, 12 Jan 2024 21:08:59 GMT - body: - encoding: '' - file: no - string: '{"ARN":"arn:aws:secretsmanager:<>:744061095407:secret:xxx","Name":"xxx","VersionId":"xxxx"}' - recorded_at: 2024-01-12 21:08:59 GMT - recorded_with: vcr/1.2.2.91, webmockr/0.9.0 diff --git a/tests/testthat/test-db-rds.R b/tests/testthat/test-db-rds.R index e891102..ff3fb75 100644 --- a/tests/testthat/test-db-rds.R +++ b/tests/testthat/test-db-rds.R @@ -1,12 +1,12 @@ -skip_on_ci() - test_that("aws_db_rds_create", { + skip_on_ci() vcr::use_cassette("aws_db_rds_create", { z <- aws_db_rds_create( - id = "aninstance", class = "db.t3.micro", - user = "xxx", pwd = "xxx", - security_group_ids = list("sg-xxxxxx"), - wait = FALSE, verbose = FALSE + id = "bananas", class = "db.t3.micro", + security_group_ids = list("sg-0ade14818d03997a4"), + BackupRetentionPeriod = 0, + wait = FALSE, + verbose = FALSE ) }) From 7e7a37b179a07840e2e85e2b3a9457706fb2b2e0 Mon Sep 17 00:00:00 2001 From: Scott Chamberlain Date: Tue, 16 Jan 2024 08:30:03 -0800 Subject: [PATCH 12/28] nolint commented line in a test --- tests/testthat/test-secrets_manager.R | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/testthat/test-secrets_manager.R b/tests/testthat/test-secrets_manager.R index 0df50cf..686d64a 100644 --- a/tests/testthat/test-secrets_manager.R +++ b/tests/testthat/test-secrets_manager.R @@ -11,7 +11,7 @@ test_that("aws_secrets_list", { expect_equal(length(res$SecretList), 0) }) -# Sys.sleep(5) # sleep to allow purge_secrets to finish aws side of deletion +# Sys.sleep(5) # sleep to allow purge_secrets to finish aws side of deletion # nolint test_that("aws_secrets_create", { secret_name <- "Testing6789" From b5f02261185af21986fc94285e6fd0d36875bb5a Mon Sep 17 00:00:00 2001 From: Scott Chamberlain Date: Tue, 16 Jan 2024 10:44:59 -0800 Subject: [PATCH 13/28] styling for a few files --- R/database-rds.R | 9 ++++++--- R/secrets_manager.R | 15 ++++++++------- man/aws_secrets_rotate.Rd | 6 +++--- man/construct_db_secret.Rd | 2 +- 4 files changed, 18 insertions(+), 14 deletions(-) diff --git a/R/database-rds.R b/R/database-rds.R index c6eb603..bde9ba2 100644 --- a/R/database-rds.R +++ b/R/database-rds.R @@ -28,7 +28,6 @@ aws_db_rds_con <- function( user = NULL, pwd = NULL, id = NULL, host = NULL, port = NULL, dbname = NULL, engine = NULL, ...) { - check_for_pkg("DBI") is_class(engine, "character") @@ -117,11 +116,15 @@ aws_db_rds_create <- aws_db_rds_client() if (is.null(user)) { user <- random_user() - if (verbose) cli::cli_alert_info("`user` is NULL; created user: {.strong {user}}") + if (verbose) { + cli::cli_alert_info("`user` is NULL; created user: {.strong {user}}") + } } if (is.null(pwd)) { pwd <- aws_secrets_pwd() - if (verbose) cli::cli_alert_info("`pwd` is NULL; created password: *******") + if (verbose) { + cli::cli_alert_info("`pwd` is NULL; created password: *******") + } } env64$rds$create_db_instance( DBName = dbname, DBInstanceIdentifier = id, diff --git a/R/secrets_manager.R b/R/secrets_manager.R index dcd75fe..55d7cc7 100644 --- a/R/secrets_manager.R +++ b/R/secrets_manager.R @@ -210,11 +210,13 @@ aws_secrets_delete <- function(id, ...) { #' @examples \dontrun{ #' aws_secrets_rotate(id = "MyTestDatabaseSecret") #' aws_secrets_rotate(id = "MyTestDatabaseSecret", rules = list( -#' Duration = "2h", -#' ScheduleExpression = "cron(0 16 1,15 * ? *)" -#' ) +#' Duration = "2h", +#' ScheduleExpression = "cron(0 16 1,15 * ? *)" +#' )) #' } -aws_secrets_rotate <- function(id, lambda_arn = NULL, rules = NULL, immediately = TRUE) { +aws_secrets_rotate <- function( + id, lambda_arn = NULL, rules = NULL, + immediately = TRUE) { env64$secretsmanager$rotate_secret( SecretId = id, ClientRequestToken = uuid::UUIDgenerate(), @@ -230,7 +232,8 @@ aws_secrets_rotate <- function(id, lambda_arn = NULL, rules = NULL, immediately #' go into either a json string or raw version of the json string #' @param as (character) one of "string" or "raw" #' @keywords internal -#' @references +#' @references +#' # nolint #' @examples \dontrun{ #' construct_db_secret("redshift", dbname = "hello", port = 5439) #' construct_db_secret("mariadb", dbname = "world", port = 3306) @@ -239,8 +242,6 @@ aws_secrets_rotate <- function(id, lambda_arn = NULL, rules = NULL, immediately construct_db_secret <- function( engine, host = "", username = "", password = "", dbname = "", port = "", as = "string") { - - dat <- list( "engine" = engine, "host" = host, diff --git a/man/aws_secrets_rotate.Rd b/man/aws_secrets_rotate.Rd index 61c4f73..898d55c 100644 --- a/man/aws_secrets_rotate.Rd +++ b/man/aws_secrets_rotate.Rd @@ -36,9 +36,9 @@ Note that we autogenerate a random UUID to pass to the \dontrun{ aws_secrets_rotate(id = "MyTestDatabaseSecret") aws_secrets_rotate(id = "MyTestDatabaseSecret", rules = list( - Duration = "2h", - ScheduleExpression = "cron(0 16 1,15 * ? *)" - ) + Duration = "2h", + ScheduleExpression = "cron(0 16 1,15 * ? *)" +)) } } \references{ diff --git a/man/construct_db_secret.Rd b/man/construct_db_secret.Rd index b8826fe..2dd942e 100644 --- a/man/construct_db_secret.Rd +++ b/man/construct_db_secret.Rd @@ -31,6 +31,6 @@ construct_db_secret("postgresql", dbname = "bears", port = 5432, as = "raw") } } \references{ -\url{https://docs.aws.amazon.com/secretsmanager/latest/userguide/reference_secret_json_structure.html} +\url{https://docs.aws.amazon.com/secretsmanager/latest/userguide/reference_secret_json_structure.html} # nolint } \keyword{internal} From 474b888ea76834d4ac950d5c546d66138b006954 Mon Sep 17 00:00:00 2001 From: Scott Chamberlain Date: Tue, 16 Jan 2024 10:49:55 -0800 Subject: [PATCH 14/28] linter detected - prefer <- and %>% over %<>% --- R/ui_fetch_secret.R | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/R/ui_fetch_secret.R b/R/ui_fetch_secret.R index de397bd..8181bc1 100644 --- a/R/ui_fetch_secret.R +++ b/R/ui_fetch_secret.R @@ -40,7 +40,7 @@ ui_fetch_secret <- function(user = NULL, password = NULL, engine = NULL) { Filter(function(x) length(x$host) > 0, new_secrets) %>% bind_rows() if (!is.null(engine)) { - new_secrets_df %<>% filter(engine == !!engine) + new_secrets_df <- filter(new_secrets_df, engine == !!engine) } if (NROW(new_secrets_df) == 0) { stop("No secrets found", call. = FALSE) From 313cfb6942dcda525332d369dc155f1102e9f00e Mon Sep 17 00:00:00 2001 From: Scott Chamberlain Date: Tue, 30 Jan 2024 10:06:49 -0800 Subject: [PATCH 15/28] change aws_db_rds_create and aws_db_redshift_create to return invisble() - update return docs for both fxns - change test for both fxns --- R/database-rds.R | 7 +++---- R/database-redshift.R | 7 +++---- man/aws_db_rds_create.Rd | 5 ++--- man/aws_db_redshift_create.Rd | 5 ++--- tests/testthat/test-db-rds.R | 6 ++---- tests/testthat/test-db-redshift.R | 6 ++---- 6 files changed, 14 insertions(+), 22 deletions(-) diff --git a/R/database-rds.R b/R/database-rds.R index bde9ba2..bf36425 100644 --- a/R/database-rds.R +++ b/R/database-rds.R @@ -105,9 +105,8 @@ aws_db_rds_con <- function( #' available for returning. That wait can be around 5 - 7 minutes. You can #' instead set `wait = FALSE` and then check on the status of the instance #' yourself in the AWS dashboard. -#' @return a list with methods for interfacing with RDS; -#' see . also prints useful -#' connection information after instance is available. +#' @return returns `NULL`, this function called for the side effect of +#' creating an RDS instance aws_db_rds_create <- function(id, class, user = NULL, pwd = NULL, dbname = "dev", engine = "mariadb", storage = 20, @@ -154,7 +153,7 @@ aws_db_rds_create <- ) } if (verbose) info(id, instance_con_info, "aws_db_rds_con") - return(env64$rds) + invisible() } #' Get the `paws` RDS client diff --git a/R/database-redshift.R b/R/database-redshift.R index 59b04f2..c23962f 100644 --- a/R/database-redshift.R +++ b/R/database-redshift.R @@ -93,9 +93,8 @@ aws_db_redshift_con <- function(user, pwd, id = NULL, host = NULL, port = NULL, #' @note See above link to `create_cluster` docs for details on requirements #' for each parameter #' @inheritSection aws_db_rds_create Waiting -#' @return a list with methods for interfacing with Redshift; -#' see . also prints useful -#' connection information after cluster is available. +#' @return returns `NULL`, this function called for the side effect of +#' creating an Redshift instance aws_db_redshift_create <- function(id, user, pwd, dbname = "dev", cluster_type = "multi-node", node_type = "dc2.large", number_nodes = 2, @@ -113,7 +112,7 @@ aws_db_redshift_create <- wait_for_cluster(id) } if (verbose) info(id, cluster_con_info, "aws_db_redshift_con") - return(env64$redshift) + invisible() } #' Get the `paws` Redshift client diff --git a/man/aws_db_rds_create.Rd b/man/aws_db_rds_create.Rd index 0324bb7..3cd4124 100644 --- a/man/aws_db_rds_create.Rd +++ b/man/aws_db_rds_create.Rd @@ -68,9 +68,8 @@ in AWS Secrets Manager? default: \code{TRUE}} \href{https://www.paws-r-sdk.com/docs/rds_create_db_instance/}{create_db_instance}} } \value{ -a list with methods for interfacing with RDS; -see \url{https://www.paws-r-sdk.com/docs/rds/}. also prints useful -connection information after instance is available. +returns \code{NULL}, this function called for the side effect of +creating an RDS instance } \description{ Create an RDS cluster diff --git a/man/aws_db_redshift_create.Rd b/man/aws_db_redshift_create.Rd index 40c5d17..bad3c5e 100644 --- a/man/aws_db_redshift_create.Rd +++ b/man/aws_db_redshift_create.Rd @@ -58,9 +58,8 @@ until the cluster is available. If \code{wait=FALSE} use \href{https://www.paws-r-sdk.com/docs/redshift_create_cluster/}{create_cluster}} } \value{ -a list with methods for interfacing with Redshift; -see \url{https://www.paws-r-sdk.com/docs/redshift/}. also prints useful -connection information after cluster is available. +returns \code{NULL}, this function called for the side effect of +creating an Redshift instance } \description{ Create a Redshift cluster diff --git a/tests/testthat/test-db-rds.R b/tests/testthat/test-db-rds.R index ff3fb75..7c266c2 100644 --- a/tests/testthat/test-db-rds.R +++ b/tests/testthat/test-db-rds.R @@ -10,10 +10,8 @@ test_that("aws_db_rds_create", { ) }) - # Note: the paws RDS client is just a list of fxns, hard - # to test it - expect_type(z, "list") - expect_type(z[[1]], "closure") + # retuns NULL b/c invisible() + expect_null(z) }) test_that("aws_db_rds_client", { diff --git a/tests/testthat/test-db-redshift.R b/tests/testthat/test-db-redshift.R index 3755065..f081b60 100644 --- a/tests/testthat/test-db-redshift.R +++ b/tests/testthat/test-db-redshift.R @@ -10,10 +10,8 @@ test_that("aws_db_redshift_create", { ) }) - # Note: the paws Redshift client is just a list of fxns, hard - # to test it - expect_type(z, "list") - expect_type(z[[1]], "closure") + # retuns NULL b/c invisible() + expect_null(z) }) test_that("aws_db_redshift_client", { From 7cd12420458236d0755e0859281187ce0d2aca38 Mon Sep 17 00:00:00 2001 From: Scott Chamberlain Date: Fri, 16 Feb 2024 13:48:30 -0800 Subject: [PATCH 16/28] modify aws_secrets_list to allow passing in params --- R/secrets_manager.R | 7 +++++-- man/aws_secrets_list.Rd | 9 ++++++++- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/R/secrets_manager.R b/R/secrets_manager.R index 55d7cc7..2fed0bf 100644 --- a/R/secrets_manager.R +++ b/R/secrets_manager.R @@ -1,11 +1,14 @@ #' List secrets #' @export +#' @param ... parameters passed on to the `paws` method +#' @note see +#' for available parameters #' @return (list) list with secrets #' @examples \dontrun{ #' aws_secrets_list() #' } -aws_secrets_list <- function() { - env64$secretsmanager$list_secrets() +aws_secrets_list <- function(...) { + env64$secretsmanager$list_secrets(...) } #' Get all secret values diff --git a/man/aws_secrets_list.Rd b/man/aws_secrets_list.Rd index f5b4491..82cb584 100644 --- a/man/aws_secrets_list.Rd +++ b/man/aws_secrets_list.Rd @@ -4,7 +4,10 @@ \alias{aws_secrets_list} \title{List secrets} \usage{ -aws_secrets_list() +aws_secrets_list(...) +} +\arguments{ +\item{...}{parameters passed on to the \code{paws} method} } \value{ (list) list with secrets @@ -12,6 +15,10 @@ aws_secrets_list() \description{ List secrets } +\note{ +see \url{https://www.paws-r-sdk.com/docs/secretsmanager_list_secrets/} +for available parameters +} \examples{ \dontrun{ aws_secrets_list() From c630033b6fce592431dde02e29030a96f8a33600 Mon Sep 17 00:00:00 2001 From: Scott Chamberlain Date: Fri, 16 Feb 2024 13:49:33 -0800 Subject: [PATCH 17/28] #29 add methods for security groups and vpcs, and use in RDS fxns --- DESCRIPTION | 5 +- NAMESPACE | 14 + R/database-rds.R | 10 +- R/globals.R | 1 + R/sixtyfour-package.R | 1 + R/vpc_security_groups.R | 332 +++++++++++++++++++++ R/vpcs.R | 36 +++ _pkgdown.yml | 9 + man/aws_db_rds_con.Rd | 4 +- man/aws_db_rds_create.Rd | 3 +- man/aws_ec2_client.Rd | 27 ++ man/aws_vpc.Rd | 37 +++ man/aws_vpc_security_group.Rd | 42 +++ man/aws_vpc_security_group_create.Rd | 63 ++++ man/aws_vpc_security_group_ingress.Rd | 52 ++++ man/aws_vpc_security_group_modify_rules.Rd | 44 +++ man/aws_vpc_security_groups.Rd | 32 ++ man/aws_vpcs.Rd | 23 ++ man/ip_address.Rd | 15 + man/ip_permissions_generator.Rd | 23 ++ 20 files changed, 765 insertions(+), 8 deletions(-) create mode 100644 R/vpc_security_groups.R create mode 100644 R/vpcs.R create mode 100644 man/aws_ec2_client.Rd create mode 100644 man/aws_vpc.Rd create mode 100644 man/aws_vpc_security_group.Rd create mode 100644 man/aws_vpc_security_group_create.Rd create mode 100644 man/aws_vpc_security_group_ingress.Rd create mode 100644 man/aws_vpc_security_group_modify_rules.Rd create mode 100644 man/aws_vpc_security_groups.Rd create mode 100644 man/aws_vpcs.Rd create mode 100644 man/ip_address.Rd create mode 100644 man/ip_permissions_generator.Rd diff --git a/DESCRIPTION b/DESCRIPTION index 951b467..e6a7b81 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -1,6 +1,6 @@ Package: sixtyfour Title: Humane Interface to AWS -Version: 0.0.0.97 +Version: 0.0.0.98 Authors@R: c( person("Sean", "Kross", role = "aut"), person("Scott", "Chamberlain", role = c("aut", "cre"), email = "sachamber@fredhutch.org") @@ -30,7 +30,8 @@ Imports: glue, memoise, uuid, - jsonlite + jsonlite, + curl Suggests: knitr, rmarkdown, diff --git a/NAMESPACE b/NAMESPACE index 788d193..50541df 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -17,6 +17,7 @@ export(aws_db_rds_create) export(aws_db_redshift_client) export(aws_db_redshift_con) export(aws_db_redshift_create) +export(aws_ec2_client) export(aws_file_attr) export(aws_file_copy) export(aws_file_delete) @@ -55,7 +56,15 @@ export(aws_user_current) export(aws_user_delete) export(aws_user_exists) export(aws_users) +export(aws_vpc) +export(aws_vpc_security_group) +export(aws_vpc_security_group_create) +export(aws_vpc_security_group_ingress) +export(aws_vpc_security_group_modify_rules) +export(aws_vpc_security_groups) +export(aws_vpcs) export(billing) +export(ip_permissions_generator) export(random_user) export(s3_path) export(set_s3_interface) @@ -70,6 +79,8 @@ importFrom(dplyr,pull) importFrom(fs,file_exists) importFrom(fs,fs_bytes) importFrom(glue,glue) +importFrom(jsonlite,fromJSON) +importFrom(jsonlite,toJSON) importFrom(lubridate,as_datetime) importFrom(magrittr,"%<>%") importFrom(magrittr,"%>%") @@ -79,9 +90,12 @@ importFrom(paws,rds) importFrom(paws,redshift) importFrom(paws,s3) importFrom(purrr,flatten) +importFrom(purrr,keep) importFrom(purrr,list_rbind) importFrom(purrr,map) importFrom(purrr,map_chr) +importFrom(purrr,map_lgl) +importFrom(purrr,pluck) importFrom(rlang,":=") importFrom(rlang,has_name) importFrom(s3fs,s3_dir_info) diff --git a/R/database-rds.R b/R/database-rds.R index c99a3b8..85bc0f4 100644 --- a/R/database-rds.R +++ b/R/database-rds.R @@ -1,13 +1,13 @@ #' Get a database connection to Amazon RDS #' -#' Supports: MariaDB, MySQL, and PostgreSQL +#' Supports: MariaDB, MySQL, and Postgres #' #' @export #' @inheritParams aws_db_redshift_con #' @param engine (character) The engine to use. optional if `user`, `pwd`, and #' `id` are supplied - otherwise required #' @details RDS supports many databases, but we only provide support for -#' MariaDB, MySQL, and PostgreSQL +#' MariaDB, MySQL, and Postgres #' #' If the `engine` you've chosen for your RDS instance is not supported #' with this function, you can likely connect to it on your own @@ -79,6 +79,7 @@ aws_db_rds_con <- function( #' the cluster is created. default: "dev". additional databases can be created #' within the cluster #' @param engine (character) The engine to use. default: "mariadb". required. +#' one of: mariadb, mysql, or postgres #' @param storage (character) The amount of storage in gibibytes (GiB) to #' allocate for the DB instance. default: 20 #' @param storage_encrypted (logical) Whether the DB instance is encrypted. @@ -127,6 +128,7 @@ aws_db_rds_create <- cli::cli_alert_info("`pwd` is NULL; created password: *******") } } + security_group_ids <- security_group_handler(security_group_ids, engine) env64$rds$create_db_instance( DBName = dbname, DBInstanceIdentifier = id, Engine = engine, DBInstanceClass = class, @@ -179,12 +181,14 @@ instance_details <- function() { } #' Get connection information for all instances +#' @importFrom purrr keep #' @inheritParams aws_db_redshift_create #' @return a list of cluster details #' @keywords internal instance_con_info <- function(id) { deets <- instance_details()$DBInstances - z <- Filter(function(x) x$DBInstanceIdentifier == id, deets)[[1]] + z <- keep(deets, \(x) x$DBInstanceIdentifier == id) + if (!length(z)) rlang::abort(glue("Instance identifier {id} not found")) list( host = z$Endpoint$Address, port = z$Endpoint$Port, diff --git a/R/globals.R b/R/globals.R index 2dd242f..febefad 100644 --- a/R/globals.R +++ b/R/globals.R @@ -10,5 +10,6 @@ utils::globalVariables(c( "secret_str", # "PasswordLastUsed", # "CreateDate", # + "IpPermissions", # NULL )) diff --git a/R/sixtyfour-package.R b/R/sixtyfour-package.R index 53cb19f..6d344af 100644 --- a/R/sixtyfour-package.R +++ b/R/sixtyfour-package.R @@ -12,5 +12,6 @@ #' @importFrom paws s3 iam costexplorer #' @importFrom s3fs s3_file_system #' @importFrom glue glue +#' @importFrom jsonlite toJSON fromJSON ## usethis namespace: end NULL diff --git a/R/vpc_security_groups.R b/R/vpc_security_groups.R new file mode 100644 index 0000000..26569eb --- /dev/null +++ b/R/vpc_security_groups.R @@ -0,0 +1,332 @@ +create_security_group <- function(engine) { + sg <- aws_vpc_security_group_create( + name = glue("{engine}-{paste0(sample(1:9, size = 4), collapse = '')}"), + engine = engine + ) + aws_vpc_security_group_ingress( + id = sg$GroupId, + ip_permissions = ip_permissions_generator(engine) + ) + sg$GroupId +} + +# FIXME: clean this mess up! +#' @importFrom purrr map_lgl pluck +#' @autoglobal +#' @keywords internal +security_group_handler <- function(ids, engine) { + if (!is.null(ids)) return(ids) + port <- engine2port(engine) + ip <- ip_address() + sgs <- aws_vpc_security_groups() + sgsdf <- jsonlite::fromJSON( + jsonlite::toJSON(sgs$SecurityGroups, auto_unbox = TRUE)) + + port_df <- dplyr::filter( + sgsdf, + map_lgl(IpPermissions, ~ .$ToPort == port) + ) + if (!NROW(port_df)) { + cli::cli_alert_danger(c( + "No security groups with access for ", + "{.strong {engine}} and port {.strong {port}}")) + cli::cli_alert_info(c( + "Creating security group with access for ", + "{.strong {engine}} and port {.strong {port}}")) + trysg <- tryCatch(create_security_group(engine), error = function(e) e) + if (rlang::is_error(trysg)) { + cli::cli_alert_danger(c( + "An error occurred while creating the security group; ", + "please use paramater {.strong security_group_ids}")) + return(NULL) + } else { + cli::cli_alert_success("Using security group {.strong {trysg}}") + return(trysg) + } + } + + ip_df <- dplyr::filter( + port_df, + map_lgl(IpPermissions, ~ any(grepl(ip, pluck(.$IpRanges, 1, "CidrIp")))) + ) + if (!NROW(ip_df)) { + cli::cli_alert_danger(c( + "Found security groups w/ access for {.strong {engine}}, ", + "{.emph but} not with your IP address {.strong {ip}}")) + cli::cli_alert_info("Which security group do you want to modify?") + pick_sg_options <- + port_df %>% + glue::glue_data( + "Security Group: {GroupId}\n", + " Group Name: {GroupName}\n", + " Description: {Description}", + .trim = FALSE + ) %>% + as.character() + + picked <- picker(c( + glue("We found {length(pick_sg_options)} security groups"), + "Which security group do you want to use?" + ), pick_sg_options) + + if (picked == 0) { + cli::cli_alert_danger( + "No security group selected; please use paramater {.strong security_group_ids}") + return(NULL) + } else { + picked_id <- port_df[picked, "GroupId"] + } + cli::cli_alert_info( + "Adding your IP address {.strong {ip}} to security group {.strong {picked_id}}") + try_ingress <- tryCatch({ + aws_vpc_security_group_ingress( + id = picked_id, + ip_permissions = ip_permissions_generator(engine) + ) + }, error = function(e) e) + if (rlang::is_error(try_ingress)) { + cli::cli_alert_danger(c( + "An error occurred while creating the security group; ", + "please use paramater {.strong security_group_ids}")) + return(NULL) + } else { + cli::cli_alert_success("Using security group {.strong {try_ingress}}") + return(try_ingress) + } + } + + if (NROW(ip_df) == 1) { + cli::cli_alert_success(c( + "Found security group {.strong {ip_df$GroupId}} ", + "w/ access for {.strong {engine}} and your IP address {.strong {ip}}")) + return(ip_df$GroupId) + } else { + sgoptions <- + ip_df %>% + glue::glue_data( + "Security Group: {GroupId}\n", + " Group Name: {GroupName}\n", + " Description: {Description}", + .trim = FALSE + ) %>% + as.character() + + picked <- picker(c( + glue("We found {length(sgoptions)} matching security groups"), + "Which security group do you want to use?" + ), sgoptions) + + if (picked == 0) { + cli::cli_alert_danger(c( + "Found security group {.strong {ip_df$GroupId}} ", + "w/ access for {.strong {engine}}, {.emph but} not with your IP address {.strong {ip}}")) + return(NULL) + } else { + idtouse <- ip_df[picked, "GroupId"] + cli::cli_alert_success("Using security group {.strong {idtouse}}") + return(idtouse) + } + } +} + + +#' List VPC security groups +#' @export +#' @param ... named parameters passed on to [describe_security_groups]( +#' https://www.paws-r-sdk.com/docs/ec2_describe_security_groups/) +#' @return (list) list with security groups +#' @family security groups +#' @examplesIf interactive() +#' aws_vpc_security_groups() +#' aws_vpc_security_groups(MaxResults = 6) +aws_vpc_security_groups <- function(...) { + aws_ec2_client() + env64$ec2$describe_security_groups(...) +} + +#' Get a security group by ID +#' @export +#' @param id (character) The id of the security group. required +#' @inheritParams aws_vpc_security_groups +#' @family security groups +#' @return (list) with fields: +#' - SecurityGroups (list) each security group +#' - Description +#' - GroupName +#' - IpPermissions +#' - OwnerId +#' - GroupId +#' - IpPermissionsEgress +#' - Tags +#' - VpcId +#' - NextToken (character) token for paginating +aws_vpc_security_group <- function(id, ...) { + aws_ec2_client() + aws_vpc_security_groups(GroupIds = id, ...) +} + +#' Create a security group +#' @export +#' @param name (character) The name of the new secret. required +#' @param engine (character) The engine to use. default: "mariadb". required. +#' one of: mariadb, mysql, or postgres +#' @param description (character) The description of the secret. optional +#' @param vpc_id (character) a VPC id. optional. if not supplied your default +#' VPC is used. To get your VPCs, see [aws_vpcs()] +#' @param tags (character) The tags to assign to the security group. optional +#' @param ... named parameters passed on to [create_secret]( +#' https://www.paws-r-sdk.com/docs/secretsmanager_create_secret/) +#' @return (list) with fields: +#' - GroupId (character) +#' - Tags (list) +#' @family security groups +#' @examples \dontrun{ +#' # create security group +#' x <- aws_vpc_security_group_create( +#' name = "testing1", +#' description = "Testing security group creation" +#' ) +#' # add ingress +#' aws_vpc_security_group_ingress( +#' id = x$GroupId, +#' ip_permissions = ip_permissions_generator("mariadb") +#' ) +#' } +aws_vpc_security_group_create <- function(name, engine, description = NULL, + vpc_id = NULL, tags = NULL, ...) { + + aws_ec2_client() + if (is.null(description)) { + description <- glue("Access to {engine}") + } + env64$ec2$create_security_group( + Description = description, + GroupName = name, + VpcId = vpc_id, + TagSpecifications = tags, + ... + ) +} + +engine2port <- function(engine) { + switch(engine, + mariadb = 3306L, + mysql = 3306L, + postgres = 5432L, + stop(glue::glue("{engine} not currently supported")) + ) +} + +#' Ip Permissions generator +#' +#' @export +#' @param engine (character) one of mariadb, mysql, or postgres +#' @param port (character) port number. port determined from `engine` +#' if `port` not given. default: `NULL` +#' @param description (character) description. if not given, autogenerated +#' depending on value of `engine` +#' @return a list with slots: FromPort, ToPort, IpProtocol, and IpRanges +ip_permissions_generator <- function(engine, port = NULL, description = NULL) { + protocol <- "tcp" + port <- engine2port(engine) + if (is.null(description)) { + description <- glue("Access for {Sys.info()[['user']]} from sixtyfour") + } + list( + FromPort = port, + ToPort = port, + IpProtocol = protocol, + IpRanges = list( + list( + CidrIp = glue("{ip_address()}/32"), + Description = description + ) + ) + ) +} + +#' Get your IP address using +#' @return (character) ip address +#' @keywords internal +ip_address <- function() { + res <- curl::curl_fetch_memory("https://ifconfig.me/ip") + rawToChar(res$content) +} + +#' Authorize Security Group Ingress +#' @export +#' @param id (character) security group id +#' @param ip_permissions (list) list of persmissions. see link to `paws` +#' docs below or use [ip_permissions_generator()] to generate the +#' list for this parameter +#' @param ... named parameters passed on to +#' [authorize_security_group_ingress]( +#' https://www.paws-r-sdk.com/docs/ec2_authorize_security_group_ingress/) +#' @family security groups +#' @return list with slots: +#' - Return (boolean) +#' - SecurityGroupRules (list) +#' - SecurityGroupRuleId +#' - GroupId +#' - GroupOwnerId +#' - IsEgress +#' - IpProtocol +#' - FromPort +#' - ToPort +#' - CidrIpv4 +#' - CidrIpv6 +#' - PrefixListId +#' - ReferencedGroupInfo +#' - Description +#' - Tags +aws_vpc_security_group_ingress <- function(id = NULL, + ip_permissions = NULL, ...) { + + aws_ec2_client() + env64$ec2$authorize_security_group_ingress( + GroupId = id, + IpPermissions = list(ip_permissions), + ... + ) +} + +#' Modify security group rules +#' @export +#' @param id (character) security group id +#' @param rules list of rules to add/modify on the security group `id` +#' @param ... named parameters passed on to [modify_security_group_rules]( +#' https://www.paws-r-sdk.com/docs/ec2_modify_security_group_rules/) +#' @family security groups +#' @examplesIf interactive() +#' aws_vpc_security_group_modify_rules( +#' id = "someid", +#' rules = list( +#' SecurityGroupRuleId = "sgr-07de36a0521f39c8b", +#' SecurityGroupRule = list( +#' IpProtocol = "tcp", +#' FromPort = 22, +#' ToPort = 22, +#' CidrIpv4 = "3.3.3.3/32", +#' Description = "added ssh port" +#' ) +#' ) +#' ) +aws_vpc_security_group_modify_rules <- function(id, rules, ...) { + aws_ec2_client() + env64$ec2$modify_security_group_rules( + GroupId = id, + SecurityGroupRules = list(rules), + ... + ) +} + +#' Get the `paws` EC2 client - primarily for usage of VPC security groups +#' @export +#' @note returns existing client if found; a new client otherwise +#' @family security groups +#' @return a list with methods for interfacing with EC2; +#' see +aws_ec2_client <- function() { + if (is.null(env64$ec2)) env64$ec2 <- paws::ec2() + return(env64$ec2) +} diff --git a/R/vpcs.R b/R/vpcs.R new file mode 100644 index 0000000..56dadde --- /dev/null +++ b/R/vpcs.R @@ -0,0 +1,36 @@ +#' List VPCs +#' @export +#' @param ... parameters passed on to [describe_vpcs]( +#' https://www.paws-r-sdk.com/docs/ec2_describe_vpcs/) +#' @return (list) list with VPCs +#' @examplesIf interactive() +#' aws_vpcs() +#' aws_vpcs(MaxResults=6) +aws_vpcs <- function(...) { + aws_ec2_client() + env64$ec2$describe_vpcs(...) +} + +#' Get a VPC by id +#' @export +#' @param id (character) The id of the VPC. required +#' @inheritParams aws_vpcs +#' @return (list) with fields: +#' - Vpcs (list) each VPC group +#' - NextToken (character) token for paginating +#' +#' Each element of Vpcs is a list with slots: +#' - CidrBlock +#' - DhcpOptionsId +#' - State +#' - VpcId +#' - OwnerId +#' - InstanceTenancy +#' - Ipv6CidrBlockAssociationSet +#' - CidrBlockAssociationSet +#' - IsDefault +#' - Tags +aws_vpc <- function(id, ...) { + aws_ec2_client() + aws_vpcs(VpcIds = id, ...) +} diff --git a/_pkgdown.yml b/_pkgdown.yml index 12b876b..ee1f207 100644 --- a/_pkgdown.yml +++ b/_pkgdown.yml @@ -43,3 +43,12 @@ reference: contents: - starts_with("aws_secret") - random_user + - title: VPCs + contents: + - aws_vpc + - aws_vpcs + - title: VPC Security Groups + contents: + - starts_with("aws_vpc_security_group") + - aws_ec2_client + - ip_permissions_generator diff --git a/man/aws_db_rds_con.Rd b/man/aws_db_rds_con.Rd index d3a3960..b6e45d6 100644 --- a/man/aws_db_rds_con.Rd +++ b/man/aws_db_rds_con.Rd @@ -30,11 +30,11 @@ those you used to create the cluster with \code{\link[=aws_db_redshift_create]{a \code{id} are supplied - otherwise required} } \description{ -Supports: MariaDB, MySQL, and PostgreSQL +Supports: MariaDB, MySQL, and Postgres } \details{ RDS supports many databases, but we only provide support for -MariaDB, MySQL, and PostgreSQL +MariaDB, MySQL, and Postgres If the \code{engine} you've chosen for your RDS instance is not supported with this function, you can likely connect to it on your own diff --git a/man/aws_db_rds_create.Rd b/man/aws_db_rds_create.Rd index bf60070..2f670f2 100644 --- a/man/aws_db_rds_create.Rd +++ b/man/aws_db_rds_create.Rd @@ -42,7 +42,8 @@ with \code{\link[=aws_secrets_pwd]{aws_secrets_pwd()}} (which uses the AWS Secre the cluster is created. default: "dev". additional databases can be created within the cluster} -\item{engine}{(character) The engine to use. default: "mariadb". required.} +\item{engine}{(character) The engine to use. default: "mariadb". required. +one of: mariadb, mysql, or postgres} \item{storage}{(character) The amount of storage in gibibytes (GiB) to allocate for the DB instance. default: 20} diff --git a/man/aws_ec2_client.Rd b/man/aws_ec2_client.Rd new file mode 100644 index 0000000..0808fd8 --- /dev/null +++ b/man/aws_ec2_client.Rd @@ -0,0 +1,27 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/vpc_security_groups.R +\name{aws_ec2_client} +\alias{aws_ec2_client} +\title{Get the \code{paws} EC2 client - primarily for usage of VPC security groups} +\usage{ +aws_ec2_client() +} +\value{ +a list with methods for interfacing with EC2; +see \url{https://www.paws-r-sdk.com/docs/ec2/} +} +\description{ +Get the \code{paws} EC2 client - primarily for usage of VPC security groups +} +\note{ +returns existing client if found; a new client otherwise +} +\seealso{ +Other security groups: +\code{\link{aws_vpc_security_group_create}()}, +\code{\link{aws_vpc_security_group_ingress}()}, +\code{\link{aws_vpc_security_group_modify_rules}()}, +\code{\link{aws_vpc_security_groups}()}, +\code{\link{aws_vpc_security_group}()} +} +\concept{security groups} diff --git a/man/aws_vpc.Rd b/man/aws_vpc.Rd new file mode 100644 index 0000000..51c29ac --- /dev/null +++ b/man/aws_vpc.Rd @@ -0,0 +1,37 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/vpcs.R +\name{aws_vpc} +\alias{aws_vpc} +\title{Get a VPC by id} +\usage{ +aws_vpc(id, ...) +} +\arguments{ +\item{id}{(character) The id of the VPC. required} + +\item{...}{parameters passed on to \href{https://www.paws-r-sdk.com/docs/ec2_describe_vpcs/}{describe_vpcs}} +} +\value{ +(list) with fields: +\itemize{ +\item Vpcs (list) each VPC group +\item NextToken (character) token for paginating +} + +Each element of Vpcs is a list with slots: +\itemize{ +\item CidrBlock +\item DhcpOptionsId +\item State +\item VpcId +\item OwnerId +\item InstanceTenancy +\item Ipv6CidrBlockAssociationSet +\item CidrBlockAssociationSet +\item IsDefault +\item Tags +} +} +\description{ +Get a VPC by id +} diff --git a/man/aws_vpc_security_group.Rd b/man/aws_vpc_security_group.Rd new file mode 100644 index 0000000..adf2fc2 --- /dev/null +++ b/man/aws_vpc_security_group.Rd @@ -0,0 +1,42 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/vpc_security_groups.R +\name{aws_vpc_security_group} +\alias{aws_vpc_security_group} +\title{Get a security group by ID} +\usage{ +aws_vpc_security_group(id, ...) +} +\arguments{ +\item{id}{(character) The id of the security group. required} + +\item{...}{named parameters passed on to \href{https://www.paws-r-sdk.com/docs/ec2_describe_security_groups/}{describe_security_groups}} +} +\value{ +(list) with fields: +\itemize{ +\item SecurityGroups (list) each security group +\itemize{ +\item Description +\item GroupName +\item IpPermissions +\item OwnerId +\item GroupId +\item IpPermissionsEgress +\item Tags +\item VpcId +} +\item NextToken (character) token for paginating +} +} +\description{ +Get a security group by ID +} +\seealso{ +Other security groups: +\code{\link{aws_ec2_client}()}, +\code{\link{aws_vpc_security_group_create}()}, +\code{\link{aws_vpc_security_group_ingress}()}, +\code{\link{aws_vpc_security_group_modify_rules}()}, +\code{\link{aws_vpc_security_groups}()} +} +\concept{security groups} diff --git a/man/aws_vpc_security_group_create.Rd b/man/aws_vpc_security_group_create.Rd new file mode 100644 index 0000000..22f0a00 --- /dev/null +++ b/man/aws_vpc_security_group_create.Rd @@ -0,0 +1,63 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/vpc_security_groups.R +\name{aws_vpc_security_group_create} +\alias{aws_vpc_security_group_create} +\title{Create a security group} +\usage{ +aws_vpc_security_group_create( + name, + engine, + description = NULL, + vpc_id = NULL, + tags = NULL, + ... +) +} +\arguments{ +\item{name}{(character) The name of the new secret. required} + +\item{engine}{(character) The engine to use. default: "mariadb". required. +one of: mariadb, mysql, or postgres} + +\item{description}{(character) The description of the secret. optional} + +\item{vpc_id}{(character) a VPC id. optional. if not supplied your default +VPC is used. To get your VPCs, see \code{\link[=aws_vpcs]{aws_vpcs()}}} + +\item{tags}{(character) The tags to assign to the security group. optional} + +\item{...}{named parameters passed on to \href{https://www.paws-r-sdk.com/docs/secretsmanager_create_secret/}{create_secret}} +} +\value{ +(list) with fields: +\itemize{ +\item GroupId (character) +\item Tags (list) +} +} +\description{ +Create a security group +} +\examples{ +\dontrun{ +# create security group +x <- aws_vpc_security_group_create( + name = "testing1", + description = "Testing security group creation" +) +# add ingress +aws_vpc_security_group_ingress( + id = x$GroupId, + ip_permissions = ip_permissions_generator("mariadb") +) +} +} +\seealso{ +Other security groups: +\code{\link{aws_ec2_client}()}, +\code{\link{aws_vpc_security_group_ingress}()}, +\code{\link{aws_vpc_security_group_modify_rules}()}, +\code{\link{aws_vpc_security_groups}()}, +\code{\link{aws_vpc_security_group}()} +} +\concept{security groups} diff --git a/man/aws_vpc_security_group_ingress.Rd b/man/aws_vpc_security_group_ingress.Rd new file mode 100644 index 0000000..df5c51c --- /dev/null +++ b/man/aws_vpc_security_group_ingress.Rd @@ -0,0 +1,52 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/vpc_security_groups.R +\name{aws_vpc_security_group_ingress} +\alias{aws_vpc_security_group_ingress} +\title{Authorize Security Group Ingress} +\usage{ +aws_vpc_security_group_ingress(id = NULL, ip_permissions = NULL, ...) +} +\arguments{ +\item{id}{(character) security group id} + +\item{ip_permissions}{(list) list of persmissions. see link to \code{paws} +docs below or use \code{\link[=ip_permissions_generator]{ip_permissions_generator()}} to generate the +list for this parameter} + +\item{...}{named parameters passed on to +\href{https://www.paws-r-sdk.com/docs/ec2_authorize_security_group_ingress/}{authorize_security_group_ingress}} +} +\value{ +list with slots: +\itemize{ +\item Return (boolean) +\item SecurityGroupRules (list) +\itemize{ +\item SecurityGroupRuleId +\item GroupId +\item GroupOwnerId +\item IsEgress +\item IpProtocol +\item FromPort +\item ToPort +\item CidrIpv4 +\item CidrIpv6 +\item PrefixListId +\item ReferencedGroupInfo +\item Description +\item Tags +} +} +} +\description{ +Authorize Security Group Ingress +} +\seealso{ +Other security groups: +\code{\link{aws_ec2_client}()}, +\code{\link{aws_vpc_security_group_create}()}, +\code{\link{aws_vpc_security_group_modify_rules}()}, +\code{\link{aws_vpc_security_groups}()}, +\code{\link{aws_vpc_security_group}()} +} +\concept{security groups} diff --git a/man/aws_vpc_security_group_modify_rules.Rd b/man/aws_vpc_security_group_modify_rules.Rd new file mode 100644 index 0000000..523fa17 --- /dev/null +++ b/man/aws_vpc_security_group_modify_rules.Rd @@ -0,0 +1,44 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/vpc_security_groups.R +\name{aws_vpc_security_group_modify_rules} +\alias{aws_vpc_security_group_modify_rules} +\title{Modify security group rules} +\usage{ +aws_vpc_security_group_modify_rules(id, rules, ...) +} +\arguments{ +\item{id}{(character) security group id} + +\item{rules}{list of rules to add/modify on the security group \code{id}} + +\item{...}{named parameters passed on to \href{https://www.paws-r-sdk.com/docs/ec2_modify_security_group_rules/}{modify_security_group_rules}} +} +\description{ +Modify security group rules +} +\examples{ +\dontshow{if (interactive()) (if (getRversion() >= "3.4") withAutoprint else force)(\{ # examplesIf} +aws_vpc_security_group_modify_rules( + id = "someid", + rules = list( + SecurityGroupRuleId = "sgr-07de36a0521f39c8b", + SecurityGroupRule = list( + IpProtocol = "tcp", + FromPort = 22, + ToPort = 22, + CidrIpv4 = "3.3.3.3/32", + Description = "added ssh port" + ) + ) +) +\dontshow{\}) # examplesIf} +} +\seealso{ +Other security groups: +\code{\link{aws_ec2_client}()}, +\code{\link{aws_vpc_security_group_create}()}, +\code{\link{aws_vpc_security_group_ingress}()}, +\code{\link{aws_vpc_security_groups}()}, +\code{\link{aws_vpc_security_group}()} +} +\concept{security groups} diff --git a/man/aws_vpc_security_groups.Rd b/man/aws_vpc_security_groups.Rd new file mode 100644 index 0000000..1c97b7f --- /dev/null +++ b/man/aws_vpc_security_groups.Rd @@ -0,0 +1,32 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/vpc_security_groups.R +\name{aws_vpc_security_groups} +\alias{aws_vpc_security_groups} +\title{List VPC security groups} +\usage{ +aws_vpc_security_groups(...) +} +\arguments{ +\item{...}{named parameters passed on to \href{https://www.paws-r-sdk.com/docs/ec2_describe_security_groups/}{describe_security_groups}} +} +\value{ +(list) list with security groups +} +\description{ +List VPC security groups +} +\examples{ +\dontshow{if (interactive()) (if (getRversion() >= "3.4") withAutoprint else force)(\{ # examplesIf} +aws_vpc_security_groups() +aws_vpc_security_groups(MaxResults = 6) +\dontshow{\}) # examplesIf} +} +\seealso{ +Other security groups: +\code{\link{aws_ec2_client}()}, +\code{\link{aws_vpc_security_group_create}()}, +\code{\link{aws_vpc_security_group_ingress}()}, +\code{\link{aws_vpc_security_group_modify_rules}()}, +\code{\link{aws_vpc_security_group}()} +} +\concept{security groups} diff --git a/man/aws_vpcs.Rd b/man/aws_vpcs.Rd new file mode 100644 index 0000000..788ed61 --- /dev/null +++ b/man/aws_vpcs.Rd @@ -0,0 +1,23 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/vpcs.R +\name{aws_vpcs} +\alias{aws_vpcs} +\title{List VPCs} +\usage{ +aws_vpcs(...) +} +\arguments{ +\item{...}{parameters passed on to \href{https://www.paws-r-sdk.com/docs/ec2_describe_vpcs/}{describe_vpcs}} +} +\value{ +(list) list with VPCs +} +\description{ +List VPCs +} +\examples{ +\dontshow{if (interactive()) (if (getRversion() >= "3.4") withAutoprint else force)(\{ # examplesIf} +aws_vpcs() +aws_vpcs(MaxResults=6) +\dontshow{\}) # examplesIf} +} diff --git a/man/ip_address.Rd b/man/ip_address.Rd new file mode 100644 index 0000000..3bd503a --- /dev/null +++ b/man/ip_address.Rd @@ -0,0 +1,15 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/vpc_security_groups.R +\name{ip_address} +\alias{ip_address} +\title{Get your IP address using \url{https://ifconfig.me/ip}} +\usage{ +ip_address() +} +\value{ +(character) ip address +} +\description{ +Get your IP address using \url{https://ifconfig.me/ip} +} +\keyword{internal} diff --git a/man/ip_permissions_generator.Rd b/man/ip_permissions_generator.Rd new file mode 100644 index 0000000..55d088d --- /dev/null +++ b/man/ip_permissions_generator.Rd @@ -0,0 +1,23 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/vpc_security_groups.R +\name{ip_permissions_generator} +\alias{ip_permissions_generator} +\title{Ip Permissions generator} +\usage{ +ip_permissions_generator(engine, port = NULL, description = NULL) +} +\arguments{ +\item{engine}{(character) one of mariadb, mysql, or postgres} + +\item{port}{(character) port number. port determined from \code{engine} +if \code{port} not given. default: \code{NULL}} + +\item{description}{(character) description. if not given, autogenerated +depending on value of \code{engine}} +} +\value{ +a list with slots: FromPort, ToPort, IpProtocol, and IpRanges +} +\description{ +Ip Permissions generator +} From 341a91db6789775bf8f6688c45d79282b2b42ab6 Mon Sep 17 00:00:00 2001 From: Scott Chamberlain Date: Fri, 16 Feb 2024 13:50:32 -0800 Subject: [PATCH 18/28] styling --- R/files.R | 9 ++++-- R/groups.R | 9 +++--- R/roles.R | 6 ++-- R/vpc_security_groups.R | 61 +++++++++++++++++++++++++---------------- R/vpcs.R | 2 +- 5 files changed, 54 insertions(+), 33 deletions(-) diff --git a/R/files.R b/R/files.R index bf32f7b..3f1a468 100644 --- a/R/files.R +++ b/R/files.R @@ -167,15 +167,18 @@ aws_file_exists <- function(remote_path) { #' @return vector of paths, length matches `length(remote_path)` #' @family files #' @examples \dontrun{ -#' aws_file_rename(s3_path("s64-test-2", "DESCRIPTION"), -#' s3_path("s64-test-2", "DESC")) +#' aws_file_rename( +#' s3_path("s64-test-2", "DESCRIPTION"), +#' s3_path("s64-test-2", "DESC") +#' ) #' #' tfiles <- replicate(n = 3, tempfile()) #' for (i in tfiles) cat("Hello\nWorld\n", file = i) #' paths <- s3_path("s64-test-2", c("aaa", "bbb", "ccc"), ext = "txt") #' aws_file_upload(tfiles, paths) #' new_paths <- s3_path("s64-test-2", c("new_aaa", "new_bbb", "new_ccc"), -#' ext = "txt") +#' ext = "txt" +#' ) #' aws_file_rename(paths, new_paths) #' } aws_file_rename <- function(remote_path, new_remote_path, ...) { diff --git a/R/groups.R b/R/groups.R index 5def3f6..3ed356a 100644 --- a/R/groups.R +++ b/R/groups.R @@ -26,7 +26,8 @@ aws_groups <- function(username = NULL, ...) { paginate_aws(env64$iam$list_groups, "Groups", ...) %>% group_list_tidy() } else { paginate_aws(env64$iam$list_groups_for_user, "Groups", - UserName = username, ...) %>% + UserName = username, ... + ) %>% group_list_tidy() } } @@ -44,7 +45,7 @@ aws_groups <- function(username = NULL, ...) { #' @autoglobal #' @family groups #' @examples \dontrun{ -#' aws_group(name="users") +#' aws_group(name = "users") #' } aws_group <- function(name) { x <- env64$iam$get_group(name) @@ -65,8 +66,8 @@ aws_group <- function(name) { #' #' @family groups #' @examples \dontrun{ -#' aws_group_exists(name="users") -#' aws_group_exists(name="apples") +#' aws_group_exists(name = "users") +#' aws_group_exists(name = "apples") #' } aws_group_exists <- function(name) { check_aws_group <- purrr::safely(aws_group, otherwise = FALSE) diff --git a/R/roles.R b/R/roles.R index 352f9a3..0609491 100644 --- a/R/roles.R +++ b/R/roles.R @@ -98,8 +98,10 @@ aws_role <- function(name) { #' ) #' doc <- jsonlite::toJSON(trust_policy, auto_unbox = TRUE) #' desc <- "My test role" -#' z <- aws_role_create(role_name, assume_role_policy_document = doc, -#' description = desc) +#' z <- aws_role_create(role_name, +#' assume_role_policy_document = doc, +#' description = desc +#' ) #' z #' # attach a policy #' z %>% aws_policy_attach("AWSLambdaBasicExecutionRole") diff --git a/R/vpc_security_groups.R b/R/vpc_security_groups.R index 26569eb..01bdbbe 100644 --- a/R/vpc_security_groups.R +++ b/R/vpc_security_groups.R @@ -15,12 +15,15 @@ create_security_group <- function(engine) { #' @autoglobal #' @keywords internal security_group_handler <- function(ids, engine) { - if (!is.null(ids)) return(ids) + if (!is.null(ids)) { + return(ids) + } port <- engine2port(engine) ip <- ip_address() sgs <- aws_vpc_security_groups() sgsdf <- jsonlite::fromJSON( - jsonlite::toJSON(sgs$SecurityGroups, auto_unbox = TRUE)) + jsonlite::toJSON(sgs$SecurityGroups, auto_unbox = TRUE) + ) port_df <- dplyr::filter( sgsdf, @@ -29,15 +32,18 @@ security_group_handler <- function(ids, engine) { if (!NROW(port_df)) { cli::cli_alert_danger(c( "No security groups with access for ", - "{.strong {engine}} and port {.strong {port}}")) + "{.strong {engine}} and port {.strong {port}}" + )) cli::cli_alert_info(c( "Creating security group with access for ", - "{.strong {engine}} and port {.strong {port}}")) + "{.strong {engine}} and port {.strong {port}}" + )) trysg <- tryCatch(create_security_group(engine), error = function(e) e) if (rlang::is_error(trysg)) { cli::cli_alert_danger(c( "An error occurred while creating the security group; ", - "please use paramater {.strong security_group_ids}")) + "please use paramater {.strong security_group_ids}" + )) return(NULL) } else { cli::cli_alert_success("Using security group {.strong {trysg}}") @@ -52,7 +58,8 @@ security_group_handler <- function(ids, engine) { if (!NROW(ip_df)) { cli::cli_alert_danger(c( "Found security groups w/ access for {.strong {engine}}, ", - "{.emph but} not with your IP address {.strong {ip}}")) + "{.emph but} not with your IP address {.strong {ip}}" + )) cli::cli_alert_info("Which security group do you want to modify?") pick_sg_options <- port_df %>% @@ -71,23 +78,29 @@ security_group_handler <- function(ids, engine) { if (picked == 0) { cli::cli_alert_danger( - "No security group selected; please use paramater {.strong security_group_ids}") + "No security group selected; please use paramater {.strong security_group_ids}" + ) return(NULL) } else { picked_id <- port_df[picked, "GroupId"] } cli::cli_alert_info( - "Adding your IP address {.strong {ip}} to security group {.strong {picked_id}}") - try_ingress <- tryCatch({ - aws_vpc_security_group_ingress( - id = picked_id, - ip_permissions = ip_permissions_generator(engine) - ) - }, error = function(e) e) + "Adding your IP address {.strong {ip}} to security group {.strong {picked_id}}" + ) + try_ingress <- tryCatch( + { + aws_vpc_security_group_ingress( + id = picked_id, + ip_permissions = ip_permissions_generator(engine) + ) + }, + error = function(e) e + ) if (rlang::is_error(try_ingress)) { cli::cli_alert_danger(c( "An error occurred while creating the security group; ", - "please use paramater {.strong security_group_ids}")) + "please use paramater {.strong security_group_ids}" + )) return(NULL) } else { cli::cli_alert_success("Using security group {.strong {try_ingress}}") @@ -98,7 +111,8 @@ security_group_handler <- function(ids, engine) { if (NROW(ip_df) == 1) { cli::cli_alert_success(c( "Found security group {.strong {ip_df$GroupId}} ", - "w/ access for {.strong {engine}} and your IP address {.strong {ip}}")) + "w/ access for {.strong {engine}} and your IP address {.strong {ip}}" + )) return(ip_df$GroupId) } else { sgoptions <- @@ -119,7 +133,8 @@ security_group_handler <- function(ids, engine) { if (picked == 0) { cli::cli_alert_danger(c( "Found security group {.strong {ip_df$GroupId}} ", - "w/ access for {.strong {engine}}, {.emph but} not with your IP address {.strong {ip}}")) + "w/ access for {.strong {engine}}, {.emph but} not with your IP address {.strong {ip}}" + )) return(NULL) } else { idtouse <- ip_df[picked, "GroupId"] @@ -192,9 +207,9 @@ aws_vpc_security_group <- function(id, ...) { #' ip_permissions = ip_permissions_generator("mariadb") #' ) #' } -aws_vpc_security_group_create <- function(name, engine, description = NULL, - vpc_id = NULL, tags = NULL, ...) { - +aws_vpc_security_group_create <- function( + name, engine, description = NULL, + vpc_id = NULL, tags = NULL, ...) { aws_ec2_client() if (is.null(description)) { description <- glue("Access to {engine}") @@ -279,9 +294,9 @@ ip_address <- function() { #' - ReferencedGroupInfo #' - Description #' - Tags -aws_vpc_security_group_ingress <- function(id = NULL, - ip_permissions = NULL, ...) { - +aws_vpc_security_group_ingress <- function( + id = NULL, + ip_permissions = NULL, ...) { aws_ec2_client() env64$ec2$authorize_security_group_ingress( GroupId = id, diff --git a/R/vpcs.R b/R/vpcs.R index 56dadde..624ad97 100644 --- a/R/vpcs.R +++ b/R/vpcs.R @@ -5,7 +5,7 @@ #' @return (list) list with VPCs #' @examplesIf interactive() #' aws_vpcs() -#' aws_vpcs(MaxResults=6) +#' aws_vpcs(MaxResults = 6) aws_vpcs <- function(...) { aws_ec2_client() env64$ec2$describe_vpcs(...) From 0448714b6f4538164e08ad1796a68328a9367b5d Mon Sep 17 00:00:00 2001 From: Scott Chamberlain Date: Fri, 16 Feb 2024 13:51:44 -0800 Subject: [PATCH 19/28] update man files after styling --- man/aws_file_rename.Rd | 9 ++++++--- man/aws_group.Rd | 2 +- man/aws_group_exists.Rd | 4 ++-- man/aws_role_create.Rd | 6 ++++-- man/aws_vpcs.Rd | 2 +- 5 files changed, 14 insertions(+), 9 deletions(-) diff --git a/man/aws_file_rename.Rd b/man/aws_file_rename.Rd index 589b041..9d266af 100644 --- a/man/aws_file_rename.Rd +++ b/man/aws_file_rename.Rd @@ -22,15 +22,18 @@ Rename a remote file } \examples{ \dontrun{ -aws_file_rename(s3_path("s64-test-2", "DESCRIPTION"), - s3_path("s64-test-2", "DESC")) +aws_file_rename( + s3_path("s64-test-2", "DESCRIPTION"), + s3_path("s64-test-2", "DESC") +) tfiles <- replicate(n = 3, tempfile()) for (i in tfiles) cat("Hello\nWorld\n", file = i) paths <- s3_path("s64-test-2", c("aaa", "bbb", "ccc"), ext = "txt") aws_file_upload(tfiles, paths) new_paths <- s3_path("s64-test-2", c("new_aaa", "new_bbb", "new_ccc"), - ext = "txt") + ext = "txt" +) aws_file_rename(paths, new_paths) } } diff --git a/man/aws_group.Rd b/man/aws_group.Rd index 7cc6694..f0d8d93 100644 --- a/man/aws_group.Rd +++ b/man/aws_group.Rd @@ -26,7 +26,7 @@ see docs \url{https://www.paws-r-sdk.com/docs/iam_get_group/} } \examples{ \dontrun{ -aws_group(name="users") +aws_group(name = "users") } } \seealso{ diff --git a/man/aws_group_exists.Rd b/man/aws_group_exists.Rd index 4a443c1..40cfebe 100644 --- a/man/aws_group_exists.Rd +++ b/man/aws_group_exists.Rd @@ -21,8 +21,8 @@ uses \code{aws_group} internally. see docs } \examples{ \dontrun{ -aws_group_exists(name="users") -aws_group_exists(name="apples") +aws_group_exists(name = "users") +aws_group_exists(name = "apples") } } \seealso{ diff --git a/man/aws_role_create.Rd b/man/aws_role_create.Rd index 95affbf..1a6138f 100644 --- a/man/aws_role_create.Rd +++ b/man/aws_role_create.Rd @@ -62,8 +62,10 @@ trust_policy <- list( ) doc <- jsonlite::toJSON(trust_policy, auto_unbox = TRUE) desc <- "My test role" -z <- aws_role_create(role_name, assume_role_policy_document = doc, - description = desc) +z <- aws_role_create(role_name, + assume_role_policy_document = doc, + description = desc +) z # attach a policy z \%>\% aws_policy_attach("AWSLambdaBasicExecutionRole") diff --git a/man/aws_vpcs.Rd b/man/aws_vpcs.Rd index 788ed61..567da13 100644 --- a/man/aws_vpcs.Rd +++ b/man/aws_vpcs.Rd @@ -18,6 +18,6 @@ List VPCs \examples{ \dontshow{if (interactive()) (if (getRversion() >= "3.4") withAutoprint else force)(\{ # examplesIf} aws_vpcs() -aws_vpcs(MaxResults=6) +aws_vpcs(MaxResults = 6) \dontshow{\}) # examplesIf} } From 4830f22d182698cb586dc4948ac1403be4415a90 Mon Sep 17 00:00:00 2001 From: Scott Chamberlain Date: Wed, 21 Feb 2024 10:49:12 -0800 Subject: [PATCH 20/28] fix security_group_handler: was passing wrong output back from fxn --- R/vpc_security_groups.R | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/R/vpc_security_groups.R b/R/vpc_security_groups.R index 01bdbbe..d713b22 100644 --- a/R/vpc_security_groups.R +++ b/R/vpc_security_groups.R @@ -103,8 +103,8 @@ security_group_handler <- function(ids, engine) { )) return(NULL) } else { - cli::cli_alert_success("Using security group {.strong {try_ingress}}") - return(try_ingress) + cli::cli_alert_success("Using security group {.strong {picked_id}}") + return(picked_id) } } From b841d4dc9ab5f61f2f6e7bb157f8e5d8d2a5d534 Mon Sep 17 00:00:00 2001 From: Scott Chamberlain Date: Wed, 21 Feb 2024 12:29:27 -0800 Subject: [PATCH 21/28] woops, perhaps keep() changed or paws return structurte changed --- R/database-rds.R | 1 + 1 file changed, 1 insertion(+) diff --git a/R/database-rds.R b/R/database-rds.R index 85bc0f4..ad5c364 100644 --- a/R/database-rds.R +++ b/R/database-rds.R @@ -189,6 +189,7 @@ instance_con_info <- function(id) { deets <- instance_details()$DBInstances z <- keep(deets, \(x) x$DBInstanceIdentifier == id) if (!length(z)) rlang::abort(glue("Instance identifier {id} not found")) + z <- z[[1]] list( host = z$Endpoint$Address, port = z$Endpoint$Port, From 23518a55dd0716739f74fbf6c5464bda7863c318 Mon Sep 17 00:00:00 2001 From: Scott Chamberlain Date: Wed, 21 Feb 2024 15:36:32 -0800 Subject: [PATCH 22/28] add aws_db_rds_list fxn to list database - built on existing internal fxn --- NAMESPACE | 1 + R/database-rds.R | 20 ++++++++++++++++++++ man/aws_db_cluster_status.Rd | 1 + man/aws_db_instance_status.Rd | 1 + man/aws_db_rds_client.Rd | 1 + man/aws_db_rds_con.Rd | 1 + man/aws_db_rds_create.Rd | 1 + man/aws_db_rds_list.Rd | 32 ++++++++++++++++++++++++++++++++ man/aws_db_redshift_client.Rd | 1 + man/aws_db_redshift_con.Rd | 1 + man/aws_db_redshift_create.Rd | 1 + 11 files changed, 61 insertions(+) create mode 100644 man/aws_db_rds_list.Rd diff --git a/NAMESPACE b/NAMESPACE index 50541df..9b12a21 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -14,6 +14,7 @@ export(aws_db_instance_status) export(aws_db_rds_client) export(aws_db_rds_con) export(aws_db_rds_create) +export(aws_db_rds_list) export(aws_db_redshift_client) export(aws_db_redshift_con) export(aws_db_redshift_create) diff --git a/R/database-rds.R b/R/database-rds.R index ad5c364..516790f 100644 --- a/R/database-rds.R +++ b/R/database-rds.R @@ -180,6 +180,26 @@ instance_details <- function() { return(instances) } +#' Get information for all RDS instances +#' @export +#' @family database +#' @return a tibble of instance details; +#' see +#' @examplesIf interactive() +#' aws_db_rds_list() +aws_db_rds_list <- function() { + lst <- instance_details() + dbs <- lst$DBInstances + map(dbs, \(x) as_tibble(x[c( + "DBInstanceIdentifier", + "DBInstanceClass", + "Engine", + "DBInstanceStatus", + "DBName" + )]) + ) %>% list_rbind() +} + #' Get connection information for all instances #' @importFrom purrr keep #' @inheritParams aws_db_redshift_create diff --git a/man/aws_db_cluster_status.Rd b/man/aws_db_cluster_status.Rd index 7dafea9..86559c2 100644 --- a/man/aws_db_cluster_status.Rd +++ b/man/aws_db_cluster_status.Rd @@ -30,6 +30,7 @@ Other database: \code{\link{aws_db_rds_client}()}, \code{\link{aws_db_rds_con}()}, \code{\link{aws_db_rds_create}()}, +\code{\link{aws_db_rds_list}()}, \code{\link{aws_db_redshift_client}()}, \code{\link{aws_db_redshift_con}()}, \code{\link{aws_db_redshift_create}()} diff --git a/man/aws_db_instance_status.Rd b/man/aws_db_instance_status.Rd index f6e4ead..678e0d5 100644 --- a/man/aws_db_instance_status.Rd +++ b/man/aws_db_instance_status.Rd @@ -31,6 +31,7 @@ Other database: \code{\link{aws_db_rds_client}()}, \code{\link{aws_db_rds_con}()}, \code{\link{aws_db_rds_create}()}, +\code{\link{aws_db_rds_list}()}, \code{\link{aws_db_redshift_client}()}, \code{\link{aws_db_redshift_con}()}, \code{\link{aws_db_redshift_create}()} diff --git a/man/aws_db_rds_client.Rd b/man/aws_db_rds_client.Rd index 9cf928a..870ba0c 100644 --- a/man/aws_db_rds_client.Rd +++ b/man/aws_db_rds_client.Rd @@ -22,6 +22,7 @@ Other database: \code{\link{aws_db_instance_status}()}, \code{\link{aws_db_rds_con}()}, \code{\link{aws_db_rds_create}()}, +\code{\link{aws_db_rds_list}()}, \code{\link{aws_db_redshift_client}()}, \code{\link{aws_db_redshift_con}()}, \code{\link{aws_db_redshift_create}()} diff --git a/man/aws_db_rds_con.Rd b/man/aws_db_rds_con.Rd index b6e45d6..0e346b1 100644 --- a/man/aws_db_rds_con.Rd +++ b/man/aws_db_rds_con.Rd @@ -61,6 +61,7 @@ Other database: \code{\link{aws_db_instance_status}()}, \code{\link{aws_db_rds_client}()}, \code{\link{aws_db_rds_create}()}, +\code{\link{aws_db_rds_list}()}, \code{\link{aws_db_redshift_client}()}, \code{\link{aws_db_redshift_con}()}, \code{\link{aws_db_redshift_create}()} diff --git a/man/aws_db_rds_create.Rd b/man/aws_db_rds_create.Rd index 2f670f2..1e00715 100644 --- a/man/aws_db_rds_create.Rd +++ b/man/aws_db_rds_create.Rd @@ -97,6 +97,7 @@ Other database: \code{\link{aws_db_instance_status}()}, \code{\link{aws_db_rds_client}()}, \code{\link{aws_db_rds_con}()}, +\code{\link{aws_db_rds_list}()}, \code{\link{aws_db_redshift_client}()}, \code{\link{aws_db_redshift_con}()}, \code{\link{aws_db_redshift_create}()} diff --git a/man/aws_db_rds_list.Rd b/man/aws_db_rds_list.Rd new file mode 100644 index 0000000..1a5c422 --- /dev/null +++ b/man/aws_db_rds_list.Rd @@ -0,0 +1,32 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/database-rds.R +\name{aws_db_rds_list} +\alias{aws_db_rds_list} +\title{Get information for all RDS instances} +\usage{ +aws_db_rds_list() +} +\value{ +a tibble of instance details; +see \url{https://www.paws-r-sdk.com/docs/describe_db_instances/} +} +\description{ +Get information for all RDS instances +} +\examples{ +\dontshow{if (interactive()) (if (getRversion() >= "3.4") withAutoprint else force)(\{ # examplesIf} +aws_db_rds_list() +\dontshow{\}) # examplesIf} +} +\seealso{ +Other database: +\code{\link{aws_db_cluster_status}()}, +\code{\link{aws_db_instance_status}()}, +\code{\link{aws_db_rds_client}()}, +\code{\link{aws_db_rds_con}()}, +\code{\link{aws_db_rds_create}()}, +\code{\link{aws_db_redshift_client}()}, +\code{\link{aws_db_redshift_con}()}, +\code{\link{aws_db_redshift_create}()} +} +\concept{database} diff --git a/man/aws_db_redshift_client.Rd b/man/aws_db_redshift_client.Rd index 15588f4..ac5ba24 100644 --- a/man/aws_db_redshift_client.Rd +++ b/man/aws_db_redshift_client.Rd @@ -20,6 +20,7 @@ Other database: \code{\link{aws_db_rds_client}()}, \code{\link{aws_db_rds_con}()}, \code{\link{aws_db_rds_create}()}, +\code{\link{aws_db_rds_list}()}, \code{\link{aws_db_redshift_con}()}, \code{\link{aws_db_redshift_create}()} } diff --git a/man/aws_db_redshift_con.Rd b/man/aws_db_redshift_con.Rd index 561e854..fb85783 100644 --- a/man/aws_db_redshift_con.Rd +++ b/man/aws_db_redshift_con.Rd @@ -61,6 +61,7 @@ Other database: \code{\link{aws_db_rds_client}()}, \code{\link{aws_db_rds_con}()}, \code{\link{aws_db_rds_create}()}, +\code{\link{aws_db_rds_list}()}, \code{\link{aws_db_redshift_client}()}, \code{\link{aws_db_redshift_create}()} } diff --git a/man/aws_db_redshift_create.Rd b/man/aws_db_redshift_create.Rd index fb6751c..70fc286 100644 --- a/man/aws_db_redshift_create.Rd +++ b/man/aws_db_redshift_create.Rd @@ -83,6 +83,7 @@ Other database: \code{\link{aws_db_rds_client}()}, \code{\link{aws_db_rds_con}()}, \code{\link{aws_db_rds_create}()}, +\code{\link{aws_db_rds_list}()}, \code{\link{aws_db_redshift_client}()}, \code{\link{aws_db_redshift_con}()} } From b949becbd3de96c98b8efa15594d246ce323e400 Mon Sep 17 00:00:00 2001 From: Scott Chamberlain Date: Wed, 21 Feb 2024 15:37:05 -0800 Subject: [PATCH 23/28] syling --- R/database-rds.R | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/R/database-rds.R b/R/database-rds.R index 516790f..d8994a6 100644 --- a/R/database-rds.R +++ b/R/database-rds.R @@ -191,13 +191,12 @@ aws_db_rds_list <- function() { lst <- instance_details() dbs <- lst$DBInstances map(dbs, \(x) as_tibble(x[c( - "DBInstanceIdentifier", - "DBInstanceClass", - "Engine", - "DBInstanceStatus", - "DBName" - )]) - ) %>% list_rbind() + "DBInstanceIdentifier", + "DBInstanceClass", + "Engine", + "DBInstanceStatus", + "DBName" + )])) %>% list_rbind() } #' Get connection information for all instances From 863dc5a609d798bcedf426180501fdb4988acfee Mon Sep 17 00:00:00 2001 From: Scott Chamberlain Date: Tue, 27 Feb 2024 12:17:24 -0800 Subject: [PATCH 24/28] users: pull out utility fxn from within an exported fxn --- R/users.R | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/R/users.R b/R/users.R index e6cb3b4..bd8684d 100644 --- a/R/users.R +++ b/R/users.R @@ -71,6 +71,8 @@ aws_user <- function(username = NULL) { ) } +check_aws_user <- purrr::safely(aws_user, otherwise = FALSE) + #' Check if a user exists #' #' @export @@ -84,7 +86,6 @@ aws_user <- function(username = NULL) { #' aws_user_exists("blueberry") #' } aws_user_exists <- function(username) { - check_aws_user <- purrr::safely(aws_user, otherwise = FALSE) is.null(check_aws_user(username)$error) } From 2ce7aee59516e36ca9e4dacadf356ae575aeec25 Mon Sep 17 00:00:00 2001 From: Scott Chamberlain Date: Tue, 27 Feb 2024 12:18:15 -0800 Subject: [PATCH 25/28] change aws_role eg to if interactive() --- R/roles.R | 3 +-- man/aws_role.Rd | 4 ++-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/R/roles.R b/R/roles.R index 0609491..4e605c0 100644 --- a/R/roles.R +++ b/R/roles.R @@ -41,7 +41,7 @@ aws_roles <- function(...) { #' and `list_attached_role_policies` #' @autoglobal #' @family roles -#' @examples \dontrun{ +#' @examplesIf interactive() #' res <- aws_role(name = "OrganizationAccountSecurityRole") #' res #' res$role @@ -50,7 +50,6 @@ aws_roles <- function(...) { #' #' aws_role("AWSServiceRoleForCloudTrail") #' aws_role("AWSServiceRoleForRedshift") -#' } aws_role <- function(name) { df <- env64$iam$get_role(name)$Role %>% list(.) %>% diff --git a/man/aws_role.Rd b/man/aws_role.Rd index 33b47dd..e3bcea7 100644 --- a/man/aws_role.Rd +++ b/man/aws_role.Rd @@ -26,7 +26,7 @@ also includes policies and attached policies by calling \code{list_role_policies and \code{list_attached_role_policies} } \examples{ -\dontrun{ +\dontshow{if (interactive()) (if (getRversion() >= "3.4") withAutoprint else force)(\{ # examplesIf} res <- aws_role(name = "OrganizationAccountSecurityRole") res res$role @@ -35,7 +35,7 @@ res$attached_policies aws_role("AWSServiceRoleForCloudTrail") aws_role("AWSServiceRoleForRedshift") -} +\dontshow{\}) # examplesIf} } \seealso{ Other roles: From f07965d80af49ecaf8bcbecc878885b31fd7ada3 Mon Sep 17 00:00:00 2001 From: Scott Chamberlain Date: Tue, 27 Feb 2024 12:19:51 -0800 Subject: [PATCH 26/28] refactor aws_secrets_all to return a tibble and eliminate code in ui_fetch_secret b/c of refactor --- R/secrets_manager.R | 19 ++++++++++++++++++- R/ui_fetch_secret.R | 14 +------------- 2 files changed, 19 insertions(+), 14 deletions(-) diff --git a/R/secrets_manager.R b/R/secrets_manager.R index 2fed0bf..000aa6d 100644 --- a/R/secrets_manager.R +++ b/R/secrets_manager.R @@ -12,15 +12,32 @@ aws_secrets_list <- function(...) { } #' Get all secret values +#' @importFrom dplyr relocate last_col #' @export #' @return (list) list with secrets +#' @autoglobal #' @examples \dontrun{ #' aws_secrets_list() #' } aws_secrets_all <- function() { - env64$secretsmanager$list_secrets() %>% + tmp <- env64$secretsmanager$list_secrets() %>% .$SecretList %>% purrr::map(function(x) aws_secrets_get(x$Name)) + + new_secrets <- list() + for (i in seq_along(tmp)) { + new_secrets[[i]] <- c( + list( + name = tmp[[i]]$Name, + arn = tmp[[i]]$ARN, + created_date = tmp[[i]]$CreatedDate + ), + jsonlite::fromJSON(tmp[[i]]$SecretString) + ) + } + Filter(function(x) length(x$host) > 0, new_secrets) %>% + bind_rows() %>% + relocate(arn, created_date, .after = last_col()) } check_secret <- function(secret) { diff --git a/R/ui_fetch_secret.R b/R/ui_fetch_secret.R index 8181bc1..669da30 100644 --- a/R/ui_fetch_secret.R +++ b/R/ui_fetch_secret.R @@ -25,20 +25,8 @@ ui_fetch_secret <- function(user = NULL, password = NULL, engine = NULL) { return(list(user = user, password = password)) } - # get all secrets data - secrets <- aws_secrets_all() + new_secrets_df <- aws_secrets_all() - # organize secrets - new_secrets <- list() - for (i in seq_along(secrets)) { - new_secrets[[i]] <- c( - list(name = secrets[[i]]$Name), - jsonlite::fromJSON(secrets[[i]]$SecretString) - ) - } - new_secrets_df <- - Filter(function(x) length(x$host) > 0, new_secrets) %>% - bind_rows() if (!is.null(engine)) { new_secrets_df <- filter(new_secrets_df, engine == !!engine) } From 080d8bc51cbe7f82fbf247321d293d2e32cd2151 Mon Sep 17 00:00:00 2001 From: Scott Chamberlain Date: Tue, 27 Feb 2024 12:47:47 -0800 Subject: [PATCH 27/28] add IAM auth param to aws_db_rds_create; improve aws_db_rds_list output --- R/database-rds.R | 24 +++++++++++++++++++++--- man/aws_db_rds_create.Rd | 4 ++++ 2 files changed, 25 insertions(+), 3 deletions(-) diff --git a/R/database-rds.R b/R/database-rds.R index d8994a6..54b02b9 100644 --- a/R/database-rds.R +++ b/R/database-rds.R @@ -94,6 +94,8 @@ aws_db_rds_con <- function( #' @param verbose (logical) verbose informational output? default: `TRUE` #' @param aws_secrets (logical) should we manage your database credentials #' in AWS Secrets Manager? default: `TRUE` +#' @param iam_database_auth (logical) Use IAM database authentication? +#' default: `FALSE` #' @param ... named parameters passed on to #' [create_db_instance](https://www.paws-r-sdk.com/docs/rds_create_db_instance/) #' @details See above link to `create_cluster` docs for details on requirements @@ -114,7 +116,8 @@ aws_db_rds_create <- function(id, class, user = NULL, pwd = NULL, dbname = "dev", engine = "mariadb", storage = 20, storage_encrypted = TRUE, security_group_ids = NULL, - wait = TRUE, verbose = TRUE, aws_secrets = TRUE, ...) { + wait = TRUE, verbose = TRUE, aws_secrets = TRUE, + iam_database_auth = FALSE,...) { aws_db_rds_client() if (is.null(user)) { user <- random_user() @@ -136,6 +139,7 @@ aws_db_rds_create <- MasterUsername = user, MasterUserPassword = pwd, VpcSecurityGroupIds = security_group_ids, StorageEncrypted = storage_encrypted, + EnableIAMDatabaseAuthentication = iam_database_auth, ... ) if (wait) { @@ -180,11 +184,17 @@ instance_details <- function() { return(instances) } +split_grep <- function(column, split, pattern) { + grep(glue("^{pattern}$"), strsplit(column, split)[[1]], value = TRUE) +} + #' Get information for all RDS instances +#' @importFrom dplyr select #' @export #' @family database #' @return a tibble of instance details; #' see +#' @autoglobal #' @examplesIf interactive() #' aws_db_rds_list() aws_db_rds_list <- function() { @@ -195,8 +205,16 @@ aws_db_rds_list <- function() { "DBInstanceClass", "Engine", "DBInstanceStatus", - "DBName" - )])) %>% list_rbind() + "DBName", + "DbiResourceId", + "DBInstanceArn" + )])) %>% + list_rbind() %>% + mutate( + AccountId = split_grep(DBInstanceArn, ":", "^[0-9]+$"), + Region = split_grep(DBInstanceArn, ":", "^[a-z]+-[a-z]+-[0-9]$") + ) %>% + select(-DBInstanceArn) } #' Get connection information for all instances diff --git a/man/aws_db_rds_create.Rd b/man/aws_db_rds_create.Rd index 1e00715..059c81e 100644 --- a/man/aws_db_rds_create.Rd +++ b/man/aws_db_rds_create.Rd @@ -17,6 +17,7 @@ aws_db_rds_create( wait = TRUE, verbose = TRUE, aws_secrets = TRUE, + iam_database_auth = FALSE, ... ) } @@ -65,6 +66,9 @@ until the cluster is available. If \code{wait=FALSE} use \item{aws_secrets}{(logical) should we manage your database credentials in AWS Secrets Manager? default: \code{TRUE}} +\item{iam_database_auth}{(logical) Use IAM database authentication? +default: \code{FALSE}} + \item{...}{named parameters passed on to \href{https://www.paws-r-sdk.com/docs/rds_create_db_instance/}{create_db_instance}} } From de0d397876faf47fd0e03ab56b58ad94da8db581 Mon Sep 17 00:00:00 2001 From: Scott Chamberlain Date: Tue, 27 Feb 2024 12:56:46 -0800 Subject: [PATCH 28/28] users and policy changes - users: add aws_user_add_to_rds fxn to give a user IAM access to an RDS database - not working yet! - policies: add two fxns for policy create and create a policy document - a few helper fxns added for stop if and stop if not --- NAMESPACE | 6 +++ R/globals.R | 6 +++ R/policies.R | 81 +++++++++++++++++++++++++++++++ R/users.R | 68 ++++++++++++++++++++++++++ R/utils.R | 7 +++ man/as_policy_arn.Rd | 1 + man/aws_policies.Rd | 1 + man/aws_policy.Rd | 1 + man/aws_policy_attach.Rd | 1 + man/aws_policy_create.Rd | 50 +++++++++++++++++++ man/aws_policy_detach.Rd | 1 + man/aws_policy_document_create.Rd | 63 ++++++++++++++++++++++++ man/aws_policy_exists.Rd | 1 + man/aws_user_add_to_rds.Rd | 24 +++++++++ 14 files changed, 311 insertions(+) create mode 100644 man/aws_policy_create.Rd create mode 100644 man/aws_policy_document_create.Rd create mode 100644 man/aws_user_add_to_rds.Rd diff --git a/NAMESPACE b/NAMESPACE index 9b12a21..a051744 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -35,7 +35,9 @@ export(aws_iam_client) export(aws_policies) export(aws_policy) export(aws_policy_attach) +export(aws_policy_create) export(aws_policy_detach) +export(aws_policy_document_create) export(aws_policy_exists) export(aws_role) export(aws_role_create) @@ -52,6 +54,7 @@ export(aws_secrets_update) export(aws_user) export(aws_user_access_key) export(aws_user_add_to_group) +export(aws_user_add_to_rds) export(aws_user_create) export(aws_user_current) export(aws_user_delete) @@ -75,8 +78,11 @@ importFrom(cli,cli_progress_update) importFrom(cli,pb_spin) importFrom(dplyr,bind_rows) importFrom(dplyr,filter) +importFrom(dplyr,last_col) importFrom(dplyr,mutate) importFrom(dplyr,pull) +importFrom(dplyr,relocate) +importFrom(dplyr,select) importFrom(fs,file_exists) importFrom(fs,fs_bytes) importFrom(glue,glue) diff --git a/R/globals.R b/R/globals.R index febefad..736290a 100644 --- a/R/globals.R +++ b/R/globals.R @@ -1,14 +1,20 @@ # Generated by roxyglobals: do not edit by hand utils::globalVariables(c( + "DBInstanceArn", # ".", # ".", # "PolicyName", # "Arn", # ".", # + ".", # + "arn", # + "created_date", # "secret_raw", # "secret_str", # "PasswordLastUsed", # + "DBInstanceIdentifier", # + "arn", # "CreateDate", # "IpPermissions", # NULL diff --git a/R/policies.R b/R/policies.R index 0a24053..76d928c 100644 --- a/R/policies.R +++ b/R/policies.R @@ -74,6 +74,87 @@ aws_policy_exists <- function(name) { !is.null(purrr::safely(aws_policy)(name)$result) } +#' Create a policy +#' +#' @export +#' @param name (character) a policy name. required +#' @param document (character) the policy document you want to use +#' as the content for the new policy. required. +#' @param path (character) the path for the policy. if not given +#' default is "/". optional +#' @param description (character) a friendly description of the policy. +#' optional. cannot be changed after assigning it +#' @param tags (character) a vector of tags that you want to attach to +#' the new IAM policy. Each tag consists of a key name and an associated +#' value. optional +#' @return a tibble with policy details +#' @details see docs +#' @family policies +#' @examples \dontrun{ +#' aws_db_rds_list() +#' aws_policy_document_create() +#' aws_policy_create("RdsAllow", document = doc) +#' } +aws_policy_create <- function(name, document, path = NULL, + description = NULL, tags = NULL) { + env64$iam$create_policy( + PolicyName = name, + PolicyDocument = document, + Path = path, + Description = description, + Tags = tags + ) +} + +#' Create a policy document +#' +#' @export +#' @param region (character) the AWS Region for the DB instance. length==1 +#' @param account_id (character) the AWS account number for the DB instance. +#' length==1. The user must be in the same account as the account for the +#' DB instance +#' @param resource_id (character) the identifier for the DB instance. length==1 +#' @param user (character) a user name that has an IAM account. length>=1 +#' @param effect (character) valid values: "Allow" (default), "Deny". length==1 +#' @param ... named args passed to [jsonlite::toJSON()] +#' @references #no lint start +#' https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_elements.html +#' #no lint end +#' @return a json class string. use [as.character()] to coerce to a regular +#' string +#' @note a few document items are hard-coded: +#' - `Version` is set to 2012-10-17" +#' - `Action` is set to "rds-db:connect" +#' @examplesIf interactive() +#' ### DB account = user in a database that has access to it +#' # all DB instances & DB accounts for a AWS account and AWS Region +#' aws_policy_document_create("us-east-2", "1234567890", "*", "*") +#' # all DB instances for a AWS account and AWS Region, single DB account +#' aws_policy_document_create("us-east-2", "1234567890", "*", "jane_doe") +#' # single DB instasnce, single DB account +#' aws_policy_document_create("us-east-2", +#' "1234567890", "db-ABCDEFGHIJKL01234", "jane_doe") +#' # single DB instance, many users +#' aws_policy_document_create("us-east-2", "1234567890", +#' "db-ABCDEFGHIJKL01234", c("jane_doe", "mary_roe")) +aws_policy_document_create <- function(region, account_id, resource_id, user, + effect = "Allow", ...) { + + resource <- glue( + "arn:aws:rds-db:{region}:{account_id}:dbuser:{resource_id}/{user}") + doc <- list( + Version = "2012-10-17", + Statement = list( + list( + Effect = effect, + Action = "rds-db:connect", + Resource = resource + ) + ) + ) + jsonlite::toJSON(doc, auto_unbox = TRUE, ...) +} + #' Convert a policy name to a policy ARN #' #' @export diff --git a/R/users.R b/R/users.R index bd8684d..5a09ec6 100644 --- a/R/users.R +++ b/R/users.R @@ -181,3 +181,71 @@ aws_user_add_to_group <- function(username, groupname) { env64$iam$add_user_to_group(groupname, username) aws_user(username) } + +#' Grant a user access to an RDS database +#' +#' @export +#' @param user (character) an IAM username. required +#' @param id (character) instance identifier. required +#' @return xxx +#' @autoglobal +#' @examples \dontrun{ +#' aws_user_add_to_rds(user = "sean", id = "bluebird") +#' } +aws_user_add_to_rds <- function(user, id) { + # error handling + checked_user <- check_aws_user(user) + stop_if(rlang::is_error(checked_user$error), + glue("user `{user}` does not exist")) + dbs <- aws_db_rds_list() + picked <- dplyr::filter(dbs, DBInstanceIdentifier == {{ id }}) + stop_if_not(NROW(picked) > 0, + glue("database `{id}` does not exist")) + + # policy handling + doc <- aws_policy_document_create( + picked$Region, + picked$AccountId, + picked$DbiResourceId, + user + ) + policy_name <- glue("RdsAllow{id}{user}") + policy <- aws_policy_create(policy_name, doc) + invisible(aws_policies(refresh = TRUE)) # refresh policy data + checked_user$result %>% aws_policy_attach(policy_name) + + # database user + current_user <- aws_user() + # current_user$user$UserName + secrets <- aws_secrets_all() + sec <- dplyr::filter(secrets, grepl(id, arn)) + if (NROW(sec) == 0) { + rlang::abort("no secrets found for current user and database") + } + if (NROW(sec) == 1) { + con <- aws_db_rds_con(sec$username, sec$password, id = id) + on.exit(DBI::dbDisconnect(con)) + qry <- glue::glue_sql(" + CREATE USER {`user`} + IDENTIFIED WITH AWSAuthenticationPlugin + AS 'RDS'", + .con = con + ) + # DBI::dbSendQuery(con, qry) + res <- DBI::dbSendQuery(con, "CREATE USER ? IDENTIFIED WITH AWSAuthenticationPlugin AS 'RDS'") + DBI::dbBind(res, list(user)) + DBI::dbFetch(res, n = 3) + DBI::dbClearResult(res) + + query <- glue::glue_sql("CREATE USER {user} IDENTIFIED WITH AWSAuthenticationPlugin AS 'RDS'", .con = con) + out <- DBI::dbGetQuery(con, query) + # head(df, 3) + + qry <- glue::glue("CREATE USER {user} + IDENTIFIED WITH AWSAuthenticationPlugin + AS 'RDS'") + out <- DBI::dbGetQuery(con, qry) + } else { + rlang::abort("more than one set of secrets found") + } +} diff --git a/R/utils.R b/R/utils.R index 6a643e5..4d44105 100644 --- a/R/utils.R +++ b/R/utils.R @@ -190,3 +190,10 @@ is_class <- function(x, class) { ) } } + +stop_if_not <- function(cond, msg) { + if (!cond) rlang::abort(msg) +} +stop_if <- function(cond, msg) { + if (cond) rlang::abort(msg) +} diff --git a/man/as_policy_arn.Rd b/man/as_policy_arn.Rd index 78f0ac5..3dce3d4 100644 --- a/man/as_policy_arn.Rd +++ b/man/as_policy_arn.Rd @@ -33,6 +33,7 @@ as_policy_arn("AmazonRDSDataFullAccess") Other policies: \code{\link{aws_policies}()}, \code{\link{aws_policy_attach}()}, +\code{\link{aws_policy_create}()}, \code{\link{aws_policy_detach}()}, \code{\link{aws_policy_exists}()}, \code{\link{aws_policy}()} diff --git a/man/aws_policies.Rd b/man/aws_policies.Rd index e66f238..5084a5e 100644 --- a/man/aws_policies.Rd +++ b/man/aws_policies.Rd @@ -34,6 +34,7 @@ aws_policies(refresh = TRUE) Other policies: \code{\link{as_policy_arn}()}, \code{\link{aws_policy_attach}()}, +\code{\link{aws_policy_create}()}, \code{\link{aws_policy_detach}()}, \code{\link{aws_policy_exists}()}, \code{\link{aws_policy}()} diff --git a/man/aws_policy.Rd b/man/aws_policy.Rd index 9995b5c..b687040 100644 --- a/man/aws_policy.Rd +++ b/man/aws_policy.Rd @@ -29,6 +29,7 @@ Other policies: \code{\link{as_policy_arn}()}, \code{\link{aws_policies}()}, \code{\link{aws_policy_attach}()}, +\code{\link{aws_policy_create}()}, \code{\link{aws_policy_detach}()}, \code{\link{aws_policy_exists}()} } diff --git a/man/aws_policy_attach.Rd b/man/aws_policy_attach.Rd index e01d4a8..36e1270 100644 --- a/man/aws_policy_attach.Rd +++ b/man/aws_policy_attach.Rd @@ -32,6 +32,7 @@ aws_user()$attached_policies Other policies: \code{\link{as_policy_arn}()}, \code{\link{aws_policies}()}, +\code{\link{aws_policy_create}()}, \code{\link{aws_policy_detach}()}, \code{\link{aws_policy_exists}()}, \code{\link{aws_policy}()} diff --git a/man/aws_policy_create.Rd b/man/aws_policy_create.Rd new file mode 100644 index 0000000..5d05c20 --- /dev/null +++ b/man/aws_policy_create.Rd @@ -0,0 +1,50 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/policies.R +\name{aws_policy_create} +\alias{aws_policy_create} +\title{Create a policy} +\usage{ +aws_policy_create(name, document, path = NULL, description = NULL, tags = NULL) +} +\arguments{ +\item{name}{(character) a policy name. required} + +\item{document}{(character) the policy document you want to use +as the content for the new policy. required.} + +\item{path}{(character) the path for the policy. if not given +default is "/". optional} + +\item{description}{(character) a friendly description of the policy. +optional. cannot be changed after assigning it} + +\item{tags}{(character) a vector of tags that you want to attach to +the new IAM policy. Each tag consists of a key name and an associated +value. optional} +} +\value{ +a tibble with policy details +} +\description{ +Create a policy +} +\details{ +see docs \url{https://www.paws-r-sdk.com/docs/iam_create_policy/} +} +\examples{ +\dontrun{ +aws_db_rds_list() +aws_policy_document_create() +aws_policy_create("RdsAllow", document = doc) +} +} +\seealso{ +Other policies: +\code{\link{as_policy_arn}()}, +\code{\link{aws_policies}()}, +\code{\link{aws_policy_attach}()}, +\code{\link{aws_policy_detach}()}, +\code{\link{aws_policy_exists}()}, +\code{\link{aws_policy}()} +} +\concept{policies} diff --git a/man/aws_policy_detach.Rd b/man/aws_policy_detach.Rd index bc5fbad..df3dd67 100644 --- a/man/aws_policy_detach.Rd +++ b/man/aws_policy_detach.Rd @@ -33,6 +33,7 @@ Other policies: \code{\link{as_policy_arn}()}, \code{\link{aws_policies}()}, \code{\link{aws_policy_attach}()}, +\code{\link{aws_policy_create}()}, \code{\link{aws_policy_exists}()}, \code{\link{aws_policy}()} } diff --git a/man/aws_policy_document_create.Rd b/man/aws_policy_document_create.Rd new file mode 100644 index 0000000..bdf8e10 --- /dev/null +++ b/man/aws_policy_document_create.Rd @@ -0,0 +1,63 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/policies.R +\name{aws_policy_document_create} +\alias{aws_policy_document_create} +\title{Create a policy document} +\usage{ +aws_policy_document_create( + region, + account_id, + resource_id, + user, + effect = "Allow", + ... +) +} +\arguments{ +\item{region}{(character) the AWS Region for the DB instance. length==1} + +\item{account_id}{(character) the AWS account number for the DB instance. +length==1. The user must be in the same account as the account for the +DB instance} + +\item{resource_id}{(character) the identifier for the DB instance. length==1} + +\item{user}{(character) a user name that has an IAM account. length>=1} + +\item{effect}{(character) valid values: "Allow" (default), "Deny". length==1} + +\item{...}{named args passed to \code{\link[jsonlite:fromJSON]{jsonlite::toJSON()}}} +} +\value{ +a json class string. use \code{\link[=as.character]{as.character()}} to coerce to a regular +string +} +\description{ +Create a policy document +} +\note{ +a few document items are hard-coded: +\itemize{ +\item \code{Version} is set to 2012-10-17" +\item \code{Action} is set to "rds-db:connect" +} +} +\examples{ +\dontshow{if (interactive()) (if (getRversion() >= "3.4") withAutoprint else force)(\{ # examplesIf} +### DB account = user in a database that has access to it +# all DB instances & DB accounts for a AWS account and AWS Region +aws_policy_document_create("us-east-2", "1234567890", "*", "*") +# all DB instances for a AWS account and AWS Region, single DB account +aws_policy_document_create("us-east-2", "1234567890", "*", "jane_doe") +# single DB instasnce, single DB account +aws_policy_document_create("us-east-2", "1234567890", "db-ABCDEFGHIJKL01234", "jane_doe") +# single DB instance, many users +aws_policy_document_create("us-east-2", "1234567890", "db-ABCDEFGHIJKL01234", + c("jane_doe", "mary_roe")) +\dontshow{\}) # examplesIf} +} +\references{ +#no lint start +https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_elements.html +#no lint end +} diff --git a/man/aws_policy_exists.Rd b/man/aws_policy_exists.Rd index ab97fcb..48cab67 100644 --- a/man/aws_policy_exists.Rd +++ b/man/aws_policy_exists.Rd @@ -28,6 +28,7 @@ Other policies: \code{\link{as_policy_arn}()}, \code{\link{aws_policies}()}, \code{\link{aws_policy_attach}()}, +\code{\link{aws_policy_create}()}, \code{\link{aws_policy_detach}()}, \code{\link{aws_policy}()} } diff --git a/man/aws_user_add_to_rds.Rd b/man/aws_user_add_to_rds.Rd new file mode 100644 index 0000000..2a9c86a --- /dev/null +++ b/man/aws_user_add_to_rds.Rd @@ -0,0 +1,24 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/users.R +\name{aws_user_add_to_rds} +\alias{aws_user_add_to_rds} +\title{Grant a user access to an RDS database} +\usage{ +aws_user_add_to_rds(user, id) +} +\arguments{ +\item{user}{(character) an IAM username. required} + +\item{id}{(character) instance identifier. required} +} +\value{ +xxx +} +\description{ +Grant a user access to an RDS database +} +\examples{ +\dontrun{ +aws_user_add_to_rds(user = "sean", id = "bluebird") +} +}