diff --git a/fuzz/fuzz_seeds/postgres_2417_alter_default_privileges.sql b/fuzz/fuzz_seeds/postgres_2417_alter_default_privileges.sql new file mode 100644 index 0000000000..a1c2036315 --- /dev/null +++ b/fuzz/fuzz_seeds/postgres_2417_alter_default_privileges.sql @@ -0,0 +1,10 @@ +ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public GRANT SELECT ON TABLES TO app_user; +ALTER DEFAULT PRIVILEGES FOR ROLE a, b IN SCHEMA s1, s2 GRANT SELECT, INSERT ON TABLES TO app WITH GRANT OPTION; +ALTER DEFAULT PRIVILEGES FOR USER bob GRANT ALL PRIVILEGES ON SEQUENCES TO app; +ALTER DEFAULT PRIVILEGES GRANT EXECUTE ON FUNCTIONS TO app; +ALTER DEFAULT PRIVILEGES GRANT USAGE ON ROUTINES TO app; +ALTER DEFAULT PRIVILEGES GRANT USAGE, CREATE ON TYPES TO app; +ALTER DEFAULT PRIVILEGES GRANT SELECT ON SCHEMAS TO app; +ALTER DEFAULT PRIVILEGES GRANT SELECT, UPDATE ON LARGE OBJECTS TO app; +ALTER DEFAULT PRIVILEGES REVOKE SELECT ON TABLES FROM app; +ALTER DEFAULT PRIVILEGES REVOKE GRANT OPTION FOR ALL ON TABLES FROM app CASCADE diff --git a/src/ast/dcl.rs b/src/ast/dcl.rs index 96b340714c..dbdfb775ae 100644 --- a/src/ast/dcl.rs +++ b/src/ast/dcl.rs @@ -30,8 +30,8 @@ use sqlparser_derive::{Visit, VisitMut}; use super::{display_comma_separated, Expr, Ident, Password, Spanned}; use crate::ast::{ - display_separated, CascadeOption, CurrentGrantsKind, GrantObjects, Grantee, ObjectName, - Privileges, + display_separated, CascadeOption, CurrentGrantsKind, GrantObjects, Grantee, GranteeName, + ObjectName, Privileges, }; use crate::tokenizer::Span; @@ -535,3 +535,195 @@ impl From for crate::ast::Statement { crate::ast::Statement::Revoke(v) } } + +/// The options of [`AlterDefaultPrivileges`], kept in the order written. +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub enum DefaultPrivilegesOption { + /// `FOR ROLE [, ...]`. + ForRole(Vec), + /// `IN SCHEMA [, ...]`. + InSchema(Vec), +} + +impl fmt::Display for DefaultPrivilegesOption { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + DefaultPrivilegesOption::ForRole(roles) => { + write!(f, "FOR ROLE {}", display_comma_separated(roles)) + } + DefaultPrivilegesOption::InSchema(schemas) => { + write!(f, "IN SCHEMA {}", display_comma_separated(schemas)) + } + } + } +} + +/// `ALTER DEFAULT PRIVILEGES`, which applies to objects created later rather +/// than to existing ones. +/// +/// [PostgreSQL](https://www.postgresql.org/docs/current/sql-alterdefaultprivileges.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 AlterDefaultPrivileges { + /// The `FOR ROLE` and `IN SCHEMA` options, in written order. + pub options: Vec, + /// The abbreviated `GRANT` or `REVOKE` that follows. + pub operation: AlterDefaultPrivilegesOperation, +} + +impl fmt::Display for AlterDefaultPrivileges { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + f.write_str("ALTER DEFAULT PRIVILEGES")?; + for option in &self.options { + write!(f, " {option}")?; + } + write!(f, " {}", self.operation) + } +} + +impl Spanned for AlterDefaultPrivileges { + fn span(&self) -> Span { + let options = self.options.iter().map(|option| match option { + DefaultPrivilegesOption::ForRole(roles) => { + Span::union_iter(roles.iter().map(|role| role.span)) + } + DefaultPrivilegesOption::InSchema(schemas) => { + Span::union_iter(schemas.iter().map(Spanned::span)) + } + }); + let grantees = match &self.operation { + AlterDefaultPrivilegesOperation::Grant { grantees, .. } + | AlterDefaultPrivilegesOperation::Revoke { grantees, .. } => grantees, + }; + Span::union_iter( + options.chain( + grantees + .iter() + .filter_map(|grantee| grantee.name.as_ref()) + .map(|name| match name { + GranteeName::ObjectName(name) => name.span(), + GranteeName::UserHost { user, host } => user.span.union(&host.span), + }), + ), + ) + } +} + +impl From for crate::ast::Statement { + fn from(v: AlterDefaultPrivileges) -> Self { + crate::ast::Statement::AlterDefaultPrivileges(v) + } +} + +/// The abbreviated `GRANT` or `REVOKE` of an [`AlterDefaultPrivileges`], naming +/// an object type rather than named objects and so unable to reuse [`Grant`]. +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub enum AlterDefaultPrivilegesOperation { + /// `GRANT ON TO [WITH GRANT OPTION]` + Grant { + /// The privileges being granted. + privileges: Privileges, + /// The kind of future object the privileges apply to. + object_type: DefaultPrivilegesObjectType, + /// The roles receiving the privileges. + grantees: Vec, + /// Whether `WITH GRANT OPTION` is present. + with_grant_option: bool, + }, + /// `REVOKE [GRANT OPTION FOR] ON FROM [CASCADE | RESTRICT]` + Revoke { + /// Whether `GRANT OPTION FOR` is present. + grant_option_for: bool, + /// The privileges being revoked. + privileges: Privileges, + /// The kind of future object the privileges apply to. + object_type: DefaultPrivilegesObjectType, + /// The roles losing the privileges. + grantees: Vec, + /// Optional `CASCADE`/`RESTRICT` behaviour. + cascade: Option, + }, +} + +impl fmt::Display for AlterDefaultPrivilegesOperation { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + AlterDefaultPrivilegesOperation::Grant { + privileges, + object_type, + grantees, + with_grant_option, + } => { + write!( + f, + "GRANT {privileges} ON {object_type} TO {}", + display_comma_separated(grantees) + )?; + if *with_grant_option { + f.write_str(" WITH GRANT OPTION")?; + } + } + AlterDefaultPrivilegesOperation::Revoke { + grant_option_for, + privileges, + object_type, + grantees, + cascade, + } => { + f.write_str("REVOKE ")?; + if *grant_option_for { + f.write_str("GRANT OPTION FOR ")?; + } + write!( + f, + "{privileges} ON {object_type} FROM {}", + display_comma_separated(grantees) + )?; + if let Some(cascade) = cascade { + write!(f, " {cascade}")?; + } + } + } + Ok(()) + } +} + +/// The kind of future object an [`AlterDefaultPrivileges`] applies to. +#[derive(Debug, Clone, Copy, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub enum DefaultPrivilegesObjectType { + /// `TABLES`, which also covers views and foreign tables. + Tables, + /// `SEQUENCES` + Sequences, + /// `FUNCTIONS` + Functions, + /// `ROUTINES`, a synonym of `FUNCTIONS`. + Routines, + /// `TYPES` + Types, + /// `SCHEMAS` + Schemas, + /// `LARGE OBJECTS` + LargeObjects, +} + +impl fmt::Display for DefaultPrivilegesObjectType { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + f.write_str(match self { + DefaultPrivilegesObjectType::Tables => "TABLES", + DefaultPrivilegesObjectType::Sequences => "SEQUENCES", + DefaultPrivilegesObjectType::Functions => "FUNCTIONS", + DefaultPrivilegesObjectType::Routines => "ROUTINES", + DefaultPrivilegesObjectType::Types => "TYPES", + DefaultPrivilegesObjectType::Schemas => "SCHEMAS", + DefaultPrivilegesObjectType::LargeObjects => "LARGE OBJECTS", + }) + } +} diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 87618bbc5f..19fde3cf78 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -58,8 +58,9 @@ pub use self::data_type::{ ExactNumberInfo, IntervalFields, MapBracketKind, StructBracketKind, TimezoneInfo, TypeModifier, }; pub use self::dcl::{ - AlterRoleOperation, CreateRole, Grant, ResetConfig, Revoke, RoleOption, SecondaryRoles, - SetConfigValue, Use, + AlterDefaultPrivileges, AlterDefaultPrivilegesOperation, AlterRoleOperation, CreateRole, + DefaultPrivilegesObjectType, DefaultPrivilegesOption, Grant, ResetConfig, Revoke, RoleOption, + SecondaryRoles, SetConfigValue, Use, }; pub use self::ddl::{ AggregateModifyKind, Alignment, AlterCollation, AlterCollationOperation, AlterColumnOperation, @@ -4677,6 +4678,10 @@ pub enum Statement { /// ``` Revoke(Revoke), /// ```sql + /// ALTER DEFAULT PRIVILEGES + /// ``` + AlterDefaultPrivileges(AlterDefaultPrivileges), + /// ```sql /// DEALLOCATE [ PREPARE ] { name | ALL } /// ``` /// @@ -6183,6 +6188,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::AlterDefaultPrivileges(stmt) => write!(f, "{stmt}"), Statement::Deallocate { name, prepare } => write!( f, "DEALLOCATE {prepare}{name}", diff --git a/src/ast/spans.rs b/src/ast/spans.rs index 74e0844d56..151b6bb31a 100644 --- a/src/ast/spans.rs +++ b/src/ast/spans.rs @@ -469,6 +469,7 @@ impl Spanned for Statement { Statement::Grant { .. } => Span::empty(), Statement::Deny { .. } => Span::empty(), Statement::Revoke { .. } => Span::empty(), + Statement::AlterDefaultPrivileges(privileges) => privileges.span(), Statement::Deallocate { .. } => Span::empty(), Statement::Execute { .. } => Span::empty(), Statement::Prepare { .. } => Span::empty(), diff --git a/src/keywords.rs b/src/keywords.rs index b35644befb..27fdfba1a9 100644 --- a/src/keywords.rs +++ b/src/keywords.rs @@ -934,6 +934,7 @@ define_keywords!( ROLLBACK, ROLLUP, ROOT, + ROUTINES, ROW, ROWGROUPSIZE, ROWID, @@ -1127,6 +1128,7 @@ define_keywords!( TSVECTOR, TUPLE, TYPE, + TYPES, TYPMOD_IN, TYPMOD_OUT, UBIGINT, diff --git a/src/parser/mod.rs b/src/parser/mod.rs index a14b902e5a..a1951d9386 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -11719,6 +11719,10 @@ impl<'a> Parser<'a> { return self.parse_alter_text_search().map(Into::into); } + if self.parse_keywords(&[Keyword::DEFAULT, Keyword::PRIVILEGES]) { + return self.parse_alter_default_privileges().map(Into::into); + } + let index = self.index; self.advance_token(); let object_type = match &self.get_current_token().token { @@ -18537,14 +18541,7 @@ impl<'a> Parser<'a> { pub fn parse_grant_deny_revoke_privileges_objects( &mut self, ) -> Result<(Privileges, Option), ParserError> { - let privileges = if self.parse_keyword(Keyword::ALL) { - Privileges::All { - with_privileges_keyword: self.parse_keyword(Keyword::PRIVILEGES), - } - } else { - let actions = self.parse_actions_list()?; - Privileges::Actions(actions) - }; + let privileges = self.parse_privileges()?; let objects = if self.parse_keyword(Keyword::ON) { if self.parse_keywords(&[Keyword::ALL, Keyword::TABLES, Keyword::IN, Keyword::SCHEMA]) { @@ -19076,6 +19073,118 @@ impl<'a> Parser<'a> { }) } + /// Parse `ALTER DEFAULT PRIVILEGES`, whose keywords are already consumed. + /// + /// See [PostgreSQL docs](https://www.postgresql.org/docs/current/sql-alterdefaultprivileges.html). + pub fn parse_alter_default_privileges( + &mut self, + ) -> Result { + // `FOR USER` is a synonym of `FOR ROLE` and is normalised to it. + let mut options = Vec::new(); + let mut for_roles = false; + let mut in_schemas = false; + loop { + if self.parse_keyword(Keyword::FOR) { + if for_roles { + return Err(ParserError::ParserError( + "FOR ROLE specified more than once".to_string(), + )); + } + self.expect_one_of_keywords(&[Keyword::ROLE, Keyword::USER])?; + for_roles = true; + options.push(DefaultPrivilegesOption::ForRole( + self.parse_comma_separated(|p| p.parse_identifier())?, + )); + } else if self.parse_keywords(&[Keyword::IN, Keyword::SCHEMA]) { + if in_schemas { + return Err(ParserError::ParserError( + "IN SCHEMA specified more than once".to_string(), + )); + } + in_schemas = true; + options.push(DefaultPrivilegesOption::InSchema( + self.parse_comma_separated(|p| p.parse_object_name(false))?, + )); + } else { + break; + } + } + + let is_grant = if self.parse_keyword(Keyword::GRANT) { + true + } else if self.parse_keyword(Keyword::REVOKE) { + false + } else { + return self.expected_ref("GRANT or REVOKE", self.peek_token_ref()); + }; + + let grant_option_for = + !is_grant && self.parse_keywords(&[Keyword::GRANT, Keyword::OPTION, Keyword::FOR]); + let privileges = self.parse_privileges()?; + self.expect_keyword_is(Keyword::ON)?; + let object_type = self.parse_default_privileges_object_type()?; + self.expect_keyword_is(if is_grant { Keyword::TO } else { Keyword::FROM })?; + let grantees = self.parse_grantees()?; + + let operation = if is_grant { + AlterDefaultPrivilegesOperation::Grant { + privileges, + object_type, + grantees, + with_grant_option: self.parse_keywords(&[ + Keyword::WITH, + Keyword::GRANT, + Keyword::OPTION, + ]), + } + } else { + AlterDefaultPrivilegesOperation::Revoke { + grant_option_for, + privileges, + object_type, + grantees, + cascade: self.parse_cascade_option(), + } + }; + + Ok(AlterDefaultPrivileges { options, operation }) + } + + /// Parse `ALL [PRIVILEGES]` or a comma separated privilege list. + fn parse_privileges(&mut self) -> Result { + if self.parse_keyword(Keyword::ALL) { + Ok(Privileges::All { + with_privileges_keyword: self.parse_keyword(Keyword::PRIVILEGES), + }) + } else { + Ok(Privileges::Actions(self.parse_actions_list()?)) + } + } + + fn parse_default_privileges_object_type( + &mut self, + ) -> Result { + if self.parse_keywords(&[Keyword::LARGE, Keyword::OBJECTS]) { + return Ok(DefaultPrivilegesObjectType::LargeObjects); + } + for (keyword, object_type) in [ + (Keyword::TABLES, DefaultPrivilegesObjectType::Tables), + (Keyword::SEQUENCES, DefaultPrivilegesObjectType::Sequences), + (Keyword::FUNCTIONS, DefaultPrivilegesObjectType::Functions), + (Keyword::ROUTINES, DefaultPrivilegesObjectType::Routines), + (Keyword::TYPES, DefaultPrivilegesObjectType::Types), + (Keyword::SCHEMAS, DefaultPrivilegesObjectType::Schemas), + ] { + if self.parse_keyword(keyword) { + return Ok(object_type); + } + } + self.expected_ref( + "TABLES, SEQUENCES, FUNCTIONS, ROUTINES, TYPES, SCHEMAS or LARGE OBJECTS", + self.peek_token_ref(), + ) + } + /// Parse an REPLACE statement pub fn parse_replace( &mut self, diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index c99a0651aa..6a82bcae89 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -5102,6 +5102,156 @@ fn parse_alter_user() { ); } +#[test] +fn parse_alter_default_privileges() { + // What `pg_dump -s` emits for a default ACL. + pg().verified_stmt("ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public GRANT SELECT ON TABLES TO app_user"); + pg().verified_stmt("ALTER DEFAULT PRIVILEGES GRANT SELECT ON TABLES TO app"); + pg().verified_stmt("ALTER DEFAULT PRIVILEGES FOR ROLE a, b IN SCHEMA s1, s2 GRANT SELECT, INSERT ON TABLES TO app WITH GRANT OPTION"); + pg().verified_stmt("ALTER DEFAULT PRIVILEGES GRANT ALL PRIVILEGES ON SEQUENCES TO app"); + pg().verified_stmt("ALTER DEFAULT PRIVILEGES GRANT EXECUTE ON FUNCTIONS TO app"); + pg().verified_stmt("ALTER DEFAULT PRIVILEGES GRANT EXECUTE ON ROUTINES TO app"); + pg().verified_stmt("ALTER DEFAULT PRIVILEGES GRANT USAGE ON TYPES TO app"); + pg().verified_stmt("ALTER DEFAULT PRIVILEGES GRANT USAGE, CREATE ON SCHEMAS TO app"); + pg().verified_stmt("ALTER DEFAULT PRIVILEGES REVOKE SELECT ON TABLES FROM app"); + pg().verified_stmt( + "ALTER DEFAULT PRIVILEGES REVOKE GRANT OPTION FOR ALL ON TABLES FROM app CASCADE", + ); + // `FOR USER` normalises to `FOR ROLE`. + pg().one_statement_parses_to( + "ALTER DEFAULT PRIVILEGES FOR USER bob GRANT SELECT ON TABLES TO app", + "ALTER DEFAULT PRIVILEGES FOR ROLE bob GRANT SELECT ON TABLES TO app", + ); + + // Options keep the order written. + for (sql, options) in [ + ( + "ALTER DEFAULT PRIVILEGES IN SCHEMA s FOR ROLE r GRANT SELECT ON TABLES TO app", + vec![ + DefaultPrivilegesOption::InSchema(vec![ObjectName::from(Ident::new("s"))]), + DefaultPrivilegesOption::ForRole(vec![Ident::new("r")]), + ], + ), + ( + "ALTER DEFAULT PRIVILEGES FOR ROLE r IN SCHEMA s GRANT SELECT ON TABLES TO app", + vec![ + DefaultPrivilegesOption::ForRole(vec![Ident::new("r")]), + DefaultPrivilegesOption::InSchema(vec![ObjectName::from(Ident::new("s"))]), + ], + ), + ] { + match pg().verified_stmt(sql) { + Statement::AlterDefaultPrivileges(stmt) => assert_eq!(stmt.options, options, "{sql}"), + _ => unreachable!(), + } + } + // `LARGE OBJECTS` is a valid object type for both `GRANT` and `REVOKE`. + pg().verified_stmt("ALTER DEFAULT PRIVILEGES GRANT SELECT, UPDATE ON LARGE OBJECTS TO app"); + pg().verified_stmt( + "ALTER DEFAULT PRIVILEGES REVOKE GRANT OPTION FOR SELECT ON LARGE OBJECTS FROM app CASCADE", + ); + // Repeated options are rejected. + assert_eq!( + pg().parse_sql_statements( + "ALTER DEFAULT PRIVILEGES FOR ROLE a FOR ROLE b GRANT SELECT ON TABLES TO app" + ) + .unwrap_err(), + ParserError::ParserError("FOR ROLE specified more than once".to_string()), + ); + assert_eq!( + pg().parse_sql_statements( + "ALTER DEFAULT PRIVILEGES IN SCHEMA s IN SCHEMA t GRANT SELECT ON TABLES TO app" + ) + .unwrap_err(), + ParserError::ParserError("IN SCHEMA specified more than once".to_string()), + ); + + // `TABLES` is a keyword only here, so a table named `tables` still parses. + pg().verified_stmt("GRANT SELECT ON TABLES TO app"); + assert_eq!( + pg().parse_sql_statements("ALTER DEFAULT PRIVILEGES FOR ROLE a") + .unwrap_err(), + ParserError::ParserError("Expected: GRANT or REVOKE, found: EOF".to_string()), + ); +} + +#[test] +fn parse_alter_default_privileges_rejects_malformed_statements() { + for (sql, expected) in [ + ( + "ALTER DEFAULT PRIVILEGES FOR ALL GRANT SELECT ON TABLES TO app", + "Expected: one of ROLE or USER, found: ALL", + ), + ( + "ALTER DEFAULT PRIVILEGES FOR ROLE", + "Expected: identifier, found: EOF", + ), + ( + "ALTER DEFAULT PRIVILEGES IN SCHEMA", + "Expected: identifier, found: EOF", + ), + ( + "ALTER DEFAULT PRIVILEGES GRANT ON TABLES TO app", + "Expected: a privilege keyword, found: ON", + ), + ( + "ALTER DEFAULT PRIVILEGES GRANT SELECT TO app", + "Expected: ON, found: TO", + ), + ( + "ALTER DEFAULT PRIVILEGES GRANT SELECT ON INDEXES TO app", + "Expected: TABLES, SEQUENCES, FUNCTIONS, ROUTINES, TYPES, SCHEMAS or LARGE OBJECTS, found: INDEXES", + ), + ( + "ALTER DEFAULT PRIVILEGES GRANT SELECT ON TABLES", + "Expected: TO, found: EOF", + ), + ( + "ALTER DEFAULT PRIVILEGES GRANT SELECT ON TABLES TO", + "Expected: identifier, found: EOF", + ), + ] { + assert_eq!( + pg().parse_sql_statements(sql).unwrap_err(), + ParserError::ParserError(expected.to_string()), + ); + } +} + +#[test] +fn parse_alter_default_privileges_spans() { + for (sql, start, end) in [ + ( + "ALTER DEFAULT PRIVILEGES FOR ROLE r IN SCHEMA s GRANT SELECT ON TABLES TO g", + 35, + 76, + ), + ( + "ALTER DEFAULT PRIVILEGES REVOKE SELECT ON TABLES FROM g", + 55, + 56, + ), + ( + "ALTER DEFAULT PRIVILEGES FOR ROLE r GRANT SELECT ON TABLES TO PUBLIC", + 35, + 36, + ), + ] { + let statements = Parser::parse_sql(&PostgreSqlDialect {}, sql).unwrap(); + assert_eq!( + statements[0].span(), + Span::new(Location::new(1, start), Location::new(1, end)), + "span for {sql}" + ); + } + let sql = "ALTER DEFAULT PRIVILEGES REVOKE SELECT ON TABLES FROM 'bob'@'localhost'"; + let statements = Parser::parse_sql(&sqlparser::dialect::GenericDialect {}, sql).unwrap(); + assert_eq!( + statements[0].span(), + Span::new(Location::new(1, 55), Location::new(1, 72)) + ); +} + #[test] fn parse_delimited_identifiers() { // check that quoted identifiers in any position remain quoted after serialization