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
1 change: 1 addition & 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
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
7 changes: 7 additions & 0 deletions R/connect.R
Original file line number Diff line number Diff line change
Expand Up @@ -687,6 +687,13 @@ 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)
},

# 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
184 changes: 184 additions & 0 deletions R/groups.R
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
#' 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 A data frame or tibble of groups
#'
#' @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)
#' }
#'
#' @family groups functions
#' @export
get_group_content <- function(src, groups) {
schloerke marked this conversation as resolved.
Show resolved Hide resolved
validate_R6_class(src, "Connect")

purrr::pmap_dfr(
dplyr::select(groups, guid, name),

Check warning on line 148 in R/groups.R

View workflow job for this annotation

GitHub Actions / lint

file=R/groups.R,line=148,col=33,[object_usage_linter] no visible binding for global variable 'name'
get_group_content_impl,
src = src
)
}

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,
content_guid,

Check warning on line 163 in R/groups.R

View workflow job for this annotation

GitHub Actions / lint

file=R/groups.R,line=163,col=5,[object_usage_linter] no visible binding for global variable 'content_guid'
toph-allen marked this conversation as resolved.
Show resolved Hide resolved
content_name,

Check warning on line 164 in R/groups.R

View workflow job for this annotation

GitHub Actions / lint

file=R/groups.R,line=164,col=5,[object_usage_linter] no visible binding for global variable 'content_name'
content_title,
access_type,
role = purrr::map_chr(
permissions,

Check warning on line 168 in R/groups.R

View workflow job for this annotation

GitHub Actions / lint

file=R/groups.R,line=168,col=7,[object_usage_linter] no visible binding for global variable 'permissions'
extract_role,
principal_name = name
)
)
}

extract_role <- function(permissions, principal_name) {
matched <- purrr:::keep(
permissions,

Check warning on line 177 in R/groups.R

View workflow job for this annotation

GitHub Actions / lint

file=R/groups.R,line=177,col=4,[indentation_linter] Hanging indent should be 26 spaces but is 4 spaces.
~ .x[["principal_name"]] == principal_name && .x[["principal_type"]] == "group")
if (length(matched) == 1) {
return(matched[[1]][["principal_role"]])
} else {
stop("Unexpected permissions structure.")
}
}
7 changes: 7 additions & 0 deletions R/ptype.R
Original file line number Diff line number Diff line change
Expand Up @@ -211,5 +211,12 @@ connectapi_ptypes <- list(
principal_guid = NA_character_,
principal_type = NA_character_,
role = NA_character_
),
group_content = tibble::tibble(
content_guid = NA_character_,
content_name = NA_character_,
content_title = NA_character_,
access_type = NA_character_,
permissions = NA_list_
Comment on lines +214 to +220
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

For use with parse_connectapi_typed()

)
)
18 changes: 18 additions & 0 deletions man/PositConnect.Rd

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

Loading
Loading