Skip to content
Open
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
1 change: 1 addition & 0 deletions docs/SUMMARY.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@
* [Service](resources/service.md)
* [SessionPolicy](resources/session_policy.md)
* [Share](resources/share.md)
* [SnowflakeCustomOAuthSecurityIntegration](resources/snowflake_custom_oauth_security_integration.md)
* [SnowflakePartnerOAuthSecurityIntegration](resources/snowflake_partner_oauth_security_integration.md)
* [SnowservicesOAuthSecurityIntegration](resources/snowservices_oauth_security_integration.md)
* [StageStream](resources/stage_stream.md)
Expand Down
81 changes: 81 additions & 0 deletions docs/resources/snowflake_custom_oauth_security_integration.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
---
description: >-
A Snowflake OAuth security integration for a custom OAuth client, with credentials generated and managed by Snowflake.
---

# SnowflakeCustomOAuthSecurityIntegration

[Snowflake Documentation](https://docs.snowflake.com/en/sql-reference/sql/create-security-integration-oauth-snowflake) | Snowcap CLI label: `snowflake_custom_oauth_security_integration`

A security integration in Snowflake for a custom OAuth client that Snowflake itself
issues and manages. Unlike the partner integrations, Snowflake generates the OAuth
client_id and client_secret for you; retrieve them with
SYSTEM$SHOW_OAUTH_CLIENT_SECRETS, they cannot be set through this resource.
ACCOUNTADMIN, ORGADMIN, GLOBALORGADMIN, and SECURITYADMIN are always blocked by
Snowflake and must not be listed in blocked_roles_list.
Every property except oauth_client_type is applied with ALTER SECURITY INTEGRATION
and never recreates the integration. oauth_client_type cannot be changed after
creation: snowcap fails the plan rather than recreating the integration, because
recreation would rotate the Snowflake-issued client_id/client_secret and break live
OAuth clients. To change it, recreate the integration manually and migrate clients.
oauth_alternate_redirect_uris is CREATE-ONLY in snowcap: it is not fetchable, so
editing it in YAML after creation produces no diff and no error. Changes must be
applied out-of-band with
ALTER SECURITY INTEGRATION <name> SET OAUTH_ALTERNATE_REDIRECT_URIS = (...).


## Examples

### YAML

```yaml
security_integrations:
- name: claude_mcp_oauth
enabled: true
oauth_client_type: CONFIDENTIAL
oauth_redirect_uri: https://claude.ai/api/mcp/auth_callback
oauth_issue_refresh_tokens: true
oauth_refresh_token_validity: 7776000
oauth_use_secondary_roles: NONE
oauth_enforce_pkce: true
blocked_roles_list:
- SYSADMIN
comment: OAuth client for the Claude MCP connector
```


### Python

```python
claude_mcp_oauth = SnowflakeCustomOAuthSecurityIntegration(
name="claude_mcp_oauth",
enabled=True,
oauth_client_type="CONFIDENTIAL",
oauth_redirect_uri="https://claude.ai/api/mcp/auth_callback",
oauth_issue_refresh_tokens=True,
oauth_refresh_token_validity=7776000,
oauth_use_secondary_roles="NONE",
oauth_enforce_pkce=True,
blocked_roles_list=["SYSADMIN"],
comment="OAuth client for the Claude MCP connector"
)
```


## Fields

* `name` (string, required) - The name of the security integration.
* `enabled` (bool) - Specifies if the security integration is enabled. Defaults to True.
* `oauth_client_type` (string or OAuthClientType, required) - The type of OAuth client. Supported values are 'CONFIDENTIAL' and 'PUBLIC'. Cannot be changed after creation.
* `oauth_redirect_uri` (string, required) - The redirect URI the client uses to complete the OAuth flow.
* `oauth_alternate_redirect_uris` (list) - Additional allowed redirect URIs, set at creation only.
* `oauth_issue_refresh_tokens` (bool) - Indicates if refresh tokens should be issued. Defaults to True.
* `oauth_refresh_token_validity` (int) - The validity period of the refresh token in seconds. Defaults to 7776000.
* `oauth_use_secondary_roles` (string or OAuthUseSecondaryRoles) - Whether secondary roles are activated for OAuth sessions. Supported values are 'IMPLICIT' and 'NONE'. Defaults to 'NONE'.
* `oauth_enforce_pkce` (bool) - Requires clients to use PKCE during the OAuth flow. Defaults to False.
* `network_policy` (string) - The network policy enforced for requests made with this integration's tokens.
* `pre_authorized_roles_list` (list) - Roles granted access without displaying a consent screen to the user.
* `blocked_roles_list` (list) - Roles that are not allowed to use this integration.
* `comment` (string) - A comment about the security integration.


1 change: 1 addition & 0 deletions mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,7 @@ nav:
- ExternalAccessIntegration: resources/external_access_integration.md
- Security:
- APIAuthenticationSecurityIntegration: resources/api_authentication_security_integration.md
- SnowflakeCustomOAuthSecurityIntegration: resources/snowflake_custom_oauth_security_integration.md
- SnowflakePartnerOAuthSecurityIntegration: resources/snowflake_partner_oauth_security_integration.md
- SnowservicesOAuthSecurityIntegration: resources/snowservices_oauth_security_integration.md
- Storage:
Expand Down
11 changes: 11 additions & 0 deletions snowcap/builtins.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,14 @@
SYSTEM_SECURITY_INTEGRATIONS = [
"APPLICA",
]

# Snowflake always blocks these roles from a CUSTOM OAuth integration, regardless of
# what's in BLOCKED_ROLES_LIST, and echoes them back in DESC. Shared by the spec (which
# rejects them in blocked_roles_list) and the fetch layer (which strips them from state
# fetched from Snowflake), so both sides agree on what "always blocked" means.
ALWAYS_BLOCKED_OAUTH_ROLES = [
"ACCOUNTADMIN",
"ORGADMIN",
"GLOBALORGADMIN",
"SECURITYADMIN",
]
35 changes: 23 additions & 12 deletions snowcap/data_provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
from snowflake.connector.errors import ProgrammingError

from .builtins import (
ALWAYS_BLOCKED_OAUTH_ROLES,
SYSTEM_DATABASES,
SYSTEM_ROLES,
SYSTEM_SECURITY_INTEGRATIONS,
Expand Down Expand Up @@ -2546,20 +2547,30 @@ def fetch_security_integration(session: SnowflakeConnection, fqn: FQN):
"enabled": data["enabled"] == "true",
"owner": owner,
}
elif oauth_client == "CUSTOM":
pre_authorized_roles_list = properties.get("pre_authorized_roles_list") or None
if pre_authorized_roles_list:
pre_authorized_roles_list = sorted(pre_authorized_roles_list)
blocked_roles_list = set(properties.get("blocked_roles_list") or []) - set(ALWAYS_BLOCKED_OAUTH_ROLES)
return {
"name": _quote_snowflake_identifier(data["name"]),
"type": type_,
"oauth_client": oauth_client,
"enabled": data["enabled"] == "true",
"oauth_client_type": properties.get("oauth_client_type"),
"oauth_redirect_uri": properties.get("oauth_redirect_uri"),
"oauth_issue_refresh_tokens": properties.get("oauth_issue_refresh_tokens"),
"oauth_refresh_token_validity": int(properties["oauth_refresh_token_validity"]),
"oauth_use_secondary_roles": properties.get("oauth_use_secondary_roles"),
"oauth_enforce_pkce": properties.get("oauth_enforce_pkce"),
"network_policy": properties.get("network_policy"),
"pre_authorized_roles_list": pre_authorized_roles_list,
"blocked_roles_list": sorted(blocked_roles_list) or None,
"comment": data["comment"] or None,
"owner": owner,
}
raise Exception(f"Unsupported security integration type {data['type']}")

# return {
# "name": _quote_snowflake_identifier(data["name"]),
# "type": type_,
# "enabled": data["enabled"] == "true",
# "oauth_client": oauth_client,
# # "oauth_client_secret": None,
# # "oauth_redirect_uri": None,
# "oauth_issue_refresh_tokens": properties["oauth_issue_refresh_tokens"] == "true",
# "oauth_refresh_token_validity": properties["oauth_refresh_token_validity"],
# "comment": data["comment"] or None,
# }


def fetch_sequence(session: SnowflakeConnection, fqn: FQN):
show_result = execute(session, f"SHOW SEQUENCES LIKE '{fqn.name}' IN SCHEMA {fqn.database}.{fqn.schema}")
Expand Down
2 changes: 2 additions & 0 deletions snowcap/resources/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@
from .secret import GenericSecret, OAuthSecret, PasswordSecret
from .security_integration import (
APIAuthenticationSecurityIntegration,
SnowflakeCustomOAuthSecurityIntegration,
SnowflakePartnerOAuthSecurityIntegration,
SnowservicesOAuthSecurityIntegration,
)
Expand Down Expand Up @@ -139,6 +140,7 @@
"Sequence",
"Service",
"Share",
"SnowflakeCustomOAuthSecurityIntegration",
"SnowflakeIcebergTable",
"SnowflakePartnerOAuthSecurityIntegration",
"SnowservicesOAuthSecurityIntegration",
Expand Down
Loading
Loading