forked from r-lib/pkgcache
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add support for sourcing repository credentials from the keyring.
This commit updates both the metadata and package caches to support downloading packages and package indexes from repositories that require HTTP basic authentication to access. Initial support for these authenticated repositories is very narrow: the repository URL must contain a username, no password, and have an entry in the system keyring. We also don't make any attempt to prompt users for credentials when requests fail. Unit tests are included for the new authentication header helpers, but there are currently no tests of end-to-end workflows with an authenticated repository, and I may have missed something. Part of r-lib/pak#729. Signed-off-by: Aaron Jacobs <[email protected]>
- Loading branch information
Showing
6 changed files
with
193 additions
and
7 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,91 @@ | ||
# Returns a set of HTTP headers for the given URL if (1) it belongs to a | ||
# package repository; and (2) has credentials stored in the keyring. | ||
repo_auth_headers <- function(url, allow_prompt = interactive()) { | ||
if (!grepl("/(src|bin)/", url)) { | ||
# Not a package or package index URL. | ||
return(NULL) | ||
} | ||
if (!rlang::is_installed("keyring")) { | ||
return(NULL) | ||
} | ||
creds <- extract_basic_auth_credentials(url) | ||
if (!is.null(creds$password)) { | ||
# The URL already contains a password. This is pretty poor practice, maybe | ||
# we should issue a warning pointing users to the keyring package instead. | ||
return(NULL) | ||
} | ||
if (is.null(creds$username)) { | ||
# No username to key the lookup in the keyring with. | ||
return(NULL) | ||
} | ||
|
||
# In non-interactive contexts, force the use of the environment variable | ||
# backend so that we never hang but can still support CI setups. | ||
backend <- keyring::backend_env | ||
if (allow_prompt) { | ||
backend <- keyring::default_backend() | ||
} | ||
kb <- backend$new() | ||
|
||
# Use the repo URL without the username as the keyring "service". | ||
svc <- extract_repo_url(url) | ||
pwd <- NULL | ||
tryCatch( | ||
{ | ||
pwd <- kb$get(svc, creds$username) | ||
}, | ||
error = function(e) NULL | ||
) | ||
|
||
# Check whether we have one for the hostname as well. | ||
svc <- extract_hostname(url) | ||
tryCatch( | ||
{ | ||
pwd <- kb$get(svc, creds$username) | ||
}, | ||
error = function(e) NULL | ||
) | ||
|
||
if (is.null(pwd)) { | ||
return(NULL) | ||
} | ||
|
||
auth <- paste(creds$username, pwd, sep = ":") | ||
c("Authorization" = paste("Basic", openssl::base64_encode(auth))) | ||
} | ||
|
||
extract_basic_auth_credentials <- function(url) { | ||
pattern <- "^https?://(?:([^:@/]+)(?::([^@/]+))?@)?.*$" | ||
if (!grepl(pattern, url, perl = TRUE)) { | ||
cli::cli_abort("Unrecognized URL format: {.url {url}}.", .internal = TRUE) | ||
} | ||
username <- sub(pattern, "\\1", url, perl = TRUE) | ||
if (!nchar(username)) { | ||
username <- NULL | ||
} | ||
password <- sub(pattern, "\\2", url, perl = TRUE) | ||
if (!nchar(password)) { | ||
password <- NULL | ||
} | ||
list(username = username, password = password) | ||
} | ||
|
||
extract_repo_url <- function(url) { | ||
url <- sub( | ||
"^(https?://)(?:[^:@/]+(?::[^@/]+)?@)?(.*)(/(src|bin)/)(.*)$", | ||
"\\1\\2", | ||
url, | ||
perl = TRUE | ||
) | ||
# Lop off any /__linux__/ subdirectories, too. | ||
sub("^(.*)/__linux__/[^/]+(/.*)$", "\\1\\2", url, perl = TRUE) | ||
} | ||
|
||
extract_hostname <- function(url) { | ||
sub( | ||
"^(https?://)(?:[^:@/]+(?::[^@/]+)?@)?([^/]+)(.*)", | ||
"\\1\\2", | ||
url, | ||
perl = TRUE | ||
) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,85 @@ | ||
test_that("looking up auth headers for repositories works as expected", { | ||
skip_if_not_installed("keyring") | ||
|
||
# No package directory in the URL. | ||
expect_null(repo_auth_headers("https://[email protected]/healthz")) | ||
|
||
# The URL already contains a password. | ||
expect_null( | ||
repo_auth_headers( | ||
"https://username:[email protected]/cran/latest/src/contrib/PACKAGES.gz" | ||
) | ||
) | ||
|
||
# No username in the repo URL. | ||
expect_null( | ||
repo_auth_headers( | ||
"https://ppm.internal/cran/latest/src/contrib/PACKAGES.gz" | ||
) | ||
) | ||
|
||
# Verify that the environment variable keyring backend is picked up correctly. | ||
withr::with_envvar( | ||
c("https://ppm.internal/cran/latest:username" = "token"), | ||
expect_equal( | ||
repo_auth_headers( | ||
"https://[email protected]/cran/__linux__/jammy/latest/src/contrib/PACKAGES.gz", | ||
allow_prompt = FALSE | ||
), | ||
c("Authorization" = "Basic dXNlcm5hbWU6dG9rZW4=") | ||
) | ||
) | ||
|
||
# Verify that we fall back to checking for a hostname credential when none | ||
# is available for a specific repo. | ||
withr::with_envvar( | ||
c("https://ppm.internal:username" = "token"), | ||
expect_equal( | ||
repo_auth_headers( | ||
"https://[email protected]/cran/latest/bin/linux/4.4-jammy/contrib/4.4/PACKAGES.gz", | ||
allow_prompt = FALSE | ||
), | ||
c("Authorization" = "Basic dXNlcm5hbWU6dG9rZW4=") | ||
) | ||
) | ||
}) | ||
|
||
test_that("basic auth credentials can be extracted from various URL formats", { | ||
expect_equal( | ||
extract_basic_auth_credentials("https://user.name:[email protected]"), | ||
list(username = "user.name", password = "pass-word123") | ||
) | ||
expect_equal( | ||
extract_basic_auth_credentials("http://[email protected]"), | ||
list(username = "user", password = NULL) | ||
) | ||
expect_equal( | ||
extract_basic_auth_credentials("https://example.com"), | ||
list(username = NULL, password = NULL) | ||
) | ||
expect_error( | ||
extract_basic_auth_credentials("notaurl"), | ||
"Unrecognized URL format" | ||
) | ||
}) | ||
|
||
test_that("we can extract hostnames and repository URLs from package URLs", { | ||
expect_equal( | ||
extract_repo_url( | ||
"https://[email protected]/cran/__linux__/jammy/latest/src/contrib/PACKAGES.gz" | ||
), | ||
"https://ppm.internal/cran/latest" | ||
) | ||
expect_equal( | ||
extract_repo_url( | ||
"https://[email protected]/cran/latest/bin/linux/4.4-jammy/contrib/pkg.tar.gz" | ||
), | ||
"https://ppm.internal/cran/latest" | ||
) | ||
expect_equal( | ||
extract_hostname( | ||
"https://[email protected]/cran/latest/__linux__/jammy/src/contrib/PACKAGES.gz" | ||
), | ||
"https://ppm.internal" | ||
) | ||
}) |