Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions fuzz/fuzz_seeds/postgres_revoke_role_membership.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
REVOKE bypasser, auditor FROM anon GRANTED BY admin CASCADE
41 changes: 41 additions & 0 deletions src/ast/dcl.rs
Original file line number Diff line number Diff line change
Expand Up @@ -536,6 +536,47 @@ impl From<Revoke> 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<Ident>,
/// Roles losing the membership.
pub grantees: Vec<Grantee>,
/// Optional `GRANTED BY` identifier.
pub granted_by: Option<Ident>,
/// Optional `CASCADE`/`RESTRICT` behavior.
pub cascade: Option<CascadeOption>,
}

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<RevokeRole> 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)
Expand Down
9 changes: 7 additions & 2 deletions src/ast/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -4681,6 +4681,10 @@ pub enum Statement {
/// ```
Revoke(Revoke),
/// ```sql
/// REVOKE roles FROM grantees
/// ```
RevokeRole(RevokeRole),
/// ```sql
/// DEALLOCATE [ PREPARE ] { name | ALL }
/// ```
///
Expand Down Expand Up @@ -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}",
Expand Down
2 changes: 2 additions & 0 deletions src/ast/spans.rs
Original file line number Diff line number Diff line change
Expand Up @@ -303,6 +303,7 @@ impl Spanned for Values {
/// - [Statement::Grant]
/// - [Statement::Revoke]
/// - [Statement::GrantRole]
/// - [Statement::RevokeRole]
/// - [Statement::Deallocate]
/// - [Statement::Execute]
/// - [Statement::Prepare]
Expand Down Expand Up @@ -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(),
Expand Down
25 changes: 24 additions & 1 deletion src/parser/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand Down Expand Up @@ -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<RevokeRole, ParserError> {
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,
Expand Down
91 changes: 91 additions & 0 deletions tests/sqlparser_common.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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));
}
}
Loading