From c53cf2a3e8359dc5007acf4636af2740a0e48929 Mon Sep 17 00:00:00 2001 From: Shawn Garbett Date: Mon, 21 Oct 2024 15:10:54 -0500 Subject: [PATCH 01/16] First attempt at refactor for #417, not working --- R/unlockREDCap.R | 152 ++++++++++++++++++++++++++--------------------- 1 file changed, 85 insertions(+), 67 deletions(-) diff --git a/R/unlockREDCap.R b/R/unlockREDCap.R index 6766b599..f0b0db30 100644 --- a/R/unlockREDCap.R +++ b/R/unlockREDCap.R @@ -19,6 +19,7 @@ ## .connectAndCheck <- function(key, url, ...) { + browser() tryCatch( { rcon <- redcapConnection(token=key, url=url, ...) @@ -83,7 +84,7 @@ ############################################################################# ## unlock via YAML override if it exists ## -.unlockYamlOverride <- function(connections, url, ...) +.unlockYamlOverride <- function(connections, connectionFUNs) { config_file <- file.path("..", paste0(basename(getwd()),".yml")) @@ -95,24 +96,21 @@ if(is.null(config$keys)) stop(paste0("Config file '",config_file,"' does not contain required 'keys' entry under the 'redcapAPI' entry")) keys <- config$keys - dest <- lapply(connections, function(conn) + dest <- lapply(seq_along(connections), function(i) { + conn <- connections[i] key <- keys[[conn]] if(is.null(key) || length(key)==0) stop(paste0("Config file '", config_file, "' does not have API_KEY for '", conn,"' under 'redcapAPI: keys:' specified.")) if(!is.character(key)) - { stop(paste0("Config file '", config_file, "' invalid entry for '", conn,"' under 'redcapAPI: keys:'.")) - } if(length(key) > 1) stop(paste0("Config file '", config_file, "' has too may key entries for '", conn,"' under 'redcapAPI: keys:' specified.")) - args <- list(...) - args$key <- key - args$url <- url + args <- list(key=key) if(!is.null(config$args)) args <- utils::modifyList(args, config$args) - do.call(.connectAndCheck, args) + do.call(connectionFUNs[[i]], args) }) names(dest) <- if(is.null(names(connections))) connections else names(connections) @@ -121,7 +119,7 @@ ############################################################################# ## unlock via ENV override if it exists ## -.unlockENVOverride <- function(connections, url, ...) +.unlockENVOverride <- function(connections, connectionFUNs) { api_key_ENV <- sapply(connections, function(x) Sys.getenv(toupper(x))) @@ -129,13 +127,10 @@ if(any(api_key_ENV == "")) stop(paste("Some matching ENV variables found but missing:",paste0(toupper(connections[api_key_ENV=='']), collapse=", "))) - - dest <- lapply(api_key_ENV, function(conn) + + dest <- lapply(seq_along(connections), function(i) { - args <- list(...) - args$key <- conn - args$url <- url - do.call(.connectAndCheck, args) + do.call(connectionFUNs[[i]], list(key = api_key_ENV[i])) }) names(dest) <- if(is.null(names(api_key_ENV))) api_key_ENV else names(api_key_ENV) @@ -203,6 +198,70 @@ } else getPass::getPass } +# Main internal algorithm +.unlockAPIKEY <- function(connections, + connectionFUNs, + keyring, + envir = NULL, + passwordFUN = .default_pass()) +{ + if(is.numeric(envir)) envir <- as.environment(envir) + + # Use YAML config if it exists + dest <- .unlockYamlOverride(connections, connectionFUNs) + if(length(dest) > 0) + return(if(is.null(envir)) dest else list2env(dest, envir=envir)) + + # Use ENV if it exists and YAML does not exist + dest <- .unlockENVOverride(connections, connectionFUNs) + if(length(dest) > 0) + return(if(is.null(envir)) dest else list2env(dest, envir=envir)) + + .unlockKeyring(keyring, passwordFUN) + + # Open Connections + dest <- lapply(seq_along(connections), function(i) + { + stored <- connections[i] %in% keyring::key_list("redcapAPI", keyring)[,2] + + api_key <- if(stored) + { + keyring::key_get("redcapAPI", connections[i], keyring) + } else + { + passwordFUN(paste0("Please enter API_KEY for '", connections[i], "'.")) + } + + if(is.null(api_key) || api_key == '') stop(paste("No API_KEY entered for", connections[i])) + + conn <- NULL + while(is.null(conn)) + { + conn <- (connectionFUNs[[i]])(api_key) # .connectAndCheck(api_key, url, ...) + if(is.null(conn)) + { + keyring::key_delete("redcapAPI", unname(connections[i]), keyring) + api_key <- passwordFUN(paste0( + "Invalid API_KEY for '", connections[i], + "' in keyring '", keyring, + "'. Possible causes include: mistyped, renewed, or revoked.", + " Please enter a new key or cancel to abort.")) + if(is.null(api_key) || api_key == '') stop("unlockAPIKEY aborted") + } else if(!stored) + { + keyring::key_set_with_value( service="redcapAPI", + username=unname(connections[i]), + password=api_key, + keyring=keyring) + } + } + conn + }) + names(dest) <- if(is.null(names(connections))) connections else names(connections) + + if(is.null(envir)) dest else list2env(dest, envir=envir) +} + #' Open REDCap connections using cryptolocker for storage of API_KEYs. #' #' Opens a set of connections to REDcap from API_KEYs stored in an encrypted keyring. @@ -297,59 +356,18 @@ unlockREDCap <- function(connections, checkmate::assert_function( x = passwordFUN, null.ok = FALSE, add = coll) checkmate::assert_class( x = envir, null.ok = TRUE, add = coll, classes="environment") checkmate::reportAssertions(coll) - - # Use YAML config if it exists - dest <- .unlockYamlOverride(connections, url, ...) - if(length(dest) > 0) - return(if(is.null(envir)) dest else list2env(dest, envir=envir)) - # Use ENV if it exists and YAML does not exist - dest <- .unlockENVOverride(connections, url, ...) - if(length(dest) > 0) - return(if(is.null(envir)) dest else list2env(dest, envir=envir)) - - .unlockKeyring(keyring, passwordFUN) - - # Open Connections - dest <- lapply(seq_along(connections), function(i) - { - stored <- connections[i] %in% keyring::key_list("redcapAPI", keyring)[,2] - - api_key <- if(stored) - { - keyring::key_get("redcapAPI", connections[i], keyring) - } else - { - passwordFUN(paste0("Please enter REDCap API_KEY for '", connections[i], "'.")) - } - - if(is.null(api_key) || api_key == '') stop(paste("No API_KEY entered for", connections[i])) - - conn <- NULL - while(is.null(conn)) - { - conn <- .connectAndCheck(api_key, url, ...) - if(is.null(conn)) - { - keyring::key_delete("redcapAPI", unname(connections[i]), keyring) - api_key <- passwordFUN(paste0( - "Invalid API_KEY for '", connections[i], - "' in keyring '", keyring, - "'. Possible causes include: mistyped, renewed, or revoked.", - " Please enter a new key or cancel to abort.")) - if(is.null(api_key) || api_key == '') stop("unlockREDCap aborted") - } else if(!stored) - { - keyring::key_set_with_value( service="redcapAPI", - username=unname(connections[i]), - password=api_key, - keyring=keyring) - } - } - conn - }) - names(dest) <- if(is.null(names(connections))) connections else names(connections) + ########################################################################### + ## Setup Internal Loop functions + connectionFUNs <- replicate(length(connections), + function(key) .connectAndCheck(key, url, ...)) - if(is.null(envir)) dest else list2env(dest, envir=envir) + ########################################################################### + ## Do it + .unlockAPIKEY(connections, + connectionFUNs, + keyring, + envir, + passwordFUN) } From 29ebd41da149c177b887434917fab93cf7b4c9d6 Mon Sep 17 00:00:00 2001 From: Shawn Garbett Date: Mon, 21 Oct 2024 15:55:51 -0500 Subject: [PATCH 02/16] First working version, curry is hacky, #417 --- R/unlockREDCap.R | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/R/unlockREDCap.R b/R/unlockREDCap.R index 8af77439..88bef22b 100644 --- a/R/unlockREDCap.R +++ b/R/unlockREDCap.R @@ -19,7 +19,6 @@ ## .connectAndCheck <- function(key, url, ...) { - browser() tryCatch( { rcon <- redcapConnection(token=key, url=url, ...) @@ -108,9 +107,7 @@ if(length(key) > 1) stop(paste0("Config file '", config_file, "' has too may key entries for '", conn,"' under 'redcapAPI: keys:' specified.")) - args <- list(key=key) - if(!is.null(config$args)) args <- utils::modifyList(args, config$args) - do.call(connectionFUNs[[i]], args) + do.call(connectionFUNs[[i]], list(key=key)) }) names(dest) <- if(is.null(names(connections))) connections else names(connections) @@ -202,8 +199,8 @@ .unlockAPIKEY <- function(connections, connectionFUNs, keyring, - envir = NULL, - passwordFUN = .default_pass()) + envir, + passwordFUN) { if(is.numeric(envir)) envir <- as.environment(envir) @@ -359,9 +356,15 @@ unlockREDCap <- function(connections, ########################################################################### ## Setup Internal Loop functions + args <- list(...) + args$url <- url connectionFUNs <- replicate(length(connections), - function(key) .connectAndCheck(key, url, ...)) - + function(key) + { + args$key <- key + do.call('.connectAndCheck', args) + }) + ########################################################################### ## Do it .unlockAPIKEY(connections, From 2a6aee4b71503eb95f3660b6f4bb66f02bc0dce0 Mon Sep 17 00:00:00 2001 From: Shawn Garbett Date: Mon, 21 Oct 2024 16:06:16 -0500 Subject: [PATCH 03/16] Preserve call order properly (thx Cole) #417 --- R/unlockREDCap.R | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/R/unlockREDCap.R b/R/unlockREDCap.R index 88bef22b..03e50c75 100644 --- a/R/unlockREDCap.R +++ b/R/unlockREDCap.R @@ -356,8 +356,7 @@ unlockREDCap <- function(connections, ########################################################################### ## Setup Internal Loop functions - args <- list(...) - args$url <- url + args <- c(key = NA, url = url, list(...)) connectionFUNs <- replicate(length(connections), function(key) { From 95df1e7620a34394f1170cebb3b37e0678402046 Mon Sep 17 00:00:00 2001 From: Shawn Garbett Date: Mon, 21 Oct 2024 16:08:43 -0500 Subject: [PATCH 04/16] Cole refactor, use for loop #417 --- R/unlockREDCap.R | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/R/unlockREDCap.R b/R/unlockREDCap.R index 03e50c75..a50211a2 100644 --- a/R/unlockREDCap.R +++ b/R/unlockREDCap.R @@ -356,13 +356,10 @@ unlockREDCap <- function(connections, ########################################################################### ## Setup Internal Loop functions - args <- c(key = NA, url = url, list(...)) - connectionFUNs <- replicate(length(connections), - function(key) - { - args$key <- key - do.call('.connectAndCheck', args) - }) + n <- length(connections) + connectionFUNs <- vector('list', n) + for(i in seq(n)) + connectionFUNs[[i]] <- function(key) .connectAndCheck(key, url, ...) ########################################################################### ## Do it From f5056322b01a8c1690495b95f8673cad837a13f7 Mon Sep 17 00:00:00 2001 From: Shawn Garbett Date: Mon, 21 Oct 2024 16:42:41 -0500 Subject: [PATCH 05/16] Now allows loading of custom API_KEY connections #417 --- NAMESPACE | 1 + R/unlockREDCap.R | 63 ++++++++++++++++++++++++++++++++++++++++----- man/unlockREDCap.Rd | 23 +++++++++++++++++ 3 files changed, 81 insertions(+), 6 deletions(-) diff --git a/NAMESPACE b/NAMESPACE index 1839570f..b3dec80a 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -209,6 +209,7 @@ export(stripHTMLandUnicode) export(stripUnicode) export(switchDag) export(unitsFieldAnnotation) +export(unlockOther) export(unlockREDCap) export(valChoice) export(valPhone) diff --git a/R/unlockREDCap.R b/R/unlockREDCap.R index a50211a2..7ea2c711 100644 --- a/R/unlockREDCap.R +++ b/R/unlockREDCap.R @@ -196,7 +196,7 @@ } # Main internal algorithm -.unlockAPIKEY <- function(connections, +.unlockAlgorithm <- function(connections, connectionFUNs, keyring, envir, @@ -312,6 +312,12 @@ #' @param url character. The url of one's institutional REDCap server api. #' @param passwordFUN function. Function to get the password for the keyring. Usually defaults `getPass::getPass`. #' On MacOS it will use rstudioapi::askForPassword if available. +#' @param otherKeys list. A list of other keys to retrieve. Each list element +#' must be a list with name, variable and connectFUN keys. The connectFUN +#' can be as simple as an id function `function(x) x` or something that +#' constructs a connection object or calls `stop` if it's invalid. +#' @param connectFUN function. A function that takes a key and returns a connection. +#' the function should call `stop` if the key is invalid in some manner. #' @param \dots Additional arguments passed to [redcapConnection()]. #' @return If `envir` is NULL returns a list of opened connections. Otherwise #' connections are assigned into the specified `envir`. @@ -332,13 +338,19 @@ #' keyring = '', #' envir = globalenv(), #' url = 'https:///api/') +#' +#' unlockOther(c(logging = 'SplunkKey'), +#' keyring = '', +#' envir = 1) #' } +#' @rdname unlockREDCap #' @export unlockREDCap <- function(connections, url, keyring, envir = NULL, passwordFUN = .default_pass(), + otherKeys = NULL, ...) { ########################################################################### @@ -363,10 +375,49 @@ unlockREDCap <- function(connections, ########################################################################### ## Do it - .unlockAPIKEY(connections, - connectionFUNs, - keyring, - envir, - passwordFUN) + .unlockAlgorithm(connections, + connectionFUNs, + keyring, + envir, + passwordFUN) +} + +#' @rdname unlockREDCap +#' @export +unlockOther <- function(connections, + keyring, + connectFUN = NULL, + envir = NULL, + passwordFUN = .default_pass(), + ...) +{ + ########################################################################### + # Check parameters passed to function + coll <- checkmate::makeAssertCollection() + + if(is.numeric(envir)) envir <- as.environment(envir) + + checkmate::assert_character(x = keyring, null.ok = FALSE, add = coll) + checkmate::assert_character(x = connections, null.ok = FALSE, add = coll) + checkmate::assert_function( x = passwordFUN, null.ok = FALSE, add = coll) + checkmate::assert_class( x = envir, null.ok = TRUE, add = coll, classes="environment") + checkmate::assert_function( x = connectFUN, null.ok = TRUE, add = coll, nargs=1) + checkmate::reportAssertions(coll) + + if(is.null(connectFUN)) connectFUN <- function(x) x + + ########################################################################### + ## Setup Internal Loop functions + n <- length(connections) + connectionFUNs <- vector('list', n) + for(i in seq(n)) connectionFUNs[[i]] <- function(key) connectFUN(key, ...) + + ########################################################################### + ## Do it + .unlockAlgorithm(connections, + connectionFUNs, + keyring, + envir, + passwordFUN) } diff --git a/man/unlockREDCap.Rd b/man/unlockREDCap.Rd index 2ee3c607..f73df27c 100644 --- a/man/unlockREDCap.Rd +++ b/man/unlockREDCap.Rd @@ -2,6 +2,7 @@ % Please edit documentation in R/unlockREDCap.R \name{unlockREDCap} \alias{unlockREDCap} +\alias{unlockOther} \title{Open REDCap connections using cryptolocker for storage of API_KEYs.} \usage{ unlockREDCap( @@ -10,6 +11,16 @@ unlockREDCap( keyring, envir = NULL, passwordFUN = .default_pass(), + otherKeys = NULL, + ... +) + +unlockOther( + connections, + keyring, + connectFUN = NULL, + envir = NULL, + passwordFUN = .default_pass(), ... ) } @@ -31,7 +42,15 @@ global environment. Will accept a number such a '1' for global as well.} \item{passwordFUN}{function. Function to get the password for the keyring. Usually defaults \code{getPass::getPass}. On MacOS it will use rstudioapi::askForPassword if available.} +\item{otherKeys}{list. A list of other keys to retrieve. Each list element +must be a list with name, variable and connectFUN keys. The connectFUN +can be as simple as an id function \code{function(x) x} or something that +constructs a connection object or calls \code{stop} if it's invalid.} + \item{\dots}{Additional arguments passed to \code{\link[=redcapConnection]{redcapConnection()}}.} + +\item{connectFUN}{function. A function that takes a key and returns a connection. +the function should call \code{stop} if the key is invalid in some manner.} } \value{ If \code{envir} is NULL returns a list of opened connections. Otherwise @@ -87,6 +106,10 @@ unlockREDCap(c(test_conn = 'TestRedcapAPI', keyring = '', envir = globalenv(), url = 'https:///api/') + +unlockOther(c(logging = 'SplunkKey'), + keyring = '', + envir = 1) } } \seealso{ From abfed147112cc28a46e98348e50f9a30774a216c Mon Sep 17 00:00:00 2001 From: Shawn Garbett Date: Tue, 22 Oct 2024 10:30:29 -0500 Subject: [PATCH 06/16] Updated tests to work with new unlockREDCap #417 --- tests/testthat/test-024-unlockREDCap.R | 34 ++++++++++++-------------- 1 file changed, 16 insertions(+), 18 deletions(-) diff --git a/tests/testthat/test-024-unlockREDCap.R b/tests/testthat/test-024-unlockREDCap.R index d816f29a..756edd22 100644 --- a/tests/testthat/test-024-unlockREDCap.R +++ b/tests/testthat/test-024-unlockREDCap.R @@ -136,8 +136,7 @@ test_that( stub(.unlockYamlOverride, "file.exists", TRUE) stub(.unlockYamlOverride, "yaml::read_yaml", list(redcapAPI=list(keys=list(TestRedcapAPI='xyz', Sandbox='xyz')))) - stub(.unlockYamlOverride, ".connectAndCheck", TRUE) - x <- .unlockYamlOverride(c("TestRedcapAPI", "Sandbox"), url) + x <- .unlockYamlOverride(c("TestRedcapAPI", "Sandbox"), list(function(...) TRUE, function(...) TRUE)) expect_true(x$TestRedcapAPI) expect_true(x$Sandbox) } @@ -149,8 +148,7 @@ test_that( stub(.unlockYamlOverride, "file.exists", TRUE) stub(.unlockYamlOverride, "yaml::read_yaml", list(redcapAPI=list(keys=list(TestRedcapAPI='xyz', Sandbox='xyz')))) - stub(.unlockYamlOverride, ".connectAndCheck", TRUE) - x <- .unlockYamlOverride(c(rcon="TestRedcapAPI", sand="Sandbox"), url) + x <- .unlockYamlOverride(c(rcon="TestRedcapAPI", sand="Sandbox"), list(function(...) TRUE, function(...) TRUE)) expect_true(x$rcon) expect_true(x$sand) } @@ -179,7 +177,8 @@ test_that( { stub(.unlockENVOverride, "Sys.getenv", "xyz") stub(.unlockENVOverride, ".connectAndCheck", TRUE) - x <- .unlockENVOverride(c("TestRedcapAPI", "Sandbox"), url) + x <- .unlockENVOverride(c("TestRedcapAPI", "Sandbox"), + list(function(...) TRUE, function(...) TRUE)) expect_true(x$TestRedcapAPI) expect_true(x$Sandbox) } @@ -190,7 +189,8 @@ test_that( { stub(.unlockENVOverride, "Sys.getenv", "xyz") stub(.unlockENVOverride, ".connectAndCheck", TRUE) - x <- .unlockENVOverride(c(rcon="TestRedcapAPI", sand="Sandbox"), url) + x <- .unlockENVOverride(c(rcon="TestRedcapAPI", sand="Sandbox"), + list(function(...) TRUE, function(...) TRUE)) expect_true(x$rcon) expect_true(x$sand) } @@ -369,14 +369,14 @@ test_that( ) test_that( - "unlockREDCap asks for API_KEY if not stored, opens connection and stores", + ".unlockAlgorithm asks for API_KEY if not stored, opens connection and stores", { m <- mock(TRUE) - stub(unlockREDCap, ".unlockYamlOverride", list()) # No yaml + stub(.unlockAlgorithm, ".unlockYamlOverride", list()) # No yaml - stub(unlockREDCap, "keyring::key_list", + stub(.unlockAlgorithm, "keyring::key_list", data.frame(service="recapAPI", username="Nadda")) - stub(unlockREDCap, "keyring::key_set_with_value", m) + stub(.unlockAlgorithm, "keyring::key_set_with_value", m) calls <- 0 passwordFUN <- function(...) {calls <<- calls + 1; "xyz"} @@ -384,14 +384,12 @@ test_that( n <- mock(TRUE) - with_mocked_bindings( - { - x <- unlockREDCap( - c(rcon="George"), url, keyring="API_KEYs", - passwordFUN=passwordFUN) - }, - .connectAndCheck = n, - ) + x <- .unlockAlgorithm( + c(rcon="George"), + list(n), + keyring="API_KEYs", + envir=NULL, + passwordFUN=passwordFUN) expect_true("rcon" %in% names(x)) expect_true(x$rcon) From b5859b557818bd1ab69b099877377d1c04b2d85b Mon Sep 17 00:00:00 2001 From: Shawn Garbett Date: Mon, 4 Nov 2024 14:49:06 -0600 Subject: [PATCH 07/16] Some final name moves --- R/unlockREDCap.R | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/R/unlockREDCap.R b/R/unlockREDCap.R index 7ea2c711..1b80b0ab 100644 --- a/R/unlockREDCap.R +++ b/R/unlockREDCap.R @@ -382,14 +382,14 @@ unlockREDCap <- function(connections, passwordFUN) } -#' @rdname unlockREDCap +#' @rdname unlockKeys #' @export -unlockOther <- function(connections, - keyring, - connectFUN = NULL, - envir = NULL, - passwordFUN = .default_pass(), - ...) +unlockKeys <- function(connections, + keyring, + connectFUN = NULL, + envir = NULL, + passwordFUN = .default_pass(), + ...) { ########################################################################### # Check parameters passed to function From bf0735cee4c106f0dcfbb7fa4056f8d32ed19d78 Mon Sep 17 00:00:00 2001 From: Shawn Garbett Date: Tue, 26 Nov 2024 11:39:10 -0600 Subject: [PATCH 08/16] Final cutover to using shelter package --- .gitlab-ci.yml | 2 +- DESCRIPTION | 4 +- NAMESPACE | 11 +- R/redcapAPI-package.R | 3 - R/redcapConnection.R | 3 +- R/unlockREDCap.R | 281 +--------------- man/redcapConnection.Rd | 7 +- man/unlockREDCap.Rd | 35 +- .../test-020-redcapConnection-Functionality.R | 21 +- tests/testthat/test-024-unlockREDCap.R | 303 +----------------- 10 files changed, 28 insertions(+), 642 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index d05017f9..8d1aa3c8 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -10,5 +10,5 @@ test: - cp $KEYRING tests/testthat.yml - apt-get update - apt-get install -y libsecret-1-dev libsodium-dev - - R --no-save -e "install.packages(c('devtools','checkmate','chron','curl','labelVector','lubridate','keyring','getPass','yaml','Hmisc','mockery','mime','jsonlite'))" + - R --no-save -e "install.packages(c('devtools','checkmate','chron','curl','labelVector','lubridate','keyring','getPass','yaml','Hmisc','mockery','mime','jsonlite','shelter'))" - R --no-save -e "Sys.setenv(CI=1); devtools::test(stop_on_failure=TRUE)" diff --git a/DESCRIPTION b/DESCRIPTION index 6375e0c2..3b859436 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -45,9 +45,7 @@ Imports: labelVector, lubridate, mime, - keyring, - getPass, - yaml + shelter LazyLoad: yes Suggests: testthat (>= 3.0.0), Hmisc, rstudioapi, mockery URL: https://github.com/vubiostat/redcapAPI diff --git a/NAMESPACE b/NAMESPACE index 21e1109a..ba045adb 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -210,7 +210,6 @@ export(stripHTMLandUnicode) export(stripUnicode) export(switchDag) export(unitsFieldAnnotation) -export(unlockKeys) export(unlockREDCap) export(valChoice) export(valPhone) @@ -230,20 +229,13 @@ importFrom(curl,handle_setheaders) importFrom(curl,handle_setopt) importFrom(curl,new_handle) importFrom(curl,parse_headers_list) -importFrom(getPass,getPass) importFrom(jsonlite,fromJSON) -importFrom(keyring,key_delete) -importFrom(keyring,key_get) -importFrom(keyring,key_list) -importFrom(keyring,key_set_with_value) -importFrom(keyring,keyring_create) -importFrom(keyring,keyring_list) -importFrom(keyring,keyring_unlock) importFrom(labelVector,get_label) importFrom(labelVector,is.labelled) importFrom(labelVector,set_label) importFrom(lubridate,parse_date_time) importFrom(mime,guess_type) +importFrom(shelter,unlockKeys) importFrom(stats,reshape) importFrom(utils,capture.output) importFrom(utils,compareVersion) @@ -255,4 +247,3 @@ importFrom(utils,read.csv) importFrom(utils,tail) importFrom(utils,write.csv) importFrom(utils,write.table) -importFrom(yaml,read_yaml) diff --git a/R/redcapAPI-package.R b/R/redcapAPI-package.R index d02cef86..34569d7b 100644 --- a/R/redcapAPI-package.R +++ b/R/redcapAPI-package.R @@ -18,17 +18,14 @@ #' @keywords internal #' @import checkmate #' @importFrom chron times -#' @importFrom getPass getPass #' @importFrom curl curl_fetch_memory curl_version form_file handle_cookies handle_reset #' handle_setform handle_setheaders handle_setopt new_handle parse_headers_list #' @importFrom jsonlite fromJSON -#' @importFrom keyring key_delete key_get key_list key_set_with_value keyring_create keyring_list keyring_unlock #' @importFrom labelVector get_label is.labelled set_label #' @importFrom lubridate parse_date_time #' @importFrom mime guess_type #' @importFrom stats reshape #' @importFrom utils capture.output compareVersion head modifyList #' osVersion packageVersion read.csv tail write.csv write.table -#' @importFrom yaml read_yaml "_PACKAGE" diff --git a/R/redcapConnection.R b/R/redcapConnection.R index 8c81842a..5c845fb6 100644 --- a/R/redcapConnection.R +++ b/R/redcapConnection.R @@ -173,7 +173,8 @@ redcapConnection <- function(url = getOption('redcap_api_url'), config = NULL, retries = 5, retry_interval = 2^(seq_len(retries)), - retry_quietly = TRUE) + retry_quietly = TRUE, + ...) { coll <- checkmate::makeAssertCollection() diff --git a/R/unlockREDCap.R b/R/unlockREDCap.R index 0eeb309c..e4c18a7f 100644 --- a/R/unlockREDCap.R +++ b/R/unlockREDCap.R @@ -74,209 +74,6 @@ connectAndCheck <- function(key, url, ...) ) } -.savePWGlobalEnv <- function(password) -{ - Sys.setenv(REDCAPAPI_PW=password) - - # Hacked work around for RStudio starting new session for everything - if(requireNamespace("rstudioapi", quietly = TRUE) && - rstudioapi::isAvailable(child_ok=TRUE)) - rstudioapi::sendToConsole(sprintf("Sys.setenv(REDCAPAPI_PW='%s')", password), execute = TRUE, echo=FALSE, focus=FALSE) -} - -.clearPWGlobalEnv <- function() -{ - Sys.unsetenv("REDCAPAPI_PW") - # Hacked work around for RStudio starting new session for everything - if(requireNamespace("rstudioapi", quietly = TRUE) && - rstudioapi::isAvailable(child_ok=TRUE)) - rstudioapi::sendToConsole('Sys.unsetenv("REDCAPAPI_PW")', execute = TRUE, echo=FALSE, focus=FALSE) -} - -.getPWGlobalEnv <- function() -{ - Sys.getenv("REDCAPAPI_PW") -} - - ############################################################################# - ## unlock via YAML override if it exists -## -.unlockYamlOverride <- function(connections, connectionFUNs) -{ - config_file <- file.path("..", paste0(basename(getwd()),".yml")) - - if(!file.exists(config_file)) return(list()) - - config <- yaml::read_yaml(config_file) - if(is.null(config$redcapAPI)) stop(paste0("Config file '",config_file,"' does not contain required 'redcapAPI' entry")) - config <- config$redcapAPI - if(is.null(config$keys)) stop(paste0("Config file '",config_file,"' does not contain required 'keys' entry under the 'redcapAPI' entry")) - keys <- config$keys - - dest <- lapply(seq_along(connections), function(i) - { - conn <- connections[i] - key <- keys[[conn]] - - if(is.null(key) || length(key)==0) - stop(paste0("Config file '", config_file, "' does not have API_KEY for '", conn,"' under 'redcapAPI: keys:' specified.")) - if(!is.character(key)) - stop(paste0("Config file '", config_file, "' invalid entry for '", conn,"' under 'redcapAPI: keys:'.")) - if(length(key) > 1) - stop(paste0("Config file '", config_file, "' has too may key entries for '", conn,"' under 'redcapAPI: keys:' specified.")) - - do.call(connectionFUNs[[i]], list(key=key)) - }) - names(dest) <- if(is.null(names(connections))) connections else names(connections) - - return(dest) -} - ############################################################################# - ## unlock via ENV override if it exists -## -.unlockENVOverride <- function(connections, connectionFUNs) -{ - api_key_ENV <- sapply(connections, function(x) Sys.getenv(toupper(x))) - - if(all(api_key_ENV == "")) return(list()) - - if(any(api_key_ENV == "")) - stop(paste("Some matching ENV variables found but missing:",paste0(toupper(connections[api_key_ENV=='']), collapse=", "))) - - dest <- lapply(seq_along(connections), function(i) - { - do.call(connectionFUNs[[i]], list(key = api_key_ENV[i])) - }) - names(dest) <- if(is.null(names(api_key_ENV))) api_key_ENV else names(api_key_ENV) - - return(dest) -} - - ############################################################################# - ## unlock keyring -## -.unlockKeyring <- function(keyring, passwordFUN) -{ - state <- keyring::keyring_list() - state <- state[state$keyring==keyring,] - msg <- paste0("Please enter password to unlock API keyring '",keyring, "'.") - - # If so, does it exist? - if(nrow(state) == 1) # Exists => UNLOCK - { - locked <- state$locked - # Is it locked - while(locked) - { - password <- .getPWGlobalEnv() - stored <- !is.null(password) && password != '' - if(!stored) password <- passwordFUN(msg) - if(is.null(password) || password == '') stop(paste0("User aborted keyring '",keyring, "' unlock.")) - - tryCatch( - { - keyring::keyring_unlock(keyring, password) - .savePWGlobalEnv(password) - locked <- FALSE - }, - error = function(e) - { - if(stored) .clearPWGlobalEnv() - - msg <<- paste0("Provided password failed. Please enter password to unlock API keyring '",keyring, "'.") - } - ) - } - } else # Keyring does not exist => Create - { - password <- passwordFUN(paste0("Creating keyring. Enter NEW password for the keyring '", - keyring, "'.")) - if(is.null(password) || password == '') stop(paste0("User cancelled creation of keyring '", keyring, "'.")) - - keyring::keyring_create(keyring, password) - .savePWGlobalEnv(password) - } -} - - ############################################################################# - ## Find the best password function -## If rstudioapi is loaded and rstudio is running, then use that. -## getOption('askpass') returns a function that does not work on MAC -## when knitting from RStudio, ugh. -.default_pass <- function() -{ - if(grepl('mac', tolower(utils::osVersion)) && - requireNamespace("rstudioapi", quietly = TRUE) && - rstudioapi::isAvailable(child_ok=TRUE)) - { - rstudioapi::askForPassword - } else getPass::getPass -} - -# Main internal algorithm -.unlockAlgorithm <- function(connections, - connectionFUNs, - keyring, - envir, - passwordFUN) -{ - if(is.numeric(envir)) envir <- as.environment(envir) - - # Use YAML config if it exists - dest <- .unlockYamlOverride(connections, connectionFUNs) - if(length(dest) > 0) - return(if(is.null(envir)) dest else list2env(dest, envir=envir)) - - # Use ENV if it exists and YAML does not exist - dest <- .unlockENVOverride(connections, connectionFUNs) - if(length(dest) > 0) - return(if(is.null(envir)) dest else list2env(dest, envir=envir)) - - .unlockKeyring(keyring, passwordFUN) - - # Open Connections - dest <- lapply(seq_along(connections), function(i) - { - stored <- connections[i] %in% keyring::key_list("redcapAPI", keyring)[,2] - - api_key <- if(stored) - { - keyring::key_get("redcapAPI", connections[i], keyring) - } else - { - passwordFUN(paste0("Please enter API_KEY for '", connections[i], "'.")) - } - - if(is.null(api_key) || api_key == '') stop(paste("No API_KEY entered for", connections[i])) - - conn <- NULL - while(is.null(conn)) - { - conn <- (connectionFUNs[[i]])(api_key) # was connectAndCheck(api_key, url, ...) - if(is.null(conn)) - { - keyring::key_delete("redcapAPI", unname(connections[i]), keyring) - api_key <- passwordFUN(paste0( - "Invalid API_KEY for '", connections[i], - "' in keyring '", keyring, - "'. Possible causes include: mistyped, renewed, or revoked.", - " Please enter a new key or cancel to abort.")) - if(is.null(api_key) || api_key == '') stop("unlockAPIKEY aborted") - } else if(!stored) - { - keyring::key_set_with_value( service="redcapAPI", - username=unname(connections[i]), - password=api_key, - keyring=keyring) - } - } - conn - }) - names(dest) <- if(is.null(names(connections))) connections else names(connections) - - if(is.null(envir)) dest else list2env(dest, envir=envir) -} - #' Open REDCap connections using cryptolocker for storage of API_KEYs. #' #' Opens a set of connections to REDcap from API_KEYs stored in an encrypted keyring. @@ -328,14 +125,9 @@ connectAndCheck <- function(key, url, ...) #' global environment. Will accept a number such a '1' for global as well. #' @param keyring character. Potential keyring, not used by default. #' @param url character. The url of one's institutional REDCap server api. -#' @param passwordFUN function. Function to get the password for the keyring. Usually defaults `getPass::getPass`. -#' On MacOS it will use rstudioapi::askForPassword if available. -#' @param otherKeys list. A list of other keys to retrieve. Each list element -#' must be a list with name, variable and connectFUN keys. The connectFUN -#' can be as simple as an id function `function(x) x` or something that -#' constructs a connection object or calls `stop` if it's invalid. #' @param connectFUN function. A function that takes a key and returns a connection. -#' the function should call `stop` if the key is invalid in some manner. +#' the function should call `stop` if unable to connect to the URL. +#' The function should return NULL if the API_KEY is invalid. #' @param \dots Additional arguments passed to [redcapConnection()]. #' @return If `envir` is NULL returns a list of opened connections. Otherwise #' connections are assigned into the specified `envir`. @@ -356,19 +148,14 @@ connectAndCheck <- function(key, url, ...) #' keyring = '', #' envir = globalenv(), #' url = 'https:///api/') -#' -#' unlockKeys(c(logging = 'SplunkKey'), -#' keyring = '', -#' envir = 1) #' } -#' @rdname unlockREDCap #' @export +#' @importFrom shelter unlockKeys unlockREDCap <- function(connections, url, keyring, envir = NULL, - passwordFUN = .default_pass(), - otherKeys = NULL, + service = 'redcapAPI', ...) { ########################################################################### @@ -380,62 +167,16 @@ unlockREDCap <- function(connections, checkmate::assert_character(x = url, null.ok = FALSE, add = coll) checkmate::assert_character(x = keyring, null.ok = FALSE, add = coll) checkmate::assert_character(x = connections, null.ok = FALSE, add = coll) - checkmate::assert_function( x = passwordFUN, null.ok = FALSE, add = coll) - checkmate::assert_class( x = envir, null.ok = TRUE, add = coll, classes="environment") - checkmate::reportAssertions(coll) - - ########################################################################### - ## Setup Internal Loop functions - n <- length(connections) - connectionFUNs <- vector('list', n) - for(i in seq(n)) - connectionFUNs[[i]] <- function(key) connectAndCheck(key, url, ...) - - ########################################################################### - ## Do it - .unlockAlgorithm(connections, - connectionFUNs, - keyring, - envir, - passwordFUN) -} - -#' @rdname unlockREDCap -#' @export -unlockKeys <- function(connections, - keyring, - connectFUN = NULL, - envir = NULL, - passwordFUN = .default_pass(), - ...) -{ - ########################################################################### - # Check parameters passed to function - coll <- checkmate::makeAssertCollection() - - if(is.numeric(envir)) envir <- as.environment(envir) - - checkmate::assert_character(x = keyring, null.ok = FALSE, add = coll) - checkmate::assert_character(x = connections, null.ok = FALSE, add = coll) - checkmate::assert_function( x = passwordFUN, null.ok = FALSE, add = coll) checkmate::assert_class( x = envir, null.ok = TRUE, add = coll, classes="environment") - checkmate::assert_function( x = connectFUN, null.ok = TRUE, add = coll, nargs=1) + checkmate::assert_character(x = service, null.ok = FALSE, add = coll) checkmate::reportAssertions(coll) - if(is.null(connectFUN)) connectFUN <- function(x) x - - ########################################################################### - ## Setup Internal Loop functions - n <- length(connections) - connectionFUNs <- vector('list', n) - for(i in seq(n)) connectionFUNs[[i]] <- function(key) connectFUN(key, ...) - ########################################################################### ## Do it - .unlockAlgorithm(connections, - connectionFUNs, - keyring, - envir, - passwordFUN) + unlockKeys(connections, + keyring, + function(key) connectAndCheck(key, url, ...), + envir=envir, + service=service, + ...) } - diff --git a/man/redcapConnection.Rd b/man/redcapConnection.Rd index 8001fffb..c1b86716 100644 --- a/man/redcapConnection.Rd +++ b/man/redcapConnection.Rd @@ -13,7 +13,8 @@ redcapConnection( config = NULL, retries = 5, retry_interval = 2^(seq_len(retries)), - retry_quietly = TRUE + retry_quietly = TRUE, + ... ) \method{print}{redcapApiConnection}(x, ...) @@ -63,10 +64,10 @@ the number of retries.} \item{retry_quietly}{\code{logical(1)}. When \code{FALSE}, messages will be shown giving the status of the API calls. Defaults to \code{TRUE}.} -\item{x}{\code{redcapConnection} object to be printed} - \item{...}{arguments to pass to other methods} +\item{x}{\code{redcapConnection} object to be printed} + \item{meta_data}{Either a \code{character} giving the file from which the metadata can be read, or a \code{data.frame}.} diff --git a/man/unlockREDCap.Rd b/man/unlockREDCap.Rd index 6d85f1b8..8a3e41c0 100644 --- a/man/unlockREDCap.Rd +++ b/man/unlockREDCap.Rd @@ -2,27 +2,9 @@ % Please edit documentation in R/unlockREDCap.R \name{unlockREDCap} \alias{unlockREDCap} -\alias{unlockKeys} \title{Open REDCap connections using cryptolocker for storage of API_KEYs.} \usage{ -unlockREDCap( - connections, - url, - keyring, - envir = NULL, - passwordFUN = .default_pass(), - otherKeys = NULL, - ... -) - -unlockKeys( - connections, - keyring, - connectFUN = NULL, - envir = NULL, - passwordFUN = .default_pass(), - ... -) +unlockREDCap(connections, url, keyring, envir = NULL, ...) } \arguments{ \item{connections}{character vector. A list of strings that define the @@ -39,18 +21,11 @@ The name in the returned list is this name.} which returns the keys as a list. Use \code{\link[=globalenv]{globalenv()}} to assign in the global environment. Will accept a number such a '1' for global as well.} -\item{passwordFUN}{function. Function to get the password for the keyring. Usually defaults \code{getPass::getPass}. -On MacOS it will use rstudioapi::askForPassword if available.} - -\item{otherKeys}{list. A list of other keys to retrieve. Each list element -must be a list with name, variable and connectFUN keys. The connectFUN -can be as simple as an id function \code{function(x) x} or something that -constructs a connection object or calls \code{stop} if it's invalid.} - \item{\dots}{Additional arguments passed to \code{\link[=redcapConnection]{redcapConnection()}}.} \item{connectFUN}{function. A function that takes a key and returns a connection. -the function should call \code{stop} if the key is invalid in some manner.} +the function should call \code{stop} if unable to connect to the URL. +The function should return NULL if the API_KEY is invalid.} } \value{ If \code{envir} is NULL returns a list of opened connections. Otherwise @@ -106,10 +81,6 @@ unlockREDCap(c(test_conn = 'TestRedcapAPI', keyring = '', envir = globalenv(), url = 'https:///api/') - -unlockKeys(c(logging = 'SplunkKey'), - keyring = '', - envir = 1) } } \seealso{ diff --git a/tests/testthat/test-020-redcapConnection-Functionality.R b/tests/testthat/test-020-redcapConnection-Functionality.R index 1ad4d323..aed36b58 100644 --- a/tests/testthat/test-020-redcapConnection-Functionality.R +++ b/tests/testthat/test-020-redcapConnection-Functionality.R @@ -1,23 +1,10 @@ context("redcapConnection Functionality") -# Look for a yaml config for automated environments -config_file <- file.path("..", paste0(basename(getwd()),".yml")) -API_KEY <- - if(file.exists(config_file)) - { - config <- read_yaml(config_file) - config$redcapAPI$keys$TestRedcapAPI - } else - { - keyring::key_get('redcapAPI', testdb, 'API_KEYs') - } - - test_that("redcapApiConnection can be created", - expect_class( - redcapConnection(url = url, token = API_KEY), - classes = c("redcapApiConnection", "redcapConnection") - ) + expect_class( + redcapConnection(url = url, token = 'YO'), + classes = c("redcapApiConnection", "redcapConnection") + ) ) diff --git a/tests/testthat/test-024-unlockREDCap.R b/tests/testthat/test-024-unlockREDCap.R index 77203988..ad8c6680 100644 --- a/tests/testthat/test-024-unlockREDCap.R +++ b/tests/testthat/test-024-unlockREDCap.R @@ -71,276 +71,12 @@ test_that( "connectAndCheck errors with bad url", expect_error(connectAndCheck("key", "badurl"), "Unable to connect") ) - -test_that( - ".unlockYamlOverride return empty list when override yaml doesn't exist", - { - stub(.unlockYamlOverride, "file.exists", FALSE) - - x <- .unlockYamlOverride("TestRedcapAPI", url) - expect_class(x, "list") - expect_true(length(x) == 0) - } -) - -test_that( - ".unlockYamlOverride stops if no redcapAPI entry is found", - { - stub(.unlockYamlOverride, "file.exists", TRUE) - stub(.unlockYamlOverride, "yaml::read_yaml", list()) - - expect_error(.unlockYamlOverride("TestRedcapAPI", url), - "does not contain required 'redcapAPI' entry") - } -) - -test_that( - ".unlockYamlOverride stops if no redcapAPI$keys entry is found", - { - stub(.unlockYamlOverride, "file.exists", TRUE) - stub(.unlockYamlOverride, "yaml::read_yaml", list(redcapAPI=list())) - stub(.unlockYamlOverride, "connectAndCheck", TRUE) - - expect_error(.unlockYamlOverride("TestRedcapAPI", url), - "does not contain required 'keys' entry") - } -) - -test_that( - ".unlockYamlOverride stops if a list redcapAPI$keys entry is found", - { - stub(.unlockYamlOverride, "file.exists", TRUE) - stub(.unlockYamlOverride, "yaml::read_yaml", list(redcapAPI=list(keys=list(TestRedcapAPI=list())))) - stub(.unlockYamlOverride, "connectAndCheck", TRUE) - - expect_error(.unlockYamlOverride("TestRedcapAPI", url), - "does not have API_KEY for") - } -) - -test_that( - ".unlockYamlOverride stops if a non string redcapAPI$keys entry is found", - { - stub(.unlockYamlOverride, "file.exists", TRUE) - stub(.unlockYamlOverride, "yaml::read_yaml", list(redcapAPI=list(keys=list(TestRedcapAPI=TRUE)))) - stub(.unlockYamlOverride, "connectAndCheck", TRUE) - - expect_error(.unlockYamlOverride("TestRedcapAPI", url), - "invalid entry") - } -) - -test_that( - ".unlockYamlOverride returns an entry for every connection", - { - stub(.unlockYamlOverride, "file.exists", TRUE) - stub(.unlockYamlOverride, "yaml::read_yaml", - list(redcapAPI=list(keys=list(TestRedcapAPI='xyz', Sandbox='xyz')))) - x <- .unlockYamlOverride(c("TestRedcapAPI", "Sandbox"), list(function(...) TRUE, function(...) TRUE)) - - expect_true(x$TestRedcapAPI) - expect_true(x$Sandbox) - } -) - -test_that( - ".unlockYamlOverride returns an entry for every connection renamed as requested", - { - stub(.unlockYamlOverride, "file.exists", TRUE) - stub(.unlockYamlOverride, "yaml::read_yaml", - list(redcapAPI=list(keys=list(TestRedcapAPI='xyz', Sandbox='xyz')))) - x <- .unlockYamlOverride(c(rcon="TestRedcapAPI", sand="Sandbox"), list(function(...) TRUE, function(...) TRUE)) - expect_true(x$rcon) - expect_true(x$sand) - } -) - -test_that( - ".unlockENVOverride return empty when override ENV doesn't exist", - { - stub(.unlockENVOverride, "Sys.getenv", "") - x <- .unlockENVOverride("TestRedcapAPI", url) - expect_class(x, "list") - expect_true(length(x$TestRedcapAPI) == 0) - } -) - -test_that( - ".unlockENVOverride will stop when only one of two ENV's are found", - { - stub(.unlockENVOverride, "sapply", c("", "YO")) - expect_error(.unlockENVOverride(c("x", "y"), url)) - } -) - -test_that( - ".unlockENVOverride returns an entry for every connection", - { - stub(.unlockENVOverride, "Sys.getenv", "xyz") - stub(.unlockENVOverride, "connectAndCheck", TRUE) - x <- .unlockENVOverride(c("TestRedcapAPI", "Sandbox"), - list(function(...) TRUE, function(...) TRUE)) - expect_true(x$TestRedcapAPI) - expect_true(x$Sandbox) - } -) - -test_that( - ".unlockENVOverride returns an entry for every connection renamed as requested", - { - stub(.unlockENVOverride, "Sys.getenv", "xyz") - stub(.unlockENVOverride, "connectAndCheck", TRUE) - x <- .unlockENVOverride(c(rcon="TestRedcapAPI", sand="Sandbox"), - list(function(...) TRUE, function(...) TRUE)) - expect_true(x$rcon) - expect_true(x$sand) - } -) - -test_that( - ".unlockKeyring pulls password from env and writes back", - { - stub(.unlockKeyring, "keyring::keyring_list", - data.frame(keyring=c("Elsewhere", "API_KEYs", "JoesGarage"), - num_secrets=0:2, - locked=rep(TRUE, 3))) - stub(.unlockKeyring, ".getPWGlobalEnv", "xyz") - stub(.unlockKeyring, "keyring::keyring_unlock", NULL) - - calls <- 0 - passwordFUN <- function(...) {calls <<- calls + 1} - - .unlockKeyring("API_KEYs", passwordFUN) - - expect_true(calls == 0) # No requests for password from user - expect_true(Sys.getenv("REDCAPAPI_PW") == "xyz") - } -) - -test_that( - ".unlockKeyring asks user for password when not in env, unlocks and writes to env", - { - stub(.unlockKeyring, "keyring::keyring_list", - data.frame(keyring=c("Elsewhere", "API_KEYs", "JoesGarage"), - num_secrets=0:2, - locked=rep(TRUE, 3))) - stub(.unlockKeyring, ".getPWGlobalEnv", "") - stub(.unlockKeyring, "keyring::keyring_unlock", NULL) - - calls <- 0 - passwordFUN <- function(...) {calls <<- calls + 1; "xyz"} - - .unlockKeyring("API_KEYs", passwordFUN) - - expect_true(calls == 1) # Requests password - expect_true(Sys.getenv("REDCAPAPI_PW") == "xyz") - } -) - -test_that( - ".unlockKeyring asks user for password and aborts when they cancel", - { - stub(.unlockKeyring, "keyring::keyring_list", - data.frame(keyring=c("Elsewhere", "API_KEYs", "JoesGarage"), - num_secrets=0:2, - locked=rep(TRUE, 3))) - stub(.unlockKeyring, ".getPWGlobalEnv", "") - stub(.unlockKeyring, "keyring::keyring_unlock", NULL) - - calls <- 0 - passwordFUN <- function(...) {calls <<- calls + 1; ""} - - expect_error(.unlockKeyring("API_KEYs", passwordFUN), "User aborted keyring") - - expect_true(calls == 1) # Requests password - } -) - - -test_that( - ".unlockKeyring asks user for password when one in env fails, unlocks and writes to env", - { - stub(.unlockKeyring, "keyring::keyring_list", - data.frame(keyring=c("Elsewhere", "API_KEYs", "JoesGarage"), - num_secrets=0:2, - locked=rep(TRUE, 3))) - stub(.unlockKeyring, "Sys.getenv", - mock("fail", "")) - stub(.unlockKeyring, "keyring::keyring_unlock", - mock(stop("fail"), "joe")) - - calls <- 0 - passwordFUN <- function(...) {calls <<- calls + 1; "xyz"} - - .unlockKeyring("API_KEYs", passwordFUN) - - expect_true(calls == 1) # Requests password - expect_true(Sys.getenv("REDCAPAPI_PW") == "xyz") - } -) - -test_that( - ".unlockKeyring creates keyring if it doesn't exist", - { - Sys.unsetenv("REDCAPAPI_PW") - ukr <- mock(data.frame(keyring=c("Elsewhere", "API_KEYs", "JoesGarage"), - num_secrets=0:2, - locked=rep(TRUE, 3))) - m <- mock(TRUE) - - calls <- 0 - passwordFUN <- function(...) {calls <<- calls + 1; "xyz"} - - with_mocked_bindings( - { - .unlockKeyring("MakeMe", passwordFUN) - expect_call(m, 1, keyring::keyring_create(keyring,password)) - }, - keyring_create = m, - keyring_list = ukr, - .package = "keyring" - ) - - expect_equal(mock_args(m)[[1]], list("MakeMe", "xyz")) - expect_true(calls == 1) # Asks user for password - expect_true(Sys.getenv("REDCAPAPI_PW") == "xyz") # Stores result - } -) - - -test_that( - ".unlockKeyring creates keyring respects user cancel", - { - Sys.unsetenv("REDCAPAPI_PW") - stub(.unlockKeyring, "keyring::keyring_list", - data.frame(keyring=c("Elsewhere", "API_KEYs", "JoesGarage"), - num_secrets=0:2, - locked=rep(TRUE, 3))) - m <- mock(TRUE) - - calls <- 0 - passwordFUN <- function(...) {calls <<- calls + 1; ""} - - with_mocked_bindings( - { - expect_error(.unlockKeyring("MakeMe", passwordFUN), "User cancelled") - expect_called(m, 0) - }, - keyring_create = m - ) - - expect_true(calls == 1) # Asks user for password - expect_true(Sys.getenv("REDCAPAPI_PW") == "") # Nothing Stored - } -) - test_that( "unlockREDCap pulls API_KEY and opens connection from keyring returning as list", { skip_if(Sys.getenv("CI") == "1", "CI cannot test user interactions") - stub(unlockREDCap, ".unlockYamlOverride", list()) # No yaml - + expect_silent(x <- unlockREDCap(c(rcon=testdb), url, keyring="API_KEYs")) expect_true("rcon" %in% names(x)) expect_class(x$rcon, "redcapApiConnection") @@ -350,11 +86,6 @@ test_that( test_that( "unlockREDCap pulls API_KEY and opens connection from keyring written to env", { - skip_if(Sys.getenv("CI") == "1", - "CI cannot test user interactions") - - stub(unlockREDCap, ".unlockYamlOverride", list()) # No yaml - calls <- 0 passwordFUN <- function(...) {calls <<- calls + 1; ""} e <- new.env() @@ -368,35 +99,3 @@ test_that( expect_true(calls == 0) # No password requests } ) - -test_that( - ".unlockAlgorithm asks for API_KEY if not stored, opens connection and stores", - { - m <- mock(TRUE) - stub(.unlockAlgorithm, ".unlockYamlOverride", list()) # No yaml - - stub(.unlockAlgorithm, "keyring::key_list", - data.frame(service="recapAPI", username="Nadda")) - stub(.unlockAlgorithm, "keyring::key_set_with_value", m) - - calls <- 0 - passwordFUN <- function(...) {calls <<- calls + 1; "xyz"} - - - n <- mock(TRUE) - - x <- .unlockAlgorithm( - c(rcon="George"), - list(n), - keyring="API_KEYs", - envir=NULL, - passwordFUN=passwordFUN) - - expect_true("rcon" %in% names(x)) - expect_true(x$rcon) - expect_equal(calls, ifelse(Sys.getenv("CI") == "1", 2, 1)) # Called to ask once - expect_called(m, 1) # Called key_set_with_value once - expect_equal(mock_args(m)[[1]], list(service="redcapAPI", username="George", password="xyz", keyring="API_KEYs")) - expect_called(n, 1) # Called connectAndCheck - } -) From 4b4678910d2a38d895d8770a105c56c73fb51574 Mon Sep 17 00:00:00 2001 From: Shawn Garbett Date: Wed, 4 Dec 2024 14:50:50 -0600 Subject: [PATCH 09/16] Updated NEWS/VERSION for #417 --- DESCRIPTION | 2 +- NEWS | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/DESCRIPTION b/DESCRIPTION index c05de5b6..7f32d663 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -1,7 +1,7 @@ Package: redcapAPI Type: Package Title: Interface to 'REDCap' -Version: 2.10.1 +Version: 2.11.0 Authors@R: c( person("Benjamin", "Nutter", email = "benjamin.nutter@gmail.com", role = c("ctb", "aut")), diff --git a/NEWS b/NEWS index 2f2e73b1..793e53b9 100644 --- a/NEWS +++ b/NEWS @@ -10,6 +10,11 @@ A future release of version 3.0.0 will introduce several breaking changes! * The `exportProjectInfo` and `exportBundle` functions are being discontinued. Their functionality is replaced by caching values on the connection object. * The `cleanseMetaData` function is being discontinued. +## 2.11.0 + +* `unlockREDCap` internal code is now in package `shelter`. +* This is a breaking change as all existing local keyrings must be reseeded. + ## 2.10.1 * `unlockREDCap` no longer changes console focus From a4bdd478adc94dfb8a01f207993bfad0e13799f4 Mon Sep 17 00:00:00 2001 From: Shawn Garbett Date: Wed, 4 Dec 2024 15:42:38 -0600 Subject: [PATCH 10/16] Fix to docs for CHECK #417 --- R/unlockREDCap.R | 4 +--- man/unlockREDCap.Rd | 15 ++++++++++----- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/R/unlockREDCap.R b/R/unlockREDCap.R index 672d7455..f63200c9 100644 --- a/R/unlockREDCap.R +++ b/R/unlockREDCap.R @@ -131,10 +131,8 @@ connectAndCheck <- function(key, url, ...) #' which returns the keys as a list. Use [globalenv()] to assign in the #' global environment. Will accept a number such a '1' for global as well. #' @param keyring character. Potential keyring, not used by default. +#' @param service character. The name to use in a yaml file for locating keys. #' @param url character. The url of one's institutional REDCap server api. -#' @param connectFUN function. A function that takes a key and returns a connection. -#' the function should call `stop` if unable to connect to the URL. -#' The function should return NULL if the API_KEY is invalid. #' @param \dots Additional arguments passed to [redcapConnection()]. #' @return If `envir` is NULL returns a list of opened connections. Otherwise #' connections are assigned into the specified `envir`. diff --git a/man/unlockREDCap.Rd b/man/unlockREDCap.Rd index 8a3e41c0..c8d08032 100644 --- a/man/unlockREDCap.Rd +++ b/man/unlockREDCap.Rd @@ -4,7 +4,14 @@ \alias{unlockREDCap} \title{Open REDCap connections using cryptolocker for storage of API_KEYs.} \usage{ -unlockREDCap(connections, url, keyring, envir = NULL, ...) +unlockREDCap( + connections, + url, + keyring, + envir = NULL, + service = "redcapAPI", + ... +) } \arguments{ \item{connections}{character vector. A list of strings that define the @@ -21,11 +28,9 @@ The name in the returned list is this name.} which returns the keys as a list. Use \code{\link[=globalenv]{globalenv()}} to assign in the global environment. Will accept a number such a '1' for global as well.} -\item{\dots}{Additional arguments passed to \code{\link[=redcapConnection]{redcapConnection()}}.} +\item{service}{character. The name to use in a yaml file for locating keys.} -\item{connectFUN}{function. A function that takes a key and returns a connection. -the function should call \code{stop} if unable to connect to the URL. -The function should return NULL if the API_KEY is invalid.} +\item{\dots}{Additional arguments passed to \code{\link[=redcapConnection]{redcapConnection()}}.} } \value{ If \code{envir} is NULL returns a list of opened connections. Otherwise From 59eb609735a317467291f78e6c2aace2097865a5 Mon Sep 17 00:00:00 2001 From: Shawn Garbett Date: Wed, 4 Dec 2024 15:50:06 -0600 Subject: [PATCH 11/16] Removed old-rel-3 due to failing Hmisc install #417 --- .github/workflows/r-cmd-check.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/r-cmd-check.yml b/.github/workflows/r-cmd-check.yml index 94f80992..71f39127 100644 --- a/.github/workflows/r-cmd-check.yml +++ b/.github/workflows/r-cmd-check.yml @@ -35,7 +35,6 @@ jobs: - {os: ubuntu-latest, r: 'release'} - {os: ubuntu-latest, r: 'oldrel-1'} - {os: ubuntu-latest, r: 'oldrel-2'} - - {os: ubuntu-latest, r: 'oldrel-3'} env: R_KEEP_PKG_SOURCE: yes From 4dec33b450289d917a88f8197e55636cd62f9f83 Mon Sep 17 00:00:00 2001 From: Shawn Garbett Date: Wed, 4 Dec 2024 16:33:17 -0600 Subject: [PATCH 12/16] Removed suggested dependency that is no longer required #417 --- DESCRIPTION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DESCRIPTION b/DESCRIPTION index 7f32d663..01ab55e7 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -48,7 +48,7 @@ Imports: mime, shelter LazyLoad: yes -Suggests: testthat (>= 3.0.0), Hmisc, rstudioapi, mockery +Suggests: testthat (>= 3.0.0), Hmisc, mockery URL: https://github.com/vubiostat/redcapAPI BugReports: https://github.com/vubiostat/redcapAPI/issues Encoding: UTF-8 From 63a18b52bd31869348ed5dc9c56177d3eb8cb9d5 Mon Sep 17 00:00:00 2001 From: Shawn Garbett Date: Wed, 4 Dec 2024 16:51:17 -0600 Subject: [PATCH 13/16] Added deepdep plot to README #417 --- README.md | 4 ++++ inst/image/dependencies.png | Bin 0 -> 63547 bytes 2 files changed, 4 insertions(+) create mode 100644 inst/image/dependencies.png diff --git a/README.md b/README.md index 8aec5f18..16148014 100644 --- a/README.md +++ b/README.md @@ -134,6 +134,10 @@ There are several vignettes with helpful information and examples to explore. Th * redcapAPI-best-practices * redcapAPI-offline-connection +## Dependencies + +[](./inst/image/dependencies.png)! + ## Back Matter *NOTE*: Ownership transfer of this package to [VUMC Biostatistics](https://www.vumc.org/biostatistics/vanderbilt-department-biostatistics) is complete. diff --git a/inst/image/dependencies.png b/inst/image/dependencies.png new file mode 100644 index 0000000000000000000000000000000000000000..e910ae0a46da0fcbdfed99bc07c3c9aada9ff0c7 GIT binary patch literal 63547 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 zⅈ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 u)O1IE3(_BjH?U(7{PK23_z2?SvfzwvGuhJxK@v literal 0 HcmV?d00001 From 6720206a7c51d18802babbca83ca7ee8178b6283 Mon Sep 17 00:00:00 2001 From: Shawn Garbett Date: Wed, 4 Dec 2024 16:52:59 -0600 Subject: [PATCH 14/16] Update to dependency plot in README #417 --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 16148014..179f78f5 100644 --- a/README.md +++ b/README.md @@ -136,7 +136,7 @@ There are several vignettes with helpful information and examples to explore. Th ## Dependencies -[](./inst/image/dependencies.png)! +![Dependency Plot](./inst/image/dependencies.png "Dependency Plot") ## Back Matter From f7dbf2c63e025229d2ca7a976eddeb052b84c8de Mon Sep 17 00:00:00 2001 From: Shawn Garbett Date: Wed, 4 Dec 2024 16:56:25 -0600 Subject: [PATCH 15/16] Verbage about dependencies in README --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 179f78f5..b8cd40fe 100644 --- a/README.md +++ b/README.md @@ -136,6 +136,8 @@ There are several vignettes with helpful information and examples to explore. Th ## Dependencies +We strive to keep the dependency chain as minimal as possible. This reduces potential breakage due to downstream packages getting changed and minimizes install time. + ![Dependency Plot](./inst/image/dependencies.png "Dependency Plot") ## Back Matter From cddfbc2627d03a9ac9e4e778e802c8d3e8f54305 Mon Sep 17 00:00:00 2001 From: Shawn Garbett Date: Wed, 11 Dec 2024 09:58:38 -0600 Subject: [PATCH 16/16] Removed superfluous test --- .../testthat/test-020-redcapConnection-Functionality.R | 10 ---------- 1 file changed, 10 deletions(-) delete mode 100644 tests/testthat/test-020-redcapConnection-Functionality.R diff --git a/tests/testthat/test-020-redcapConnection-Functionality.R b/tests/testthat/test-020-redcapConnection-Functionality.R deleted file mode 100644 index aed36b58..00000000 --- a/tests/testthat/test-020-redcapConnection-Functionality.R +++ /dev/null @@ -1,10 +0,0 @@ -context("redcapConnection Functionality") - -test_that("redcapApiConnection can be created", - expect_class( - redcapConnection(url = url, token = 'YO'), - classes = c("redcapApiConnection", "redcapConnection") - ) -) - -