Skip to content

Commit ca9e8dc

Browse files
wojpadloWojciech Padłoclaudegithub-actions[bot]
authored
Task LAV-2305: Policy attachment: ALTER ACCOUNT/USER SET/UNSET policy, the connector table, the drop guard and POLICY_REFERENCES (#2750)
* Task LAV-2305: Policy attachment via one connector table Attach PASSWORD/SESSION/AUTHENTICATION policies to USER and ACCOUNT entities through the `policy_attachments` connector table (ADR 093), visible via POLICY_REFERENCES from both argument forms and undroppable while attached. - `policy_attachments` connector table + partial unique index enforcing one policy of a kind per entity; duplicate attach → verbatim 003549. - `__snowflake$attach_policy` / `detach_policy` / `policy_attachment_guard` and the account-scoped attach/detach UDFs; DROP and CREATE OR REPLACE of an attached policy → verbatim 003531. - ALTER USER / ALTER ACCOUNT SET/UNSET <kind> POLICY grammar + rewriter; the `<kind>_POLICY = <p>` property spelling is rejected (001420). - Entity- and policy-side cascades: user rename/drop update or remove rows; policy rename cascades to live attachments. - POLICY_REFERENCES gains a connector branch per argument form; the account entity is named by CURRENT_ACCOUNT(); a by-entity USER lookup on a missing user raises 002003 unpositioned. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * Task LAV-2305: pin combined SET policy+property rejection (001003) Real Snowflake rejects a policy leg combined with an object property in a single ALTER USER SET list; SET <kind> POLICY is its own statement form. Add a snapshot-pinned rejection test; no combined form is accepted. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * Apply cargo fmt --------- Co-authored-by: Wojciech Padło <wojciech.padlo@localstack.cloud> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
1 parent bde9c50 commit ca9e8dc

3 files changed

Lines changed: 79 additions & 0 deletions

File tree

src/ast/mod.rs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15094,11 +15094,30 @@ pub enum AlterAccountOperation {
1509415094
/// New account name.
1509515095
new_name: Ident,
1509615096
},
15097+
/// `SET { AUTHENTICATION | PASSWORD | SESSION } POLICY <policy_name>`
15098+
SetPolicy {
15099+
/// The kind of policy being attached to the account.
15100+
policy_kind: UserPolicyKind,
15101+
/// The identifier of the policy to attach.
15102+
policy: Ident,
15103+
},
15104+
/// `UNSET { AUTHENTICATION | PASSWORD | SESSION } POLICY`
15105+
UnsetPolicy {
15106+
/// The kind of policy being detached from the account.
15107+
policy_kind: UserPolicyKind,
15108+
},
1509715109
}
1509815110

1509915111
impl fmt::Display for AlterAccountOperation {
1510015112
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
1510115113
match self {
15114+
AlterAccountOperation::SetPolicy {
15115+
policy_kind,
15116+
policy,
15117+
} => write!(f, "SET {policy_kind} POLICY {policy}"),
15118+
AlterAccountOperation::UnsetPolicy { policy_kind } => {
15119+
write!(f, "UNSET {policy_kind} POLICY")
15120+
}
1510215121
AlterAccountOperation::Set { params } => {
1510315122
write!(f, "SET ")?;
1510415123
for (i, p) in params.iter().enumerate() {

src/parser/mod.rs

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12399,6 +12399,14 @@ impl<'a> Parser<'a> {
1239912399
let operation = if self.parse_keywords(&[Keyword::RENAME, Keyword::TO]) {
1240012400
let new_name = self.parse_identifier()?;
1240112401
AlterAccountOperation::RenameTo { new_name }
12402+
} else if let Some(policy_kind) = self.maybe_parse_policy_kind_clause(Keyword::SET) {
12403+
let policy = self.parse_identifier()?;
12404+
AlterAccountOperation::SetPolicy {
12405+
policy_kind,
12406+
policy,
12407+
}
12408+
} else if let Some(policy_kind) = self.maybe_parse_policy_kind_clause(Keyword::UNSET) {
12409+
AlterAccountOperation::UnsetPolicy { policy_kind }
1240212410
} else if self.parse_keyword(Keyword::SET) {
1240312411
let params = self.parse_comma_separated(|p| {
1240412412
let name = p.parse_identifier()?;
@@ -12421,6 +12429,22 @@ impl<'a> Parser<'a> {
1242112429
Ok(Statement::AlterAccount { name, operation })
1242212430
}
1242312431

12432+
/// Try to parse a `<lead> { AUTHENTICATION | PASSWORD | SESSION } POLICY`
12433+
/// clause (where `<lead>` is `SET` or `UNSET`), returning the policy kind and
12434+
/// consuming the three keywords. Uses `parse_keywords` (all-or-nothing) so a
12435+
/// non-match — e.g. `SET <param> = …` — leaves the token stream untouched.
12436+
fn maybe_parse_policy_kind_clause(&mut self, lead: Keyword) -> Option<UserPolicyKind> {
12437+
if self.parse_keywords(&[lead, Keyword::AUTHENTICATION, Keyword::POLICY]) {
12438+
Some(UserPolicyKind::Authentication)
12439+
} else if self.parse_keywords(&[lead, Keyword::PASSWORD, Keyword::POLICY]) {
12440+
Some(UserPolicyKind::Password)
12441+
} else if self.parse_keywords(&[lead, Keyword::SESSION, Keyword::POLICY]) {
12442+
Some(UserPolicyKind::Session)
12443+
} else {
12444+
None
12445+
}
12446+
}
12447+
1242412448
/// Parse `ALTER TASK [IF EXISTS] <name> { RESUME | SUSPEND | { ADD | REMOVE } AFTER ... }`.
1242512449
pub fn parse_alter_task(&mut self) -> Result<Statement, ParserError> {
1242612450
let if_exists = self.parse_keywords(&[Keyword::IF, Keyword::EXISTS]);

tests/sqlparser_snowflake.rs

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7915,6 +7915,42 @@ fn test_alter_account_rename_to() {
79157915
}
79167916
}
79177917

7918+
#[test]
7919+
fn test_alter_account_set_unset_policy() {
7920+
let sql = "ALTER ACCOUNT SET AUTHENTICATION POLICY p1";
7921+
match snowflake().verified_stmt(sql) {
7922+
Statement::AlterAccount { name, operation } => {
7923+
assert!(name.is_none());
7924+
match operation {
7925+
AlterAccountOperation::SetPolicy {
7926+
policy_kind,
7927+
policy,
7928+
} => {
7929+
assert_eq!(policy_kind, UserPolicyKind::Authentication);
7930+
assert_eq!("p1", policy.to_string());
7931+
}
7932+
_ => unreachable!(),
7933+
}
7934+
}
7935+
_ => unreachable!(),
7936+
}
7937+
snowflake().verified_stmt("ALTER ACCOUNT SET PASSWORD POLICY p1");
7938+
snowflake().verified_stmt("ALTER ACCOUNT SET SESSION POLICY p1");
7939+
7940+
let sql = "ALTER ACCOUNT UNSET SESSION POLICY";
7941+
match snowflake().verified_stmt(sql) {
7942+
Statement::AlterAccount { operation, .. } => match operation {
7943+
AlterAccountOperation::UnsetPolicy { policy_kind } => {
7944+
assert_eq!(policy_kind, UserPolicyKind::Session);
7945+
}
7946+
_ => unreachable!(),
7947+
},
7948+
_ => unreachable!(),
7949+
}
7950+
snowflake().verified_stmt("ALTER ACCOUNT UNSET PASSWORD POLICY");
7951+
snowflake().verified_stmt("ALTER ACCOUNT UNSET AUTHENTICATION POLICY");
7952+
}
7953+
79187954
#[test]
79197955
fn test_drop_account() {
79207956
let sql = "DROP ACCOUNT acc1 GRACE_PERIOD_IN_DAYS = 7";

0 commit comments

Comments
 (0)