Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions NEWS.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
# rsconnect (development version)

* SPCS/Snowflake authentication supports Connect API keys for user
identification. The `connectSPCSUser()` function now requires an `apiKey`
parameter, and the API key is included in the `X-RSC-Authorization` header
alongside Snowflake token authentication. This aligns with updated Connect
server requirements where Snowflake tokens provide proxied authentication
while API keys identify users to the Connect server itself.

# rsconnect 1.6.0

* Support deploying to Posit Connect Cloud. Use `connectCloudUser()` to add
Expand Down
12 changes: 10 additions & 2 deletions R/accounts.R
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,11 @@ connectApiUser <- function(
#' [`connections.toml` file](https://docs.snowflake.com/en/developer-guide/snowflake-cli/connecting/configure-cli#location-of-the-toml-configuration-fil)
#' in the appropriate location.
#'
#' SPCS deployments require both Snowflake authentication (via the connection
#' name) and a Posit Connect API key. The Snowflake token provides proxied
#' authentication to reach the Connect server, while the API key identifies
#' the user to Connect itself.
#'
#' Supported servers: Posit Connect servers
#'
#' @inheritParams connectApiUser
Expand All @@ -104,18 +109,20 @@ connectApiUser <- function(
connectSPCSUser <- function(
account = NULL,
server = NULL,
apiKey,
snowflakeConnectionName,
quiet = FALSE
) {
server <- findServer(server)
checkConnectServer(server)

user <- getSPCSAuthedUser(server, snowflakeConnectionName)
user <- getSPCSAuthedUser(server, apiKey, snowflakeConnectionName)

registerAccount(
serverName = server,
accountName = account %||% user$username,
accountId = user$id,
apiKey = apiKey,
snowflakeConnectionName = snowflakeConnectionName
)

Expand All @@ -127,10 +134,11 @@ connectSPCSUser <- function(
invisible()
}

getSPCSAuthedUser <- function(server, snowflakeConnectionName) {
getSPCSAuthedUser <- function(server, apiKey, snowflakeConnectionName) {
serverAddress <- serverInfo(server)
account <- list(
server = server,
apiKey = apiKey,
snowflakeConnectionName = snowflakeConnectionName
)

Expand Down
14 changes: 10 additions & 4 deletions R/http.R
Original file line number Diff line number Diff line change
Expand Up @@ -541,15 +541,21 @@ requestCertificate <- function(protocol, certificate = NULL) {
}

authHeaders <- function(authInfo, method, path, file = NULL) {
if (!is.null(authInfo$secret) || !is.null(authInfo$private_key)) {
if (!is.null(authInfo$snowflakeToken)) {
# snowflakeauth returns a list of named header values
headers <- authInfo$snowflakeToken
# The SPCS/Snowflake token is in the Authorization header and the Connect API key is passed
# using X-RSC-Authorization.
if (!is.null(authInfo$apiKey)) {
headers$`X-RSC-Authorization` <- paste("Key", authInfo$apiKey)
}
headers
} else if (!is.null(authInfo$secret) || !is.null(authInfo$private_key)) {
signatureHeaders(authInfo, method, path, file)
} else if (!is.null(authInfo$apiKey)) {
list(`Authorization` = paste("Key", authInfo$apiKey))
} else if (!is.null(authInfo$accessToken)) {
list(`Authorization` = paste("Bearer", authInfo$accessToken))
} else if (!is.null(authInfo$snowflakeToken)) {
# snowflakeauth returns a list of named header values
authInfo$snowflakeToken
} else {
# The value doesn't actually matter here, but the header needs to be set.
list(`X-Auth-Token` = "anonymous-access")
Expand Down
8 changes: 8 additions & 0 deletions man/connectSPCSUser.Rd

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

17 changes: 0 additions & 17 deletions tests/testthat/test-http.R
Original file line number Diff line number Diff line change
Expand Up @@ -255,20 +255,3 @@ test_that("rcf2616 returns an ASCII date and undoes changes to the locale", {
expect_equal(date, "Mon, 01 Jan 2024 06:02:03 GMT")
expect_equal(Sys.getlocale("LC_TIME"), old)
})

test_that("authHeaders handles snowflakeToken", {
# Create mock authInfo with snowflakeToken
authInfo <- list(
snowflakeToken = list(
Authorization = "Snowflake Token=\"mock_token\"",
`X-Custom-Header` = "custom-value"
)
)

# Test authHeaders
headers <- authHeaders(authInfo, "GET", "/path")

# Verify snowflakeToken headers were used
expect_equal(headers$Authorization, "Snowflake Token=\"mock_token\"")
expect_equal(headers$`X-Custom-Header`, "custom-value")
})
36 changes: 35 additions & 1 deletion tests/testthat/test-spcs.R
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@ test_that("isSPCSServer correctly identifies Snowpark Container Services servers
})

test_that("authHeaders handles snowflakeToken", {
# mock authInfo with snowflakeToken
authInfo <- list(
snowflakeToken = list(
Authorization = "Snowflake Token=\"mock_token\"",
Expand All @@ -32,8 +31,25 @@ test_that("authHeaders handles snowflakeToken", {

headers <- authHeaders(authInfo, "GET", "/path")

expect_equal(headers$Authorization, "Snowflake Token=\"mock_token\"")
expect_equal(headers$`X-Custom-Header`, "custom-value")
})

test_that("authHeaders handles snowflakeToken with API key", {
authInfo <- list(
apiKey = secret("the-api-key"),
snowflakeToken = list(
Authorization = "Snowflake Token=\"mock_token\"",
`X-Custom-Header` = "custom-value"
)
)

# Test authHeaders
headers <- authHeaders(authInfo, "GET", "/path")

# Verify snowflakeToken headers were used
expect_equal(headers$Authorization, "Snowflake Token=\"mock_token\"")
expect_equal(headers$`X-RSC-Authorization`, "Key the-api-key")
expect_equal(headers$`X-Custom-Header`, "custom-value")
})

Expand All @@ -52,3 +68,21 @@ test_that("registerAccount stores snowflakeConnectionName", {
info <- accountInfo("testuser", "example.com")
expect_equal(info$snowflakeConnectionName, "test_connection")
})

test_that("registerAccount stores both apiKey and snowflakeConnectionName for SPCS accounts", {
local_temp_config()

# Register an SPCS account with both apiKey and snowflakeConnectionName
registerAccount(
serverName = "spcs.example.com",
accountName = "spcsuser",
accountId = "user456",
apiKey = "test-api-key-789",
snowflakeConnectionName = "spcs_connection"
)

# Check the account info has both fields
info <- accountInfo("spcsuser", "spcs.example.com")
expect_equal(info$snowflakeConnectionName, "spcs_connection")
expect_equal(as.character(info$apiKey), "test-api-key-789")
})