Salesforce resources owned by viewer with R

Problem

Your organization’s Sales team contains a diverse group of people with varying roles in Salesforce. Tasked to help generate sales reports per-user, you are not sure what resources each person can access. While researching the roles and their associated permissions you wonder if each user could query Salesforce resources as themselves instead of using a Salesforce Developer API key.

Solution

You create a simple Shiny application that uses the Salesforce Viewer OAuth integration set up by your Posit Connect administrator. Members of the Sales team are able to query various resources assigned to them in Salesforce after logging in to the integration. Folks who do not have access to the resource do not see any results, and only resources that the user is the owner of are shown.

app.R
library(shiny)
library(httr2)
library(bslib)
library(connectapi)

ui <- page_sidebar(
  title = "Salesforce Resource by Owner",
  sidebar = sidebar(
    title = "Salesforce",
    selectInput("resource", 
                "Resource to query",
                list("Tasks" = "tasks", "Accounts" = "accounts", "Leads" = "leads")),
    ),
  layout_columns(
    card(
      card_header("Results"),
      verbatimTextOutput("results")
    )
  )
)

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

  # check if running on Posit Connect
  # note: use RSTUDIO_PRODUCT (deprecated) for Connect versions < 2025.02.0
  if (Sys.getenv("POSIT_PRODUCT") == "CONNECT") {
    # initialize Connect API client
    client <- connect()
    # read the user-session-token header
    user_session_token <- session$request$HTTP_POSIT_CONNECT_USER_SESSION_TOKEN
    # grab the OAuth Integration access token using the session token
    credentials <- get_oauth_credentials(client, user_session_token)
    token <- credentials$access_token
  } else {
    # grab the access token from the SALESFORCE_TOKEN env var if running locally
    token <- Sys.getenv("SALESFORCE_TOKEN")
  }
  
  observeEvent(input$resource, {
    # grab the salesforce domain and form the query endpoint
    sf_domain <- Sys.getenv("SALESFORCE_DOMAIN") 
    query_path <- paste0(sf_domain, "/services/data/v62.0/query?q=")
    
    # grab the user ID from user info endpoint
    user_url <- "https://login.salesforce.com/services/oauth2/userinfo"
    resp <- httr2::request(user_url) |>
      httr2::req_headers("Accept" = "application/json") |>
      httr2::req_auth_bearer_token(token) |>
      httr2::req_perform() |>
      httr2::resp_body_json()
    user_id <- resp$user_id

    # set URL encoded query based on provided resource
    resource <- input$resource
    query <- switch(resource,
      tasks = {paste0("SELECT Subject, Status, Priority FROM Task WHERE OwnerId = '", user_id, "'")},
      accounts = {paste0("SELECT Id, Name, Industry, BillingCity FROM Account WHERE OwnerId = '", user_id, "'")},
      leads = {paste0("SELECT Id, Name, Company, Status FROM Lead WHERE OwnerId = '", user_id, "'")},
      ) 
    url_query <- URLencode(query, reserved = TRUE)

    # append the URL encoded query to the path
    path <- paste0(query_path, url_query)
  
    # make the request
    resp <- httr2::request(path) |>
      httr2::req_headers("Accept" = "application/json") |>
      httr2::req_auth_bearer_token(token) |>
      # to avoid HTTP response error codes being surfaced as R errors
      httr2::req_error(is_error = ~FALSE) |>
      httr2::req_perform()
    
    # check the HTTP response code
    http_code <- httr2::resp_status(resp)
    if (http_code == 200) {
      # format response body now that we have the HTTP status code
      resp <- httr2::resp_body_json(resp)
      output$results <- renderText({
        paste0(resp) 
      })
    } else {
      # output the unexpected HTTP code
      output$results <- renderUI({
        HTML("Received non-200 HTTP response code: ", http_code)
      })
    }
  })
}

shinyApp(ui, server)

Running the app locally

Terminal
Sys.setenv(CONNECT_SERVER = "<connect-host>")
Sys.setenv(CONNECT_API_KEY = "<connect-api-key>")
# SALESFORCE_TOKEN is only required when running the example locally
Sys.setenv(SALESFORCE_TOKEN = "<salesforce-token>")
# SALESFORCE_DOMAIN must not contain a trailing slash 
Sys.setenv(SALESFORCE_DOMAIN = "<salesforce-domain>")
shiny::runApp()