diff --git a/fuzz/fuzz_seeds/postgres_revoke_role_option_for.sql b/fuzz/fuzz_seeds/postgres_revoke_role_option_for.sql new file mode 100644 index 0000000000..3e5a6c0a30 --- /dev/null +++ b/fuzz/fuzz_seeds/postgres_revoke_role_option_for.sql @@ -0,0 +1 @@ +REVOKE ADMIN OPTION FOR bypasser FROM anon GRANTED BY admin RESTRICT diff --git a/src/ast/dcl.rs b/src/ast/dcl.rs index b5fe61d4b5..518b818f7f 100644 --- a/src/ast/dcl.rs +++ b/src/ast/dcl.rs @@ -536,13 +536,15 @@ impl From for crate::ast::Statement { } } -/// `REVOKE role [, ...] FROM grantee [, ...] [GRANTED BY grantor] [CASCADE | RESTRICT]` +/// `REVOKE [option OPTION FOR] role [, ...] FROM grantee [, ...] [GRANTED BY grantor] [CASCADE | RESTRICT]` /// /// See [PostgreSQL](https://www.postgresql.org/docs/current/sql-revoke.html) #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] pub struct RevokeRole { + /// The option named in `OPTION FOR`, such as `ADMIN`, withdrawn while the membership stays. + pub option_for: Option, /// Roles whose membership is revoked. pub roles: Vec, /// Roles losing the membership. @@ -555,9 +557,13 @@ pub struct RevokeRole { impl fmt::Display for RevokeRole { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "REVOKE ")?; + if let Some(option_for) = &self.option_for { + write!(f, "{option_for} OPTION FOR ")?; + } write!( f, - "REVOKE {} FROM {}", + "{} FROM {}", display_comma_separated(&self.roles), display_comma_separated(&self.grantees) )?; diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 6a89b3f21c..b555ecc6df 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -721,11 +721,17 @@ impl<'a> Parser<'a> { self.prev_token(); self.parse_deny() } - Keyword::REVOKE => self.parse_privileges_or_roles( - Keyword::FROM, - Parser::parse_revoke, - Parser::parse_revoke_role, - ), + Keyword::REVOKE => { + if self.peek_role_grant_option_for() { + self.parse_revoke_role().map(Into::into) + } else { + self.parse_privileges_or_roles( + Keyword::FROM, + Parser::parse_revoke, + Parser::parse_revoke_role, + ) + } + } Keyword::START => self.parse_start_transaction(), Keyword::BEGIN => self.parse_begin(), Keyword::END => self.parse_end(), @@ -19095,6 +19101,7 @@ impl<'a> Parser<'a> { /// Parse a `REVOKE` of role membership, after the `REVOKE` keyword. pub fn parse_revoke_role(&mut self) -> Result { + let option_for = self.parse_role_grant_option_for(); let roles = self.parse_comma_separated(Parser::parse_identifier)?; self.expect_keyword_is(Keyword::FROM)?; let grantees = self.parse_grantees()?; @@ -19105,6 +19112,7 @@ impl<'a> Parser<'a> { }; let cascade = self.parse_cascade_option(); Ok(RevokeRole { + option_for, roles, grantees, granted_by, @@ -19112,6 +19120,27 @@ impl<'a> Parser<'a> { }) } + /// Parses `name OPTION FOR`, leaving `GRANT OPTION FOR` to the privilege form. + fn parse_role_grant_option_for(&mut self) -> Option { + let index = self.index; + if !self.peek_keyword(Keyword::GRANT) { + if let Ok(name) = self.parse_identifier() { + if self.parse_keywords(&[Keyword::OPTION, Keyword::FOR]) { + return Some(name); + } + } + } + self.index = index; + None + } + + fn peek_role_grant_option_for(&mut self) -> bool { + let index = self.index; + let matched = self.parse_role_grant_option_for().is_some(); + self.index = index; + matched + } + /// Parse an REPLACE statement pub fn parse_replace( &mut self, diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index cbcb97c6ff..fc08f2b327 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -22531,6 +22531,7 @@ fn parse_revoke_role() { assert_eq!( all_dialects().verified_stmt(sql), Statement::RevokeRole(RevokeRole { + option_for: None, roles: vec![Ident::new("bypasser"), Ident::new("auditor")], grantees: vec![Grantee { grantee_type: GranteesType::None, @@ -22615,3 +22616,53 @@ fn display_revoke_role_propagates_write_errors() { assert_eq!(write!(writer, "{statement}"), Err(fmt::Error)); } } + +#[test] +fn parse_revoke_role_option_for() { + let Statement::RevokeRole(RevokeRole { option_for, .. }) = + all_dialects().verified_stmt("REVOKE SET OPTION FOR bypasser FROM anon") + else { + panic!("Expected REVOKE role"); + }; + assert_eq!(option_for, Some(Ident::new("SET"))); + + all_dialects() + .verified_stmt("REVOKE ADMIN OPTION FOR bypasser FROM anon GRANTED BY admin RESTRICT"); + all_dialects().verified_stmt("REVOKE INHERIT OPTION FOR bypasser, auditor FROM anon"); + + assert_eq!( + all_dialects() + .parse_sql_statements("REVOKE ADMIN OPTION FOR bypasser TO anon") + .unwrap_err(), + ParserError::ParserError("Expected: FROM, found: TO".to_string()) + ); + assert_eq!( + all_dialects() + .parse_sql_statements("REVOKE 1 FROM anon") + .unwrap_err(), + ParserError::ParserError("Expected: a privilege keyword, found: 1".to_string()) + ); +} + +#[test] +fn display_revoke_role_option_for_propagates_write_errors() { + use core::fmt::{self, Write}; + + struct RejectingWriter<'a>(&'a str); + + impl Write for RejectingWriter<'_> { + fn write_str(&mut self, value: &str) -> fmt::Result { + if value.contains(self.0) { + Err(fmt::Error) + } else { + Ok(()) + } + } + } + + let statement = all_dialects().verified_stmt("REVOKE ADMIN OPTION FOR bypasser FROM anon"); + for rejected in ["REVOKE ", "ADMIN", " OPTION FOR "] { + let mut writer = RejectingWriter(rejected); + assert_eq!(write!(writer, "{statement}"), Err(fmt::Error)); + } +}