Skip to main content

reddb_server/storage/query/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::storage::query::ast::{
35    AlterUserAttribute, AlterUserStmt, GrantObject, GrantObjectKind, GrantPrincipalRef, GrantStmt,
36    LintPolicySource, PolicyPrincipalRef, PolicyResourceRef, PolicyUserRef, QueryExpr, RevokeStmt,
37};
38use crate::storage::query::lexer::Token;
39use crate::storage::query::parser::{ParseError, Parser};
40
41impl<'a> Parser<'a> {
42    /// Parse a `GRANT` statement. Caller must have already verified the
43    /// current token is the `GRANT` ident (it is not a lexer keyword —
44    /// the lexer maps it to `Token::Ident("GRANT")`).
45    pub fn parse_grant_statement(&mut self) -> Result<GrantStmt, ParseError> {
46        // Eat `GRANT`.
47        self.advance()?;
48
49        let (actions, all, columns) = self.parse_privilege_list()?;
50        self.expect(Token::On)?;
51        let object_kind = self.parse_grant_object_kind()?;
52        let objects = self.parse_grant_object_list(&object_kind)?;
53        self.expect(Token::To)?;
54        let principals = self.parse_grant_principal_list()?;
55
56        let with_grant_option = self.consume_grant_option_suffix()?;
57
58        Ok(GrantStmt {
59            actions,
60            columns,
61            object_kind,
62            objects,
63            principals,
64            with_grant_option,
65            all,
66        })
67    }
68
69    /// Parse a `REVOKE` statement. Caller must have already verified the
70    /// current token is the `REVOKE` ident.
71    pub fn parse_revoke_statement(&mut self) -> Result<RevokeStmt, ParseError> {
72        // Eat `REVOKE`.
73        self.advance()?;
74
75        // Optional `GRANT OPTION FOR`.
76        let grant_option_for = self.consume_grant_option_for_prefix()?;
77
78        let (actions, all, columns) = self.parse_privilege_list()?;
79        self.expect(Token::On)?;
80        let object_kind = self.parse_grant_object_kind()?;
81        let objects = self.parse_grant_object_list(&object_kind)?;
82        self.expect(Token::From)?;
83        let principals = self.parse_grant_principal_list()?;
84
85        Ok(RevokeStmt {
86            actions,
87            columns,
88            object_kind,
89            objects,
90            principals,
91            grant_option_for,
92            all,
93        })
94    }
95
96    /// Parse `ALTER USER name <attrs>`. Caller has just consumed
97    /// `Token::Alter`.
98    pub fn parse_alter_user_statement(&mut self) -> Result<AlterUserStmt, ParseError> {
99        // `ALTER` was already consumed by the dispatcher; expect USER ident.
100        if !self.consume_ident_ci("USER")? {
101            return Err(ParseError::expected(
102                vec!["USER"],
103                self.peek(),
104                self.position(),
105            ));
106        }
107        let (tenant, username) = self.parse_user_name()?;
108
109        let mut attributes = Vec::new();
110        loop {
111            if self.consume_ident_ci("VALID")? {
112                if !self.consume_ident_ci("UNTIL")? {
113                    return Err(ParseError::expected(
114                        vec!["UNTIL"],
115                        self.peek(),
116                        self.position(),
117                    ));
118                }
119                let ts = self.parse_string()?;
120                attributes.push(AlterUserAttribute::ValidUntil(ts));
121            } else if self.consume_ident_ci("CONNECTION")? {
122                if !self.consume(&Token::Limit)? && !self.consume_ident_ci("LIMIT")? {
123                    return Err(ParseError::expected(
124                        vec!["LIMIT"],
125                        self.peek(),
126                        self.position(),
127                    ));
128                }
129                let n = self.parse_integer()?;
130                attributes.push(AlterUserAttribute::ConnectionLimit(n));
131            } else if self.consume(&Token::Enable)? {
132                attributes.push(AlterUserAttribute::Enable);
133            } else if self.consume(&Token::Disable)? {
134                attributes.push(AlterUserAttribute::Disable);
135            } else if self.consume(&Token::Set)? {
136                // SET search_path = 'csv'  |  SET search_path TO 'csv'
137                if !self.consume_ident_ci("SEARCH_PATH")? {
138                    return Err(ParseError::expected(
139                        vec!["search_path"],
140                        self.peek(),
141                        self.position(),
142                    ));
143                }
144                if !self.consume(&Token::Eq)? && !self.consume(&Token::To)? {
145                    return Err(ParseError::expected(
146                        vec!["="],
147                        self.peek(),
148                        self.position(),
149                    ));
150                }
151                let value = self.parse_string()?;
152                attributes.push(AlterUserAttribute::SetSearchPath(value));
153            } else if self.consume(&Token::Add)? || self.consume_ident_ci("ADD")? {
154                if !self.consume(&Token::Group)? && !self.consume_ident_ci("GROUP")? {
155                    return Err(ParseError::expected(
156                        vec!["GROUP"],
157                        self.peek(),
158                        self.position(),
159                    ));
160                }
161                let group = self.expect_ident()?;
162                attributes.push(AlterUserAttribute::AddGroup(group));
163            } else if self.consume(&Token::Drop)? || self.consume_ident_ci("DROP")? {
164                if !self.consume(&Token::Group)? && !self.consume_ident_ci("GROUP")? {
165                    return Err(ParseError::expected(
166                        vec!["GROUP"],
167                        self.peek(),
168                        self.position(),
169                    ));
170                }
171                let group = self.expect_ident()?;
172                attributes.push(AlterUserAttribute::DropGroup(group));
173            } else if self.consume_ident_ci("PASSWORD")? {
174                let pw = self.parse_string()?;
175                attributes.push(AlterUserAttribute::Password(pw));
176            } else {
177                break;
178            }
179        }
180
181        if attributes.is_empty() {
182            return Err(ParseError::expected(
183                vec![
184                    "VALID",
185                    "CONNECTION",
186                    "ENABLE",
187                    "DISABLE",
188                    "SET",
189                    "ADD",
190                    "DROP",
191                    "PASSWORD",
192                ],
193                self.peek(),
194                self.position(),
195            ));
196        }
197
198        Ok(AlterUserStmt {
199            tenant,
200            username,
201            attributes,
202        })
203    }
204
205    // -----------------------------------------------------------------
206    // IAM policy DDL — CREATE / DROP / ATTACH / DETACH / SHOW / SIMULATE
207    // -----------------------------------------------------------------
208
209    /// Parse `CREATE POLICY '<id>' AS '<json>'`. Caller has consumed
210    /// `CREATE POLICY` already and confirmed the next token is a
211    /// string literal (the IAM-flavoured form). Returns the
212    /// `QueryExpr::CreateIamPolicy` variant.
213    pub fn parse_create_iam_policy_after_keywords(&mut self) -> Result<QueryExpr, ParseError> {
214        let id = self.parse_string()?;
215        if !self.consume(&Token::As)? && !self.consume_ident_ci("AS")? {
216            return Err(ParseError::expected(
217                vec!["AS"],
218                self.peek(),
219                self.position(),
220            ));
221        }
222        let json = self.parse_string()?;
223        Ok(QueryExpr::CreateIamPolicy { id, json })
224    }
225
226    /// Parse `DROP POLICY '<id>'`. Caller has consumed `DROP POLICY`
227    /// and verified the next token is a string literal.
228    pub fn parse_drop_iam_policy_after_keywords(&mut self) -> Result<QueryExpr, ParseError> {
229        let id = self.parse_string()?;
230        Ok(QueryExpr::DropIamPolicy { id })
231    }
232
233    /// Parse `ATTACH POLICY '<id>' TO { USER | GROUP } <name>`.
234    /// Caller has consumed nothing — leading `ATTACH` is still on
235    /// the token stream.
236    pub fn parse_attach_policy(&mut self) -> Result<QueryExpr, ParseError> {
237        self.advance()?; // ATTACH
238        if !self.consume(&Token::Policy)? && !self.consume_ident_ci("POLICY")? {
239            return Err(ParseError::expected(
240                vec!["POLICY"],
241                self.peek(),
242                self.position(),
243            ));
244        }
245        let policy_id = self.parse_string()?;
246        self.expect(Token::To)?;
247        let principal = self.parse_iam_principal_kind()?;
248        Ok(QueryExpr::AttachPolicy {
249            policy_id,
250            principal,
251        })
252    }
253
254    /// Parse `DETACH POLICY '<id>' FROM { USER | GROUP } <name>`.
255    pub fn parse_detach_policy(&mut self) -> Result<QueryExpr, ParseError> {
256        self.advance()?; // DETACH
257        if !self.consume(&Token::Policy)? && !self.consume_ident_ci("POLICY")? {
258            return Err(ParseError::expected(
259                vec!["POLICY"],
260                self.peek(),
261                self.position(),
262            ));
263        }
264        let policy_id = self.parse_string()?;
265        self.expect(Token::From)?;
266        let principal = self.parse_iam_principal_kind()?;
267        Ok(QueryExpr::DetachPolicy {
268            policy_id,
269            principal,
270        })
271    }
272
273    /// Parse `SIMULATE <name> ACTION <verb> ON <kind>:<name>`.
274    pub fn parse_simulate_policy(&mut self) -> Result<QueryExpr, ParseError> {
275        self.advance()?; // ident "SIMULATE"
276        let user = self.parse_iam_user_ref()?;
277        if !self.consume_ident_ci("ACTION")? {
278            return Err(ParseError::expected(
279                vec!["ACTION"],
280                self.peek(),
281                self.position(),
282            ));
283        }
284        let action = self.parse_iam_action_token()?;
285        self.expect(Token::On)?;
286        let resource = self.parse_iam_resource_ref()?;
287        Ok(QueryExpr::SimulatePolicy {
288            user,
289            action,
290            resource,
291        })
292    }
293
294    /// Parse `MIGRATE POLICY MODE TO '<mode>' [DRY RUN]`. Caller has
295    /// just observed the leading `MIGRATE` ident; the token is still
296    /// queued. Issue #714.
297    pub fn parse_migrate_policy_mode(&mut self) -> Result<QueryExpr, ParseError> {
298        self.advance()?; // ident "MIGRATE"
299        if !self.consume(&Token::Policy)? && !self.consume_ident_ci("POLICY")? {
300            return Err(ParseError::expected(
301                vec!["POLICY"],
302                self.peek(),
303                self.position(),
304            ));
305        }
306        if !self.consume(&Token::Mode)? && !self.consume_ident_ci("MODE")? {
307            return Err(ParseError::expected(
308                vec!["MODE"],
309                self.peek(),
310                self.position(),
311            ));
312        }
313        if !self.consume(&Token::To)? && !self.consume_ident_ci("TO")? {
314            return Err(ParseError::expected(
315                vec!["TO"],
316                self.peek(),
317                self.position(),
318            ));
319        }
320        let target = self.parse_string()?;
321        let dry_run = if self.consume_ident_ci("DRY")? {
322            if !self.consume_ident_ci("RUN")? {
323                return Err(ParseError::expected(
324                    vec!["RUN"],
325                    self.peek(),
326                    self.position(),
327                ));
328            }
329            true
330        } else {
331            false
332        };
333        Ok(QueryExpr::MigratePolicyMode { target, dry_run })
334    }
335
336    /// Parse `LINT POLICY '<id>'` or `LINT POLICY JSON '<json>'`. Caller
337    /// has just observed the `LINT` ident; the leading token is still
338    /// queued. Issue #710.
339    pub fn parse_lint_policy(&mut self) -> Result<QueryExpr, ParseError> {
340        self.advance()?; // ident "LINT"
341        if !self.consume(&Token::Policy)? && !self.consume_ident_ci("POLICY")? {
342            return Err(ParseError::expected(
343                vec!["POLICY"],
344                self.peek(),
345                self.position(),
346            ));
347        }
348        // Disambiguate the two forms by the next token:
349        //   * `JSON '<...>'`     → lint the supplied JSON literal.
350        //   * `'<id>'`           → fetch by id from the AuthStore.
351        if self.consume(&Token::Json)? || self.consume_ident_ci("JSON")? {
352            let json = self.parse_string()?;
353            return Ok(QueryExpr::LintPolicy {
354                source: LintPolicySource::Json(json),
355            });
356        }
357        let id = self.parse_string()?;
358        Ok(QueryExpr::LintPolicy {
359            source: LintPolicySource::Id(id),
360        })
361    }
362
363    /// Parse `SHOW POLICIES [FOR USER <name> | FOR GROUP <name>]` or
364    /// `SHOW EFFECTIVE PERMISSIONS FOR <name> [ON <kind>:<name>]`.
365    /// Caller has just consumed `SHOW`.
366    pub fn parse_show_iam_after_show(&mut self) -> Result<Option<QueryExpr>, ParseError> {
367        // Disambiguate: SHOW POLICIES vs SHOW EFFECTIVE
368        if self.consume_ident_ci("POLICIES")? {
369            // Optional FOR USER / FOR GROUP
370            if self.consume(&Token::For)? || self.consume_ident_ci("FOR")? {
371                let principal = self.parse_iam_principal_kind()?;
372                return Ok(Some(QueryExpr::ShowPolicies {
373                    filter: Some(principal),
374                }));
375            }
376            return Ok(Some(QueryExpr::ShowPolicies { filter: None }));
377        }
378        if self.consume_ident_ci("EFFECTIVE")? {
379            if !self.consume_ident_ci("PERMISSIONS")? {
380                return Err(ParseError::expected(
381                    vec!["PERMISSIONS"],
382                    self.peek(),
383                    self.position(),
384                ));
385            }
386            if !self.consume(&Token::For)? && !self.consume_ident_ci("FOR")? {
387                return Err(ParseError::expected(
388                    vec!["FOR"],
389                    self.peek(),
390                    self.position(),
391                ));
392            }
393            let user = self.parse_iam_user_ref()?;
394            let resource = if self.consume(&Token::On)? || self.consume_ident_ci("ON")? {
395                Some(self.parse_iam_resource_ref()?)
396            } else {
397                None
398            };
399            return Ok(Some(QueryExpr::ShowEffectivePermissions { user, resource }));
400        }
401        Ok(None)
402    }
403
404    // ----- helpers used by the IAM policy parsers -----
405
406    pub(crate) fn parse_iam_principal_kind(&mut self) -> Result<PolicyPrincipalRef, ParseError> {
407        if self.consume_ident_ci("USER")? {
408            let user = self.parse_iam_user_ref()?;
409            Ok(PolicyPrincipalRef::User(user))
410        } else if self.consume(&Token::Group)? || self.consume_ident_ci("GROUP")? {
411            let g = self.expect_ident()?;
412            Ok(PolicyPrincipalRef::Group(g))
413        } else {
414            Err(ParseError::expected(
415                vec!["USER", "GROUP"],
416                self.peek(),
417                self.position(),
418            ))
419        }
420    }
421
422    fn parse_iam_user_ref(&mut self) -> Result<PolicyUserRef, ParseError> {
423        let (tenant, username) = self.parse_user_name()?;
424        Ok(PolicyUserRef { tenant, username })
425    }
426
427    fn parse_iam_resource_ref(&mut self) -> Result<PolicyResourceRef, ParseError> {
428        // Two accepted forms:
429        //   * `<kind>:<name>` as one string literal
430        //   * `<kind>:<dotted_name>` as `kind ':' part ('.' part)*`
431        if matches!(self.peek(), Token::String(_)) {
432            let raw = self.parse_string()?;
433            let (kind, name) = raw.split_once(':').ok_or_else(|| {
434                ParseError::new(
435                    // F-05: `raw` is caller-controlled string-literal bytes.
436                    // Render via `{:?}` so CR/LF/NUL/quotes are escaped
437                    // before the message reaches the downstream JSON /
438                    // audit / log / gRPC sinks.
439                    format!("resource must be `kind:name`, got {raw:?}"),
440                    self.position(),
441                )
442            })?;
443            return Ok(PolicyResourceRef {
444                kind: kind.to_string(),
445                name: name.to_string(),
446            });
447        }
448        // Normalise both halves to lowercase so the kernel's allowlist
449        // (`table`, `function`, …) lines up regardless of how the SQL
450        // tokens were cased / promoted by the lexer.
451        let kind = self.expect_ident_or_keyword()?.to_ascii_lowercase();
452        if !self.consume(&Token::Colon)? {
453            return Err(ParseError::expected(
454                vec![":"],
455                self.peek(),
456                self.position(),
457            ));
458        }
459        // Accept dotted resource names — `public.orders` arrives as
460        // `Ident("public")`, `Dot`, `Ident("orders")` from the lexer.
461        let mut name = self.expect_ident_or_keyword()?;
462        while self.consume(&Token::Dot)? {
463            let next = self.expect_ident_or_keyword()?;
464            name.push('.');
465            name.push_str(&next);
466        }
467        Ok(PolicyResourceRef { kind, name })
468    }
469
470    fn parse_iam_action_token(&mut self) -> Result<String, ParseError> {
471        if matches!(self.peek(), Token::String(_)) {
472            return self.parse_string();
473        }
474        // SELECT / INSERT / UPDATE / DELETE are real tokens; everything
475        // else is exposed as an `Ident` by the lexer.
476        match self.peek() {
477            Token::Select => {
478                self.advance()?;
479                Ok("select".into())
480            }
481            Token::Insert => {
482                self.advance()?;
483                Ok("insert".into())
484            }
485            Token::Update => {
486                self.advance()?;
487                Ok("update".into())
488            }
489            Token::Delete => {
490                self.advance()?;
491                Ok("delete".into())
492            }
493            Token::Ident(_) => {
494                let raw = self.expect_ident()?;
495                Ok(raw.to_ascii_lowercase())
496            }
497            other => Err(ParseError::expected(
498                vec!["action keyword"],
499                other,
500                self.position(),
501            )),
502        }
503    }
504
505    // -----------------------------------------------------------------
506    // Helpers
507    // -----------------------------------------------------------------
508
509    /// Parse a comma-separated privilege list (`SELECT, INSERT, ...`)
510    /// or `ALL [PRIVILEGES]`. Returns `(actions, is_all, columns?)`.
511    /// Column-level lists are accepted at parse time but enforcement is
512    /// deferred — see `auth::privileges` module docstring.
513    fn parse_privilege_list(
514        &mut self,
515    ) -> Result<(Vec<String>, bool, Option<Vec<String>>), ParseError> {
516        // ALL [PRIVILEGES]
517        if self.consume(&Token::All)? || self.consume_ident_ci("ALL")? {
518            let _ = self.consume_ident_ci("PRIVILEGES")?;
519            let columns = self.parse_optional_column_list()?;
520            return Ok((vec!["ALL".to_string()], true, columns));
521        }
522
523        // Privilege list.
524        let mut actions = Vec::new();
525        loop {
526            actions.push(self.parse_privilege_keyword()?);
527            if !self.consume(&Token::Comma)? {
528                break;
529            }
530        }
531        let columns = self.parse_optional_column_list()?;
532        Ok((actions, false, columns))
533    }
534
535    /// Recognise SELECT / INSERT / UPDATE / DELETE / TRUNCATE /
536    /// REFERENCES / EXECUTE / USAGE. SELECT/INSERT/UPDATE/DELETE are
537    /// real tokens; the rest are idents.
538    fn parse_privilege_keyword(&mut self) -> Result<String, ParseError> {
539        match self.peek() {
540            Token::Select => {
541                self.advance()?;
542                Ok("SELECT".to_string())
543            }
544            Token::Insert => {
545                self.advance()?;
546                Ok("INSERT".to_string())
547            }
548            Token::Update => {
549                self.advance()?;
550                Ok("UPDATE".to_string())
551            }
552            Token::Delete => {
553                self.advance()?;
554                Ok("DELETE".to_string())
555            }
556            Token::Truncate => {
557                self.advance()?;
558                Ok("TRUNCATE".to_string())
559            }
560            Token::Ident(name)
561                if matches!(
562                    name.to_ascii_uppercase().as_str(),
563                    "REFERENCES" | "EXECUTE" | "USAGE"
564                ) =>
565            {
566                let upper = name.to_ascii_uppercase();
567                self.advance()?;
568                Ok(upper)
569            }
570            other => Err(ParseError::expected(
571                vec![
572                    "SELECT",
573                    "INSERT",
574                    "UPDATE",
575                    "DELETE",
576                    "TRUNCATE",
577                    "REFERENCES",
578                    "EXECUTE",
579                    "USAGE",
580                ],
581                other,
582                self.position(),
583            )),
584        }
585    }
586
587    /// Optional `( col1, col2, ... )` after a privilege list. Returns
588    /// `None` when the next token isn't `(`.
589    fn parse_optional_column_list(&mut self) -> Result<Option<Vec<String>>, ParseError> {
590        if !self.check(&Token::LParen) {
591            return Ok(None);
592        }
593        self.expect(Token::LParen)?;
594        let mut cols = Vec::new();
595        loop {
596            cols.push(self.expect_ident()?);
597            if !self.consume(&Token::Comma)? {
598                break;
599            }
600        }
601        self.expect(Token::RParen)?;
602        Ok(Some(cols))
603    }
604
605    /// Parse the optional `[ TABLE | SCHEMA | DATABASE | FUNCTION ]`
606    /// keyword between `ON` and the object list. Defaults to `TABLE`
607    /// when absent (matches PG).
608    fn parse_grant_object_kind(&mut self) -> Result<GrantObjectKind, ParseError> {
609        if self.consume(&Token::Table)? {
610            Ok(GrantObjectKind::Table)
611        } else if self.consume(&Token::Schema)? {
612            Ok(GrantObjectKind::Schema)
613        } else if self.consume_ident_ci("DATABASE")? {
614            Ok(GrantObjectKind::Database)
615        } else if self.consume_ident_ci("FUNCTION")? {
616            Ok(GrantObjectKind::Function)
617        } else {
618            // Default: TABLE
619            Ok(GrantObjectKind::Table)
620        }
621    }
622
623    /// Parse a comma-separated list of `[schema.]name` objects.
624    fn parse_grant_object_list(
625        &mut self,
626        kind: &GrantObjectKind,
627    ) -> Result<Vec<GrantObject>, ParseError> {
628        let mut out = Vec::new();
629        loop {
630            // DATABASE objects use the database name as the object —
631            // accept a single ident.
632            if matches!(kind, GrantObjectKind::Database) {
633                let name = self.expect_ident()?;
634                out.push(GrantObject { schema: None, name });
635            } else {
636                let first = self.expect_ident()?;
637                let (schema, name) = if self.consume(&Token::Dot)? {
638                    let second = self.expect_ident_or_keyword()?;
639                    (Some(first), second)
640                } else {
641                    (None, first)
642                };
643                out.push(GrantObject { schema, name });
644            }
645            if !self.consume(&Token::Comma)? {
646                break;
647            }
648        }
649        Ok(out)
650    }
651
652    /// Parse a comma-separated principal list. Each principal is one of:
653    ///   * `PUBLIC` — every authenticated user.
654    ///   * `GROUP groupname` — role-as-group (parsed, not enforced).
655    ///   * `username` or `tenant.username` — a specific user.
656    fn parse_grant_principal_list(&mut self) -> Result<Vec<GrantPrincipalRef>, ParseError> {
657        let mut out = Vec::new();
658        loop {
659            if self.consume_ident_ci("PUBLIC")? {
660                out.push(GrantPrincipalRef::Public);
661            } else if self.consume(&Token::Group)? || self.consume_ident_ci("GROUP")? {
662                let g = self.expect_ident()?;
663                out.push(GrantPrincipalRef::Group(g));
664            } else {
665                let (tenant, name) = self.parse_user_name()?;
666                out.push(GrantPrincipalRef::User { tenant, name });
667            }
668            if !self.consume(&Token::Comma)? {
669                break;
670            }
671        }
672        Ok(out)
673    }
674
675    /// Parse a `user` or `tenant.user` form. Returns `(tenant, name)`.
676    fn parse_user_name(&mut self) -> Result<(Option<String>, String), ParseError> {
677        let first = self.expect_ident()?;
678        if self.consume(&Token::Dot)? {
679            let name = self.expect_ident()?;
680            Ok((Some(first), name))
681        } else {
682            Ok((None, first))
683        }
684    }
685
686    /// Recognise the optional `WITH GRANT OPTION` suffix on a GRANT.
687    fn consume_grant_option_suffix(&mut self) -> Result<bool, ParseError> {
688        if self.consume(&Token::With)? {
689            if !self.consume_ident_ci("GRANT")? {
690                return Err(ParseError::expected(
691                    vec!["GRANT"],
692                    self.peek(),
693                    self.position(),
694                ));
695            }
696            if !self.consume_ident_ci("OPTION")? {
697                return Err(ParseError::expected(
698                    vec!["OPTION"],
699                    self.peek(),
700                    self.position(),
701                ));
702            }
703            Ok(true)
704        } else {
705            Ok(false)
706        }
707    }
708
709    /// Recognise the optional `GRANT OPTION FOR` prefix on a REVOKE.
710    fn consume_grant_option_for_prefix(&mut self) -> Result<bool, ParseError> {
711        // `GRANT` is an ident, not a keyword — we must peek the ident
712        // text without consuming until we know the full prefix matches.
713        let saved_pos = self.position();
714        if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("GRANT")) {
715            return Ok(false);
716        }
717        // Consume GRANT.
718        self.advance()?;
719        if !self.consume_ident_ci("OPTION")? {
720            // Not the prefix we expected — but `REVOKE GRANT ...`
721            // makes no other sense, so this is a parse error rather
722            // than a non-match.
723            return Err(ParseError::expected(vec!["OPTION"], self.peek(), saved_pos));
724        }
725        if !self.consume(&Token::For)? && !self.consume_ident_ci("FOR")? {
726            return Err(ParseError::expected(vec!["FOR"], self.peek(), saved_pos));
727        }
728        Ok(true)
729    }
730}