Skip to content
Merged
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_option_for.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
REVOKE ADMIN OPTION FOR bypasser FROM anon GRANTED BY admin RESTRICT
10 changes: 8 additions & 2 deletions src/ast/dcl.rs
Original file line number Diff line number Diff line change
Expand Up @@ -536,13 +536,15 @@ impl From<Revoke> for crate::ast::Statement {
}
}

/// `REVOKE role [, ...] FROM grantee [, ...] [GRANTED BY grantor] [CASCADE | RESTRICT]`
/// `REVOKE [option OPTION FOR] 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 {
/// The option named in `OPTION FOR`, such as `ADMIN`, withdrawn while the membership stays.
pub option_for: Option<Ident>,
/// Roles whose membership is revoked.
pub roles: Vec<Ident>,
/// Roles losing the membership.
Expand All @@ -555,9 +557,13 @@ pub struct RevokeRole {

impl fmt::Display for RevokeRole {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "REVOKE ")?;
if let Some(option_for) = &self.option_for {
write!(f, "{option_for} OPTION FOR ")?;
}
write!(
f,
"REVOKE {} FROM {}",
"{} FROM {}",
display_comma_separated(&self.roles),
display_comma_separated(&self.grantees)
)?;
Expand Down
39 changes: 34 additions & 5 deletions src/parser/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -721,11 +721,17 @@ impl<'a> Parser<'a> {
self.prev_token();
self.parse_deny()
}
Keyword::REVOKE => self.parse_privileges_or_roles(
Keyword::FROM,
Parser::parse_revoke,
Parser::parse_revoke_role,
),
Keyword::REVOKE => {
if self.peek_role_grant_option_for() {
self.parse_revoke_role().map(Into::into)
} else {
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 @@ -19095,6 +19101,7 @@ 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 option_for = self.parse_role_grant_option_for();
let roles = self.parse_comma_separated(Parser::parse_identifier)?;
self.expect_keyword_is(Keyword::FROM)?;
let grantees = self.parse_grantees()?;
Expand All @@ -19105,13 +19112,35 @@ impl<'a> Parser<'a> {
};
let cascade = self.parse_cascade_option();
Ok(RevokeRole {
option_for,
roles,
grantees,
granted_by,
cascade,
})
}

/// Parses `name OPTION FOR`, leaving `GRANT OPTION FOR` to the privilege form.
fn parse_role_grant_option_for(&mut self) -> Option<Ident> {
let index = self.index;
if !self.peek_keyword(Keyword::GRANT) {
if let Ok(name) = self.parse_identifier() {
if self.parse_keywords(&[Keyword::OPTION, Keyword::FOR]) {
return Some(name);
}
}
}
self.index = index;
None
}

fn peek_role_grant_option_for(&mut self) -> bool {
let index = self.index;
let matched = self.parse_role_grant_option_for().is_some();
self.index = index;
matched
}

/// Parse an REPLACE statement
pub fn parse_replace(
&mut self,
Expand Down
51 changes: 51 additions & 0 deletions tests/sqlparser_common.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22531,6 +22531,7 @@ fn parse_revoke_role() {
assert_eq!(
all_dialects().verified_stmt(sql),
Statement::RevokeRole(RevokeRole {
option_for: None,
roles: vec![Ident::new("bypasser"), Ident::new("auditor")],
grantees: vec![Grantee {
grantee_type: GranteesType::None,
Expand Down Expand Up @@ -22615,3 +22616,53 @@ fn display_revoke_role_propagates_write_errors() {
assert_eq!(write!(writer, "{statement}"), Err(fmt::Error));
}
}

#[test]
fn parse_revoke_role_option_for() {
let Statement::RevokeRole(RevokeRole { option_for, .. }) =
all_dialects().verified_stmt("REVOKE SET OPTION FOR bypasser FROM anon")
else {
panic!("Expected REVOKE role");
};
assert_eq!(option_for, Some(Ident::new("SET")));

all_dialects()
.verified_stmt("REVOKE ADMIN OPTION FOR bypasser FROM anon GRANTED BY admin RESTRICT");
all_dialects().verified_stmt("REVOKE INHERIT OPTION FOR bypasser, auditor FROM anon");

assert_eq!(
all_dialects()
.parse_sql_statements("REVOKE ADMIN OPTION FOR bypasser TO anon")
.unwrap_err(),
ParserError::ParserError("Expected: FROM, found: TO".to_string())
);
assert_eq!(
all_dialects()
.parse_sql_statements("REVOKE 1 FROM anon")
.unwrap_err(),
ParserError::ParserError("Expected: a privilege keyword, found: 1".to_string())
);
}

#[test]
fn display_revoke_role_option_for_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 ADMIN OPTION FOR bypasser FROM anon");
for rejected in ["REVOKE ", "ADMIN", " OPTION FOR "] {
let mut writer = RejectingWriter(rejected);
assert_eq!(write!(writer, "{statement}"), Err(fmt::Error));
}
}
Loading