From d7e84be3e14697f70dd57cc4e154d2ba7d3d7dfc Mon Sep 17 00:00:00 2001 From: Ben Cook Date: Wed, 8 Dec 2021 12:55:23 -0800 Subject: [PATCH] Add basic support for GRANT and REVOKE (#365) * Add basic support for GRANT privileges on tables and sequences * Cargo fmt * Make enum for granted privileges * Implement and use Display for GrantObjects * Simplify Display for GrantPrivileges * Add column privileges * Add second column privilege to test * Add REVOKE privileges and reformat * Fix some clippy warnings * Fix more clippy Co-authored-by: Andrew Lamb --- src/ast/mod.rs | 181 +++++++++++++++++++++++++++++++++++ src/keywords.rs | 7 ++ src/parser.rs | 144 ++++++++++++++++++++++++++++ tests/sqlparser_common.rs | 196 ++++++++++++++++++++++++++++++++++++++ 4 files changed, 528 insertions(+) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index dac02460..ca25844b 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -763,6 +763,22 @@ pub enum Statement { condition: Expr, message: Option, }, + /// GRANT privileges ON objects TO grantees + Grant { + privileges: Privileges, + objects: GrantObjects, + grantees: Vec, + with_grant_option: bool, + granted_by: Option, + }, + /// REVOKE privileges ON objects FROM grantees + Revoke { + privileges: Privileges, + objects: GrantObjects, + grantees: Vec, + granted_by: Option, + cascade: bool, + }, /// `DEALLOCATE [ PREPARE ] { name | ALL }` /// /// Note: this is a PostgreSQL-specific statement. @@ -1330,6 +1346,40 @@ impl fmt::Display for Statement { } Ok(()) } + Statement::Grant { + privileges, + objects, + grantees, + with_grant_option, + granted_by, + } => { + write!(f, "GRANT {} ", privileges)?; + write!(f, "ON {} ", objects)?; + write!(f, "TO {}", display_comma_separated(grantees))?; + if *with_grant_option { + write!(f, " WITH GRANT OPTION")?; + } + if let Some(grantor) = granted_by { + write!(f, " GRANTED BY {}", grantor)?; + } + Ok(()) + } + Statement::Revoke { + privileges, + objects, + grantees, + granted_by, + cascade, + } => { + write!(f, "REVOKE {} ", privileges)?; + write!(f, "ON {} ", objects)?; + write!(f, "FROM {}", display_comma_separated(grantees))?; + if let Some(grantor) = granted_by { + write!(f, " GRANTED BY {}", grantor)?; + } + write!(f, " {}", if *cascade { "CASCADE" } else { "RESTRICT" })?; + Ok(()) + } Statement::Deallocate { name, prepare } => write!( f, "DEALLOCATE {prepare}{name}", @@ -1358,6 +1408,137 @@ impl fmt::Display for Statement { } } +/// Privileges granted in a GRANT statement or revoked in a REVOKE statement. +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub enum Privileges { + /// All privileges applicable to the object type + All { + /// Optional keyword from the spec, ignored in practice + with_privileges_keyword: bool, + }, + /// Specific privileges (e.g. `SELECT`, `INSERT`) + Actions(Vec), +} + +impl fmt::Display for Privileges { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + Privileges::All { + with_privileges_keyword, + } => { + write!( + f, + "ALL{}", + if *with_privileges_keyword { + " PRIVILEGES" + } else { + "" + } + ) + } + Privileges::Actions(actions) => { + write!(f, "{}", display_comma_separated(actions).to_string()) + } + } + } +} + +/// A privilege on a database object (table, sequence, etc.). +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub enum Action { + Connect, + Create, + Delete, + Execute, + Insert { columns: Option> }, + References { columns: Option> }, + Select { columns: Option> }, + Temporary, + Trigger, + Truncate, + Update { columns: Option> }, + Usage, +} + +impl fmt::Display for Action { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + Action::Connect => f.write_str("CONNECT")?, + Action::Create => f.write_str("CREATE")?, + Action::Delete => f.write_str("DELETE")?, + Action::Execute => f.write_str("EXECUTE")?, + Action::Insert { .. } => f.write_str("INSERT")?, + Action::References { .. } => f.write_str("REFERENCES")?, + Action::Select { .. } => f.write_str("SELECT")?, + Action::Temporary => f.write_str("TEMPORARY")?, + Action::Trigger => f.write_str("TRIGGER")?, + Action::Truncate => f.write_str("TRUNCATE")?, + Action::Update { .. } => f.write_str("UPDATE")?, + Action::Usage => f.write_str("USAGE")?, + }; + match self { + Action::Insert { columns } + | Action::References { columns } + | Action::Select { columns } + | Action::Update { columns } => { + if let Some(columns) = columns { + write!(f, " ({})", display_comma_separated(columns))?; + } + } + _ => (), + }; + Ok(()) + } +} + +/// Objects on which privileges are granted in a GRANT statement. +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub enum GrantObjects { + /// Grant privileges on `ALL SEQUENCES IN SCHEMA [, ...]` + AllSequencesInSchema { schemas: Vec }, + /// Grant privileges on `ALL TABLES IN SCHEMA [, ...]` + AllTablesInSchema { schemas: Vec }, + /// Grant privileges on specific schemas + Schemas(Vec), + /// Grant privileges on specific sequences + Sequences(Vec), + /// Grant privileges on specific tables + Tables(Vec), +} + +impl fmt::Display for GrantObjects { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + GrantObjects::Sequences(sequences) => { + write!(f, "SEQUENCE {}", display_comma_separated(sequences)) + } + GrantObjects::Schemas(schemas) => { + write!(f, "SCHEMA {}", display_comma_separated(schemas)) + } + GrantObjects::Tables(tables) => { + write!(f, "{}", display_comma_separated(tables)) + } + GrantObjects::AllSequencesInSchema { schemas } => { + write!( + f, + "ALL SEQUENCES IN SCHEMA {}", + display_comma_separated(schemas) + ) + } + GrantObjects::AllTablesInSchema { schemas } => { + write!( + f, + "ALL TABLES IN SCHEMA {}", + display_comma_separated(schemas) + ) + } + } + } +} + /// SQL assignment `foo = expr` as used in SQLUpdate #[derive(Debug, Clone, PartialEq, Eq, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] diff --git a/src/keywords.rs b/src/keywords.rs index 8f73836c..2bbdbfae 100644 --- a/src/keywords.rs +++ b/src/keywords.rs @@ -225,6 +225,7 @@ define_keywords!( GET, GLOBAL, GRANT, + GRANTED, GROUP, GROUPING, GROUPS, @@ -318,6 +319,7 @@ define_keywords!( ON, ONLY, OPEN, + OPTION, OR, ORC, ORDER, @@ -348,6 +350,7 @@ define_keywords!( PRECISION, PREPARE, PRIMARY, + PRIVILEGES, PROCEDURE, PURGE, RANGE, @@ -395,7 +398,9 @@ define_keywords!( SECOND, SELECT, SENSITIVE, + SEQUENCE, SEQUENCEFILE, + SEQUENCES, SERDE, SERIALIZABLE, SESSION, @@ -432,6 +437,7 @@ define_keywords!( SYSTEM_TIME, SYSTEM_USER, TABLE, + TABLES, TABLESAMPLE, TBLPROPERTIES, TEMP, @@ -468,6 +474,7 @@ define_keywords!( UNNEST, UPDATE, UPPER, + USAGE, USER, USING, UUID, diff --git a/src/parser.rs b/src/parser.rs index 8711cdd7..3f8ca2c5 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -156,6 +156,8 @@ impl<'a> Parser<'a> { Keyword::COPY => Ok(self.parse_copy()?), Keyword::SET => Ok(self.parse_set()?), Keyword::SHOW => Ok(self.parse_show()?), + Keyword::GRANT => Ok(self.parse_grant()?), + Keyword::REVOKE => Ok(self.parse_revoke()?), Keyword::START => Ok(self.parse_start_transaction()?), // `BEGIN` is a nonstandard but common alias for the // standard `START TRANSACTION` statement. It is supported @@ -2882,6 +2884,148 @@ impl<'a> Parser<'a> { } } + /// Parse a GRANT statement. + pub fn parse_grant(&mut self) -> Result { + let (privileges, objects) = self.parse_grant_revoke_privileges_objects()?; + + self.expect_keyword(Keyword::TO)?; + let grantees = self.parse_comma_separated(Parser::parse_identifier)?; + + let with_grant_option = + self.parse_keywords(&[Keyword::WITH, Keyword::GRANT, Keyword::OPTION]); + + let granted_by = self + .parse_keywords(&[Keyword::GRANTED, Keyword::BY]) + .then(|| self.parse_identifier().unwrap()); + + Ok(Statement::Grant { + privileges, + objects, + grantees, + with_grant_option, + granted_by, + }) + } + + fn parse_grant_revoke_privileges_objects( + &mut self, + ) -> Result<(Privileges, GrantObjects), ParserError> { + let privileges = if self.parse_keyword(Keyword::ALL) { + Privileges::All { + with_privileges_keyword: self.parse_keyword(Keyword::PRIVILEGES), + } + } else { + Privileges::Actions( + self.parse_comma_separated(Parser::parse_grant_permission)? + .into_iter() + .map(|(kw, columns)| match kw { + Keyword::DELETE => Action::Delete, + Keyword::INSERT => Action::Insert { columns }, + Keyword::REFERENCES => Action::References { columns }, + Keyword::SELECT => Action::Select { columns }, + Keyword::TRIGGER => Action::Trigger, + Keyword::TRUNCATE => Action::Truncate, + Keyword::UPDATE => Action::Update { columns }, + Keyword::USAGE => Action::Usage, + _ => unreachable!(), + }) + .collect(), + ) + }; + + self.expect_keyword(Keyword::ON)?; + + let objects = if self.parse_keywords(&[ + Keyword::ALL, + Keyword::TABLES, + Keyword::IN, + Keyword::SCHEMA, + ]) { + GrantObjects::AllTablesInSchema { + schemas: self.parse_comma_separated(Parser::parse_object_name)?, + } + } else if self.parse_keywords(&[ + Keyword::ALL, + Keyword::SEQUENCES, + Keyword::IN, + Keyword::SCHEMA, + ]) { + GrantObjects::AllSequencesInSchema { + schemas: self.parse_comma_separated(Parser::parse_object_name)?, + } + } else { + let object_type = + self.parse_one_of_keywords(&[Keyword::SEQUENCE, Keyword::SCHEMA, Keyword::TABLE]); + let objects = self.parse_comma_separated(Parser::parse_object_name); + match object_type { + Some(Keyword::SCHEMA) => GrantObjects::Schemas(objects?), + Some(Keyword::SEQUENCE) => GrantObjects::Sequences(objects?), + Some(Keyword::TABLE) | None => GrantObjects::Tables(objects?), + _ => unreachable!(), + } + }; + + Ok((privileges, objects)) + } + + fn parse_grant_permission(&mut self) -> Result<(Keyword, Option>), ParserError> { + if let Some(kw) = self.parse_one_of_keywords(&[ + Keyword::CONNECT, + Keyword::CREATE, + Keyword::DELETE, + Keyword::EXECUTE, + Keyword::INSERT, + Keyword::REFERENCES, + Keyword::SELECT, + Keyword::TEMPORARY, + Keyword::TRIGGER, + Keyword::TRUNCATE, + Keyword::UPDATE, + Keyword::USAGE, + ]) { + let columns = match kw { + Keyword::INSERT | Keyword::REFERENCES | Keyword::SELECT | Keyword::UPDATE => { + let columns = self.parse_parenthesized_column_list(Optional)?; + if columns.is_empty() { + None + } else { + Some(columns) + } + } + _ => None, + }; + Ok((kw, columns)) + } else { + self.expected("a privilege keyword", self.peek_token())? + } + } + + /// Parse a REVOKE statement + pub fn parse_revoke(&mut self) -> Result { + let (privileges, objects) = self.parse_grant_revoke_privileges_objects()?; + + self.expect_keyword(Keyword::FROM)?; + let grantees = self.parse_comma_separated(Parser::parse_identifier)?; + + let granted_by = self + .parse_keywords(&[Keyword::GRANTED, Keyword::BY]) + .then(|| self.parse_identifier().unwrap()); + + let cascade = self.parse_keyword(Keyword::CASCADE); + let restrict = self.parse_keyword(Keyword::RESTRICT); + if cascade && restrict { + return parser_err!("Cannot specify both CASCADE and RESTRICT in REVOKE"); + } + + Ok(Statement::Revoke { + privileges, + objects, + grantees, + granted_by, + cascade, + }) + } + /// Parse an INSERT statement pub fn parse_insert(&mut self) -> Result { let or = if !dialect_of!(self is SQLiteDialect) { diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 65b7869d..02a500cc 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -3589,6 +3589,202 @@ fn parse_drop_index() { } } +#[test] +fn parse_grant() { + let sql = "GRANT SELECT, INSERT, UPDATE (shape, size), USAGE, DELETE, TRUNCATE, REFERENCES, TRIGGER ON abc, def TO xyz, m WITH GRANT OPTION GRANTED BY jj"; + match verified_stmt(sql) { + Statement::Grant { + privileges, + objects, + grantees, + with_grant_option, + granted_by, + .. + } => match (privileges, objects) { + (Privileges::Actions(actions), GrantObjects::Tables(objects)) => { + assert_eq!( + vec![ + Action::Select { columns: None }, + Action::Insert { columns: None }, + Action::Update { + columns: Some(vec![ + Ident { + value: "shape".into(), + quote_style: None + }, + Ident { + value: "size".into(), + quote_style: None + } + ]) + }, + Action::Usage, + Action::Delete, + Action::Truncate, + Action::References { columns: None }, + Action::Trigger, + ], + actions + ); + assert_eq!( + vec!["abc", "def"], + objects.iter().map(ToString::to_string).collect::>() + ); + assert_eq!( + vec!["xyz", "m"], + grantees.iter().map(ToString::to_string).collect::>() + ); + assert!(with_grant_option); + assert_eq!("jj", granted_by.unwrap().to_string()); + } + _ => unreachable!(), + }, + _ => unreachable!(), + } + + let sql2 = "GRANT INSERT ON ALL TABLES IN SCHEMA public TO browser"; + match verified_stmt(sql2) { + Statement::Grant { + privileges, + objects, + grantees, + with_grant_option, + .. + } => match (privileges, objects) { + (Privileges::Actions(actions), GrantObjects::AllTablesInSchema { schemas }) => { + assert_eq!(vec![Action::Insert { columns: None }], actions); + assert_eq!( + vec!["public"], + schemas.iter().map(ToString::to_string).collect::>() + ); + assert_eq!( + vec!["browser"], + grantees.iter().map(ToString::to_string).collect::>() + ); + assert!(!with_grant_option); + } + _ => unreachable!(), + }, + _ => unreachable!(), + } + + let sql3 = "GRANT USAGE, SELECT ON SEQUENCE p TO u"; + match verified_stmt(sql3) { + Statement::Grant { + privileges, + objects, + grantees, + granted_by, + .. + } => match (privileges, objects, granted_by) { + (Privileges::Actions(actions), GrantObjects::Sequences(objects), None) => { + assert_eq!( + vec![Action::Usage, Action::Select { columns: None }], + actions + ); + assert_eq!( + vec!["p"], + objects.iter().map(ToString::to_string).collect::>() + ); + assert_eq!( + vec!["u"], + grantees.iter().map(ToString::to_string).collect::>() + ); + } + _ => unreachable!(), + }, + _ => unreachable!(), + } + + let sql4 = "GRANT ALL PRIVILEGES ON aa, b TO z"; + match verified_stmt(sql4) { + Statement::Grant { privileges, .. } => { + assert_eq!( + Privileges::All { + with_privileges_keyword: true + }, + privileges + ); + } + _ => unreachable!(), + } + + let sql5 = "GRANT ALL ON SCHEMA aa, b TO z"; + match verified_stmt(sql5) { + Statement::Grant { + privileges, + objects, + .. + } => match (privileges, objects) { + ( + Privileges::All { + with_privileges_keyword, + }, + GrantObjects::Schemas(schemas), + ) => { + assert!(!with_privileges_keyword); + assert_eq!( + vec!["aa", "b"], + schemas.iter().map(ToString::to_string).collect::>() + ); + } + _ => unreachable!(), + }, + _ => unreachable!(), + } + + let sql6 = "GRANT USAGE ON ALL SEQUENCES IN SCHEMA bus TO a, beta WITH GRANT OPTION"; + match verified_stmt(sql6) { + Statement::Grant { + privileges, + objects, + .. + } => match (privileges, objects) { + (Privileges::Actions(actions), GrantObjects::AllSequencesInSchema { schemas }) => { + assert_eq!(vec![Action::Usage], actions); + assert_eq!( + vec!["bus"], + schemas.iter().map(ToString::to_string).collect::>() + ); + } + _ => unreachable!(), + }, + _ => unreachable!(), + } +} + +#[test] +fn test_revoke() { + let sql = "REVOKE ALL PRIVILEGES ON users, auth FROM analyst CASCADE"; + match verified_stmt(sql) { + Statement::Revoke { + privileges, + objects: GrantObjects::Tables(tables), + grantees, + cascade, + granted_by, + } => { + assert_eq!( + Privileges::All { + with_privileges_keyword: true + }, + privileges + ); + assert_eq!( + vec!["users", "auth"], + tables.iter().map(ToString::to_string).collect::>() + ); + assert_eq!( + vec!["analyst"], + grantees.iter().map(ToString::to_string).collect::>() + ); + assert!(cascade); + assert_eq!(None, granted_by); + } + _ => unreachable!(), + } +} + #[test] fn all_keywords_sorted() { // assert!(ALL_KEYWORDS.is_sorted())