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            Token::Ident(_) => {
532                let raw = self.expect_ident()?;
533                Ok(raw.to_ascii_lowercase())
534            }
535            other => Err(ParseError::expected(
536                vec!["action keyword"],
537                other,
538                self.position(),
539            )),
540        }
541    }
542
543    // -----------------------------------------------------------------
544    // Helpers
545    // -----------------------------------------------------------------
546
547    /// Parse a comma-separated privilege list (`SELECT, INSERT, ...`)
548    /// or `ALL [PRIVILEGES]`. Returns `(actions, is_all, columns?)`.
549    /// Column-level lists are accepted at parse time but enforcement is
550    /// deferred — see `auth::privileges` module docstring.
551    fn parse_privilege_list(
552        &mut self,
553    ) -> Result<(Vec<String>, bool, Option<Vec<String>>), ParseError> {
554        // ALL [PRIVILEGES]
555        if self.consume(&Token::All)? || self.consume_ident_ci("ALL")? {
556            let _ = self.consume_ident_ci("PRIVILEGES")?;
557            let columns = self.parse_optional_column_list()?;
558            return Ok((vec!["ALL".to_string()], true, columns));
559        }
560
561        // Privilege list.
562        let mut actions = Vec::new();
563        loop {
564            actions.push(self.parse_privilege_keyword()?);
565            if !self.consume(&Token::Comma)? {
566                break;
567            }
568        }
569        let columns = self.parse_optional_column_list()?;
570        Ok((actions, false, columns))
571    }
572
573    /// Recognise SELECT / INSERT / UPDATE / DELETE / TRUNCATE /
574    /// REFERENCES / EXECUTE / USAGE. SELECT/INSERT/UPDATE/DELETE are
575    /// real tokens; the rest are idents.
576    fn parse_privilege_keyword(&mut self) -> Result<String, ParseError> {
577        match self.peek() {
578            Token::Select => {
579                self.advance()?;
580                Ok("SELECT".to_string())
581            }
582            Token::Insert => {
583                self.advance()?;
584                Ok("INSERT".to_string())
585            }
586            Token::Update => {
587                self.advance()?;
588                Ok("UPDATE".to_string())
589            }
590            Token::Delete => {
591                self.advance()?;
592                Ok("DELETE".to_string())
593            }
594            Token::Truncate => {
595                self.advance()?;
596                Ok("TRUNCATE".to_string())
597            }
598            Token::Ident(name)
599                if matches!(
600                    name.to_ascii_uppercase().as_str(),
601                    "REFERENCES" | "EXECUTE" | "USAGE"
602                ) =>
603            {
604                let upper = name.to_ascii_uppercase();
605                self.advance()?;
606                Ok(upper)
607            }
608            other => Err(ParseError::expected(
609                vec![
610                    "SELECT",
611                    "INSERT",
612                    "UPDATE",
613                    "DELETE",
614                    "TRUNCATE",
615                    "REFERENCES",
616                    "EXECUTE",
617                    "USAGE",
618                ],
619                other,
620                self.position(),
621            )),
622        }
623    }
624
625    /// Optional `( col1, col2, ... )` after a privilege list. Returns
626    /// `None` when the next token isn't `(`.
627    fn parse_optional_column_list(&mut self) -> Result<Option<Vec<String>>, ParseError> {
628        if !self.check(&Token::LParen) {
629            return Ok(None);
630        }
631        self.expect(Token::LParen)?;
632        let mut cols = Vec::new();
633        loop {
634            cols.push(self.expect_ident()?);
635            if !self.consume(&Token::Comma)? {
636                break;
637            }
638        }
639        self.expect(Token::RParen)?;
640        Ok(Some(cols))
641    }
642
643    /// Parse the optional `[ TABLE | SCHEMA | DATABASE | FUNCTION ]`
644    /// keyword between `ON` and the object list. Defaults to `TABLE`
645    /// when absent (matches PG).
646    fn parse_grant_object_kind(&mut self) -> Result<GrantObjectKind, ParseError> {
647        if self.consume(&Token::Table)? {
648            Ok(GrantObjectKind::Table)
649        } else if self.consume(&Token::Schema)? {
650            Ok(GrantObjectKind::Schema)
651        } else if self.consume_ident_ci("DATABASE")? {
652            Ok(GrantObjectKind::Database)
653        } else if self.consume_ident_ci("FUNCTION")? {
654            Ok(GrantObjectKind::Function)
655        } else {
656            // Default: TABLE
657            Ok(GrantObjectKind::Table)
658        }
659    }
660
661    /// Parse a comma-separated list of `[schema.]name` objects.
662    fn parse_grant_object_list(
663        &mut self,
664        kind: &GrantObjectKind,
665    ) -> Result<Vec<GrantObject>, ParseError> {
666        let mut out = Vec::new();
667        loop {
668            // DATABASE objects use the database name as the object —
669            // accept a single ident.
670            if matches!(kind, GrantObjectKind::Database) {
671                let name = self.expect_ident()?;
672                out.push(GrantObject { schema: None, name });
673            } else {
674                let first = self.expect_ident()?;
675                let (schema, name) = if self.consume(&Token::Dot)? {
676                    let second = self.expect_ident_or_keyword()?;
677                    (Some(first), second)
678                } else {
679                    (None, first)
680                };
681                out.push(GrantObject { schema, name });
682            }
683            if !self.consume(&Token::Comma)? {
684                break;
685            }
686        }
687        Ok(out)
688    }
689
690    /// Parse a comma-separated principal list. Each principal is one of:
691    ///   * `PUBLIC` — every authenticated user.
692    ///   * `GROUP groupname` — role-as-group (parsed, not enforced).
693    ///   * `username` or `tenant.username` — a specific user.
694    fn parse_grant_principal_list(&mut self) -> Result<Vec<GrantPrincipalRef>, ParseError> {
695        let mut out = Vec::new();
696        loop {
697            if self.consume_ident_ci("PUBLIC")? {
698                out.push(GrantPrincipalRef::Public);
699            } else if self.consume(&Token::Group)? || self.consume_ident_ci("GROUP")? {
700                let g = self.expect_ident()?;
701                out.push(GrantPrincipalRef::Group(g));
702            } else {
703                let (tenant, name) = self.parse_user_name()?;
704                out.push(GrantPrincipalRef::User { tenant, name });
705            }
706            if !self.consume(&Token::Comma)? {
707                break;
708            }
709        }
710        Ok(out)
711    }
712
713    /// Parse a `user` or `tenant.user` form. Returns `(tenant, name)`.
714    fn parse_user_name(&mut self) -> Result<(Option<String>, String), ParseError> {
715        let first = self.expect_ident()?;
716        if self.consume(&Token::Dot)? {
717            let name = self.expect_ident()?;
718            Ok((Some(first), name))
719        } else {
720            Ok((None, first))
721        }
722    }
723
724    /// Recognise the optional `WITH GRANT OPTION` suffix on a GRANT.
725    fn consume_grant_option_suffix(&mut self) -> Result<bool, ParseError> {
726        if self.consume(&Token::With)? {
727            if !self.consume_ident_ci("GRANT")? {
728                return Err(ParseError::expected(
729                    vec!["GRANT"],
730                    self.peek(),
731                    self.position(),
732                ));
733            }
734            if !self.consume_ident_ci("OPTION")? {
735                return Err(ParseError::expected(
736                    vec!["OPTION"],
737                    self.peek(),
738                    self.position(),
739                ));
740            }
741            Ok(true)
742        } else {
743            Ok(false)
744        }
745    }
746
747    /// Recognise the optional `GRANT OPTION FOR` prefix on a REVOKE.
748    fn consume_grant_option_for_prefix(&mut self) -> Result<bool, ParseError> {
749        // `GRANT` is an ident, not a keyword — we must peek the ident
750        // text without consuming until we know the full prefix matches.
751        let saved_pos = self.position();
752        if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("GRANT")) {
753            return Ok(false);
754        }
755        // Consume GRANT.
756        self.advance()?;
757        if !self.consume_ident_ci("OPTION")? {
758            // Not the prefix we expected — but `REVOKE GRANT ...`
759            // makes no other sense, so this is a parse error rather
760            // than a non-match.
761            return Err(ParseError::expected(vec!["OPTION"], self.peek(), saved_pos));
762        }
763        if !self.consume(&Token::For)? && !self.consume_ident_ci("FOR")? {
764            return Err(ParseError::expected(vec!["FOR"], self.peek(), saved_pos));
765        }
766        Ok(true)
767    }
768}
769
770#[cfg(test)]
771mod tests {
772    use super::*;
773
774    fn parser(input: &str) -> Parser<'_> {
775        Parser::new(input).unwrap_or_else(|err| panic!("failed to lex {input:?}: {err:?}"))
776    }
777
778    #[test]
779    fn parse_grant_statement_covers_columns_default_table_and_principals() {
780        let grant = parser(
781            "GRANT SELECT, UPDATE (id, email) ON public.users TO PUBLIC, GROUP analysts, tenant.alice WITH GRANT OPTION",
782        )
783        .parse_grant_statement()
784        .expect("grant");
785
786        assert_eq!(grant.actions, vec!["SELECT", "UPDATE"]);
787        assert_eq!(
788            grant.columns.as_deref(),
789            Some(&["id".to_string(), "email".to_string()][..])
790        );
791        assert_eq!(grant.object_kind, GrantObjectKind::Table);
792        assert_eq!(grant.objects.len(), 1);
793        assert_eq!(grant.objects[0].schema.as_deref(), Some("public"));
794        assert_eq!(grant.objects[0].name, "users");
795        assert!(matches!(grant.principals[0], GrantPrincipalRef::Public));
796        assert!(matches!(
797            &grant.principals[1],
798            GrantPrincipalRef::Group(group) if group == "analysts"
799        ));
800        assert!(matches!(
801            &grant.principals[2],
802            GrantPrincipalRef::User { tenant: Some(t), name } if t == "tenant" && name == "alice"
803        ));
804        assert!(grant.with_grant_option);
805        assert!(!grant.all);
806    }
807
808    #[test]
809    fn parse_revoke_statement_covers_grant_option_for_all_and_function_objects() {
810        let revoke = parser(
811            "REVOKE GRANT OPTION FOR ALL PRIVILEGES (id) ON FUNCTION public.recalc FROM GROUP analysts",
812        )
813        .parse_revoke_statement()
814        .expect("revoke");
815
816        assert!(revoke.grant_option_for);
817        assert!(revoke.all);
818        assert_eq!(revoke.actions, vec!["ALL"]);
819        assert_eq!(revoke.columns.as_deref(), Some(&["id".to_string()][..]));
820        assert_eq!(revoke.object_kind, GrantObjectKind::Function);
821        assert_eq!(revoke.objects[0].schema.as_deref(), Some("public"));
822        assert_eq!(revoke.objects[0].name, "recalc");
823        assert!(matches!(
824            &revoke.principals[0],
825            GrantPrincipalRef::Group(group) if group == "analysts"
826        ));
827
828        let revoke = parser("REVOKE USAGE ON SCHEMA analytics FROM bob")
829            .parse_revoke_statement()
830            .expect("revoke without grant option");
831        assert!(!revoke.grant_option_for);
832        assert_eq!(revoke.object_kind, GrantObjectKind::Schema);
833        assert_eq!(revoke.objects[0].name, "analytics");
834    }
835
836    #[test]
837    fn parse_grant_and_revoke_option_errors_are_specific() {
838        let err = parser("GRANT SELECT ON TABLE users TO alice WITH OPTION")
839            .parse_grant_statement()
840            .unwrap_err();
841        assert!(err.to_string().contains("expected: GRANT"), "{err}");
842
843        let err = parser("GRANT SELECT ON TABLE users TO alice WITH GRANT")
844            .parse_grant_statement()
845            .unwrap_err();
846        assert!(err.to_string().contains("expected: OPTION"), "{err}");
847
848        let err = parser("REVOKE GRANT SELECT ON TABLE users FROM alice")
849            .parse_revoke_statement()
850            .unwrap_err();
851        assert!(err.to_string().contains("expected: OPTION"), "{err}");
852
853        let err = parser("REVOKE GRANT OPTION SELECT ON TABLE users FROM alice")
854            .parse_revoke_statement()
855            .unwrap_err();
856        assert!(err.to_string().contains("expected: FOR"), "{err}");
857    }
858
859    #[test]
860    fn parse_alter_user_statement_covers_attribute_variants_and_errors() {
861        let mut p = parser(
862            "ALTER USER tenant.bob VALID UNTIL '2030-01-01' CONNECTION LIMIT 10 ENABLE \
863             SET search_path TO 'public,analytics' ADD GROUP analysts DROP GROUP temp PASSWORD 'pw'",
864        );
865        p.expect(Token::Alter).expect("ALTER");
866        let stmt = p.parse_alter_user_statement().expect("alter user");
867
868        assert_eq!(stmt.tenant.as_deref(), Some("tenant"));
869        assert_eq!(stmt.username, "bob");
870        assert!(matches!(
871            &stmt.attributes[0],
872            AlterUserAttribute::ValidUntil(value) if value == "2030-01-01"
873        ));
874        assert!(matches!(
875            stmt.attributes[1],
876            AlterUserAttribute::ConnectionLimit(10)
877        ));
878        assert!(matches!(stmt.attributes[2], AlterUserAttribute::Enable));
879        assert!(matches!(
880            &stmt.attributes[3],
881            AlterUserAttribute::SetSearchPath(value) if value == "public,analytics"
882        ));
883        assert!(matches!(
884            &stmt.attributes[4],
885            AlterUserAttribute::AddGroup(group) if group == "analysts"
886        ));
887        assert!(matches!(
888            &stmt.attributes[5],
889            AlterUserAttribute::DropGroup(group) if group == "temp"
890        ));
891        assert!(matches!(
892            &stmt.attributes[6],
893            AlterUserAttribute::Password(password) if password == "pw"
894        ));
895
896        let mut p = parser("ALTER USER alice");
897        p.expect(Token::Alter).expect("ALTER");
898        let err = p.parse_alter_user_statement().unwrap_err();
899        assert!(err.to_string().contains("expected:"), "{err}");
900
901        let mut p = parser("ALTER USER alice ADD ROLE analysts");
902        p.expect(Token::Alter).expect("ALTER");
903        let err = p.parse_alter_user_statement().unwrap_err();
904        assert!(err.to_string().contains("expected: GROUP"), "{err}");
905    }
906
907    #[test]
908    fn parse_create_user_statement_covers_role_and_password_errors() {
909        let mut p = parser("CREATE USER tenant.alice WITH PASSWORD 'pw' ROLE admin");
910        p.expect(Token::Create).expect("CREATE");
911        let stmt = p.parse_create_user_statement().expect("create user");
912        assert_eq!(stmt.tenant.as_deref(), Some("tenant"));
913        assert_eq!(stmt.username, "alice");
914        assert_eq!(stmt.password, "pw");
915        assert_eq!(stmt.role, "admin");
916
917        let mut p = parser("CREATE USER bob PASSWORD 'pw'");
918        p.expect(Token::Create).expect("CREATE");
919        let stmt = p.parse_create_user_statement().expect("create user");
920        assert_eq!(stmt.tenant, None);
921        assert_eq!(stmt.username, "bob");
922        assert_eq!(stmt.role, "read");
923
924        let mut p = parser("CREATE USER alice ROLE write");
925        p.expect(Token::Create).expect("CREATE");
926        let err = p.parse_create_user_statement().unwrap_err();
927        assert!(err.to_string().contains("expected: PASSWORD"), "{err}");
928    }
929
930    #[test]
931    fn parse_iam_policy_helpers_cover_policy_sources_and_principals() {
932        assert!(matches!(
933            parser("'readonly' AS '{\"Statement\":[]}'")
934                .parse_create_iam_policy_after_keywords()
935                .expect("create iam policy"),
936            QueryExpr::CreateIamPolicy { ref id, ref json }
937                if id == "readonly" && json == "{\"Statement\":[]}"
938        ));
939        assert!(matches!(
940            parser("'readonly'")
941                .parse_drop_iam_policy_after_keywords()
942                .expect("drop iam policy"),
943            QueryExpr::DropIamPolicy { ref id } if id == "readonly"
944        ));
945        assert!(matches!(
946            parser("LINT POLICY JSON '{\"Statement\":[]}'")
947                .parse_lint_policy()
948                .expect("lint json"),
949            QueryExpr::LintPolicy {
950                source: LintPolicySource::Json(ref json),
951            } if json == "{\"Statement\":[]}"
952        ));
953        assert!(matches!(
954            parser("LINT POLICY 'readonly'")
955                .parse_lint_policy()
956                .expect("lint id"),
957            QueryExpr::LintPolicy {
958                source: LintPolicySource::Id(ref id),
959            } if id == "readonly"
960        ));
961        assert!(matches!(
962            parser("MIGRATE POLICY MODE TO 'policy_only' DRY RUN")
963                .parse_migrate_policy_mode()
964                .expect("migrate policy mode"),
965            QueryExpr::MigratePolicyMode { ref target, dry_run }
966                if target == "policy_only" && dry_run
967        ));
968
969        assert!(matches!(
970            parser("ATTACH POLICY 'readonly' TO USER tenant.alice")
971                .parse_attach_policy()
972                .expect("attach policy"),
973            QueryExpr::AttachPolicy {
974                ref policy_id,
975                principal: PolicyPrincipalRef::User(ref user),
976            } if policy_id == "readonly"
977                && user.tenant.as_deref() == Some("tenant")
978                && user.username == "alice"
979        ));
980        assert!(matches!(
981            parser("DETACH POLICY 'readonly' FROM GROUP analysts")
982                .parse_detach_policy()
983                .expect("detach policy"),
984            QueryExpr::DetachPolicy {
985                ref policy_id,
986                principal: PolicyPrincipalRef::Group(ref group),
987            } if policy_id == "readonly" && group == "analysts"
988        ));
989    }
990
991    #[test]
992    fn parse_show_and_simulate_helpers_cover_resources_and_action_errors() {
993        let mut p = parser("SHOW POLICIES FOR USER tenant.alice");
994        p.advance().expect("SHOW");
995        assert!(matches!(
996            p.parse_show_iam_after_show()
997                .expect("show policies")
998                .expect("iam show"),
999            QueryExpr::ShowPolicies {
1000                filter: Some(PolicyPrincipalRef::User(ref user)),
1001            } if user.tenant.as_deref() == Some("tenant") && user.username == "alice"
1002        ));
1003
1004        let mut p = parser("SHOW EFFECTIVE PERMISSIONS FOR alice ON TABLE:public.orders");
1005        p.advance().expect("SHOW");
1006        assert!(matches!(
1007            p.parse_show_iam_after_show()
1008                .expect("show effective")
1009                .expect("iam show"),
1010            QueryExpr::ShowEffectivePermissions {
1011                ref user,
1012                resource: Some(ref resource),
1013            } if user.tenant.is_none()
1014                && user.username == "alice"
1015                && resource.kind == "table"
1016                && resource.name == "public.orders"
1017        ));
1018
1019        assert!(matches!(
1020            parser("SIMULATE alice ACTION DELETE ON 'table:public.orders'")
1021                .parse_simulate_policy()
1022                .expect("simulate"),
1023            QueryExpr::SimulatePolicy {
1024                ref user,
1025                ref action,
1026                ref resource,
1027            } if user.username == "alice"
1028                && action == "delete"
1029                && resource.kind == "table"
1030                && resource.name == "public.orders"
1031        ));
1032
1033        let err = parser("SIMULATE alice ACTION 42 ON table:public.orders")
1034            .parse_simulate_policy()
1035            .unwrap_err();
1036        assert!(
1037            err.to_string().contains("expected: action keyword"),
1038            "{err}"
1039        );
1040
1041        let err = parser("SIMULATE alice ACTION SELECT ON 'missing-colon'")
1042            .parse_simulate_policy()
1043            .unwrap_err();
1044        assert!(err.to_string().contains("kind:name"), "{err}");
1045    }
1046}