rstudio / connectwidgets

The {connectwidgets} package allows you to curate your content on RStudio Connect, helping to create organized groups of content within an RMarkdown document or Shiny app.
https://rstudio.github.io/connectwidgets/
Other
21 stars 7 forks source link

Users see all content #94

Open Jeffrey-Zimmerman-tfs opened 11 months ago

Jeffrey-Zimmerman-tfs commented 11 months ago

I was under the impression from the documentation here: 'The data frame contains one row for each item visible to the requesting user. For users in an “administrator” role, that will be all content items.', that each user (with Viewer role) would only see the apps provisioned to them. However, It appears every user sees the same list of apps, but those they do not have access to, the button 'request access' appears instead of 'view content'. Is there a way for me subset the tibble returned to only the products/apps the user has provisioned access to?

SamEdwardes commented 10 months ago

Hi Jeffrey - thank you for the question, and I apologize for the delay.

One question I have is, what kind of content are you using to expose the list of content to your users? Is it a Rmarkdown document or a Quarto document?

If yes, keep in mind that these documents are "static" and generated at a point in time. So, the list of content is created once when the code executes. It will include all of the content that the user who executes the code has access to. Could that explain why users are seeing all the content?

I think for your use case, you need something more dynamic, like a Shiny App that can show the logged-in user all of the content they have access to. Here is an example, but note this example shows all of the content you own, not all of the content you can view. I am following up internally to find a way to filter to show all content I can view.

library(shiny)
library(connectapi)
library(dplyr)
library(glue)

ui <- fluidPage(
  titlePanel("🏠 Home page"),
  mainPanel(
    textOutput("welcome_message"),
    p("Content you own:"),
    dataTableOutput("content_you_own")
  )
)

server <- function(input, output, session) {

  client <- connect()

  if (is.null(session$user)) {
    # On Workbench use USER environment variable
    user_name = Sys.getenv("USER")
  } else {
    # On Connect get the session data.
    user_name <- session$user
  }

  print(glue("user_name: {user_name}"))

  user_guid <- get_users(client, prefix = user_name) |> 
    slice(1) |> 
    pull(guid)

  print(glue("user_guid: {user_guid}"))

  content <- get_content(client, owner_guid = user_guid)

  output$welcome_message <- renderText(glue("Welcome user: {user_name} ({user_guid})"))
  output$content_you_own <- renderDataTable(content)

}

shinyApp(ui = ui, server = server)

Screenshot 2023-12-07 at 11 11 23@2x

One last thing to consider. It sounds like you are trying to recreate the functionality provided by the default Connect landing page. It it feasible to just use the default Connect landing page, which by default shows users all the content they can view:

Screenshot 2023-12-07 at 10 58 52@2x

SamEdwardes commented 10 months ago

Hi @Jeffrey-Zimmerman-tfs, I want to share some more work with you. I devised two approaches to identify viewable content by the logged-in user. Note that they are both "expensive" (slow). Below is an example on a Connect instance that 11 items of content:

library(connectapi)
library(dplyr)
#> 
#> Attaching package: 'dplyr'
#> The following objects are masked from 'package:stats':
#> 
#>     filter, lag
#> The following objects are masked from 'package:base':
#> 
#>     intersect, setdiff, setequal, union
library(glue)

client <- connect()
#> Defining Connect with server: https://daffy-mackerel.staging.eval.posit.co/cnct/
#> Warning: You are using a newer version of Posit Connect (2023.10.0) than was tested (2022.09.0). Most APIs should function as expected.
#> This warning is displayed once per session.

user_name = Sys.getenv("USER")
print(glue("user_name: {user_name}"))
#> user_name: admin1

user_guid <- get_users(client, prefix = user_name) |> 
  slice(1) |> 
  pull(guid)

print(user_guid)
#> [1] "5794c1b9-89e7-4924-a35d-d0024fa2f6dc"

# //////////////////////////////////////////////////////////////////////////////
# Approach #1 - use the `content_list_with_permissions` function ---------------
# //////////////////////////////////////////////////////////////////////////////
# This method will show you all content you have access to. That includes content
# that has been explpicity shared with you, and content that is open to everyone.
tictoc::tic()

# Get content with permissions. Warning! Per the docs this command can be 
# expensive if your Connect instance has lots of content.
# https://pkgs.rstudio.com/connectapi/articles/content_permissions.html#retrieve-the-content-list
all_content <- content_list_with_permissions(client)
#> Warning: The `content_list_with_permissions` function is experimental and subject to change without warning in a future release
#> This warning is displayed once per session.
#> Getting content list
#> Getting permission list
nrow(all_content)
#> [1] 11

viewable_content <- content_list_guid_has_access(all_content, user_guid)
#> Warning: The `content_list_filter_by_guid` function is experimental and subject to change without warning in a future release
#> This warning is displayed once per session.
nrow(viewable_content)
#> [1] 11

tictoc::toc()
#> 10.27 sec elapsed

# //////////////////////////////////////////////////////////////////////////////
# Approach #2 - use the `get_content`, `get_user_permissions` functions --------
# //////////////////////////////////////////////////////////////////////////////
# This method will only sho you content that has been explicity shared with you.
# It will not include content that is viewable by everyone.
tictoc::tic()

# Get a dataframe containing all of the content on the server. Note that this 
# can be expensive if your Connect server has lots of content.
all_content <- get_content(client)
nrow(all_content)
#> [1] 11

# For each content, check if the user running the shiny app has permission to
# access it.
viewable_content_guids <- c()

for (content_guid in all_content$guid) {
  print(content_guid)
  content <- content_item(client, content_guid)
  user_permissions <- get_user_permission(content, user_guid)
  # If the response is not null, that means the user is on the explicit list of
  # persons who have access to the content.
  if (!is.null(user_permissions)) {
    print("✅ User has access")
    viewable_content_guids <- c(viewable_content_guids, content_guid)
  } else {
    print("❌ User does not have access")
  }
}
#> [1] "c13cdd37-ce83-4896-baf3-1bb5326090f0"
#> [1] "✅ User has access"
#> [1] "eaeb7c89-5f36-481a-aa09-8db22857a811"
#> [1] "✅ User has access"
#> [1] "25e4a49c-ae59-4630-a1cf-c41ba2c9f59f"
#> [1] "❌ User does not have access"
#> [1] "d636cd33-c6a9-402e-aabc-31984d797895"
#> [1] "❌ User does not have access"
#> [1] "c1ff2298-4bf4-41c5-91b4-7bd6613bc0bb"
#> [1] "❌ User does not have access"
#> [1] "c4e6e455-a5aa-4529-8e75-efe660bdf5d8"
#> [1] "❌ User does not have access"
#> [1] "1d0b2b90-06e8-4173-9fee-00d18bc384ae"
#> [1] "❌ User does not have access"
#> [1] "8408adc8-946b-4ace-8906-67a3adb0307f"
#> [1] "❌ User does not have access"
#> [1] "bdc2d72e-df85-46fe-8f5e-0cca6611ee7f"
#> [1] "❌ User does not have access"
#> [1] "4d3db7a1-5445-4d88-afd3-41ab6fe4d170"
#> [1] "❌ User does not have access"
#> [1] "d69b1a73-5cbb-4626-8524-1337e324920a"
#> [1] "❌ User does not have access"
print(length(viewable_content_guids))
#> [1] 2

viewable_content <- all_content %>% 
  filter(guid %in% viewable_content_guids)

nrow(viewable_content)
#> [1] 2

tictoc::toc()
#> 10.163 sec elapsed

Created on 2023-12-14 with reprex v2.0.2

Here is an example of you would implement with shiny:

library(shiny)
library(connectapi)
library(dplyr)
library(glue)
library(shinybusy)

ui <- fluidPage(
  titlePanel("🏠 Home page"),
  mainPanel(
    textOutput("welcome_message"),
    add_busy_spinner(spin = "fading-circle"),
    dataTableOutput("content_you_can_access")
  )
)

server <- function(input, output, session) {

  client <- connect()

  if (is.null(session$user)) {
    # On Workbench use USER environment variable
    user_name <- Sys.getenv("USER")
  } else {
    # On Connect get the session data.
    user_name <- session$user
  }

  print(glue("user_name: {user_name}"))

  user_guid <- get_users(client, prefix = user_name) |> 
    slice(1) |> 
    pull(guid)

  print(glue("user_guid: {user_guid}"))

  content <- get_content(client, owner_guid = user_guid)

  # //////////////////////////////////////////////////////////////////////////////
  # Approach #1 - use the `content_list_with_permissions` function ---------------
  # //////////////////////////////////////////////////////////////////////////////
  all_content <- content_list_with_permissions(client)
  viewable_content <- content_list_guid_has_access(all_content, user_guid)

  output$welcome_message <- renderText(glue("Welcome user: {user_name} ({user_guid}). Here is a list of the content you can access:"))
  output$content_you_can_access <- renderDataTable(viewable_content)

}

shinyApp(ui = ui, server = server)

2023-12-14 08 34 47 daffy-mackerel staging eval posit co 8610c1dac9b2

2023-12-14 08 34 26 daffy-mackerel staging eval posit co 185061d96bbe