diff --git a/fuzz/fuzz_seeds/postgres_revoke_role_membership.sql b/fuzz/fuzz_seeds/postgres_revoke_role_membership.sql new file mode 100644 index 0000000000..9bd2af4aa3 --- /dev/null +++ b/fuzz/fuzz_seeds/postgres_revoke_role_membership.sql @@ -0,0 +1 @@ +REVOKE bypasser, auditor FROM anon GRANTED BY admin CASCADE diff --git a/src/ast/dcl.rs b/src/ast/dcl.rs index 70d3a1563a..b5fe61d4b5 100644 --- a/src/ast/dcl.rs +++ b/src/ast/dcl.rs @@ -536,6 +536,47 @@ impl From for crate::ast::Statement { } } +/// `REVOKE 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 { + /// Roles whose membership is revoked. + pub roles: Vec, + /// Roles losing the membership. + pub grantees: Vec, + /// Optional `GRANTED BY` identifier. + pub granted_by: Option, + /// Optional `CASCADE`/`RESTRICT` behavior. + pub cascade: Option, +} + +impl fmt::Display for RevokeRole { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!( + f, + "REVOKE {} FROM {}", + display_comma_separated(&self.roles), + display_comma_separated(&self.grantees) + )?; + if let Some(granted_by) = &self.granted_by { + write!(f, " GRANTED BY {granted_by}")?; + } + if let Some(cascade) = &self.cascade { + write!(f, " {cascade}")?; + } + Ok(()) + } +} + +impl From for crate::ast::Statement { + fn from(v: RevokeRole) -> Self { + crate::ast::Statement::RevokeRole(v) + } +} + /// `GRANT role [, ...] TO grantee [, ...] [WITH option [, ...]] [GRANTED BY grantor]` /// /// See [PostgreSQL](https://www.postgresql.org/docs/current/sql-grant.html) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 9b5441738a..1da0cfdbb7 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -58,8 +58,8 @@ pub use self::data_type::{ ExactNumberInfo, IntervalFields, MapBracketKind, StructBracketKind, TimezoneInfo, TypeModifier, }; pub use self::dcl::{ - AlterRoleOperation, CreateRole, Grant, GrantRole, ResetConfig, Revoke, RoleGrantOption, - RoleGrantOptionValue, RoleOption, SecondaryRoles, SetConfigValue, Use, + AlterRoleOperation, CreateRole, Grant, GrantRole, ResetConfig, Revoke, RevokeRole, + RoleGrantOption, RoleGrantOptionValue, RoleOption, SecondaryRoles, SetConfigValue, Use, }; pub use self::ddl::{ AggregateModifyKind, Alignment, AlterCollation, AlterCollationOperation, AlterColumnOperation, @@ -4681,6 +4681,10 @@ pub enum Statement { /// ``` Revoke(Revoke), /// ```sql + /// REVOKE roles FROM grantees + /// ``` + RevokeRole(RevokeRole), + /// ```sql /// DEALLOCATE [ PREPARE ] { name | ALL } /// ``` /// @@ -6188,6 +6192,7 @@ impl fmt::Display for Statement { Statement::Deny(s) => write!(f, "{s}"), Statement::Revoke(revoke) => write!(f, "{revoke}"), Statement::GrantRole(grant) => write!(f, "{grant}"), + Statement::RevokeRole(revoke) => write!(f, "{revoke}"), Statement::Deallocate { name, prepare } => write!( f, "DEALLOCATE {prepare}{name}", diff --git a/src/ast/spans.rs b/src/ast/spans.rs index 4ed3df2bbc..8c36dc7b71 100644 --- a/src/ast/spans.rs +++ b/src/ast/spans.rs @@ -303,6 +303,7 @@ impl Spanned for Values { /// - [Statement::Grant] /// - [Statement::Revoke] /// - [Statement::GrantRole] +/// - [Statement::RevokeRole] /// - [Statement::Deallocate] /// - [Statement::Execute] /// - [Statement::Prepare] @@ -471,6 +472,7 @@ impl Spanned for Statement { Statement::Deny { .. } => Span::empty(), Statement::Revoke { .. } => Span::empty(), Statement::GrantRole { .. } => Span::empty(), + Statement::RevokeRole { .. } => Span::empty(), Statement::Deallocate { .. } => Span::empty(), Statement::Execute { .. } => Span::empty(), Statement::Prepare { .. } => Span::empty(), diff --git a/src/parser/mod.rs b/src/parser/mod.rs index c5a6f4e854..6a89b3f21c 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -721,7 +721,11 @@ impl<'a> Parser<'a> { self.prev_token(); self.parse_deny() } - Keyword::REVOKE => self.parse_revoke().map(Into::into), + Keyword::REVOKE => 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(), @@ -19089,6 +19093,25 @@ impl<'a> Parser<'a> { }) } + /// Parse a `REVOKE` of role membership, after the `REVOKE` keyword. + pub fn parse_revoke_role(&mut self) -> Result { + let roles = self.parse_comma_separated(Parser::parse_identifier)?; + self.expect_keyword_is(Keyword::FROM)?; + let grantees = self.parse_grantees()?; + let granted_by = if self.parse_keywords(&[Keyword::GRANTED, Keyword::BY]) { + Some(self.parse_identifier()?) + } else { + None + }; + let cascade = self.parse_cascade_option(); + Ok(RevokeRole { + roles, + grantees, + granted_by, + cascade, + }) + } + /// Parse an REPLACE statement pub fn parse_replace( &mut self, diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 018c3d8fc5..cbcb97c6ff 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -22524,3 +22524,94 @@ fn display_grant_role_options_propagates_write_errors() { assert_eq!(write!(writer, "{statement}"), Err(fmt::Error)); } } + +#[test] +fn parse_revoke_role() { + let sql = "REVOKE bypasser, auditor FROM anon GRANTED BY admin CASCADE"; + assert_eq!( + all_dialects().verified_stmt(sql), + Statement::RevokeRole(RevokeRole { + roles: vec![Ident::new("bypasser"), Ident::new("auditor")], + grantees: vec![Grantee { + grantee_type: GranteesType::None, + name: Some(GranteeName::ObjectName(ObjectName::from(vec![Ident::new( + "anon" + )]))), + }], + granted_by: Some(Ident::new("admin")), + cascade: Some(CascadeOption::Cascade), + }) + ); + + all_dialects().verified_stmt("REVOKE bypasser FROM anon RESTRICT"); + all_dialects().verified_stmt("REVOKE bypasser FROM anon"); + + assert_eq!( + all_dialects() + .parse_sql_statements("REVOKE bypasser ON t FROM anon") + .unwrap_err(), + ParserError::ParserError("Expected: a privilege keyword, found: bypasser".to_string()) + ); + for (sql, expected) in [ + ("REVOKE bypasser FROM", "Expected: identifier, found: EOF"), + ( + "REVOKE bypasser FROM anon GRANTED BY", + "Expected: identifier, found: EOF", + ), + ] { + assert_eq!( + all_dialects().parse_sql_statements(sql).unwrap_err(), + ParserError::ParserError(expected.to_string()), + "{sql}" + ); + } + + let dialect = GenericDialect {}; + for (sql, expected) in [ + ( + "1 FROM anon", + "Expected: identifier, found: 1 at Line: 1, Column: 1", + ), + ( + "bypasser TO anon", + "Expected: FROM, found: TO at Line: 1, Column: 10", + ), + ] { + let err = Parser::new(&dialect) + .try_with_sql(sql) + .and_then(|mut parser| parser.parse_revoke_role()) + .unwrap_err(); + assert_eq!(err, ParserError::ParserError(expected.to_string()), "{sql}"); + } +} + +#[test] +fn display_revoke_role_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 bypasser FROM anon GRANTED BY admin CASCADE"); + for rejected in [ + "REVOKE ", + "bypasser", + "anon", + " GRANTED BY ", + "admin", + "CASCADE", + ] { + let mut writer = RejectingWriter(rejected); + assert_eq!(write!(writer, "{statement}"), Err(fmt::Error)); + } +}