Support CREATE ROLE and DROP ROLE (#598)

* Parse GRANT ROLE and DROP ROLE

* Gate create role on dialect

* cargo fmt

* clippy

* no-std

* clippy again
This commit is contained in:
Ben Cook 2022-09-27 06:58:36 -07:00 committed by GitHub
parent 604f755a59
commit 91087fcba0
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 543 additions and 42 deletions

View file

@ -954,6 +954,13 @@ impl fmt::Display for CommentObject {
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub enum Password {
Password(Expr),
NullPassword,
}
/// A top-level statement (SELECT, INSERT, CREATE, etc.)
#[allow(clippy::large_enum_variant)]
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
@ -1109,6 +1116,27 @@ pub enum Statement {
unique: bool,
if_not_exists: bool,
},
/// CREATE ROLE
CreateRole {
names: Vec<ObjectName>,
if_not_exists: bool,
// Postgres
login: Option<bool>,
inherit: Option<bool>,
bypassrls: Option<bool>,
password: Option<Password>,
superuser: Option<bool>,
create_db: Option<bool>,
create_role: Option<bool>,
replication: Option<bool>,
connection_limit: Option<Expr>,
valid_until: Option<Expr>,
in_role: Vec<Ident>,
role: Vec<Ident>,
admin: Vec<Ident>,
// MSSQL
authorization_owner: Option<ObjectName>,
},
/// ALTER TABLE
AlterTable {
/// Table name
@ -1963,6 +1991,90 @@ impl fmt::Display for Statement {
table_name = table_name,
columns = display_separated(columns, ",")
),
Statement::CreateRole {
names,
if_not_exists,
inherit,
login,
bypassrls,
password,
create_db,
create_role,
superuser,
replication,
connection_limit,
valid_until,
in_role,
role,
admin,
authorization_owner,
} => {
write!(
f,
"CREATE ROLE {if_not_exists}{names}{superuser}{create_db}{create_role}{inherit}{login}{replication}{bypassrls}",
if_not_exists = if *if_not_exists { "IF NOT EXISTS " } else { "" },
names = display_separated(names, ", "),
superuser = match *superuser {
Some(true) => " SUPERUSER",
Some(false) => " NOSUPERUSER",
None => ""
},
create_db = match *create_db {
Some(true) => " CREATEDB",
Some(false) => " NOCREATEDB",
None => ""
},
create_role = match *create_role {
Some(true) => " CREATEROLE",
Some(false) => " NOCREATEROLE",
None => ""
},
inherit = match *inherit {
Some(true) => " INHERIT",
Some(false) => " NOINHERIT",
None => ""
},
login = match *login {
Some(true) => " LOGIN",
Some(false) => " NOLOGIN",
None => ""
},
replication = match *replication {
Some(true) => " REPLICATION",
Some(false) => " NOREPLICATION",
None => ""
},
bypassrls = match *bypassrls {
Some(true) => " BYPASSRLS",
Some(false) => " NOBYPASSRLS",
None => ""
}
)?;
if let Some(limit) = connection_limit {
write!(f, " CONNECTION LIMIT {}", limit)?;
}
match password {
Some(Password::Password(pass)) => write!(f, " PASSWORD {}", pass),
Some(Password::NullPassword) => write!(f, " PASSWORD NULL"),
None => Ok(()),
}?;
if let Some(until) = valid_until {
write!(f, " VALID UNTIL {}", until)?;
}
if !in_role.is_empty() {
write!(f, " IN ROLE {}", display_comma_separated(in_role))?;
}
if !role.is_empty() {
write!(f, " ROLE {}", display_comma_separated(role))?;
}
if !admin.is_empty() {
write!(f, " ADMIN {}", display_comma_separated(admin))?;
}
if let Some(owner) = authorization_owner {
write!(f, " AUTHORIZATION {}", owner)?;
}
Ok(())
}
Statement::AlterTable { name, operation } => {
write!(f, "ALTER TABLE {} {}", name, operation)
}
@ -2701,6 +2813,7 @@ pub enum ObjectType {
View,
Index,
Schema,
Role,
}
impl fmt::Display for ObjectType {
@ -2710,6 +2823,7 @@ impl fmt::Display for ObjectType {
ObjectType::View => "VIEW",
ObjectType::Index => "INDEX",
ObjectType::Schema => "SCHEMA",
ObjectType::Role => "ROLE",
})
}
}

View file

@ -70,6 +70,7 @@ define_keywords!(
ABSOLUTE,
ACTION,
ADD,
ADMIN,
ALL,
ALLOCATE,
ALTER,
@ -105,6 +106,7 @@ define_keywords!(
BOOLEAN,
BOTH,
BY,
BYPASSRLS,
BYTEA,
CACHE,
CALL,
@ -152,6 +154,8 @@ define_keywords!(
COVAR_POP,
COVAR_SAMP,
CREATE,
CREATEDB,
CREATEROLE,
CROSS,
CSV,
CUBE,
@ -272,6 +276,7 @@ define_keywords!(
IN,
INDEX,
INDICATOR,
INHERIT,
INNER,
INOUT,
INPUTFORMAT,
@ -313,6 +318,7 @@ define_keywords!(
LOCALTIME,
LOCALTIMESTAMP,
LOCATION,
LOGIN,
LOWER,
MANAGEDLOCATION,
MATCH,
@ -342,9 +348,16 @@ define_keywords!(
NEW,
NEXT,
NO,
NOBYPASSRLS,
NOCREATEDB,
NOCREATEROLE,
NOINHERIT,
NOLOGIN,
NONE,
NOREPLICATION,
NORMALIZE,
NOSCAN,
NOSUPERUSER,
NOT,
NTH_VALUE,
NTILE,
@ -380,6 +393,7 @@ define_keywords!(
PARTITION,
PARTITIONED,
PARTITIONS,
PASSWORD,
PERCENT,
PERCENTILE_CONT,
PERCENTILE_DISC,
@ -432,6 +446,7 @@ define_keywords!(
REPAIR,
REPEATABLE,
REPLACE,
REPLICATION,
RESTRICT,
RESULT,
RETURN,
@ -492,6 +507,7 @@ define_keywords!(
SUCCEEDS,
SUM,
SUPER,
SUPERUSER,
SYMMETRIC,
SYNC,
SYSTEM,
@ -538,6 +554,7 @@ define_keywords!(
UNLOGGED,
UNNEST,
UNSIGNED,
UNTIL,
UPDATE,
UPPER,
USAGE,
@ -545,6 +562,7 @@ define_keywords!(
USER,
USING,
UUID,
VALID,
VALUE,
VALUES,
VALUE_OF,

View file

@ -1866,6 +1866,8 @@ impl<'a> Parser<'a> {
self.parse_create_database()
} else if dialect_of!(self is HiveDialect) && self.parse_keyword(Keyword::FUNCTION) {
self.parse_create_function(temporary)
} else if self.parse_keyword(Keyword::ROLE) {
self.parse_create_role()
} else {
self.expected("an object type after CREATE", self.peek_token())
}
@ -2056,6 +2058,207 @@ impl<'a> Parser<'a> {
})
}
pub fn parse_create_role(&mut self) -> Result<Statement, ParserError> {
let if_not_exists = self.parse_keywords(&[Keyword::IF, Keyword::NOT, Keyword::EXISTS]);
let names = self.parse_comma_separated(Parser::parse_object_name)?;
let _ = self.parse_keyword(Keyword::WITH);
let optional_keywords = if dialect_of!(self is MsSqlDialect) {
vec![Keyword::AUTHORIZATION]
} else if dialect_of!(self is PostgreSqlDialect) {
vec![
Keyword::LOGIN,
Keyword::NOLOGIN,
Keyword::INHERIT,
Keyword::NOINHERIT,
Keyword::BYPASSRLS,
Keyword::NOBYPASSRLS,
Keyword::PASSWORD,
Keyword::CREATEDB,
Keyword::NOCREATEDB,
Keyword::CREATEROLE,
Keyword::NOCREATEROLE,
Keyword::SUPERUSER,
Keyword::NOSUPERUSER,
Keyword::REPLICATION,
Keyword::NOREPLICATION,
Keyword::CONNECTION,
Keyword::VALID,
Keyword::IN,
Keyword::ROLE,
Keyword::ADMIN,
Keyword::USER,
]
} else {
vec![]
};
// MSSQL
let mut authorization_owner = None;
// Postgres
let mut login = None;
let mut inherit = None;
let mut bypassrls = None;
let mut password = None;
let mut create_db = None;
let mut create_role = None;
let mut superuser = None;
let mut replication = None;
let mut connection_limit = None;
let mut valid_until = None;
let mut in_role = vec![];
let mut roles = vec![];
let mut admin = vec![];
while let Some(keyword) = self.parse_one_of_keywords(&optional_keywords) {
match keyword {
Keyword::AUTHORIZATION => {
if authorization_owner.is_some() {
parser_err!("Found multiple AUTHORIZATION")
} else {
authorization_owner = Some(self.parse_object_name()?);
Ok(())
}
}
Keyword::LOGIN | Keyword::NOLOGIN => {
if login.is_some() {
parser_err!("Found multiple LOGIN or NOLOGIN")
} else {
login = Some(keyword == Keyword::LOGIN);
Ok(())
}
}
Keyword::INHERIT | Keyword::NOINHERIT => {
if inherit.is_some() {
parser_err!("Found multiple INHERIT or NOINHERIT")
} else {
inherit = Some(keyword == Keyword::INHERIT);
Ok(())
}
}
Keyword::BYPASSRLS | Keyword::NOBYPASSRLS => {
if bypassrls.is_some() {
parser_err!("Found multiple BYPASSRLS or NOBYPASSRLS")
} else {
bypassrls = Some(keyword == Keyword::BYPASSRLS);
Ok(())
}
}
Keyword::CREATEDB | Keyword::NOCREATEDB => {
if create_db.is_some() {
parser_err!("Found multiple CREATEDB or NOCREATEDB")
} else {
create_db = Some(keyword == Keyword::CREATEDB);
Ok(())
}
}
Keyword::CREATEROLE | Keyword::NOCREATEROLE => {
if create_role.is_some() {
parser_err!("Found multiple CREATEROLE or NOCREATEROLE")
} else {
create_role = Some(keyword == Keyword::CREATEROLE);
Ok(())
}
}
Keyword::SUPERUSER | Keyword::NOSUPERUSER => {
if superuser.is_some() {
parser_err!("Found multiple SUPERUSER or NOSUPERUSER")
} else {
superuser = Some(keyword == Keyword::SUPERUSER);
Ok(())
}
}
Keyword::REPLICATION | Keyword::NOREPLICATION => {
if replication.is_some() {
parser_err!("Found multiple REPLICATION or NOREPLICATION")
} else {
replication = Some(keyword == Keyword::REPLICATION);
Ok(())
}
}
Keyword::PASSWORD => {
if password.is_some() {
parser_err!("Found multiple PASSWORD")
} else {
password = if self.parse_keyword(Keyword::NULL) {
Some(Password::NullPassword)
} else {
Some(Password::Password(Expr::Value(self.parse_value()?)))
};
Ok(())
}
}
Keyword::CONNECTION => {
self.expect_keyword(Keyword::LIMIT)?;
if connection_limit.is_some() {
parser_err!("Found multiple CONNECTION LIMIT")
} else {
connection_limit = Some(Expr::Value(self.parse_number_value()?));
Ok(())
}
}
Keyword::VALID => {
self.expect_keyword(Keyword::UNTIL)?;
if valid_until.is_some() {
parser_err!("Found multiple VALID UNTIL")
} else {
valid_until = Some(Expr::Value(self.parse_value()?));
Ok(())
}
}
Keyword::IN => {
if self.parse_keyword(Keyword::ROLE) || self.parse_keyword(Keyword::GROUP) {
if !in_role.is_empty() {
parser_err!("Found multiple IN ROLE or IN GROUP")
} else {
in_role = self.parse_comma_separated(Parser::parse_identifier)?;
Ok(())
}
} else {
self.expected("ROLE or GROUP after IN", self.peek_token())
}
}
Keyword::ROLE | Keyword::USER => {
if !roles.is_empty() {
parser_err!("Found multiple ROLE or USER")
} else {
roles = self.parse_comma_separated(Parser::parse_identifier)?;
Ok(())
}
}
Keyword::ADMIN => {
if !admin.is_empty() {
parser_err!("Found multiple ADMIN")
} else {
admin = self.parse_comma_separated(Parser::parse_identifier)?;
Ok(())
}
}
_ => break,
}?
}
Ok(Statement::CreateRole {
names,
if_not_exists,
login,
inherit,
bypassrls,
password,
create_db,
create_role,
replication,
superuser,
connection_limit,
valid_until,
in_role,
role: roles,
admin,
authorization_owner,
})
}
pub fn parse_drop(&mut self) -> Result<Statement, ParserError> {
let object_type = if self.parse_keyword(Keyword::TABLE) {
ObjectType::Table
@ -2063,10 +2266,15 @@ impl<'a> Parser<'a> {
ObjectType::View
} else if self.parse_keyword(Keyword::INDEX) {
ObjectType::Index
} else if self.parse_keyword(Keyword::ROLE) {
ObjectType::Role
} else if self.parse_keyword(Keyword::SCHEMA) {
ObjectType::Schema
} else {
return self.expected("TABLE, VIEW, INDEX or SCHEMA after DROP", self.peek_token());
return self.expected(
"TABLE, VIEW, INDEX, ROLE, or SCHEMA after DROP",
self.peek_token(),
);
};
// Many dialects support the non standard `IF EXISTS` clause and allow
// specifying multiple objects to delete in a single statement
@ -2078,6 +2286,9 @@ impl<'a> Parser<'a> {
if cascade && restrict {
return parser_err!("Cannot specify both CASCADE and RESTRICT in DROP");
}
if object_type == ObjectType::Role && (cascade || restrict || purge) {
return parser_err!("Cannot specify CASCADE, RESTRICT, or PURGE in DROP ROLE");
}
Ok(Statement::Drop {
object_type,
if_exists,

View file

@ -146,6 +146,13 @@ pub fn all_dialects() -> TestedDialects {
}
}
pub fn assert_eq_vec<T: ToString>(expected: &[&str], actual: &[T]) {
assert_eq!(
expected,
actual.iter().map(ToString::to_string).collect::<Vec<_>>()
);
}
pub fn only<T>(v: impl IntoIterator<Item = T>) -> T {
let mut iter = v.into_iter();
if let (Some(item), None) = (iter.next(), iter.next()) {

View file

@ -32,7 +32,8 @@ use sqlparser::keywords::ALL_KEYWORDS;
use sqlparser::parser::{Parser, ParserError};
use test_utils::{
all_dialects, expr_from_projection, join, number, only, table, table_alias, TestedDialects,
all_dialects, assert_eq_vec, expr_from_projection, join, number, only, table, table_alias,
TestedDialects,
};
#[test]
@ -4850,6 +4851,63 @@ fn parse_drop_index() {
}
}
#[test]
fn parse_create_role() {
let sql = "CREATE ROLE consultant";
match verified_stmt(sql) {
Statement::CreateRole { names, .. } => {
assert_eq_vec(&["consultant"], &names);
}
_ => unreachable!(),
}
let sql = "CREATE ROLE IF NOT EXISTS mysql_a, mysql_b";
match verified_stmt(sql) {
Statement::CreateRole {
names,
if_not_exists,
..
} => {
assert_eq_vec(&["mysql_a", "mysql_b"], &names);
assert!(if_not_exists);
}
_ => unreachable!(),
}
}
#[test]
fn parse_drop_role() {
let sql = "DROP ROLE abc";
match verified_stmt(sql) {
Statement::Drop {
names,
object_type,
if_exists,
..
} => {
assert_eq_vec(&["abc"], &names);
assert_eq!(ObjectType::Role, object_type);
assert!(!if_exists);
}
_ => unreachable!(),
};
let sql = "DROP ROLE IF EXISTS def, magician, quaternion";
match verified_stmt(sql) {
Statement::Drop {
names,
object_type,
if_exists,
..
} => {
assert_eq_vec(&["def", "magician", "quaternion"], &names);
assert_eq!(ObjectType::Role, object_type);
assert!(if_exists);
}
_ => unreachable!(),
}
}
#[test]
fn parse_grant() {
let sql = "GRANT SELECT, INSERT, UPDATE (shape, size), USAGE, DELETE, TRUNCATE, REFERENCES, TRIGGER, CONNECT, CREATE, EXECUTE, TEMPORARY ON abc, def TO xyz, m WITH GRANT OPTION GRANTED BY jj";
@ -4891,14 +4949,8 @@ fn parse_grant() {
],
actions
);
assert_eq!(
vec!["abc", "def"],
objects.iter().map(ToString::to_string).collect::<Vec<_>>()
);
assert_eq!(
vec!["xyz", "m"],
grantees.iter().map(ToString::to_string).collect::<Vec<_>>()
);
assert_eq_vec(&["abc", "def"], &objects);
assert_eq_vec(&["xyz", "m"], &grantees);
assert!(with_grant_option);
assert_eq!("jj", granted_by.unwrap().to_string());
}
@ -4918,14 +4970,8 @@ fn parse_grant() {
} => 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::<Vec<_>>()
);
assert_eq!(
vec!["browser"],
grantees.iter().map(ToString::to_string).collect::<Vec<_>>()
);
assert_eq_vec(&["public"], &schemas);
assert_eq_vec(&["browser"], &grantees);
assert!(!with_grant_option);
}
_ => unreachable!(),
@ -4947,14 +4993,8 @@ fn parse_grant() {
vec![Action::Usage, Action::Select { columns: None }],
actions
);
assert_eq!(
vec!["p"],
objects.iter().map(ToString::to_string).collect::<Vec<_>>()
);
assert_eq!(
vec!["u"],
grantees.iter().map(ToString::to_string).collect::<Vec<_>>()
);
assert_eq_vec(&["p"], &objects);
assert_eq_vec(&["u"], &grantees);
}
_ => unreachable!(),
},
@ -4988,10 +5028,7 @@ fn parse_grant() {
GrantObjects::Schemas(schemas),
) => {
assert!(!with_privileges_keyword);
assert_eq!(
vec!["aa", "b"],
schemas.iter().map(ToString::to_string).collect::<Vec<_>>()
);
assert_eq_vec(&["aa", "b"], &schemas);
}
_ => unreachable!(),
},
@ -5007,10 +5044,7 @@ fn parse_grant() {
} => 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::<Vec<_>>()
);
assert_eq_vec(&["bus"], &schemas);
}
_ => unreachable!(),
},
@ -5035,14 +5069,8 @@ fn test_revoke() {
},
privileges
);
assert_eq!(
vec!["users", "auth"],
tables.iter().map(ToString::to_string).collect::<Vec<_>>()
);
assert_eq!(
vec!["analyst"],
grantees.iter().map(ToString::to_string).collect::<Vec<_>>()
);
assert_eq_vec(&["users", "auth"], &tables);
assert_eq_vec(&["analyst"], &grantees);
assert!(cascade);
assert_eq!(None, granted_by);
}

View file

@ -118,6 +118,28 @@ fn parse_mssql_bin_literal() {
let _ = ms_and_generic().one_statement_parses_to("SELECT 0xdeadBEEF", "SELECT X'deadBEEF'");
}
#[test]
fn parse_mssql_create_role() {
let sql = "CREATE ROLE mssql AUTHORIZATION helena";
match ms().verified_stmt(sql) {
Statement::CreateRole {
names,
authorization_owner,
..
} => {
assert_eq_vec(&["mssql"], &names);
assert_eq!(
authorization_owner,
Some(ObjectName(vec![Ident {
value: "helena".into(),
quote_style: None
}]))
);
}
_ => unreachable!(),
}
}
fn ms() -> TestedDialects {
TestedDialects {
dialects: vec![Box::new(MsSqlDialect {})],

View file

@ -1739,3 +1739,104 @@ fn parse_custom_operator() {
})
);
}
#[test]
fn parse_create_role() {
let sql = "CREATE ROLE IF NOT EXISTS mysql_a, mysql_b";
match pg().verified_stmt(sql) {
Statement::CreateRole {
names,
if_not_exists,
..
} => {
assert_eq_vec(&["mysql_a", "mysql_b"], &names);
assert!(if_not_exists);
}
_ => unreachable!(),
}
let sql = "CREATE ROLE abc LOGIN PASSWORD NULL";
match pg().parse_sql_statements(sql).as_deref() {
Ok(
[Statement::CreateRole {
names,
login,
password,
..
}],
) => {
assert_eq_vec(&["abc"], names);
assert_eq!(*login, Some(true));
assert_eq!(*password, Some(Password::NullPassword));
}
err => panic!("Failed to parse CREATE ROLE test case: {:?}", err),
}
let sql = "CREATE ROLE magician WITH SUPERUSER CREATEROLE NOCREATEDB BYPASSRLS INHERIT PASSWORD 'abcdef' LOGIN VALID UNTIL '2025-01-01' IN ROLE role1, role2 ROLE role3 ADMIN role4, role5 REPLICATION";
// Roundtrip order of optional parameters is not preserved
match pg().parse_sql_statements(sql).as_deref() {
Ok(
[Statement::CreateRole {
names,
if_not_exists,
bypassrls,
login,
inherit,
password,
superuser,
create_db,
create_role,
replication,
connection_limit,
valid_until,
in_role,
role,
admin,
authorization_owner,
}],
) => {
assert_eq_vec(&["magician"], names);
assert!(!*if_not_exists);
assert_eq!(*login, Some(true));
assert_eq!(*inherit, Some(true));
assert_eq!(*bypassrls, Some(true));
assert_eq!(
*password,
Some(Password::Password(Expr::Value(Value::SingleQuotedString(
"abcdef".into()
))))
);
assert_eq!(*superuser, Some(true));
assert_eq!(*create_db, Some(false));
assert_eq!(*create_role, Some(true));
assert_eq!(*replication, Some(true));
assert_eq!(*connection_limit, None);
assert_eq!(
*valid_until,
Some(Expr::Value(Value::SingleQuotedString("2025-01-01".into())))
);
assert_eq_vec(&["role1", "role2"], in_role);
assert_eq_vec(&["role3"], role);
assert_eq_vec(&["role4", "role5"], admin);
assert_eq!(*authorization_owner, None);
}
err => panic!("Failed to parse CREATE ROLE test case: {:?}", err),
}
let negatables = vec![
"BYPASSRLS",
"CREATEDB",
"CREATEROLE",
"INHERIT",
"LOGIN",
"REPLICATION",
"SUPERUSER",
];
for negatable_kw in negatables.iter() {
let sql = format!("CREATE ROLE abc {kw} NO{kw}", kw = negatable_kw);
if pg().parse_sql_statements(&sql).is_ok() {
panic!("Should not be able to parse CREATE ROLE containing both negated and non-negated versions of the same keyword: {}", negatable_kw)
}
}
}