Skip to content
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions NEWS.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@
(#341)
- New `get_job_list()` function returns a list of jobs for a content item.
(#341)
- New `token` parameter to `connect()` function allows you to create a Connect
client with permissions scoped to the content viewer when running on a Connect
server. (#362)

## Newly deprecated

Expand Down
53 changes: 51 additions & 2 deletions R/connect.R
Original file line number Diff line number Diff line change
Expand Up @@ -888,14 +888,30 @@ Connect <- R6::R6Class(

#' Create a connection to Posit Connect
#'
#' Creates a connection to Posit Connect using the server URL and an api key.
#' Creates a connection to Posit Connect using the server URL and an API key.
#' Validates the connection and checks that the version of the server is
#' compatible with the current version of the package.
#'
#' When running on Connect, the client's environment will contain default
#' `CONNECT_SERVER` and `CONNECT_API_KEY` variables. The API key's permissions
#' are scoped to the publishing user's account.
#'
#' To create a client with permissions scoped to the content viewer's account,
#' call `connect()` passing a user session token from content session headers
#' to the `token` argument. To do this, you must first add a Connect API
#' integration in your published content's Access sidebar.
#'
#' @param server The URL for accessing Posit Connect. Defaults to environment
#' variable CONNECT_SERVER
#' @param api_key The API Key to authenticate to Posit Connect with. Defaults
#' to environment variable CONNECT_API_KEY
#' @param token Optional. A user session token. When running on a Connect server,
#' creates a client using the content viewer's account. Running locally, the
#' created client uses the provided API key.
#' @param token_fallback_api_key Optional. When a `token` is provided, but
#' content is running locally, this API key is used to create the client.
#' By default, the primary `api_key` is used, but by providing a different
#' key you can test a visitor client with differently-scoped permissions.
#' @param prefix The prefix used to determine environment variables
#' @param ... Additional arguments. Not used at present
#' @param .check_is_fatal Whether to fail if "check" requests fail. Useful in
Expand All @@ -907,7 +923,17 @@ Connect <- R6::R6Class(
#'
#' @examples
#' \dontrun{
#' connect()
#' client <- connect()
#'
#' # Running in Connect, create a client using the content visitor's account.
#' # This example assumes code is being executed in a Shiny app's `server`
#' # function with a `session` object available.
#' token <- session$request$HTTP_POSIT_CONNECT_USER_SESSION_TOKEN
#' client <- connect(token = token)
#'
#' # Use with a differently-scoped API key
#' fallback_key <- Sys.getenv("VIEWER_SCOPED_API_KEY")
#' client <- connect(token = token, token_fallback_api_key = fallback_key)
#' }
#'
#' @examplesIf identical(Sys.getenv("IN_PKGDOWN"), "true")
Expand All @@ -920,6 +946,8 @@ Connect <- R6::R6Class(
connect <- function(
server = Sys.getenv(paste0(prefix, "_SERVER"), NA_character_),
api_key = Sys.getenv(paste0(prefix, "_API_KEY"), NA_character_),
token,
token_fallback_api_key = api_key,
prefix = "CONNECT",
...,
.check_is_fatal = TRUE) {
Expand All @@ -933,6 +961,25 @@ connect <- function(
}
con <- Connect$new(server = server, api_key = api_key)

if (!missing(token)) {
error_if_less_than(con$version, "2025.01.0")
if (on_connect()) {
message()
visitor_creds <- get_oauth_credentials(
con,
user_session_token = token,
requested_token_type = "urn:posit:connect:api-key"
)
con <- Connect$new(server = server, api_key = visitor_creds$access_token)
} else {
con <- Connect$new(server = server, api_key = token_fallback_api_key)
message(paste0(
"Called with `token` but not running on Connect. ",
"Continuing with fallback API key."
))
}
}

tryCatch(
{
check_connect_license(con)
Expand All @@ -954,6 +1001,8 @@ connect <- function(
con
}

# viewer_client <- function(access_token)

check_debug <- function(res) {
# Check for deprecation warnings from the server.
# You might get these if you've upgraded the Connect server but not connectapi.
Expand Down
8 changes: 6 additions & 2 deletions R/get.R
Original file line number Diff line number Diff line change
Expand Up @@ -585,6 +585,9 @@ get_procs <- function(src) {
#' @param user_session_token The content viewer's session token. This token
#' can only be obtained when the content is running on a Connect server. The token
#' identifies the user who is viewing the content interactively on the Connect server.
#' @param requested_token_type Optional. You may pass `"urn:posit:connect:api-key"` to
#' request an ephemeral Connect API key scoped to the content viewer's account.
#'
#'
#' Read this value from the HTTP header: `Posit-Connect-User-Session-Token`
#'
Expand Down Expand Up @@ -612,13 +615,14 @@ get_procs <- function(src) {
#' for more information.
#'
#' @export
get_oauth_credentials <- function(connect, user_session_token) {
get_oauth_credentials <- function(connect, user_session_token, requested_token_type = NULL) {
validate_R6_class(connect, "Connect")
url <- v1_url("oauth", "integrations", "credentials")
body <- list(
grant_type = "urn:ietf:params:oauth:grant-type:token-exchange",
subject_token_type = "urn:posit:connect:user-session-token",
subject_token = user_session_token
subject_token = user_session_token,
requested_token_type = requested_token_type
)
connect$POST(
url,
Expand Down
6 changes: 6 additions & 0 deletions R/utils.R
Original file line number Diff line number Diff line change
Expand Up @@ -194,3 +194,9 @@ endpoint_does_not_exist <- function(res) {
!("code" %in% names(httr::content(res, as = "parsed")))
)
}

# Returns `TRUE` if we're running on Connect as determined by the
# `RSTUDIO_PRODUCT` env var, else `FALSE`.
on_connect <- function() {
Sys.getenv("RSTUDIO_PRODUCT") == "CONNECT"
}
35 changes: 33 additions & 2 deletions man/connect.Rd

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

12 changes: 6 additions & 6 deletions man/connectapi-package.Rd

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

7 changes: 5 additions & 2 deletions man/get_oauth_credentials.Rd

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

Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"access_token": "viewer-api-key",
"issued_token_type": "urn:posit:connect:api-key",
"token_type": "Key"
}
75 changes: 75 additions & 0 deletions tests/testthat/test-connect.R
Original file line number Diff line number Diff line change
Expand Up @@ -115,3 +115,78 @@ test_that("client$version is returns version when server settings exposes it", {
expect_equal(con$version, "2024.09.0")
})
})

test_that("Viewer client can successfully be created running on Connect", {
with_mock_api({
withr::local_envvar(
CONNECT_SERVER = "https://connect.example",
CONNECT_API_KEY = "fake",
RSTUDIO_PRODUCT = "CONNECT"
)

client <- connect(token = "my-token")

expect_equal(
client$server,
"https://connect.example"
)
expect_equal(
client$api_key,
"viewer-api-key"
)
})
})

test_that("Viewer client uses fallback api key when running locally", {
with_mock_api({
withr::local_envvar(
CONNECT_SERVER = "https://connect.example",
CONNECT_API_KEY = "fake"
)

# With default fallback
expect_message(
client <- connect(token = NULL),
"Called with `token` but not running on Connect. Continuing with fallback API key."
)

expect_equal(
client$server,
"https://connect.example"
)
expect_equal(
client$api_key,
"fake"
)

# With explicitly-defined fallback
expect_message(
client <- connect(token = NULL, token_fallback_api_key = "fallback_fake"),
"Called with `token` but not running on Connect. Continuing with fallback API key."
)

expect_equal(
client$server,
"https://connect.example"
)
expect_equal(
client$api_key,
"fallback_fake"
)
})
})

test_that("Viewer client code path errs with older Connect version", {
with_mock_dir("2024.09.0", {
withr::local_envvar(
CONNECT_SERVER = "https://connect.example",
CONNECT_API_KEY = "fake",
RSTUDIO_PRODUCT = "CONNECT"
)

expect_error(
client <- connect(token = "my-token"),
"This feature requires Posit Connect version 2025.01.0 but you are using 2024.09.0"
)
})
})
Loading