Skip to main content

reddb_rql/parser/
auth_ddl.rs

1//! Auth-related DDL parsers — `GRANT`, `REVOKE`, `ALTER USER`.
2//!
3//! These statements live alongside the rest of DDL but their AST nodes
4//! and downstream dispatch are in `crate::auth::privileges`. The
5//! parser is intentionally thin: every shape the user types maps
6//! directly onto the [`GrantStmt`] / [`RevokeStmt`] / [`AlterUserStmt`]
7//! AST so the runtime can apply the change in one match arm.
8//!
9//! Grammar (conservative — defers the long-tail PG modifiers):
10//! ```text
11//!   GRANT { privilege_list | ALL [PRIVILEGES] }
12//!         [ ( column_list ) ]
13//!         ON [ TABLE | SCHEMA | DATABASE | FUNCTION ] object_list
14//!         TO grant_principal_list
15//!         [ WITH GRANT OPTION ]
16//!
17//!   REVOKE [ GRANT OPTION FOR ] { privilege_list | ALL [PRIVILEGES] }
18//!         [ ( column_list ) ]
19//!         ON [ TABLE | SCHEMA | DATABASE | FUNCTION ] object_list
20//!         FROM grant_principal_list
21//!
22//!   ALTER USER name
23//!         [ VALID UNTIL 'timestamp' ]
24//!         [ CONNECTION LIMIT n ]
25//!         [ ENABLE | DISABLE ]
26//!         [ SET search_path = 'csv' ]
27//!         [ PASSWORD 'plaintext' ]
28//! ```
29//!
30//! `name` accepts `tenant.username` form so a platform admin can target
31//! a tenant-scoped account. `PUBLIC` is recognised as a reserved
32//! principal.
33
34use crate::ast::{
35    AlterUserAttribute, AlterUserStmt, CreateUserStmt, GrantObject, GrantObjectKind,
36    GrantPrincipalRef, GrantStmt, LintPolicySource, PolicyPrincipalRef, PolicyResourceRef,
37    PolicyUserRef, QueryExpr, RevokeStmt,
38};
39use crate::lexer::Token;
40use crate::parser::{ParseError, Parser};
41
42impl<'a> Parser<'a> {
43    /// Parse `CREATE USER name [WITH] PASSWORD 'plaintext' [ROLE role]`.
44    /// `role` defaults to `read`, the least-privileged RedDB role.
45    pub fn parse_create_user_statement(&mut self) -> Result<CreateUserStmt, ParseError> {
46        if !self.consume_ident_ci("USER")? {
47            return Err(ParseError::expected(
48                vec!["USER"],
49                self.peek(),
50                self.position(),
51            ));
52        }
53        let (tenant, username) = self.parse_user_name()?;
54
55        let mut password = None;
56        let mut role = "read".to_string();
57
58        let _ = self.consume(&Token::With)? || self.consume_ident_ci("WITH")?;
59        loop {
60            if self.consume_ident_ci("PASSWORD")? {
61                password = Some(self.parse_string()?);
62            } else if self.consume_ident_ci("ROLE")? {
63                role = self.expect_ident()?.to_ascii_lowercase();
64            } else {
65                break;
66            }
67        }
68
69        let password = password
70            .ok_or_else(|| ParseError::expected(vec!["PASSWORD"], self.peek(), self.position()))?;
71
72        Ok(CreateUserStmt {
73            tenant,
74            username,
75            password,
76            role,
77        })
78    }
79
80    /// Parse a `GRANT` statement. Caller must have already verified the
81    /// current token is the `GRANT` ident (it is not a lexer keyword —
82    /// the lexer maps it to `Token::Ident("GRANT")`).
83    pub fn parse_grant_statement(&mut self) -> Result<GrantStmt, ParseError> {
84        // Eat `GRANT`.
85        self.advance()?;
86
87        let (actions, all, columns) = self.parse_privilege_list()?;
88        self.expect(Token::On)?;
89        let object_kind = self.parse_grant_object_kind()?;
90        let objects = self.parse_grant_object_list(&object_kind)?;
91        self.expect(Token::To)?;
92        let principals = self.parse_grant_principal_list()?;
93
94        let with_grant_option = self.consume_grant_option_suffix()?;
95
96        Ok(GrantStmt {
97            actions,
98            columns,
99            object_kind,
100            objects,
101            principals,
102            with_grant_option,
103            all,
104        })
105    }
106
107    /// Parse a `REVOKE` statement. Caller must have already verified the
108    /// current token is the `REVOKE` ident.
109    pub fn parse_revoke_statement(&mut self) -> Result<RevokeStmt, ParseError> {
110        // Eat `REVOKE`.
111        self.advance()?;
112
113        // Optional `GRANT OPTION FOR`.
114        let grant_option_for = self.consume_grant_option_for_prefix()?;
115
116        let (actions, all, columns) = self.parse_privilege_list()?;
117        self.expect(Token::On)?;
118        let object_kind = self.parse_grant_object_kind()?;
119        let objects = self.parse_grant_object_list(&object_kind)?;
120        self.expect(Token::From)?;
121        let principals = self.parse_grant_principal_list()?;
122
123        Ok(RevokeStmt {
124            actions,
125            columns,
126            object_kind,
127            objects,
128            principals,
129            grant_option_for,
130            all,
131        })
132    }
133
134    /// Parse `ALTER USER name <attrs>`. Caller has just consumed
135    /// `Token::Alter`.
136    pub fn parse_alter_user_statement(&mut self) -> Result<AlterUserStmt, ParseError> {
137        // `ALTER` was already consumed by the dispatcher; expect USER ident.
138        if !self.consume_ident_ci("USER")? {
139            return Err(ParseError::expected(
140                vec!["USER"],
141                self.peek(),
142                self.position(),
143            ));
144        }
145        let (tenant, username) = self.parse_user_name()?;
146
147        let mut attributes = Vec::new();
148        loop {
149            if self.consume_ident_ci("VALID")? {
150                if !self.consume_ident_ci("UNTIL")? {
151                    return Err(ParseError::expected(
152                        vec!["UNTIL"],
153                        self.peek(),
154                        self.position(),
155                    ));
156                }
157                let ts = self.parse_string()?;
158                attributes.push(AlterUserAttribute::ValidUntil(ts));
159            } else if self.consume_ident_ci("CONNECTION")? {
160                if !self.consume(&Token::Limit)? && !self.consume_ident_ci("LIMIT")? {
161                    return Err(ParseError::expected(
162                        vec!["LIMIT"],
163                        self.peek(),
164                        self.position(),
165                    ));
166                }
167                let n = self.parse_integer()?;
168                attributes.push(AlterUserAttribute::ConnectionLimit(n));
169            } else if self.consume(&Token::Enable)? {
170                attributes.push(AlterUserAttribute::Enable);
171            } else if self.consume(&Token::Disable)? {
172                attributes.push(AlterUserAttribute::Disable);
173            } else if self.consume(&Token::Set)? {
174                // SET search_path = 'csv'  |  SET search_path TO 'csv'
175                if !self.consume_ident_ci("SEARCH_PATH")? {
176                    return Err(ParseError::expected(
177                        vec!["search_path"],
178                        self.peek(),
179                        self.position(),
180                    ));
181                }
182                if !self.consume(&Token::Eq)? && !self.consume(&Token::To)? {
183                    return Err(ParseError::expected(
184                        vec!["="],
185                        self.peek(),
186                        self.position(),
187                    ));
188                }
189                let value = self.parse_string()?;
190                attributes.push(AlterUserAttribute::SetSearchPath(value));
191            } else if self.consume(&Token::Add)? || self.consume_ident_ci("ADD")? {
192                if !self.consume(&Token::Group)? && !self.consume_ident_ci("GROUP")? {
193                    return Err(ParseError::expected(
194                        vec!["GROUP"],
195                        self.peek(),
196                        self.position(),
197                    ));
198                }
199                let group = self.expect_ident()?;
200                attributes.push(AlterUserAttribute::AddGroup(group));
201            } else if self.consume(&Token::Drop)? || self.consume_ident_ci("DROP")? {
202                if !self.consume(&Token::Group)? && !self.consume_ident_ci("GROUP")? {
203                    return Err(ParseError::expected(
204                        vec!["GROUP"],
205                        self.peek(),
206                        self.position(),
207                    ));
208                }
209                let group = self.expect_ident()?;
210                attributes.push(AlterUserAttribute::DropGroup(group));
211            } else if self.consume_ident_ci("PASSWORD")? {
212                let pw = self.parse_string()?;
213                attributes.push(AlterUserAttribute::Password(pw));
214            } else {
215                break;
216            }
217        }
218
219        if attributes.is_empty() {
220            return Err(ParseError::expected(
221                vec![
222                    "VALID",
223                    "CONNECTION",
224                    "ENABLE",
225                    "DISABLE",
226                    "SET",
227                    "ADD",
228                    "DROP",
229                    "PASSWORD",
230                ],
231                self.peek(),
232                self.position(),
233            ));
234        }
235
236        Ok(AlterUserStmt {
237            tenant,
238            username,
239            attributes,
240        })
241    }
242
243    // -----------------------------------------------------------------
244    // IAM policy DDL — CREATE / DROP / ATTACH / DETACH / SHOW / SIMULATE
245    // -----------------------------------------------------------------
246
247    /// Parse `CREATE POLICY '<id>' AS '<json>'`. Caller has consumed
248    /// `CREATE POLICY` already and confirmed the next token is a
249    /// string literal (the IAM-flavoured form). Returns the
250    /// `QueryExpr::CreateIamPolicy` variant.
251    pub fn parse_create_iam_policy_after_keywords(&mut self) -> Result<QueryExpr, ParseError> {
252        let id = self.parse_string()?;
253        if !self.consume(&Token::As)? && !self.consume_ident_ci("AS")? {
254            return Err(ParseError::expected(
255                vec!["AS"],
256                self.peek(),
257                self.position(),
258            ));
259        }
260        let json = self.parse_string()?;
261        Ok(QueryExpr::CreateIamPolicy { id, json })
262    }
263
264    /// Parse `DROP POLICY '<id>'`. Caller has consumed `DROP POLICY`
265    /// and verified the next token is a string literal.
266    pub fn parse_drop_iam_policy_after_keywords(&mut self) -> Result<QueryExpr, ParseError> {
267        let id = self.parse_string()?;
268        Ok(QueryExpr::DropIamPolicy { id })
269    }
270
271    /// Parse `ATTACH POLICY '<id>' TO { USER | GROUP } <name>`.
272    /// Caller has consumed nothing — leading `ATTACH` is still on
273    /// the token stream.
274    pub fn parse_attach_policy(&mut self) -> Result<QueryExpr, ParseError> {
275        self.advance()?; // ATTACH
276        if !self.consume(&Token::Policy)? && !self.consume_ident_ci("POLICY")? {
277            return Err(ParseError::expected(
278                vec!["POLICY"],
279                self.peek(),
280                self.position(),
281            ));
282        }
283        let policy_id = self.parse_string()?;
284        self.expect(Token::To)?;
285        let principal = self.parse_iam_principal_kind()?;
286        Ok(QueryExpr::AttachPolicy {
287            policy_id,
288            principal,
289        })
290    }
291
292    /// Parse `DETACH POLICY '<id>' FROM { USER | GROUP } <name>`.
293    pub fn parse_detach_policy(&mut self) -> Result<QueryExpr, ParseError> {
294        self.advance()?; // DETACH
295        if !self.consume(&Token::Policy)? && !self.consume_ident_ci("POLICY")? {
296            return Err(ParseError::expected(
297                vec!["POLICY"],
298                self.peek(),
299                self.position(),
300            ));
301        }
302        let policy_id = self.parse_string()?;
303        self.expect(Token::From)?;
304        let principal = self.parse_iam_principal_kind()?;
305        Ok(QueryExpr::DetachPolicy {
306            policy_id,
307            principal,
308        })
309    }
310
311    /// Parse `SIMULATE <name> ACTION <verb> ON <kind>:<name>`.
312    pub fn parse_simulate_policy(&mut self) -> Result<QueryExpr, ParseError> {
313        self.advance()?; // ident "SIMULATE"
314        let user = self.parse_iam_user_ref()?;
315        if !self.consume_ident_ci("ACTION")? {
316            return Err(ParseError::expected(
317                vec!["ACTION"],
318                self.peek(),
319                self.position(),
320            ));
321        }
322        let action = self.parse_iam_action_token()?;
323        self.expect(Token::On)?;
324        let resource = self.parse_iam_resource_ref()?;
325        Ok(QueryExpr::SimulatePolicy {
326            user,
327            action,
328            resource,
329        })
330    }
331
332    /// Parse `MIGRATE POLICY MODE TO '<mode>' [DRY RUN]`. Caller has
333    /// just observed the leading `MIGRATE` ident; the token is still
334    /// queued. Issue #714.
335    pub fn parse_migrate_policy_mode(&mut self) -> Result<QueryExpr, ParseError> {
336        self.advance()?; // ident "MIGRATE"
337        if !self.consume(&Token::Policy)? && !self.consume_ident_ci("POLICY")? {
338            return Err(ParseError::expected(
339                vec!["POLICY"],
340                self.peek(),
341                self.position(),
342            ));
343        }
344        if !self.consume(&Token::Mode)? && !self.consume_ident_ci("MODE")? {
345            return Err(ParseError::expected(
346                vec!["MODE"],
347                self.peek(),
348                self.position(),
349            ));
350        }
351        if !self.consume(&Token::To)? && !self.consume_ident_ci("TO")? {
352            return Err(ParseError::expected(
353                vec!["TO"],
354                self.peek(),
355                self.position(),
356            ));
357        }
358        let target = self.parse_string()?;
359        let dry_run = if self.consume_ident_ci("DRY")? {
360            if !self.consume_ident_ci("RUN")? {
361                return Err(ParseError::expected(
362                    vec!["RUN"],
363                    self.peek(),
364                    self.position(),
365                ));
366            }
367            true
368        } else {
369            false
370        };
371        Ok(QueryExpr::MigratePolicyMode { target, dry_run })
372    }
373
374    /// Parse `LINT POLICY '<id>'` or `LINT POLICY JSON '<json>'`. Caller
375    /// has just observed the `LINT` ident; the leading token is still
376    /// queued. Issue #710.
377    pub fn parse_lint_policy(&mut self) -> Result<QueryExpr, ParseError> {
378        self.advance()?; // ident "LINT"
379        if !self.consume(&Token::Policy)? && !self.consume_ident_ci("POLICY")? {
380            return Err(ParseError::expected(
381                vec!["POLICY"],
382                self.peek(),
383                self.position(),
384            ));
385        }
386        // Disambiguate the two forms by the next token:
387        //   * `JSON '<...>'`     → lint the supplied JSON literal.
388        //   * `'<id>'`           → fetch by id from the AuthStore.
389        if self.consume(&Token::Json)? || self.consume_ident_ci("JSON")? {
390            let json = self.parse_string()?;
391            return Ok(QueryExpr::LintPolicy {
392                source: LintPolicySource::Json(json),
393            });
394        }
395        let id = self.parse_string()?;
396        Ok(QueryExpr::LintPolicy {
397            source: LintPolicySource::Id(id),
398        })
399    }
400
401    /// Parse `SHOW POLICIES [FOR USER <name> | FOR GROUP <name>]` or
402    /// `SHOW EFFECTIVE PERMISSIONS FOR <name> [ON <kind>:<name>]`.
403    /// Caller has just consumed `SHOW`.
404    pub fn parse_show_iam_after_show(&mut self) -> Result<Option<QueryExpr>, ParseError> {
405        // Disambiguate: SHOW POLICIES vs SHOW EFFECTIVE
406        if self.consume_ident_ci("POLICIES")? {
407            // Optional FOR USER / FOR GROUP
408            if self.consume(&Token::For)? || self.consume_ident_ci("FOR")? {
409                let principal = self.parse_iam_principal_kind()?;
410                return Ok(Some(QueryExpr::ShowPolicies {
411                    filter: Some(principal),
412                }));
413            }
414            return Ok(Some(QueryExpr::ShowPolicies { filter: None }));
415        }
416        if self.consume_ident_ci("EFFECTIVE")? {
417            if !self.consume_ident_ci("PERMISSIONS")? {
418                return Err(ParseError::expected(
419                    vec!["PERMISSIONS"],
420                    self.peek(),
421                    self.position(),
422                ));
423            }
424            if !self.consume(&Token::For)? && !self.consume_ident_ci("FOR")? {
425                return Err(ParseError::expected(
426                    vec!["FOR"],
427                    self.peek(),
428                    self.position(),
429                ));
430            }
431            let user = self.parse_iam_user_ref()?;
432            let resource = if self.consume(&Token::On)? || self.consume_ident_ci("ON")? {
433                Some(self.parse_iam_resource_ref()?)
434            } else {
435                None
436            };
437            return Ok(Some(QueryExpr::ShowEffectivePermissions { user, resource }));
438        }
439        Ok(None)
440    }
441
442    // ----- helpers used by the IAM policy parsers -----
443
444    pub(crate) fn parse_iam_principal_kind(&mut self) -> Result<PolicyPrincipalRef, ParseError> {
445        if self.consume_ident_ci("USER")? {
446            let user = self.parse_iam_user_ref()?;
447            Ok(PolicyPrincipalRef::User(user))
448        } else if self.consume(&Token::Group)? || self.consume_ident_ci("GROUP")? {
449            let g = self.expect_ident()?;
450            Ok(PolicyPrincipalRef::Group(g))
451        } else {
452            Err(ParseError::expected(
453                vec!["USER", "GROUP"],
454                self.peek(),
455                self.position(),
456            ))
457        }
458    }
459
460    fn parse_iam_user_ref(&mut self) -> Result<PolicyUserRef, ParseError> {
461        let (tenant, username) = self.parse_user_name()?;
462        Ok(PolicyUserRef { tenant, username })
463    }
464
465    fn parse_iam_resource_ref(&mut self) -> Result<PolicyResourceRef, ParseError> {
466        // Two accepted forms:
467        //   * `<kind>:<name>` as one string literal
468        //   * `<kind>:<dotted_name>` as `kind ':' part ('.' part)*`
469        if matches!(self.peek(), Token::String(_)) {
470            let raw = self.parse_string()?;
471            let (kind, name) = raw.split_once(':').ok_or_else(|| {
472                ParseError::new(
473                    // F-05: `raw` is caller-controlled string-literal bytes.
474                    // Render via `{:?}` so CR/LF/NUL/quotes are escaped
475                    // before the message reaches the downstream JSON /
476                    // audit / log / gRPC sinks.
477                    format!("resource must be `kind:name`, got {raw:?}"),
478                    self.position(),
479                )
480            })?;
481            return Ok(PolicyResourceRef {
482                kind: kind.to_string(),
483                name: name.to_string(),
484            });
485        }
486        // Normalise both halves to lowercase so the kernel's allowlist
487        // (`table`, `function`, …) lines up regardless of how the SQL
488        // tokens were cased / promoted by the lexer.
489        let kind = self.expect_ident_or_keyword()?.to_ascii_lowercase();
490        if !self.consume(&Token::Colon)? {
491            return Err(ParseError::expected(
492                vec![":"],
493                self.peek(),
494                self.position(),
495            ));
496        }
497        // Accept dotted resource names — `public.orders` arrives as
498        // `Ident("public")`, `Dot`, `Ident("orders")` from the lexer.
499        let mut name = self.expect_ident_or_keyword()?;
500        while self.consume(&Token::Dot)? {
501            let next = self.expect_ident_or_keyword()?;
502            name.push('.');
503            name.push_str(&next);
504        }
505        Ok(PolicyResourceRef { kind, name })
506    }
507
508    fn parse_iam_action_token(&mut self) -> Result<String, ParseError> {
509        if matches!(self.peek(), Token::String(_)) {
510            return self.parse_string();
511        }
512        // SELECT / INSERT / UPDATE / DELETE are real tokens; everything
513        // else is exposed as an `Ident` by the lexer.
514        match self.peek() {
515            Token::Select => {
516                self.advance()?;
517                Ok("select".into())
518            }
519            Token::Insert => {
520                self.advance()?;
521                Ok("insert".into())
522            }
523            Token::Update => {
524                self.advance()?;
525                Ok("update".into())
526            }
527            Token::Delete => {
528                self.advance()?;
529                Ok("delete".into())
530            }
531            // #1374 — DDL action verbs tokenize as their own keyword tokens,
532            // not Ident; accept them as action names. Literals (numbers, etc.)
533            // and structural tokens still error with "expected: action keyword".
534            Token::Drop => {
535                self.advance()?;
536                Ok("drop".into())
537            }
538            Token::Create => {
539                self.advance()?;
540                Ok("create".into())
541            }
542            Token::Alter => {
543                self.advance()?;
544                Ok("alter".into())
545            }
546            Token::Ident(_) => {
547                let raw = self.expect_ident()?;
548                Ok(raw.to_ascii_lowercase())
549            }
550            other => Err(ParseError::expected(
551                vec!["action keyword"],
552                other,
553                self.position(),
554            )),
555        }
556    }
557
558    // -----------------------------------------------------------------
559    // Helpers
560    // -----------------------------------------------------------------
561
562    /// Parse a comma-separated privilege list (`SELECT, INSERT, ...`)
563    /// or `ALL [PRIVILEGES]`. Returns `(actions, is_all, columns?)`.
564    /// Column-level lists are accepted at parse time but enforcement is
565    /// deferred — see `auth::privileges` module docstring.
566    fn parse_privilege_list(
567        &mut self,
568    ) -> Result<(Vec<String>, bool, Option<Vec<String>>), ParseError> {
569        // ALL [PRIVILEGES]
570        if self.consume(&Token::All)? || self.consume_ident_ci("ALL")? {
571            let _ = self.consume_ident_ci("PRIVILEGES")?;
572            let columns = self.parse_optional_column_list()?;
573            return Ok((vec!["ALL".to_string()], true, columns));
574        }
575
576        // Privilege list.
577        let mut actions = Vec::new();
578        loop {
579            actions.push(self.parse_privilege_keyword()?);
580            if !self.consume(&Token::Comma)? {
581                break;
582            }
583        }
584        let columns = self.parse_optional_column_list()?;
585        Ok((actions, false, columns))
586    }
587
588    /// Recognise SELECT / INSERT / UPDATE / DELETE / TRUNCATE /
589    /// REFERENCES / EXECUTE / USAGE. SELECT/INSERT/UPDATE/DELETE are
590    /// real tokens; the rest are idents.
591    fn parse_privilege_keyword(&mut self) -> Result<String, ParseError> {
592        match self.peek() {
593            Token::Select => {
594                self.advance()?;
595                Ok("SELECT".to_string())
596            }
597            Token::Insert => {
598                self.advance()?;
599                Ok("INSERT".to_string())
600            }
601            Token::Update => {
602                self.advance()?;
603                Ok("UPDATE".to_string())
604            }
605            Token::Delete => {
606                self.advance()?;
607                Ok("DELETE".to_string())
608            }
609            Token::Truncate => {
610                self.advance()?;
611                Ok("TRUNCATE".to_string())
612            }
613            Token::Ident(name)
614                if matches!(
615                    name.to_ascii_uppercase().as_str(),
616                    "REFERENCES" | "EXECUTE" | "USAGE"
617                ) =>
618            {
619                let upper = name.to_ascii_uppercase();
620                self.advance()?;
621                Ok(upper)
622            }
623            other => Err(ParseError::expected(
624                vec![
625                    "SELECT",
626                    "INSERT",
627                    "UPDATE",
628                    "DELETE",
629                    "TRUNCATE",
630                    "REFERENCES",
631                    "EXECUTE",
632                    "USAGE",
633                ],
634                other,
635                self.position(),
636            )),
637        }
638    }
639
640    /// Optional `( col1, col2, ... )` after a privilege list. Returns
641    /// `None` when the next token isn't `(`.
642    fn parse_optional_column_list(&mut self) -> Result<Option<Vec<String>>, ParseError> {
643        if !self.check(&Token::LParen) {
644            return Ok(None);
645        }
646        self.expect(Token::LParen)?;
647        let mut cols = Vec::new();
648        loop {
649            cols.push(self.expect_ident()?);
650            if !self.consume(&Token::Comma)? {
651                break;
652            }
653        }
654        self.expect(Token::RParen)?;
655        Ok(Some(cols))
656    }
657
658    /// Parse the optional `[ TABLE | SCHEMA | DATABASE | FUNCTION ]`
659    /// keyword between `ON` and the object list. Defaults to `TABLE`
660    /// when absent (matches PG).
661    fn parse_grant_object_kind(&mut self) -> Result<GrantObjectKind, ParseError> {
662        if self.consume(&Token::Table)? {
663            Ok(GrantObjectKind::Table)
664        } else if self.consume(&Token::Schema)? {
665            Ok(GrantObjectKind::Schema)
666        } else if self.consume_ident_ci("DATABASE")? {
667            Ok(GrantObjectKind::Database)
668        } else if self.consume_ident_ci("FUNCTION")? {
669            Ok(GrantObjectKind::Function)
670        } else {
671            // Default: TABLE
672            Ok(GrantObjectKind::Table)
673        }
674    }
675
676    /// Parse a comma-separated list of `[schema.]name` objects.
677    fn parse_grant_object_list(
678        &mut self,
679        kind: &GrantObjectKind,
680    ) -> Result<Vec<GrantObject>, ParseError> {
681        let mut out = Vec::new();
682        loop {
683            // DATABASE objects use the database name as the object —
684            // accept a single ident.
685            if matches!(kind, GrantObjectKind::Database) {
686                let name = self.expect_ident()?;
687                out.push(GrantObject { schema: None, name });
688            } else {
689                let first = self.expect_ident()?;
690                let (schema, name) = if self.consume(&Token::Dot)? {
691                    let second = self.expect_ident_or_keyword()?;
692                    (Some(first), second)
693                } else {
694                    (None, first)
695                };
696                out.push(GrantObject { schema, name });
697            }
698            if !self.consume(&Token::Comma)? {
699                break;
700            }
701        }
702        Ok(out)
703    }
704
705    /// Parse a comma-separated principal list. Each principal is one of:
706    ///   * `PUBLIC` — every authenticated user.
707    ///   * `GROUP groupname` — role-as-group (parsed, not enforced).
708    ///   * `username` or `tenant.username` — a specific user.
709    fn parse_grant_principal_list(&mut self) -> Result<Vec<GrantPrincipalRef>, ParseError> {
710        let mut out = Vec::new();
711        loop {
712            if self.consume_ident_ci("PUBLIC")? {
713                out.push(GrantPrincipalRef::Public);
714            } else if self.consume(&Token::Group)? || self.consume_ident_ci("GROUP")? {
715                let g = self.expect_ident()?;
716                out.push(GrantPrincipalRef::Group(g));
717            } else {
718                let (tenant, name) = self.parse_user_name()?;
719                out.push(GrantPrincipalRef::User { tenant, name });
720            }
721            if !self.consume(&Token::Comma)? {
722                break;
723            }
724        }
725        Ok(out)
726    }
727
728    /// Parse a `user` or `tenant.user` form. Returns `(tenant, name)`.
729    fn parse_user_name(&mut self) -> Result<(Option<String>, String), ParseError> {
730        let first = self.expect_ident()?;
731        if self.consume(&Token::Dot)? {
732            let name = self.expect_ident()?;
733            Ok((Some(first), name))
734        } else {
735            Ok((None, first))
736        }
737    }
738
739    /// Recognise the optional `WITH GRANT OPTION` suffix on a GRANT.
740    fn consume_grant_option_suffix(&mut self) -> Result<bool, ParseError> {
741        if self.consume(&Token::With)? {
742            if !self.consume_ident_ci("GRANT")? {
743                return Err(ParseError::expected(
744                    vec!["GRANT"],
745                    self.peek(),
746                    self.position(),
747                ));
748            }
749            if !self.consume_ident_ci("OPTION")? {
750                return Err(ParseError::expected(
751                    vec!["OPTION"],
752                    self.peek(),
753                    self.position(),
754                ));
755            }
756            Ok(true)
757        } else {
758            Ok(false)
759        }
760    }
761
762    /// Recognise the optional `GRANT OPTION FOR` prefix on a REVOKE.
763    fn consume_grant_option_for_prefix(&mut self) -> Result<bool, ParseError> {
764        // `GRANT` is an ident, not a keyword — we must peek the ident
765        // text without consuming until we know the full prefix matches.
766        let saved_pos = self.position();
767        if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("GRANT")) {
768            return Ok(false);
769        }
770        // Consume GRANT.
771        self.advance()?;
772        if !self.consume_ident_ci("OPTION")? {
773            // Not the prefix we expected — but `REVOKE GRANT ...`
774            // makes no other sense, so this is a parse error rather
775            // than a non-match.
776            return Err(ParseError::expected(vec!["OPTION"], self.peek(), saved_pos));
777        }
778        if !self.consume(&Token::For)? && !self.consume_ident_ci("FOR")? {
779            return Err(ParseError::expected(vec!["FOR"], self.peek(), saved_pos));
780        }
781        Ok(true)
782    }
783}
784
785#[cfg(test)]
786mod tests {
787    use super::*;
788
789    fn parser(input: &str) -> Parser<'_> {
790        Parser::new(input).unwrap_or_else(|err| panic!("failed to lex {input:?}: {err:?}"))
791    }
792
793    #[test]
794    fn parse_grant_statement_covers_columns_default_table_and_principals() {
795        let grant = parser(
796            "GRANT SELECT, UPDATE (id, email) ON public.users TO PUBLIC, GROUP analysts, tenant.alice WITH GRANT OPTION",
797        )
798        .parse_grant_statement()
799        .expect("grant");
800
801        assert_eq!(grant.actions, vec!["SELECT", "UPDATE"]);
802        assert_eq!(
803            grant.columns.as_deref(),
804            Some(&["id".to_string(), "email".to_string()][..])
805        );
806        assert_eq!(grant.object_kind, GrantObjectKind::Table);
807        assert_eq!(grant.objects.len(), 1);
808        assert_eq!(grant.objects[0].schema.as_deref(), Some("public"));
809        assert_eq!(grant.objects[0].name, "users");
810        assert!(matches!(grant.principals[0], GrantPrincipalRef::Public));
811        assert!(matches!(
812            &grant.principals[1],
813            GrantPrincipalRef::Group(group) if group == "analysts"
814        ));
815        assert!(matches!(
816            &grant.principals[2],
817            GrantPrincipalRef::User { tenant: Some(t), name } if t == "tenant" && name == "alice"
818        ));
819        assert!(grant.with_grant_option);
820        assert!(!grant.all);
821    }
822
823    #[test]
824    fn parse_revoke_statement_covers_grant_option_for_all_and_function_objects() {
825        let revoke = parser(
826            "REVOKE GRANT OPTION FOR ALL PRIVILEGES (id) ON FUNCTION public.recalc FROM GROUP analysts",
827        )
828        .parse_revoke_statement()
829        .expect("revoke");
830
831        assert!(revoke.grant_option_for);
832        assert!(revoke.all);
833        assert_eq!(revoke.actions, vec!["ALL"]);
834        assert_eq!(revoke.columns.as_deref(), Some(&["id".to_string()][..]));
835        assert_eq!(revoke.object_kind, GrantObjectKind::Function);
836        assert_eq!(revoke.objects[0].schema.as_deref(), Some("public"));
837        assert_eq!(revoke.objects[0].name, "recalc");
838        assert!(matches!(
839            &revoke.principals[0],
840            GrantPrincipalRef::Group(group) if group == "analysts"
841        ));
842
843        let revoke = parser("REVOKE USAGE ON SCHEMA analytics FROM bob")
844            .parse_revoke_statement()
845            .expect("revoke without grant option");
846        assert!(!revoke.grant_option_for);
847        assert_eq!(revoke.object_kind, GrantObjectKind::Schema);
848        assert_eq!(revoke.objects[0].name, "analytics");
849    }
850
851    #[test]
852    fn parse_grant_and_revoke_option_errors_are_specific() {
853        let err = parser("GRANT SELECT ON TABLE users TO alice WITH OPTION")
854            .parse_grant_statement()
855            .unwrap_err();
856        assert!(err.to_string().contains("expected: GRANT"), "{err}");
857
858        let err = parser("GRANT SELECT ON TABLE users TO alice WITH GRANT")
859            .parse_grant_statement()
860            .unwrap_err();
861        assert!(err.to_string().contains("expected: OPTION"), "{err}");
862
863        let err = parser("REVOKE GRANT SELECT ON TABLE users FROM alice")
864            .parse_revoke_statement()
865            .unwrap_err();
866        assert!(err.to_string().contains("expected: OPTION"), "{err}");
867
868        let err = parser("REVOKE GRANT OPTION SELECT ON TABLE users FROM alice")
869            .parse_revoke_statement()
870            .unwrap_err();
871        assert!(err.to_string().contains("expected: FOR"), "{err}");
872    }
873
874    #[test]
875    fn parse_alter_user_statement_covers_attribute_variants_and_errors() {
876        let mut p = parser(
877            "ALTER USER tenant.bob VALID UNTIL '2030-01-01' CONNECTION LIMIT 10 ENABLE \
878             SET search_path TO 'public,analytics' ADD GROUP analysts DROP GROUP temp PASSWORD 'pw'",
879        );
880        p.expect(Token::Alter).expect("ALTER");
881        let stmt = p.parse_alter_user_statement().expect("alter user");
882
883        assert_eq!(stmt.tenant.as_deref(), Some("tenant"));
884        assert_eq!(stmt.username, "bob");
885        assert!(matches!(
886            &stmt.attributes[0],
887            AlterUserAttribute::ValidUntil(value) if value == "2030-01-01"
888        ));
889        assert!(matches!(
890            stmt.attributes[1],
891            AlterUserAttribute::ConnectionLimit(10)
892        ));
893        assert!(matches!(stmt.attributes[2], AlterUserAttribute::Enable));
894        assert!(matches!(
895            &stmt.attributes[3],
896            AlterUserAttribute::SetSearchPath(value) if value == "public,analytics"
897        ));
898        assert!(matches!(
899            &stmt.attributes[4],
900            AlterUserAttribute::AddGroup(group) if group == "analysts"
901        ));
902        assert!(matches!(
903            &stmt.attributes[5],
904            AlterUserAttribute::DropGroup(group) if group == "temp"
905        ));
906        assert!(matches!(
907            &stmt.attributes[6],
908            AlterUserAttribute::Password(password) if password == "pw"
909        ));
910
911        let mut p = parser("ALTER USER alice");
912        p.expect(Token::Alter).expect("ALTER");
913        let err = p.parse_alter_user_statement().unwrap_err();
914        assert!(err.to_string().contains("expected:"), "{err}");
915
916        let mut p = parser("ALTER USER alice ADD ROLE analysts");
917        p.expect(Token::Alter).expect("ALTER");
918        let err = p.parse_alter_user_statement().unwrap_err();
919        assert!(err.to_string().contains("expected: GROUP"), "{err}");
920    }
921
922    #[test]
923    fn parse_create_user_statement_covers_role_and_password_errors() {
924        let mut p = parser("CREATE USER tenant.alice WITH PASSWORD 'pw' ROLE admin");
925        p.expect(Token::Create).expect("CREATE");
926        let stmt = p.parse_create_user_statement().expect("create user");
927        assert_eq!(stmt.tenant.as_deref(), Some("tenant"));
928        assert_eq!(stmt.username, "alice");
929        assert_eq!(stmt.password, "pw");
930        assert_eq!(stmt.role, "admin");
931
932        let mut p = parser("CREATE USER bob PASSWORD 'pw'");
933        p.expect(Token::Create).expect("CREATE");
934        let stmt = p.parse_create_user_statement().expect("create user");
935        assert_eq!(stmt.tenant, None);
936        assert_eq!(stmt.username, "bob");
937        assert_eq!(stmt.role, "read");
938
939        let mut p = parser("CREATE USER alice ROLE write");
940        p.expect(Token::Create).expect("CREATE");
941        let err = p.parse_create_user_statement().unwrap_err();
942        assert!(err.to_string().contains("expected: PASSWORD"), "{err}");
943    }
944
945    #[test]
946    fn parse_iam_policy_helpers_cover_policy_sources_and_principals() {
947        assert!(matches!(
948            parser("'readonly' AS '{\"Statement\":[]}'")
949                .parse_create_iam_policy_after_keywords()
950                .expect("create iam policy"),
951            QueryExpr::CreateIamPolicy { ref id, ref json }
952                if id == "readonly" && json == "{\"Statement\":[]}"
953        ));
954        assert!(matches!(
955            parser("'readonly'")
956                .parse_drop_iam_policy_after_keywords()
957                .expect("drop iam policy"),
958            QueryExpr::DropIamPolicy { ref id } if id == "readonly"
959        ));
960        assert!(matches!(
961            parser("LINT POLICY JSON '{\"Statement\":[]}'")
962                .parse_lint_policy()
963                .expect("lint json"),
964            QueryExpr::LintPolicy {
965                source: LintPolicySource::Json(ref json),
966            } if json == "{\"Statement\":[]}"
967        ));
968        assert!(matches!(
969            parser("LINT POLICY 'readonly'")
970                .parse_lint_policy()
971                .expect("lint id"),
972            QueryExpr::LintPolicy {
973                source: LintPolicySource::Id(ref id),
974            } if id == "readonly"
975        ));
976        assert!(matches!(
977            parser("MIGRATE POLICY MODE TO 'policy_only' DRY RUN")
978                .parse_migrate_policy_mode()
979                .expect("migrate policy mode"),
980            QueryExpr::MigratePolicyMode { ref target, dry_run }
981                if target == "policy_only" && dry_run
982        ));
983
984        assert!(matches!(
985            parser("ATTACH POLICY 'readonly' TO USER tenant.alice")
986                .parse_attach_policy()
987                .expect("attach policy"),
988            QueryExpr::AttachPolicy {
989                ref policy_id,
990                principal: PolicyPrincipalRef::User(ref user),
991            } if policy_id == "readonly"
992                && user.tenant.as_deref() == Some("tenant")
993                && user.username == "alice"
994        ));
995        assert!(matches!(
996            parser("DETACH POLICY 'readonly' FROM GROUP analysts")
997                .parse_detach_policy()
998                .expect("detach policy"),
999            QueryExpr::DetachPolicy {
1000                ref policy_id,
1001                principal: PolicyPrincipalRef::Group(ref group),
1002            } if policy_id == "readonly" && group == "analysts"
1003        ));
1004    }
1005
1006    #[test]
1007    fn parse_show_and_simulate_helpers_cover_resources_and_action_errors() {
1008        let mut p = parser("SHOW POLICIES FOR USER tenant.alice");
1009        p.advance().expect("SHOW");
1010        assert!(matches!(
1011            p.parse_show_iam_after_show()
1012                .expect("show policies")
1013                .expect("iam show"),
1014            QueryExpr::ShowPolicies {
1015                filter: Some(PolicyPrincipalRef::User(ref user)),
1016            } if user.tenant.as_deref() == Some("tenant") && user.username == "alice"
1017        ));
1018
1019        let mut p = parser("SHOW EFFECTIVE PERMISSIONS FOR alice ON TABLE:public.orders");
1020        p.advance().expect("SHOW");
1021        assert!(matches!(
1022            p.parse_show_iam_after_show()
1023                .expect("show effective")
1024                .expect("iam show"),
1025            QueryExpr::ShowEffectivePermissions {
1026                ref user,
1027                resource: Some(ref resource),
1028            } if user.tenant.is_none()
1029                && user.username == "alice"
1030                && resource.kind == "table"
1031                && resource.name == "public.orders"
1032        ));
1033
1034        assert!(matches!(
1035            parser("SIMULATE alice ACTION DELETE ON 'table:public.orders'")
1036                .parse_simulate_policy()
1037                .expect("simulate"),
1038            QueryExpr::SimulatePolicy {
1039                ref user,
1040                ref action,
1041                ref resource,
1042            } if user.username == "alice"
1043                && action == "delete"
1044                && resource.kind == "table"
1045                && resource.name == "public.orders"
1046        ));
1047
1048        let err = parser("SIMULATE alice ACTION 42 ON table:public.orders")
1049            .parse_simulate_policy()
1050            .unwrap_err();
1051        assert!(
1052            err.to_string().contains("expected: action keyword"),
1053            "{err}"
1054        );
1055
1056        let err = parser("SIMULATE alice ACTION SELECT ON 'missing-colon'")
1057            .parse_simulate_policy()
1058            .unwrap_err();
1059        assert!(err.to_string().contains("kind:name"), "{err}");
1060    }
1061}