Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: get_group_content() lets you get content that groups can access #337

Merged
merged 12 commits into from
Nov 27, 2024
2 changes: 2 additions & 0 deletions NAMESPACE
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ export(get_content)
export(get_content_permissions)
export(get_content_tags)
export(get_environment)
export(get_group_content)
export(get_group_members)
export(get_group_permission)
export(get_groups)
Expand Down Expand Up @@ -147,6 +148,7 @@ importFrom(lifecycle,deprecated)
importFrom(magrittr,"%>%")
importFrom(rlang,"%||%")
importFrom(rlang,":=")
importFrom(rlang,.data)
toph-allen marked this conversation as resolved.
Show resolved Hide resolved
importFrom(rlang,arg_match)
importFrom(utils,browseURL)
importFrom(utils,capture.output)
Expand Down
2 changes: 2 additions & 0 deletions NEWS.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
- `get_users()` now supports filtering users with the `account_status` and
`user_role` parameters. For example, this allows you to find all licensed
users on a Connect server. (#311)
- The new `get_group_content()` function lets you view the content that groups
have permission to access. (#334)

# connectapi 0.4.0

Expand Down
14 changes: 14 additions & 0 deletions R/connect.R
Original file line number Diff line number Diff line change
Expand Up @@ -687,6 +687,20 @@ Connect <- R6::R6Class(
self$GET(path, query = query)
},

#' @description Get content to which a group has access
#' @param guid The group GUID.
group_content = function(guid) {
path <- v1_url("experimental", "groups", guid, "content")
self$GET(path)
},

#' @description Get the details for a group
#' @param guid The group GUID.
group_details = function(guid) {
path <- v1_url("groups", guid)
self$GET(path)
},

Copy link
Collaborator Author

@toph-allen toph-allen Nov 22, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Two new endpoints to support new group functionality. Makes me wonder if I should add a function to call the group details endpoint, to get the details from just the GUID. Probably not high priority for this PR.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is group_details() called anywhere?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It was previously, when I was looking up the group name for GUID-only invocations. But it's not now, so I've removed it.

# instrumentation --------------------------------------------

#' @description Get (non-interactive) content visits.
Expand Down
105 changes: 0 additions & 105 deletions R/get.R
Original file line number Diff line number Diff line change
Expand Up @@ -78,111 +78,6 @@ get_users <- function(
return(out)
}

#' Get group information from the Posit Connect server
#'
#' @param src The source object.
#' @param page_size The number of records to return per page (max 500).
#' @param prefix Filters groups by prefix (group name).
#' The filter is case insensitive.
#' @param limit The number of groups to retrieve before paging stops.
#'
#' `limit` will be ignored is `prefix` is not `NULL`.
#' To limit results when `prefix` is not `NULL`, change `page_size`.
#'
#' @return
#' A tibble with the following columns:
#'
#' * `guid`: The unique identifier of the group
#' * `name`: The group name
#' * `owner_guid`: The group owner's unique identifier. When using LDAP or
#' Proxied authentication with group provisioning enabled this property
#' will always be null.
#'
#' @details
#' Please see https://docs.posit.co/connect/api/#get-/v1/groups for more information.
#'
#' @examples
#' \dontrun{
#' library(connectapi)
#' client <- connect()
#'
#' # get all groups
#' get_groups(client, limit = Inf)
#' }
#'
#' @export
get_groups <- function(src, page_size = 500, prefix = NULL, limit = Inf) {
validate_R6_class(src, "Connect")

# The `v1/groups` endpoint always returns the first page when `prefix` is
# specified, so the page_offset function, which increments until it hits an
# empty page, fails.
if (!is.null(prefix)) {
response <- src$groups(page_size = page_size, prefix = prefix)
res <- response$results
} else {
res <- page_offset(src, src$groups(page_size = page_size, prefix = NULL), limit = limit)
}

out <- parse_connectapi_typed(res, connectapi_ptypes$groups)

return(out)
}

#' Get users within a specific group
#'
#' @param src The source object
#' @param guid A group GUID identifier
#'
#' @return
#' A tibble with the following columns:
#'
#' * `email`: The user's email
#' * `username`: The user's username
#' * `first_name`: The user's first name
#' * `last_name`: The user's last name
#' * `user_role`: The user's role. It may have a value of administrator,
#' publisher or viewer.
#' * `created_time`: The timestamp (in RFC3339 format) when the user
#' was created in the Posit Connect server
#' * `updated_time`: The timestamp (in RFC3339 format) when the user
#' was last updated in the Posit Connect server
#' * `active_time`: The timestamp (in RFC3339 format) when the user
#' was last active on the Posit Connect server
#' * `confirmed`: When false, the created user must confirm their
#' account through an email. This feature is unique to password
#' authentication.
#' * `locked`: Whether or not the user is locked
#' * `guid`: The user's GUID, or unique identifier, in UUID RFC4122 format
#'
#' @details
#' Please see https://docs.posit.co/connect/api/#get-/v1/groups/-group_guid-/members
#' for more information.
#'
#' @examples
#' \dontrun{
#' library(connectapi)
#' client <- connect()
#'
#' # get the first 20 groups
#' groups <- get_groups(client)
#'
#' group_guid <- groups$guid[1]
#'
#' get_group_members(client, guid = group_guid)
#' }
#'
#' @export
get_group_members <- function(src, guid) {
validate_R6_class(src, "Connect")

res <- src$group_members(guid)

out <- parse_connectapi(res$results)

return(out)
}

#' Get information about content on the Posit Connect server
#'
#' @param src A Connect object
Expand Down
205 changes: 205 additions & 0 deletions R/groups.R
Original file line number Diff line number Diff line change
@@ -0,0 +1,205 @@
#' Get group information from the Posit Connect server
#'
#' @param src The source object.
#' @param page_size The number of records to return per page (max 500).
#' @param prefix Filters groups by prefix (group name).
#' The filter is case insensitive.
#' @param limit The number of groups to retrieve before paging stops.
#'
#' `limit` will be ignored is `prefix` is not `NULL`.
#' To limit results when `prefix` is not `NULL`, change `page_size`.
#'
#' @return
#' A tibble with the following columns:
#'
#' * `guid`: The unique identifier of the group
#' * `name`: The group name
#' * `owner_guid`: The group owner's unique identifier. When using LDAP or
#' Proxied authentication with group provisioning enabled this property
#' will always be null.
#'
#' @details
#' Please see https://docs.posit.co/connect/api/#get-/v1/groups for more information.
#'
#' @examples
#' \dontrun{
#' library(connectapi)
#' client <- connect()
#'
#' # get all groups
#' get_groups(client, limit = Inf)
#' }
#'
#' @family groups functions
#' @export
get_groups <- function(src, page_size = 500, prefix = NULL, limit = Inf) {
validate_R6_class(src, "Connect")

# The `v1/groups` endpoint always returns the first page when `prefix` is
# specified, so the page_offset function, which increments until it hits an
# empty page, fails.
if (!is.null(prefix)) {
response <- src$groups(page_size = page_size, prefix = prefix)
res <- response$results
} else {
res <- page_offset(src, src$groups(page_size = page_size, prefix = NULL), limit = limit)
}

out <- parse_connectapi_typed(res, connectapi_ptypes$groups)

return(out)
}

#' Get users within a specific group
#'
#' @param src The source object
#' @param guid A group GUID identifier
#'
#' @return
#' A tibble with the following columns:
#'
#' * `email`: The user's email
#' * `username`: The user's username
#' * `first_name`: The user's first name
#' * `last_name`: The user's last name
#' * `user_role`: The user's role. It may have a value of administrator,
#' publisher or viewer.
#' * `created_time`: The timestamp (in RFC3339 format) when the user
#' was created in the Posit Connect server
#' * `updated_time`: The timestamp (in RFC3339 format) when the user
#' was last updated in the Posit Connect server
#' * `active_time`: The timestamp (in RFC3339 format) when the user
#' was last active on the Posit Connect server
#' * `confirmed`: When false, the created user must confirm their
#' account through an email. This feature is unique to password
#' authentication.
#' * `locked`: Whether or not the user is locked
#' * `guid`: The user's GUID, or unique identifier, in UUID RFC4122 format
#'
#' @details
#' Please see https://docs.posit.co/connect/api/#get-/v1/groups/-group_guid-/members
#' for more information.
#'
#' @examples
#' \dontrun{
#' library(connectapi)
#' client <- connect()
#'
#' # get the first 20 groups
#' groups <- get_groups(client)
#'
#' group_guid <- groups$guid[1]
#'
#' get_group_members(client, guid = group_guid)
#' }
#'
#' @family groups functions
#' @export
get_group_members <- function(src, guid) {
validate_R6_class(src, "Connect")

res <- src$group_members(guid)

out <- parse_connectapi(res$results)

return(out)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
out <- parse_connectapi(res$results)
return(out)
parse_connectapi(res$results)

Two things: (1) we don't need to write this to out yeah? (2) We use implicit returns at the end of functions, right?

}
Copy link
Collaborator Author

@toph-allen toph-allen Nov 22, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The first part of the file (get_groups() and get_group_members()) were just moved verbatim from get.R. I think it's probably more helpful to organize the code consistently by subject domain, and to move away from the paradigm of having most of the "get this type of object" functions in get.R.


#' Get content access permissions for a group or groups
#'
#' @param src The source object
#' @param groups Either a data frame of groups, or a character vector of group guids
#'
#' @return
#' A tibble with the following columns:

#' * `group_guid`: The group's GUID
#' * `group_name`: The group's name
#' * `content_guid`: The content item's GUID
#' * `content_name`: The content item's name
#' * `content_title`: The content item's title
#' * `access_type`: The access type of the content item ("all", "logged_in", or "acl")
#' * `role`: The access type that members of the group have to the
#' content item, "publisher" or "viewer".
#'
#' @examples
#' \dontrun{
#' library(connectapi)
#' client <- connect()
#'
#' # Get a data frame of groups
#' groups <- get_groups(client)
#'
#' # Get permissions for a single group by passing in the corresponding row.
#' get_group_content(client, groups[1, ])
#' dplyr::filter(groups, name = "research_scientists") %>%
#' get_group_content(client, groups = .)
#'
#' # Get permissions for all groups by passing in the entire groups data frame.
#' get_group_content(client, groups)
#'
#' # You can also pass in a guid or guids as a character vector.
#' get_group_content(client, groups$guid[1])
#' }
#'
#' @family groups functions
#' @export
#' @importFrom rlang .data
get_group_content <- function(src, groups) {
schloerke marked this conversation as resolved.
Show resolved Hide resolved
validate_R6_class(src, "Connect")
if (inherits(groups, "data.frame")) {
validate_df_ptype(groups, tibble::tibble(
guid = NA_character_,
name = NA_character_
))
} else if (inherits(groups, "character")) {
# If a character vector, we assume we are receiving group guids, and call
# the endpoint to fetch the group name.
groups <- purrr::map_dfr(groups, src$group_details)
} else {
stop("`groups` must be a data frame or character vector.")
}

purrr::pmap_dfr(
dplyr::select(groups, .data$guid, .data$name),
get_group_content_impl,
src = src
)
}

#' @importFrom rlang .data
get_group_content_impl <- function(src, guid, name) {
validate_R6_class(src, "Connect")

res <- src$group_content(guid)
parsed <- parse_connectapi_typed(res, connectapi_ptypes$group_content)

dplyr::transmute(parsed,
group_guid = guid,
group_name = name,
.data$content_guid,
.data$content_name,
.data$content_title,
.data$access_type,
role = purrr::map_chr(
.data$permissions,
extract_role,
principal_guid = guid
)
)
}

# Given the list of permissions for a content item, extract the role for the
# provided principal_guid
extract_role <- function(permissions, principal_guid) {
matched <- purrr::keep(
permissions,
~ .x[["principal_guid"]] == principal_guid
)
if (length(matched) == 1) {
return(matched[[1]][["principal_role"]])
} else {
stop("Unexpected permissions structure.")
}
stop(glue::glue("Could not find permissions for \"{principal_guid}\""))
}
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

New functions that comprise the implementations of the get_group_content(), factored out slightly for clarity of code and testability.

Notes for reviewers here, which should probably become code comments:

  • The exported function, get_group_content(), handles input validation and normalization (to allow the use of both GUIDs and data frames), and calls get_group_content_impl() across all input groups.
  • get_group_content_impl() gets the content for a single group from the guid and name. It isn't exported because it is the strictly less flexible core of get_group_content().
  • extract_role() is factored out for unit tests. Probably also useful elsewhere.

Loading
Loading