Add support for DENY statements (#1836)

This commit is contained in:
Andrew Harper 2025-05-14 03:21:23 -04:00 committed by GitHub
parent 178a351812
commit 74a95fdbd1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 185 additions and 15 deletions

View file

@ -3903,9 +3903,14 @@ pub enum Statement {
objects: Option<GrantObjects>,
grantees: Vec<Grantee>,
with_grant_option: bool,
as_grantor: Option<Ident>,
granted_by: Option<Ident>,
},
/// ```sql
/// DENY privileges ON object TO grantees
/// ```
Deny(DenyStatement),
/// ```sql
/// REVOKE privileges ON objects FROM grantees
/// ```
Revoke {
@ -5580,6 +5585,7 @@ impl fmt::Display for Statement {
objects,
grantees,
with_grant_option,
as_grantor,
granted_by,
} => {
write!(f, "GRANT {privileges} ")?;
@ -5590,11 +5596,15 @@ impl fmt::Display for Statement {
if *with_grant_option {
write!(f, " WITH GRANT OPTION")?;
}
if let Some(grantor) = as_grantor {
write!(f, " AS {grantor}")?;
}
if let Some(grantor) = granted_by {
write!(f, " GRANTED BY {grantor}")?;
}
Ok(())
}
Statement::Deny(s) => write!(f, "{s}"),
Statement::Revoke {
privileges,
objects,
@ -6380,6 +6390,9 @@ pub enum Action {
},
Delete,
EvolveSchema,
Exec {
obj_type: Option<ActionExecuteObjectType>,
},
Execute {
obj_type: Option<ActionExecuteObjectType>,
},
@ -6446,6 +6459,12 @@ impl fmt::Display for Action {
Action::DatabaseRole { role } => write!(f, "DATABASE ROLE {role}")?,
Action::Delete => f.write_str("DELETE")?,
Action::EvolveSchema => f.write_str("EVOLVE SCHEMA")?,
Action::Exec { obj_type } => {
f.write_str("EXEC")?;
if let Some(obj_type) = obj_type {
write!(f, " {obj_type}")?
}
}
Action::Execute { obj_type } => {
f.write_str("EXECUTE")?;
if let Some(obj_type) = obj_type {
@ -6867,6 +6886,37 @@ impl fmt::Display for GrantObjects {
}
}
/// A `DENY` statement
///
/// [MsSql](https://learn.microsoft.com/en-us/sql/t-sql/statements/deny-transact-sql)
#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))]
pub struct DenyStatement {
pub privileges: Privileges,
pub objects: GrantObjects,
pub grantees: Vec<Grantee>,
pub granted_by: Option<Ident>,
pub cascade: Option<CascadeOption>,
}
impl fmt::Display for DenyStatement {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "DENY {}", self.privileges)?;
write!(f, " ON {}", self.objects)?;
if !self.grantees.is_empty() {
write!(f, " TO {}", display_comma_separated(&self.grantees))?;
}
if let Some(cascade) = &self.cascade {
write!(f, " {cascade}")?;
}
if let Some(granted_by) = &self.granted_by {
write!(f, " AS {granted_by}")?;
}
Ok(())
}
}
/// SQL assignment `foo = expr` as used in SQLUpdate
#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]

View file

@ -491,6 +491,7 @@ impl Spanned for Statement {
Statement::CreateStage { .. } => Span::empty(),
Statement::Assert { .. } => Span::empty(),
Statement::Grant { .. } => Span::empty(),
Statement::Deny { .. } => Span::empty(),
Statement::Revoke { .. } => Span::empty(),
Statement::Deallocate { .. } => Span::empty(),
Statement::Execute { .. } => Span::empty(),

View file

@ -49,7 +49,7 @@ pub use self::postgresql::PostgreSqlDialect;
pub use self::redshift::RedshiftSqlDialect;
pub use self::snowflake::SnowflakeDialect;
pub use self::sqlite::SQLiteDialect;
use crate::ast::{ColumnOption, Expr, Statement};
use crate::ast::{ColumnOption, Expr, GranteesType, Statement};
pub use crate::keywords;
use crate::keywords::Keyword;
use crate::parser::{Parser, ParserError};
@ -910,6 +910,11 @@ pub trait Dialect: Debug + Any {
&[]
}
/// Returns grantee types that should be treated as identifiers
fn get_reserved_grantees_types(&self) -> &[GranteesType] {
&[]
}
/// Returns true if this dialect supports the `TABLESAMPLE` option
/// before the table alias option. For example:
///

View file

@ -17,8 +17,8 @@
use crate::ast::helpers::attached_token::AttachedToken;
use crate::ast::{
BeginEndStatements, ConditionalStatementBlock, ConditionalStatements, IfStatement, Statement,
TriggerObject,
BeginEndStatements, ConditionalStatementBlock, ConditionalStatements, GranteesType,
IfStatement, Statement, TriggerObject,
};
use crate::dialect::Dialect;
use crate::keywords::{self, Keyword};
@ -52,6 +52,10 @@ impl Dialect for MsSqlDialect {
|| ch == '_'
}
fn identifier_quote_style(&self, _identifier: &str) -> Option<char> {
Some('[')
}
/// SQL Server has `CONVERT(type, value)` instead of `CONVERT(value, type)`
/// <https://learn.microsoft.com/en-us/sql/t-sql/functions/cast-and-convert-transact-sql?view=sql-server-ver16>
fn convert_type_before_value(&self) -> bool {
@ -119,6 +123,11 @@ impl Dialect for MsSqlDialect {
true
}
/// See <https://learn.microsoft.com/en-us/sql/relational-databases/security/authentication-access/server-level-roles>
fn get_reserved_grantees_types(&self) -> &[GranteesType] {
&[GranteesType::Public]
}
fn is_column_alias(&self, kw: &Keyword, _parser: &mut Parser) -> bool {
!keywords::RESERVED_FOR_COLUMN_ALIAS.contains(kw) && !RESERVED_FOR_COLUMN_ALIAS.contains(kw)
}

View file

@ -278,6 +278,7 @@ define_keywords!(
DELIMITER,
DELTA,
DENSE_RANK,
DENY,
DEREF,
DESC,
DESCRIBE,

View file

@ -583,6 +583,10 @@ impl<'a> Parser<'a> {
Keyword::SHOW => self.parse_show(),
Keyword::USE => self.parse_use(),
Keyword::GRANT => self.parse_grant(),
Keyword::DENY => {
self.prev_token();
self.parse_deny()
}
Keyword::REVOKE => self.parse_revoke(),
Keyword::START => self.parse_start_transaction(),
Keyword::BEGIN => self.parse_begin(),
@ -13381,7 +13385,7 @@ impl<'a> Parser<'a> {
/// Parse a GRANT statement.
pub fn parse_grant(&mut self) -> Result<Statement, ParserError> {
let (privileges, objects) = self.parse_grant_revoke_privileges_objects()?;
let (privileges, objects) = self.parse_grant_deny_revoke_privileges_objects()?;
self.expect_keyword_is(Keyword::TO)?;
let grantees = self.parse_grantees()?;
@ -13389,15 +13393,24 @@ impl<'a> Parser<'a> {
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());
let as_grantor = if self.parse_keywords(&[Keyword::AS]) {
Some(self.parse_identifier()?)
} else {
None
};
let granted_by = if self.parse_keywords(&[Keyword::GRANTED, Keyword::BY]) {
Some(self.parse_identifier()?)
} else {
None
};
Ok(Statement::Grant {
privileges,
objects,
grantees,
with_grant_option,
as_grantor,
granted_by,
})
}
@ -13406,7 +13419,7 @@ impl<'a> Parser<'a> {
let mut values = vec![];
let mut grantee_type = GranteesType::None;
loop {
grantee_type = if self.parse_keyword(Keyword::ROLE) {
let new_grantee_type = if self.parse_keyword(Keyword::ROLE) {
GranteesType::Role
} else if self.parse_keyword(Keyword::USER) {
GranteesType::User
@ -13423,9 +13436,19 @@ impl<'a> Parser<'a> {
} else if self.parse_keyword(Keyword::APPLICATION) {
GranteesType::Application
} else {
grantee_type // keep from previous iteraton, if not specified
grantee_type.clone() // keep from previous iteraton, if not specified
};
if self
.dialect
.get_reserved_grantees_types()
.contains(&new_grantee_type)
{
self.prev_token();
} else {
grantee_type = new_grantee_type;
}
let grantee = if grantee_type == GranteesType::Public {
Grantee {
grantee_type: grantee_type.clone(),
@ -13460,7 +13483,7 @@ impl<'a> Parser<'a> {
Ok(values)
}
pub fn parse_grant_revoke_privileges_objects(
pub fn parse_grant_deny_revoke_privileges_objects(
&mut self,
) -> Result<(Privileges, Option<GrantObjects>), ParserError> {
let privileges = if self.parse_keyword(Keyword::ALL) {
@ -13510,7 +13533,6 @@ impl<'a> Parser<'a> {
let object_type = self.parse_one_of_keywords(&[
Keyword::SEQUENCE,
Keyword::DATABASE,
Keyword::DATABASE,
Keyword::SCHEMA,
Keyword::TABLE,
Keyword::VIEW,
@ -13605,6 +13627,9 @@ impl<'a> Parser<'a> {
Ok(Action::Create { obj_type })
} else if self.parse_keyword(Keyword::DELETE) {
Ok(Action::Delete)
} else if self.parse_keyword(Keyword::EXEC) {
let obj_type = self.maybe_parse_action_execute_obj_type();
Ok(Action::Exec { obj_type })
} else if self.parse_keyword(Keyword::EXECUTE) {
let obj_type = self.maybe_parse_action_execute_obj_type();
Ok(Action::Execute { obj_type })
@ -13803,16 +13828,51 @@ impl<'a> Parser<'a> {
}
}
/// Parse [`Statement::Deny`]
pub fn parse_deny(&mut self) -> Result<Statement, ParserError> {
self.expect_keyword(Keyword::DENY)?;
let (privileges, objects) = self.parse_grant_deny_revoke_privileges_objects()?;
let objects = match objects {
Some(o) => o,
None => {
return parser_err!(
"DENY statements must specify an object",
self.peek_token().span.start
)
}
};
self.expect_keyword_is(Keyword::TO)?;
let grantees = self.parse_grantees()?;
let cascade = self.parse_cascade_option();
let granted_by = if self.parse_keywords(&[Keyword::AS]) {
Some(self.parse_identifier()?)
} else {
None
};
Ok(Statement::Deny(DenyStatement {
privileges,
objects,
grantees,
cascade,
granted_by,
}))
}
/// Parse a REVOKE statement
pub fn parse_revoke(&mut self) -> Result<Statement, ParserError> {
let (privileges, objects) = self.parse_grant_revoke_privileges_objects()?;
let (privileges, objects) = self.parse_grant_deny_revoke_privileges_objects()?;
self.expect_keyword_is(Keyword::FROM)?;
let grantees = self.parse_grantees()?;
let granted_by = self
.parse_keywords(&[Keyword::GRANTED, Keyword::BY])
.then(|| self.parse_identifier().unwrap());
let granted_by = if self.parse_keywords(&[Keyword::GRANTED, Keyword::BY]) {
Some(self.parse_identifier()?)
} else {
None
};
let cascade = self.parse_cascade_option();

View file

@ -9331,6 +9331,39 @@ fn parse_grant() {
verified_stmt("GRANT USAGE ON WAREHOUSE wh1 TO ROLE role1");
verified_stmt("GRANT OWNERSHIP ON INTEGRATION int1 TO ROLE role1");
verified_stmt("GRANT SELECT ON VIEW view1 TO ROLE role1");
verified_stmt("GRANT EXEC ON my_sp TO runner");
verified_stmt("GRANT UPDATE ON my_table TO updater_role AS dbo");
all_dialects_where(|d| d.identifier_quote_style("none") == Some('['))
.verified_stmt("GRANT SELECT ON [my_table] TO [public]");
}
#[test]
fn parse_deny() {
let sql = "DENY INSERT, DELETE ON users TO analyst CASCADE AS admin";
match verified_stmt(sql) {
Statement::Deny(deny) => {
assert_eq!(
Privileges::Actions(vec![Action::Insert { columns: None }, Action::Delete]),
deny.privileges
);
assert_eq!(
&GrantObjects::Tables(vec![ObjectName::from(vec![Ident::new("users")])]),
&deny.objects
);
assert_eq_vec(&["analyst"], &deny.grantees);
assert_eq!(Some(CascadeOption::Cascade), deny.cascade);
assert_eq!(Some(Ident::from("admin")), deny.granted_by);
}
_ => unreachable!(),
}
verified_stmt("DENY SELECT, INSERT, UPDATE, DELETE ON db1.sc1 TO role1, role2");
verified_stmt("DENY ALL ON db1.sc1 TO role1");
verified_stmt("DENY EXEC ON my_sp TO runner");
all_dialects_where(|d| d.identifier_quote_style("none") == Some('['))
.verified_stmt("DENY SELECT ON [my_table] TO [public]");
}
#[test]

View file

@ -2340,3 +2340,13 @@ fn parse_print() {
let _ = ms().verified_stmt("PRINT N'Hello, ⛄️!'");
let _ = ms().verified_stmt("PRINT @my_variable");
}
#[test]
fn parse_mssql_grant() {
ms().verified_stmt("GRANT SELECT ON my_table TO public, db_admin");
}
#[test]
fn parse_mssql_deny() {
ms().verified_stmt("DENY SELECT ON my_table TO public, db_admin");
}

View file

@ -3538,6 +3538,7 @@ fn parse_grant() {
objects,
grantees,
with_grant_option,
as_grantor: _,
granted_by,
} = stmt
{