From f973164c120dbb9b8079a32dc63e21a29b278242 Mon Sep 17 00:00:00 2001 From: Jacqueline Buros Date: Wed, 4 Oct 2023 10:21:34 -0400 Subject: [PATCH 1/3] migrate from ENV vars to keyring, via configure() function --- R/geco_api.R | 99 +++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 94 insertions(+), 5 deletions(-) diff --git a/R/geco_api.R b/R/geco_api.R index e0633d5..e1aba34 100644 --- a/R/geco_api.R +++ b/R/geco_api.R @@ -72,12 +72,16 @@ geco_api_url <- function(..., project = NULL, project_version_id = NULL, run_id= #' @param password User password. If not provided, will read the `GECO_API_PASSWORD` environment variable. #' @return The OAuth 2.0 Bearer Token for the Generable API #' @export -login <- function(user, password) { - if (missing(user)) { - user <- Sys.getenv('GECO_API_USER') - } +login <- function(user, password, host) { + url <- .get_url(host) if (missing(password)) { - password <- Sys.getenv('GECO_API_PASSWORD') + # get credentials from keyring + creds <- .get_credentials(host = host, user = user) + user = creds[1] + password = creds[2] + } + if (is.null(user)) { + cli::cli_inform('Note: credential storage has changed; please run `configure()` to migrate to the new storage.') } body <- list(email = user, password = password) resp <- geco_api(LOGIN, body = body, encode = 'json', method = 'POST') @@ -85,6 +89,91 @@ login <- function(user, password) { invisible(resp$content) } +#' Configure Geco credentials, saving in keyring +#' @param user Geco user name (email address). Defaults to GECO_API_USER environment variable +#' @param password Geco password (will prompt if not provided). Defaults to GECO_API_PASSWORD environment variable +#' @param host (optional) alternate host for API, only used for testing +#' @import keyring +#' @export +configure <- function(user, password, host) { + cli::cli_inform('Configuring credentials for Geco API') + url <- .get_url(host) + if (missing(user)) { + cli::cli_inform('Reading username from environment variable: GECO_API_USER') + user <- Sys.getenv('GECO_API_USER', unset = '') + if (user == '') { + stop('Username not provided.') + } + } + if (missing(password)) { + cli::cli_inform('Reading password from environment variable: GECO_API_PASSWORD') + password <- Sys.getenv('GECO_API_PASSWORD', unset = '') + if (password == '' && interactive()) { + password <- rstudioapi::askForPassword() + } + } + cli::cli_inform('Attempting to log in ...') + res <- tryCatch(login(user=user, password = password)) + if (inherits(res, 'try-error')) { + cli::cli_alert_warning('Failed to authenticate.') + } else { + cli::cli_alert_success('Success!') + if (keyring::has_keyring_support() && interactive() && askYesNo("Do you want to save credentials in your keyring?")) { + keyring::key_set_with_value(service = .get_keyring_service(), + username = user, password = password) + cli::cli_alert_success('Credentials saved.') + } else { + cli::cli_inform('Populating credentials in environment variables.') + Sys.setenv('GECO_API_USER'=user) + Sys.setenv('GECO_API_PASSWORD'=password) + } + } +} + +.get_keyring_service <- function(host) { + url <- .get_url(host) + stringr::str_c('R-GECO_API', url, sep = '-') +} +.get_url <- function(host) { + if (missing(host)) { + url <- Sys.getenv('GECO_API_URL', unset = 'https://geco.generable.com') + } else { + url <- glue::glue('https://{host}.generable.com') + Sys.setenv('GECO_API_URL' = url) + } + url +} + +# returns vector as username, password +.get_credentials <- function(host, user) { + if (!keyring::has_keyring_support()) { + if (missing(user)) { + user <- Sys.getenv('GECO_API_USER') + } + password <- Sys.getenv('GECO_API_PASSWORD') + return(c(user, password)) + } + # get user & reconcile with keychain + service <- .get_keyring_service(host) + keys <- key_list(service) + if (missing(user)) { + if (nrow(keys) == 1) { + user <- unique(keys$username) + } else { + user <- Sys.getenv('GECO_API_USER', unset = 'null') + if (is.null(user)) { + stop("Multiple users configured; please set default user with GECO_API_USER environment variable") + } + } + } else if (user %in% keys$username) { + Sys.setenv('GECO_API_USER' = user) + } else { + stop('User does not exist in keyring; please run `configure()`') + } + # get password + password <- key_get(service, username = user) + return(c(user, password)) +} get_auth <- function() { if (!exists(envir = ENV, '.GECO_AUTH')) { From 335e901b08f91dbe2049d6cde7bd689cd999f481 Mon Sep 17 00:00:00 2001 From: Jacqueline Buros Date: Wed, 4 Oct 2023 10:23:01 -0400 Subject: [PATCH 2/3] update documentation --- DESCRIPTION | 5 +++-- NAMESPACE | 4 ++++ man/configure.Rd | 18 ++++++++++++++++++ man/login.Rd | 2 +- 4 files changed, 26 insertions(+), 3 deletions(-) create mode 100644 man/configure.Rd diff --git a/DESCRIPTION b/DESCRIPTION index 21c3bff..14673c8 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -35,7 +35,8 @@ Imports: reticulate, boot, cli, - httpcache + httpcache, + keyring Suggests: testthat, knitr, @@ -44,5 +45,5 @@ Suggests: scales, tidyverse, utils -RoxygenNote: 7.2.0 +RoxygenNote: 7.2.3 VignetteBuilder: knitr diff --git a/NAMESPACE b/NAMESPACE index 798599e..5af3bd8 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -1,5 +1,6 @@ # Generated by roxygen2: do not edit by hand +export(configure) export(extract_subsample_info) export(fetch_association_state) export(fetch_biomarker_params) @@ -37,6 +38,7 @@ export(sample_groups) import(checkmate) import(cli) import(httr) +import(keyring) importFrom(RJSONIO,fromJSON) importFrom(boot,inv.logit) importFrom(broom,tidy) @@ -49,6 +51,8 @@ importFrom(dplyr,select) importFrom(dplyr,semi_join) importFrom(futile.logger,flog.logger) importFrom(glue,glue_safe) +importFrom(httpcache,GET) +importFrom(httpcache,POST) importFrom(httr,modify_url) importFrom(lubridate,ymd_hms) importFrom(magrittr,"%>%") diff --git a/man/configure.Rd b/man/configure.Rd new file mode 100644 index 0000000..c703b91 --- /dev/null +++ b/man/configure.Rd @@ -0,0 +1,18 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/geco_api.R +\name{configure} +\alias{configure} +\title{Configure Geco credentials, saving in keyring} +\usage{ +configure(user, password, host) +} +\arguments{ +\item{user}{Geco user name (email address). Defaults to GECO_API_USER environment variable} + +\item{password}{Geco password (will prompt if not provided). Defaults to GECO_API_PASSWORD environment variable} + +\item{host}{(optional) alternate host for API, only used for testing} +} +\description{ +Configure Geco credentials, saving in keyring +} diff --git a/man/login.Rd b/man/login.Rd index 502e0bc..ecd4428 100644 --- a/man/login.Rd +++ b/man/login.Rd @@ -4,7 +4,7 @@ \alias{login} \title{Log in to the Generable API} \usage{ -login(user, password) +login(user, password, host) } \arguments{ \item{user}{User email address. If not provided, will read the `GECO_API_USER` environment variable.} From 15c960142a4c1a8f522129bacfc762588091f12e Mon Sep 17 00:00:00 2001 From: Jacqueline Buros Date: Wed, 4 Oct 2023 10:24:06 -0400 Subject: [PATCH 3/3] update tests --- tests/testthat/setup_test_environment.R | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/testthat/setup_test_environment.R b/tests/testthat/setup_test_environment.R index 6d5d033..c2c446a 100644 --- a/tests/testthat/setup_test_environment.R +++ b/tests/testthat/setup_test_environment.R @@ -5,6 +5,7 @@ test_login <- function() { } futile.logger::flog.info('Logging in as test user ...') Sys.setenv(GECO_API_URL=Sys.getenv('GECO_API_TEST_URL')) + configure() a <- login(Sys.getenv('GECO_API_TEST_USER'), password = Sys.getenv('GECO_API_TEST_PASSWORD')) }