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
41 changes: 26 additions & 15 deletions docs/resources/authentication_policy.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,26 @@ Defines the rules and constraints for authentication within the system, ensuring

## Examples

### Python

```python
authentication_policy = AuthenticationPolicy(
name="some_authentication_policy",
authentication_methods=["PASSWORD", "SAML", "PROGRAMMATIC_ACCESS_TOKEN"],
mfa_authentication_methods=["PASSWORD"],
mfa_enrollment="REQUIRED",
client_types=["SNOWFLAKE_UI"],
security_integrations=["ALL"],
pat_policy={
"network_policy_evaluation": "ENFORCED_NOT_REQUIRED",
"default_expiry_in_days": 30,
"max_expiry_in_days": 180,
},
comment="Policy for secure authentication."
)
```


### YAML

```yaml
Expand All @@ -20,32 +40,22 @@ authentication_policies:
authentication_methods:
- PASSWORD
- SAML
- PROGRAMMATIC_ACCESS_TOKEN
mfa_authentication_methods:
- PASSWORD
mfa_enrollment: REQUIRED
client_types:
- SNOWFLAKE_UI
security_integrations:
- ALL
pat_policy:
network_policy_evaluation: ENFORCED_NOT_REQUIRED
default_expiry_in_days: 30
max_expiry_in_days: 180
comment: Policy for secure authentication.
```


### Python

```python
authentication_policy = AuthenticationPolicy(
name="some_authentication_policy",
authentication_methods=["PASSWORD", "SAML"],
mfa_authentication_methods=["PASSWORD"],
mfa_enrollment="REQUIRED",
client_types=["SNOWFLAKE_UI"],
security_integrations=["ALL"],
comment="Policy for secure authentication."
)
```


## Fields

* `name` (string, required) - The name of the authentication policy.
Expand All @@ -54,6 +64,7 @@ authentication_policy = AuthenticationPolicy(
* `mfa_enrollment` (string) - Determines whether a user must enroll in multi-factor authentication. Defaults to OPTIONAL.
* `client_types` (list) - A list of clients that can authenticate with Snowflake.
* `security_integrations` (list) - A list of security integrations the authentication policy is associated with.
* `pat_policy` (dict) - Controls programmatic access token issuance: network_policy_evaluation, default_expiry_in_days, and max_expiry_in_days must all be given or all omitted; declaring exactly the Snowflake defaults (ENFORCED_REQUIRED, 15, 365) compares as unset.
* `comment` (string) - A comment or description for the authentication policy.
* `owner` (string or [Role](role.md)) - The owner role of the authentication policy. Defaults to SECURITYADMIN.

Expand Down
47 changes: 47 additions & 0 deletions snowcap/data_provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -1384,13 +1384,23 @@ def fetch_authentication_policy(session: SnowflakeConnection, fqn: FQN):
if mfa_auth_methods == ["PASSWORD"]:
mfa_auth_methods = None

# pat_policy's brace shape is doc-derived and unverified against a live account (see
# _suppress_default_pat_policy above), so a parse failure must degrade to None instead of
# aborting the whole account-wide fetch pipeline (blueprint.py re-raises fetch exceptions).
try:
pat_policy = _suppress_default_pat_policy(_parse_pat_policy_property(properties.get("pat_policy")))
except Exception as err:
logger.warning(f"Failed to parse pat_policy property {properties.get('pat_policy')!r}: {err}")
pat_policy = None

return {
"name": _quote_snowflake_identifier(data["name"]),
"authentication_methods": _parse_list_property(properties["authentication_methods"]),
"mfa_authentication_methods": mfa_auth_methods,
"mfa_enrollment": properties["mfa_enrollment"],
"client_types": _parse_list_property(properties["client_types"]),
"security_integrations": _parse_list_property(properties["security_integrations"]),
"pat_policy": pat_policy,
"comment": data["comment"] or None,
"owner": _get_owner_identifier(data),
}
Expand Down Expand Up @@ -1487,6 +1497,43 @@ def _parse_enum_map(value):
return out


# Snowflake's documented AUTHENTICATION POLICY pat_policy defaults. This constant is the single
# source of truth for the fetch layer's pat_policy key schema (used to filter out sub-keys like
# REQUIRE_ROLE_RESTRICTION_FOR_SERVICE_USERS) and doubles as the suppression sentinel below.
_PAT_POLICY_DEFAULT = {
"network_policy_evaluation": "ENFORCED_REQUIRED",
"default_expiry_in_days": 15,
"max_expiry_in_days": 365,
}


def _parse_pat_policy_property(prop_value: Optional[str]) -> Optional[dict]:
# Contract: returns a COMPLETE dict (all _PAT_POLICY_DEFAULT keys), None (absent/empty/null
# input), or raises. A partial dict must never escape — __post_init__ on the resource dataclass
# enforces "None or complete" and blueprint.py re-raises any exception, aborting the
# account-wide fetch for every resource, so malformed input has to raise here and be caught by
# the fetch layer's except -> logger.warning -> None fallback.
if prop_value is None or prop_value == "" or prop_value == "null":
return None
parsed = _parse_enum_map(prop_value)
if not isinstance(parsed, dict):
raise ValueError(f"Unexpected pat_policy format: {prop_value!r}")
pat_policy = {key: parsed[key] for key in _PAT_POLICY_DEFAULT if key in parsed}
if pat_policy.keys() != _PAT_POLICY_DEFAULT.keys():
raise ValueError(f"Incomplete pat_policy fields: {prop_value!r}")
pat_policy["default_expiry_in_days"] = int(pat_policy["default_expiry_in_days"])
pat_policy["max_expiry_in_days"] = int(pat_policy["max_expiry_in_days"])
return pat_policy


def _suppress_default_pat_policy(pat_policy: Optional[dict]) -> Optional[dict]:
# pat_policy is doc-derived (unverified against a live DESC AUTHENTICATION POLICY output),
# mirroring the mfa_authentication_methods drift-avoidance above: Snowflake echoes this
# default even when pat_policy was never explicitly set, so treat it as "unset" to avoid
# false drift detection.
return None if pat_policy == _PAT_POLICY_DEFAULT else pat_policy


def fetch_columns(session: SnowflakeConnection, resource_type: str, fqn: FQN):
desc_result = execute(session, f"DESC {resource_type} {fqn}")
columns = []
Expand Down
47 changes: 45 additions & 2 deletions snowcap/resources/authentication_policy.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from dataclasses import dataclass

from ..enums import ParseableEnum, ResourceType
from ..props import EnumProp, Props, StringListProp, StringProp
from ..props import EnumProp, IntProp, Props, PropSet, StringListProp, StringProp
from ..resource_name import ResourceName
from ..role_ref import RoleRef
from ..scope import SchemaScope
Expand All @@ -14,6 +14,7 @@ class AuthenticationMethods(ParseableEnum):
SAML = "SAML"
OAUTH = "OAUTH"
KEYPAIR = "KEYPAIR"
PROGRAMMATIC_ACCESS_TOKEN = "PROGRAMMATIC_ACCESS_TOKEN"


class MFAEnrollment(ParseableEnum):
Expand All @@ -28,6 +29,21 @@ class ClientTypes(ParseableEnum):
SNOWSQL = "SNOWSQL"


class NetworkPolicyEvaluation(ParseableEnum):
ENFORCED_REQUIRED = "ENFORCED_REQUIRED"
ENFORCED_NOT_REQUIRED = "ENFORCED_NOT_REQUIRED"
NOT_ENFORCED = "NOT_ENFORCED"


# Single source of truth for the PAT_POLICY sub-key schema, shared by __post_init__
# (to derive the required-key set) and AuthenticationPolicy.props (to render/parse it).
_PAT_POLICY_PROPS = Props(
network_policy_evaluation=EnumProp("NETWORK_POLICY_EVALUATION", NetworkPolicyEvaluation),
default_expiry_in_days=IntProp("default_expiry_in_days"),
max_expiry_in_days=IntProp("max_expiry_in_days"),
)


@dataclass(unsafe_hash=True)
class _AuthenticationPolicy(ResourceSpec):
name: ResourceName
Expand All @@ -36,6 +52,7 @@ class _AuthenticationPolicy(ResourceSpec):
mfa_enrollment: MFAEnrollment = "OPTIONAL"
client_types: list[ClientTypes] = None
security_integrations: list[str] = None
pat_policy: dict = None
comment: str = None
owner: RoleRef = "SECURITYADMIN"

Expand Down Expand Up @@ -68,6 +85,18 @@ def __post_init__(self):
if self.security_integrations is None:
self.security_integrations = ["ALL"]

if self.pat_policy is not None:
self.pat_policy = {k: v for k, v in self.pat_policy.items() if k in _PAT_POLICY_PROPS.props}
required_keys = set(_PAT_POLICY_PROPS.props)
missing_keys = required_keys - self.pat_policy.keys()
if missing_keys:
raise ValueError(f"pat_policy is missing required keys: {sorted(missing_keys)}")

for key, prop in _PAT_POLICY_PROPS.props.items():
self.pat_policy[key] = prop.typecheck(self.pat_policy[key])
if self.pat_policy["default_expiry_in_days"] > self.pat_policy["max_expiry_in_days"]:
raise ValueError("pat_policy.default_expiry_in_days cannot exceed pat_policy.max_expiry_in_days")


class AuthenticationPolicy(NamedResource, Resource):
"""
Expand All @@ -84,6 +113,7 @@ class AuthenticationPolicy(NamedResource, Resource):
mfa_enrollment (string): Determines whether a user must enroll in multi-factor authentication. Defaults to OPTIONAL.
client_types (list): A list of clients that can authenticate with Snowflake.
security_integrations (list): A list of security integrations the authentication policy is associated with.
pat_policy (dict): Controls programmatic access token issuance: network_policy_evaluation, default_expiry_in_days, and max_expiry_in_days must all be given or all omitted; declaring exactly the Snowflake defaults (ENFORCED_REQUIRED, 15, 365) compares as unset.
comment (string): A comment or description for the authentication policy.
owner (string or Role): The owner role of the authentication policy. Defaults to SECURITYADMIN.

Expand All @@ -92,11 +122,16 @@ class AuthenticationPolicy(NamedResource, Resource):
```python
authentication_policy = AuthenticationPolicy(
name="some_authentication_policy",
authentication_methods=["PASSWORD", "SAML"],
authentication_methods=["PASSWORD", "SAML", "PROGRAMMATIC_ACCESS_TOKEN"],
mfa_authentication_methods=["PASSWORD"],
mfa_enrollment="REQUIRED",
client_types=["SNOWFLAKE_UI"],
security_integrations=["ALL"],
pat_policy={
"network_policy_evaluation": "ENFORCED_NOT_REQUIRED",
"default_expiry_in_days": 30,
"max_expiry_in_days": 180,
},
comment="Policy for secure authentication."
)
```
Expand All @@ -109,13 +144,18 @@ class AuthenticationPolicy(NamedResource, Resource):
authentication_methods:
- PASSWORD
- SAML
- PROGRAMMATIC_ACCESS_TOKEN
mfa_authentication_methods:
- PASSWORD
mfa_enrollment: REQUIRED
client_types:
- SNOWFLAKE_UI
security_integrations:
- ALL
pat_policy:
network_policy_evaluation: ENFORCED_NOT_REQUIRED
default_expiry_in_days: 30
max_expiry_in_days: 180
comment: Policy for secure authentication.
```
"""
Expand All @@ -127,6 +167,7 @@ class AuthenticationPolicy(NamedResource, Resource):
mfa_enrollment=EnumProp("mfa_enrollment", MFAEnrollment),
client_types=StringListProp("client_types", parens=True),
security_integrations=StringListProp("security_integrations", parens=True),
pat_policy=PropSet("PAT_POLICY", _PAT_POLICY_PROPS),
comment=StringProp("comment"),
)
scope = SchemaScope()
Expand All @@ -140,6 +181,7 @@ def __init__(
mfa_enrollment: str = "OPTIONAL",
client_types: list[str] = None,
security_integrations: list[str] = None,
pat_policy: dict = None,
comment: str = None,
owner: str = "SECURITYADMIN",
**kwargs,
Expand All @@ -152,6 +194,7 @@ def __init__(
mfa_enrollment=mfa_enrollment,
client_types=client_types,
security_integrations=security_integrations,
pat_policy=pat_policy,
comment=comment,
owner=owner,
)
10 changes: 8 additions & 2 deletions tests/fixtures/json/authentication_policy.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,12 @@
"ALL"
],
"authentication_methods": [
"ALL"
]
"PROGRAMMATIC_ACCESS_TOKEN",
"KEYPAIR"
],
"pat_policy": {
"network_policy_evaluation": "ENFORCED_NOT_REQUIRED",
"default_expiry_in_days": 30,
"max_expiry_in_days": 180
}
}
4 changes: 3 additions & 1 deletion tests/fixtures/sql/authentication_policy.sql
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
CREATE AUTHENTICATION POLICY restrict_client_types_policy
CLIENT_TYPES = ('SNOWFLAKE_UI');
CLIENT_TYPES = ('SNOWFLAKE_UI')
AUTHENTICATION_METHODS = ('PROGRAMMATIC_ACCESS_TOKEN', 'KEYPAIR')
PAT_POLICY = (NETWORK_POLICY_EVALUATION = ENFORCED_NOT_REQUIRED DEFAULT_EXPIRY_IN_DAYS = 30 MAX_EXPIRY_IN_DAYS = 180);
6 changes: 6 additions & 0 deletions tests/integration/data_provider/test_fetch_resource_simple.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,12 @@ def resource_fixtures() -> list:
mfa_enrollment="REQUIRED",
client_types=["SNOWFLAKE_UI"],
security_integrations=["ALL"],
authentication_methods=["PROGRAMMATIC_ACCESS_TOKEN", "KEYPAIR"],
pat_policy={
"network_policy_evaluation": "ENFORCED_NOT_REQUIRED",
"default_expiry_in_days": 30,
"max_expiry_in_days": 180,
},
owner=TEST_ROLE,
),
res.AzureStorageIntegration(
Expand Down
Loading
Loading