-
Notifications
You must be signed in to change notification settings - Fork 27
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Keyring 417 #424
base: main
Are you sure you want to change the base?
Keyring 417 #424
Changes from all commits
c53cf2a
e897853
29ebd41
2a6aee4
95df1e7
f505632
abfed14
b5859b5
2633209
bf0735c
5cad54f
1f717e6
4b46789
17ef650
a4bdd47
59eb609
4dec33b
63a18b5
6720206
f7dbf2c
cddfbc2
e54da0a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 = "[email protected]", | ||
role = c("ctb", "aut")), | ||
|
@@ -46,11 +46,9 @@ Imports: | |
labelVector, | ||
lubridate, | ||
mime, | ||
keyring, | ||
getPass, | ||
yaml | ||
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 | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -81,153 +81,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, url, ...) | ||
{ | ||
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(connections, function(conn) | ||
{ | ||
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 | ||
if(!is.null(config$args)) args <- utils::modifyList(args, config$args) | ||
do.call(connectAndCheck, args) | ||
}) | ||
names(dest) <- if(is.null(names(connections))) connections else names(connections) | ||
|
||
return(dest) | ||
} | ||
############################################################################# | ||
## unlock via ENV override if it exists | ||
## | ||
.unlockENVOverride <- function(connections, url, ...) | ||
{ | ||
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(api_key_ENV, function(conn) | ||
{ | ||
args <- list(...) | ||
args$key <- conn | ||
args$url <- url | ||
do.call(connectAndCheck, args) | ||
}) | ||
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(isTRUE(grepl('mac', tolower(utils::osVersion))) && | ||
requireNamespace("rstudioapi", quietly = TRUE) && | ||
rstudioapi::isAvailable(child_ok=TRUE)) | ||
{ | ||
rstudioapi::askForPassword | ||
} else getPass::getPass | ||
} | ||
|
||
#' 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. | ||
|
@@ -278,9 +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. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Maybe refer back to the expected structure section where the service is "redcapAPI" (which is the default argument, and should rarely if ever be changed)? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Side note, the section above the YAML structure (lines 94-98, at least in this version) reference "keyring_delete" and "backend_file". This needs to be cleaned up (use |
||
#' @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 \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`. | ||
|
@@ -303,11 +155,12 @@ connectAndCheck <- function(key, url, ...) | |
#' url = 'https://<INSTITUTIONS_REDCAP_DOMAIN>/api/') | ||
#' } | ||
#' @export | ||
#' @importFrom shelter unlockKeys | ||
unlockREDCap <- function(connections, | ||
url, | ||
keyring, | ||
envir = NULL, | ||
passwordFUN = .default_pass(), | ||
service = 'redcapAPI', | ||
...) | ||
{ | ||
########################################################################### | ||
|
@@ -319,62 +172,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::assert_character(x = service, null.ok = FALSE, add = coll) | ||
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) | ||
|
||
if(is.null(envir)) dest else list2env(dest, envir=envir) | ||
########################################################################### | ||
## Do it | ||
unlockKeys(connections, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. use |
||
keyring, | ||
function(key) connectAndCheck(key, url, ...), | ||
envir=envir, | ||
service=service, | ||
...) | ||
} | ||
|
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think "keyring" is required - fix may be needed in "shelter::unlockKeys" documentation too