Skip to content

Commit

Permalink
Add a new function to set up keyring
Browse files Browse the repository at this point in the history
I've tested this by deleting my `.Renviron` and deleting my keyring `keyring::keyring_delete("createslf")` and it seems to work. Would be great to have someone with an existing set-up (Jen) test it, and to have someone who doesn't have it set up to test it.

The code looks complicated but I've just tried to catch every scenario, so the process should be smooth and clear (from the user's point of view).

I've also expanded the code relating to the username, which will now hopefully work in more cases.
  • Loading branch information
Moohan committed Aug 10, 2023
1 parent 04399a4 commit af8dee6
Show file tree
Hide file tree
Showing 4 changed files with 271 additions and 39 deletions.
1 change: 1 addition & 0 deletions NAMESPACE
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,7 @@ export(read_sc_all_care_home)
export(read_sc_all_home_care)
export(read_sc_all_sds)
export(run_episode_file)
export(setup_keyring)
export(start_fy)
export(start_fy_quarter)
export(start_next_fy_quarter)
Expand Down
247 changes: 215 additions & 32 deletions R/get_connection_PHS_database.R
Original file line number Diff line number Diff line change
@@ -1,69 +1,252 @@
#' Open a connection to a PHS database
#'
#' @description Opens a connection to PHS database to allow data to be collected
#' @description Opens a connection to PHS database given a Data Source Name
#' (DSN) it will try to get the username, asking for input if in an interactive
#' session. It will also use [keyring][keyring::keyring-package] to find
#' an existing keyring called 'createslf' which should contain a `db_password`
#' key with the users database password.
#'
#' @param dsn The Data Source Name passed on to `odbc::dbconnect`
#' the dsn must be setup first. e.g. SMRA or DVPROD
#' @param dsn The Data Source Name (DSN) passed on to [odbc::dbConnect()]
#' the DSN must be set up first. e.g. `SMRA` or `DVPROD`
#' @param username The username to use for authentication,
#' if not supplied it first will check the environment variable
#' and finally ask the user for input.
#' if not supplied it will try to find it automatically and if possible ask the
#' user for input.
#'
#' @return a connection to the specified dsn
#' @return a connection to the specified Data Source.
#' @export
#'
phs_db_connection <- function(dsn, username = Sys.getenv("USER")) {
# Collect username from the environment
username <- Sys.getenv("USER")
phs_db_connection <- function(dsn, username) {
if (missing(username)) {
# Collect username if possible
username <- dplyr::case_when(
Sys.info()["USER"] != "unknown" ~ Sys.info()["USER"],
Sys.getenv("USER") != "" ~ Sys.getenv("USER"),
system2("whoami", stdout = TRUE) != "" ~ system2("whoami", stdout = TRUE),
.default = NA
)
}

# Check the username is not empty and take input if not
if (is.na(username) || username == "") {
# If the username is missing try to get input from the user
if (is.na(username)) {
if (rlang::is_interactive()) {
username <- rstudioapi::showPrompt(
title = "Username",
message = "Username",
default = ""
)
} else {
cli::cli_abort("No username found, you should supply one with {.arg username}")
cli::cli_abort(
c(
"x" = "No username found, you can use the {.arg username} argument.",
"i" = "Alternatively, add {.code USER = \"<your username>\"} to your
{.file .Renviron} file."
)
)
}
}

# TODO improve error messages and provide instructions for setting up keyring
# Add the following code to R profile.
# Sys.setenv("CREATESLF_KEYRING_PASS" = "createslf"),
# keyring_create("createslf", password = Sys.getenv("CREATESLF_KEYRING_PASS")),
# key_set(keyring = "createslf", service = "db_password")
# Check the status of keyring
# Does the 'createslf' keyring exist
keyring_exists <- "createslf" %in% keyring::keyring_list()[["keyring"]]

if (!("createslf" %in% keyring::keyring_list()[["keyring"]])) {
cli::cli_abort("The {.val createslf} keyring does not exist.")
# Does the 'db_password' key exist in the 'createslf' keyring
if (keyring_exists) {
key_exists <- "db_password" %in% keyring::key_list(keyring = "createslf")[["service"]]
} else {
key_exists <- FALSE
}

if (!("db_password" %in% keyring::key_list(keyring = "createslf")[["service"]])) {
cli::cli_abort("{.val db_password} is missing from the {.val createslf} keyring.")
}
# Does the 'CREATESLF_KEYRING_PASS' environment variable exist
env_var_pass_exists <- Sys.getenv("CREATESLF_KEYRING_PASS") != ""

if (Sys.getenv("CREATESLF_KEYRING_PASS") == "") {
cli::cli_abort("You must have the password to unlock the {.val createslf} keyring in your environment as
{.envvar CREATESLF_KEYRING_PASS}. Please set this up in your {.file .Renviron} or {.file .Rprofile}")
if (!all(keyring_exists, key_exists, env_var_pass_exists)) {
if (rlang::is_interactive()) {
setup_keyring(
keyring = "createslf",
key = "db_password",
keyring_exists = keyring_exists,
key_exists = key_exists,
env_var_pass_exists = env_var_pass_exists
)
} else {
if (any(keyring_exists, key_exists, env_var_pass_exists)) {
cli::cli_abort(
c(
"x" = "Your keyring needs to be set up, run:",
"{.code setup_keyring(keyring = \"createslf\", key = \"db_password\",
keyring_exists = {keyring_exists}, key_exists = {key_exists},
env_var_pass_exists = {env_var_pass_exists})}"
)
)
} else {
cli::cli_abort(
c(
"x" = "Your keyring needs to be set up, run:",
"{.code setup_keyring(keyring = \"createslf\",
key = \"db_password\")}"
)
)
}
}
}

keyring::keyring_unlock(keyring = "createslf", password = Sys.getenv("CREATESLF_KEYRING_PASS"))

if (keyring::keyring_is_locked(keyring = "createslf")) {
cli::cli_abort("Keyring is locked. To unlock createslf keyring, please use {.fun keyring::keyring_unlock}")
if (env_var_pass_exists) {
keyring::keyring_unlock(
keyring = "createslf",
password = Sys.getenv("CREATESLF_KEYRING_PASS")
)
} else {
keyring::keyring_unlock(
keyring = "createslf",
password = rstudioapi::askForPassword(
prompt = "Enter the password for the keyring you just created."
)
)
}


# Create the connection
password_text <- stringr::str_glue("{dsn} password for user: {username}")
db_connection <- odbc::dbConnect(
odbc::odbc(),
dsn = dsn,
uid = username,
pwd = keyring::key_get(keyring = "createslf", service = "db_password")
pwd = keyring::key_get(
keyring = "createslf",
service = "db_password"
)
)

keyring::keyring_lock(keyring = "createslf")

return(db_connection)
}

#' Interactively set up the keyring
#'
#' @description
#' This is meant to be used with [phs_db_connection()], it can only be used
#' interactively i.e. not in targets or in a workbench job.
#'
#' With the default options it will go through the steps to set up a keyring
#' which can be used to supply passwords to [odbc::dbConnect()] (or others) in a
#' secure and seamless way.
#'
#' 1. Create an .Renviron file in the project and add a password (for the
#' keyring) to it.
#' 2. Create a keyring with the password - Since we have saved the password as
#' an environment variable it can be picked unlocked and used automatically.
#' 3. Add the database password to the keyring.
#'
#'
#' @param keyring Name of the keyring
#' @param key Name of the key
#' @param keyring_exists Does the keyring already exist
#' @param key_exists Does the key already exist
#' @param env_var_pass_exists Does the password for the keyring already exist
#' in the environment.
#'
#' @return NULL (invisibly)
#' @export
setup_keyring <- function(
keyring = "createslf",
key = "db_password",
keyring_exists = FALSE,
key_exists = FALSE,
env_var_pass_exists = FALSE) {
# First we need the password as an environment variable
if (!env_var_pass_exists) {
if (Sys.getenv("CREATESLF_KEYRING_PASS") != "") {
cli::cli_alert_warning(
"{.env CREATESLF_KEYRING_PASS} already exists in the environment, you
will need to clean this up manually if it's not correct."
)
keyring_password <- Sys.getenv("CREATESLF_KEYRING_PASS")
} else if (
any(stringr::str_detect(
readr::read_lines(".Renviron"),
"^CREATESLF_KEYRING_PASS\\s*?=\\s*?['\"].+?['\"]$"
))

) {
cli::cli_abort(
"Your {.file .Renviron} file looks ok, try restarting your session."
)
} else {
keyring_password <- rstudioapi::askForPassword(
prompt = stringr::str_glue(
"Enter a password for the '{keyring}' keyring, this should
not be your LDAP / database password."
)
)
if (is.null(keyring_password)) {
cli::cli_abort("No keyring password entered.")
}
if (!fs::file_exists(".Renviron")) {
cli::cli_alert_success("Creating an {.file .Renviron} file.")
}

renviron_text <- stringr::str_glue(

Check failure on line 187 in R/get_connection_PHS_database.R

View workflow job for this annotation

GitHub Actions / Check Spelling

`renviron` is not a recognized word. (unrecognized-spelling)

Check failure on line 187 in R/get_connection_PHS_database.R

View workflow job for this annotation

GitHub Actions / Check Spelling

`renviron` is not a recognized word. (unrecognized-spelling)
"CREATESLF_KEYRING_PASS = \"{keyring_password}\""
)

readr::write_lines(
x = renviron_text,

Check failure on line 192 in R/get_connection_PHS_database.R

View workflow job for this annotation

GitHub Actions / Check Spelling

`renviron` is not a recognized word. (unrecognized-spelling)

Check failure on line 192 in R/get_connection_PHS_database.R

View workflow job for this annotation

GitHub Actions / Check Spelling

`renviron` is not a recognized word. (unrecognized-spelling)
file = ".Renviron",
append = TRUE
)

cli::cli_alert_success(
"Added {.code {renviron_text}} to the {.file .Renviron} file."

Check failure on line 198 in R/get_connection_PHS_database.R

View workflow job for this annotation

GitHub Actions / Check Spelling

`renviron` is not a recognized word. (unrecognized-spelling)

Check failure on line 198 in R/get_connection_PHS_database.R

View workflow job for this annotation

GitHub Actions / Check Spelling

`renviron` is not a recognized word. (unrecognized-spelling)
)

cli::cli_alert_info("You will need to restart your R session.")
}
} else {
keyring_password <- Sys.getenv("CREATESLF_KEYRING_PASS")
}

# If the keyring doesn't exist create it now.
if (!keyring_exists) {
if (keyring %in% keyring::keyring_list()[["keyring"]]) {
cli::cli_alert_warning(
"The {keyring} keyring already exists, you will be asked to
overwrite it."
)
}
keyring::keyring_create(
keyring = keyring,
password = keyring_password
)

cli::cli_alert_success(
"Created the '{keyring}' keyring with {.fun keyring::keyring_create}."
)
}

# If we just created the keyring it will already be unlocked
keyring::keyring_unlock(
keyring = keyring,
password = keyring_password
)

# Now add the password to the keyring
if (!key_exists) {
keyring::key_set(
keyring = keyring,
service = key,
prompt = "Enter you LDAP password for database connections."
)

cli::cli_alert_success(
"Added the '{key}' key to the '{keyring}' keyring with
{.fun keyring::keyring_set}."
)
}

keyring::keyring_lock(keyring = keyring)

cli::cli_alert_success(
"The keyring should now be set up correctly."
)

return(invisible(NULL))
}
18 changes: 11 additions & 7 deletions man/phs_db_connection.Rd

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

44 changes: 44 additions & 0 deletions man/setup_keyring.Rd

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit af8dee6

Please sign in to comment.