diff --git a/DESCRIPTION b/DESCRIPTION index ba602bee..3bc58308 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -78,6 +78,7 @@ Collate: 'get.R' 'git.R' 'groups.R' + 'integrations.R' 'lazy.R' 'page.R' 'parse.R' diff --git a/NAMESPACE b/NAMESPACE index 30118f21..340e5a15 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -6,8 +6,10 @@ S3method("[[",connect_tag_tree) S3method(api_build,op_base_connect) S3method(api_build,op_head) S3method(as.data.frame,connect_list_hits) +S3method(as.data.frame,connect_list_integrations) S3method(as.data.frame,tbl_connect) S3method(as_tibble,connect_list_hits) +S3method(as_tibble,connect_list_integrations) S3method(connect_vars,op_base) S3method(connect_vars,op_single) S3method(connect_vars,tbl_connect) @@ -84,6 +86,7 @@ export(get_group_members) export(get_group_permission) export(get_groups) export(get_image) +export(get_integrations) export(get_job) export(get_job_list) export(get_jobs) diff --git a/NEWS.md b/NEWS.md index b31cd8fa..66b7dd15 100644 --- a/NEWS.md +++ b/NEWS.md @@ -5,6 +5,9 @@ - New `get_usage()` function returns content usage data from Connect's `GET v1/instrumentation/content/hits` endpoint on Connect v2025.04.0 and higher. (#390) +- New `get_integrations()` function lists all OAuth integrations available on the + Connect server from the `GET v1/oauth/integrations` endpoint on Connect v2024.12.0 + and higher. (#413) ## Enhancements and fixes diff --git a/R/get.R b/R/get.R index fded2a3b..01e41f06 100644 --- a/R/get.R +++ b/R/get.R @@ -820,6 +820,8 @@ get_procs <- function(src) { #' Please see https://docs.posit.co/connect/user/oauth-integrations/#obtaining-a-viewer-oauth-access-token #' for more information. #' +#' @seealso [get_integrations()], [get_oauth_content_credentials()] +#' #' @export get_oauth_credentials <- function( connect, @@ -886,6 +888,8 @@ get_oauth_credentials <- function( #' Please see https://docs.posit.co/connect/user/oauth-integrations/#obtaining-a-service-account-oauth-access-token #' for more information. #' +#' @seealso [get_integrations()], [get_oauth_credentials()] +#' #' @export get_oauth_content_credentials <- function( connect, diff --git a/R/integrations.R b/R/integrations.R new file mode 100644 index 00000000..b39122a0 --- /dev/null +++ b/R/integrations.R @@ -0,0 +1,106 @@ +#' List all OAuth integrations on the Connect server +#' +#' @description +#' Retrieve information about all OAuth integrations available to Posit Connect. +#' You must have administrator or publisher privileges to perform this action. +#' +#' @param client A `Connect` R6 client object. +#' +#' @return A list of OAuth integrations. Each integration is a list with the +#' following elements (all character strings unless indicated otherwise): +#' +#' * `id`: The internal identifier of this OAuth integration. +#' * `guid`: The GUID of this OAuth integration. +#' * `created_time`: The timestamp (RFC3339) indicating when this integration +#' was created. +#' * `updated_time`: The timestamp (RFC3339) indicating when this integration +#' was last updated +#' * `name`: A descriptive name to identify the OAuth integration. +#' * `description`: A brief text to describe the OAuth integration. +#' * `template`: The template used to configure this OAuth integration. +#' * `auth_type`: The authentication type indicates which OAuth flow is used by +#' this integration. +#' * `config`: A sub-list list with the OAuth integration configuration. Fields +#' differ between integrations. +#' +#' Use [as.data.frame()] or [tibble::as_tibble()] to convert to a data frame with +#' parsed types. In the resulting data frame: +#' +#' * `created_time` and `updated_time` are parsed to `POSIXct`. +#' * `config` remains as a list-column. +#' +#' @seealso [get_oauth_credentials()], [get_oauth_content_credentials()] +#' +#' @examples +#' \dontrun{ +#' client <- connect() +#' +#' # Fetch all OAuth integrations +#' integrations <- get_integrations(client) +#' +#' +#' # Update the configuration and metadata for a subset of integrations. +#' json_payload <- toJSON(list( +#' description = "New Description", +#' config = list( +#' client_secret = "new-client-secret" +#' ) +#' ), auto_unbox = TRUE) +#' +#' results <- integrations |> +#' purrr::keep(\(x) x$template == "service_to_update") |> +#' purrr::map(\(x) client$PATCH(paste0("v1/oauth/integrations/", x$guid), body = json_payload)) +#' +#' +#' # Convert to tibble or data frame +#' integrations_df <- tibble::as_tibble(integrations) +#' } +#' +#' @export +get_integrations <- function(client) { + error_if_less_than(client$version, "2024.12.0") + integrations <- client$GET(v1_url("oauth", "integrations")) + class(integrations) <- c("connect_list_integrations", class(integrations)) + integrations +} + +#' Convert integrations data to a data frame +#' +#' @description +#' Converts an list returned by [get_integrations()] into a data frame. +#' +#' @param x A `connect_list_integrations` object (from [get_integrations()]). +#' @param row.names Passed to [base::as.data.frame()]. +#' @param optional Passed to [base::as.data.frame()]. +#' @param ... Passed to [base::as.data.frame()]. +#' +#' @return A `data.frame` with one row per integration. +#' @export +as.data.frame.connect_list_integrations <- function( + x, + row.names = NULL, # nolint + optional = FALSE, + ... +) { + integrations_tbl <- as_tibble(x) + as.data.frame( + integrations_tbl, + row.names = row.names, + optional = optional, + ... + ) +} + +#' Convert integrations data to a tibble +#' +#' @description +#' Converts a list returned by [get_integrations()] to a tibble. +#' +#' @param x A `connect_list_integrations` object. +#' @param ... Unused. +#' +#' @return A tibble with one row per integration. +#' @export +as_tibble.connect_list_integrations <- function(x, ...) { + parse_connectapi_typed(x, connectapi_ptypes$integrations) +} diff --git a/R/ptype.R b/R/ptype.R index 7febeba8..3600266a 100644 --- a/R/ptype.R +++ b/R/ptype.R @@ -271,5 +271,16 @@ connectapi_ptypes <- list( name = NA_character_, version = NA_character_, hash = NA_character_ + ), + integrations = tibble::tibble( + id = NA_character_, + guid = NA_character_, + created_time = NA_datetime_, + updated_time = NA_datetime_, + name = NA_character_, + description = NA_character_, + template = NA_character_, + auth_type = NA_character_, + config = NA_list_ ) ) diff --git a/_pkgdown.yml b/_pkgdown.yml index d8f943c8..ae9f6d7d 100644 --- a/_pkgdown.yml +++ b/_pkgdown.yml @@ -69,6 +69,8 @@ reference: - starts_with("get") - as.data.frame.connect_list_hits - as_tibble.connect_list_hits + - as.data.frame.connect_list_integrations + - as_tibble.connect_list_integrations - title: "Other" desc: > diff --git a/man/as.data.frame.connect_list_integrations.Rd b/man/as.data.frame.connect_list_integrations.Rd new file mode 100644 index 00000000..8f54ff5b --- /dev/null +++ b/man/as.data.frame.connect_list_integrations.Rd @@ -0,0 +1,23 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/integrations.R +\name{as.data.frame.connect_list_integrations} +\alias{as.data.frame.connect_list_integrations} +\title{Convert integrations data to a data frame} +\usage{ +\method{as.data.frame}{connect_list_integrations}(x, row.names = NULL, optional = FALSE, ...) +} +\arguments{ +\item{x}{A \code{connect_list_integrations} object (from \code{\link[=get_integrations]{get_integrations()}}).} + +\item{row.names}{Passed to \code{\link[base:as.data.frame]{base::as.data.frame()}}.} + +\item{optional}{Passed to \code{\link[base:as.data.frame]{base::as.data.frame()}}.} + +\item{...}{Passed to \code{\link[base:as.data.frame]{base::as.data.frame()}}.} +} +\value{ +A \code{data.frame} with one row per integration. +} +\description{ +Converts an list returned by \code{\link[=get_integrations]{get_integrations()}} into a data frame. +} diff --git a/man/as_tibble.connect_list_integrations.Rd b/man/as_tibble.connect_list_integrations.Rd new file mode 100644 index 00000000..a060b497 --- /dev/null +++ b/man/as_tibble.connect_list_integrations.Rd @@ -0,0 +1,19 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/integrations.R +\name{as_tibble.connect_list_integrations} +\alias{as_tibble.connect_list_integrations} +\title{Convert integrations data to a tibble} +\usage{ +\method{as_tibble}{connect_list_integrations}(x, ...) +} +\arguments{ +\item{x}{A \code{connect_list_integrations} object.} + +\item{...}{Unused.} +} +\value{ +A tibble with one row per integration. +} +\description{ +Converts a list returned by \code{\link[=get_integrations]{get_integrations()}} to a tibble. +} diff --git a/man/get_integrations.Rd b/man/get_integrations.Rd new file mode 100644 index 00000000..26dad24c --- /dev/null +++ b/man/get_integrations.Rd @@ -0,0 +1,70 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/integrations.R +\name{get_integrations} +\alias{get_integrations} +\title{List all OAuth integrations on the Connect server} +\usage{ +get_integrations(client) +} +\arguments{ +\item{client}{A \code{Connect} R6 client object.} +} +\value{ +A list of OAuth integrations. Each integration is a list with the +following elements (all character strings unless indicated otherwise): +\itemize{ +\item \code{id}: The internal identifier of this OAuth integration. +\item \code{guid}: The GUID of this OAuth integration. +\item \code{created_time}: The timestamp (RFC3339) indicating when this integration +was created. +\item \code{updated_time}: The timestamp (RFC3339) indicating when this integration +was last updated +\item \code{name}: A descriptive name to identify the OAuth integration. +\item \code{description}: A brief text to describe the OAuth integration. +\item \code{template}: The template used to configure this OAuth integration. +\item \code{auth_type}: The authentication type indicates which OAuth flow is used by +this integration. +\item \code{config}: A sub-list list with the OAuth integration configuration. Fields +differ between integrations. +} + +Use \code{\link[=as.data.frame]{as.data.frame()}} or \code{\link[tibble:as_tibble]{tibble::as_tibble()}} to convert to a data frame with +parsed types. In the resulting data frame: +\itemize{ +\item \code{created_time} and \code{updated_time} are parsed to \code{POSIXct}. +\item \code{config} remains as a list-column. +} +} +\description{ +Retrieve information about all OAuth integrations available to Posit Connect. +You must have administrator or publisher privileges to perform this action. +} +\examples{ +\dontrun{ +client <- connect() + +# Fetch all OAuth integrations +integrations <- get_integrations(client) + + +# Update the configuration and metadata for a subset of integrations. +json_payload <- toJSON(list( + description = "New Description", + config = list( + client_secret = "new-client-secret" + ) +), auto_unbox = TRUE) + +results <- integrations |> + purrr::keep(\(x) x$template == "service_to_update") |> + purrr::map(\(x) client$PATCH(paste0("v1/oauth/integrations/", x$guid), body = json_payload)) + + +# Convert to tibble or data frame +integrations_df <- tibble::as_tibble(integrations) +} + +} +\seealso{ +\code{\link[=get_oauth_credentials]{get_oauth_credentials()}}, \code{\link[=get_oauth_content_credentials]{get_oauth_content_credentials()}} +} diff --git a/man/get_oauth_content_credentials.Rd b/man/get_oauth_content_credentials.Rd index fda3c2e2..812fe464 100644 --- a/man/get_oauth_content_credentials.Rd +++ b/man/get_oauth_content_credentials.Rd @@ -57,3 +57,6 @@ function(req) { } } +\seealso{ +\code{\link[=get_integrations]{get_integrations()}}, \code{\link[=get_oauth_credentials]{get_oauth_credentials()}} +} diff --git a/man/get_oauth_credentials.Rd b/man/get_oauth_credentials.Rd index 05b54efb..6fd71c5e 100644 --- a/man/get_oauth_credentials.Rd +++ b/man/get_oauth_credentials.Rd @@ -55,3 +55,6 @@ function(req) { } } +\seealso{ +\code{\link[=get_integrations]{get_integrations()}}, \code{\link[=get_oauth_content_credentials]{get_oauth_content_credentials()}} +} diff --git a/tests/testthat/2024.12.0/__api__/server_settings.json b/tests/testthat/2024.12.0/__api__/server_settings.json new file mode 100644 index 00000000..716ea04e --- /dev/null +++ b/tests/testthat/2024.12.0/__api__/server_settings.json @@ -0,0 +1,3 @@ +{ + "version": "2024.12.0" +} diff --git a/tests/testthat/2024.12.0/__api__/v1/oauth/integrations.json b/tests/testthat/2024.12.0/__api__/v1/oauth/integrations.json new file mode 100644 index 00000000..926a60fc --- /dev/null +++ b/tests/testthat/2024.12.0/__api__/v1/oauth/integrations.json @@ -0,0 +1,38 @@ +[ + { + "id": "4", + "guid": "f8688548", + "created_time": "2024-08-01T20:14:31Z", + "updated_time": "2025-03-25T19:08:26Z", + "name": "GitHub Integration", + "description": "with refresh support ", + "template": "custom", + "auth_type": "Viewer", + "config": { + "auth_mode": "Confidential", + "auth_type": "Viewer", + "authorization_uri": "https://github.com/login/oauth/authorize", + "client_id": "client_id_123", + "scopes": "offline_access openid profile email repo read:user", + "token_endpoint_auth_method": "client_secret_post", + "token_uri": "https://github.com/login/oauth/access_token", + "use_pkce": true + } + }, + { + "id": "5", + "guid": "cacc0cf1", + "created_time": "2024-09-09T15:01:29Z", + "updated_time": "2025-03-25T19:07:01Z", + "name": "Example Service", + "description": "The service provides utility to your company", + "template": "generic_service", + "auth_type": "Viewer", + "config": { + "account_url": "https://service.example.com", + "auth_mode": "Confidential", + "client_id": "client_id_456", + "scopes": "refresh_token" + } + } +] diff --git a/tests/testthat/2024.12.0/__ping__.json b/tests/testthat/2024.12.0/__ping__.json new file mode 100644 index 00000000..0db3279e --- /dev/null +++ b/tests/testthat/2024.12.0/__ping__.json @@ -0,0 +1,3 @@ +{ + +} diff --git a/tests/testthat/test-integrations.R b/tests/testthat/test-integrations.R new file mode 100644 index 00000000..30127a2d --- /dev/null +++ b/tests/testthat/test-integrations.R @@ -0,0 +1,48 @@ +with_mock_dir("2024.12.0", { + test_that("get_integrations() gets integrations", { + client <- Connect$new(server = "https://connect.example", api_key = "fake") + integrations <- get_integrations(client) + expect_true(inherits(integrations, "connect_list_integrations")) + + # Check a few fields + expect_equal(integrations[[1]]$name, "GitHub Integration") + expect_equal(integrations[[2]]$updated_time, "2025-03-25T19:07:01Z") + expect_equal(integrations[[1]]$config$client_id, "client_id_123") + }) + + test_that("get_integrations() can be converted to a data frame correctly", { + client <- Connect$new(server = "https://connect.example", api_key = "fake") + integrations_df <- get_integrations(client) |> + as_tibble() + expect_named( + integrations_df, + c( + "id", + "guid", + "created_time", + "updated_time", + "name", + "description", + "template", + "auth_type", + "config" + ) + ) + expect_equal( + integrations_df$description, + c( + "with refresh support ", + "The service provides utility to your company" + ) + ) + }) +}) + +test_that("get_integrations() errs on older Connect versions", { + client <- MockConnect$new("2024.11.1") + client$version + expect_error( + get_integrations(client), + "This feature requires Posit Connect version 2024.12.0 but you are using 2024.11.1" + ) +})