diff --git a/fuzz/fuzz_seeds/postgres_grant_role_membership.sql b/fuzz/fuzz_seeds/postgres_grant_role_membership.sql new file mode 100644 index 0000000000..9bf6fb2f4b --- /dev/null +++ b/fuzz/fuzz_seeds/postgres_grant_role_membership.sql @@ -0,0 +1 @@ +GRANT bypasser, auditor TO anon, authenticated GRANTED BY admin diff --git a/src/ast/dcl.rs b/src/ast/dcl.rs index 96b340714c..ad4cc3a6b5 100644 --- a/src/ast/dcl.rs +++ b/src/ast/dcl.rs @@ -535,3 +535,39 @@ impl From for crate::ast::Statement { crate::ast::Statement::Revoke(v) } } + +/// `GRANT role [, ...] TO grantee [, ...] [GRANTED BY grantor]` +/// +/// See [PostgreSQL](https://www.postgresql.org/docs/current/sql-grant.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 GrantRole { + /// Roles whose membership is granted. + pub roles: Vec, + /// Roles receiving the membership. + pub grantees: Vec, + /// Optional `GRANTED BY` identifier. + pub granted_by: Option, +} + +impl fmt::Display for GrantRole { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!( + f, + "GRANT {} TO {}", + display_comma_separated(&self.roles), + display_comma_separated(&self.grantees) + )?; + if let Some(granted_by) = &self.granted_by { + write!(f, " GRANTED BY {granted_by}")?; + } + Ok(()) + } +} + +impl From for crate::ast::Statement { + fn from(v: GrantRole) -> Self { + crate::ast::Statement::GrantRole(v) + } +} diff --git a/src/ast/mod.rs b/src/ast/mod.rs index d3ffdc2ab0..8dfb8ad845 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, ResetConfig, Revoke, RoleOption, SecondaryRoles, - SetConfigValue, Use, + AlterRoleOperation, CreateRole, Grant, GrantRole, ResetConfig, Revoke, RoleOption, + SecondaryRoles, SetConfigValue, Use, }; pub use self::ddl::{ AggregateModifyKind, Alignment, AlterCollation, AlterCollationOperation, AlterColumnOperation, @@ -4669,6 +4669,10 @@ pub enum Statement { /// ``` Grant(Grant), /// ```sql + /// GRANT roles TO grantees + /// ``` + GrantRole(GrantRole), + /// ```sql /// DENY privileges ON object TO grantees /// ``` Deny(DenyStatement), @@ -6183,6 +6187,7 @@ impl fmt::Display for Statement { Statement::Grant(grant) => write!(f, "{grant}"), Statement::Deny(s) => write!(f, "{s}"), Statement::Revoke(revoke) => write!(f, "{revoke}"), + Statement::GrantRole(grant) => write!(f, "{grant}"), Statement::Deallocate { name, prepare } => write!( f, "DEALLOCATE {prepare}{name}", diff --git a/src/ast/spans.rs b/src/ast/spans.rs index 74e0844d56..4ed3df2bbc 100644 --- a/src/ast/spans.rs +++ b/src/ast/spans.rs @@ -302,6 +302,7 @@ impl Spanned for Values { /// - [Statement::Assert] /// - [Statement::Grant] /// - [Statement::Revoke] +/// - [Statement::GrantRole] /// - [Statement::Deallocate] /// - [Statement::Execute] /// - [Statement::Prepare] @@ -469,6 +470,7 @@ impl Spanned for Statement { Statement::Grant { .. } => Span::empty(), Statement::Deny { .. } => Span::empty(), Statement::Revoke { .. } => Span::empty(), + Statement::GrantRole { .. } => 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 d09c601eb9..9012270f18 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -712,7 +712,11 @@ impl<'a> Parser<'a> { Keyword::SET => self.parse_set(), Keyword::SHOW => self.parse_show(), Keyword::USE => self.parse_use(), - Keyword::GRANT => self.parse_grant().map(Into::into), + Keyword::GRANT => self.parse_privileges_or_roles( + Keyword::TO, + Parser::parse_grant, + Parser::parse_grant_role, + ), Keyword::DENY => { self.prev_token(); self.parse_deny() @@ -18441,6 +18445,47 @@ impl<'a> Parser<'a> { }) } + /// Parse a `GRANT` of role membership, after the `GRANT` keyword. + pub fn parse_grant_role(&mut self) -> Result { + let roles = self.parse_comma_separated(Parser::parse_identifier)?; + self.expect_keyword_is(Keyword::TO)?; + let grantees = self.parse_grantees()?; + let granted_by = if self.parse_keywords(&[Keyword::GRANTED, Keyword::BY]) { + Some(self.parse_identifier()?) + } else { + None + }; + Ok(GrantRole { + roles, + grantees, + granted_by, + }) + } + + /// Parses a statement body with `privileges`, and with `roles` instead when that fails and + /// the body opens with a role list followed by `keyword`. + fn parse_privileges_or_roles, R: Into>( + &mut self, + keyword: Keyword, + privileges: fn(&mut Self) -> Result, + roles: fn(&mut Self) -> Result, + ) -> Result { + let index = self.index; + let privileges_err = match privileges(self) { + Ok(statement) => return Ok(statement.into()), + Err(err) => err, + }; + self.index = index; + let is_role_list = self.parse_comma_separated(Parser::parse_identifier).is_ok() + && self.parse_keyword(keyword); + self.index = index; + if is_role_list { + roles(self).map(Into::into) + } else { + Err(privileges_err) + } + } + fn parse_grantees(&mut self) -> Result, ParserError> { let mut values = vec![]; let mut grantee_type = GranteesType::None; diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 7649790742..2e0a814ea4 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -22386,3 +22386,77 @@ fn parse_grant_function_signature() { ParserError::ParserError("Expected: ), found: EOF".to_string()) ); } + +#[test] +fn parse_grant_role() { + let role_grantee = |name: &str| Grantee { + grantee_type: GranteesType::None, + name: Some(GranteeName::ObjectName(ObjectName::from(vec![Ident::new( + name, + )]))), + }; + + let sql = "GRANT bypasser, auditor TO anon, authenticated GRANTED BY admin"; + assert_eq!( + all_dialects().verified_stmt(sql), + Statement::GrantRole(GrantRole { + roles: vec![Ident::new("bypasser"), Ident::new("auditor")], + grantees: vec![role_grantee("anon"), role_grantee("authenticated")], + granted_by: Some(Ident::new("admin")), + }) + ); + + all_dialects().verified_stmt("GRANT bypasser TO anon"); + assert_eq!( + all_dialects() + .parse_sql_statements("GRANT bypasser ON t TO anon") + .unwrap_err(), + ParserError::ParserError("Expected: a privilege keyword, found: bypasser".to_string()) + ); + + assert_matches!( + all_dialects().verified_stmt(r#"GRANT "select" TO anon"#), + Statement::GrantRole(_) + ); + assert_matches!( + all_dialects().verified_stmt("GRANT SELECT TO anon"), + Statement::Grant(_) + ); + for (sql, err) in [ + ("GRANT bypasser TO", "Expected: identifier, found: EOF"), + ( + "GRANT bypasser TO anon GRANTED BY 1", + "Expected: identifier, found: 1", + ), + ("GRANT 1 TO anon", "Expected: a privilege keyword, found: 1"), + ] { + assert_eq!( + all_dialects().parse_sql_statements(sql).unwrap_err(), + ParserError::ParserError(err.to_string()), + "{sql}" + ); + } +} + +#[test] +fn display_grant_role_propagates_formatter_errors() { + use core::fmt::{self, Write}; + + struct RejectingWriter(&'static 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 grant = verified_stmt("GRANT bypasser TO anon GRANTED BY admin"); + for rejected in ["anon", "admin"] { + let mut writer = RejectingWriter(rejected); + assert_eq!(write!(writer, "{grant}"), Err(fmt::Error), "{rejected}"); + } +}