use crate::catalog::error::CatalogError;
use crate::identifier::Identifier;
use crate::ir::grant::{Grant, GrantTarget, Privilege};
#[must_use]
pub fn strip_public_implicit_grants(
grants: Vec<Grant>,
object_type: crate::ir::default_privileges::DefaultPrivObjectType,
) -> Vec<Grant> {
use crate::ir::default_privileges::DefaultPrivObjectType;
grants
.into_iter()
.filter(|g| {
if g.with_grant_option {
return true; }
match (&g.grantee, g.privilege, object_type) {
(GrantTarget::Public, Privilege::Usage, DefaultPrivObjectType::Types)
| (GrantTarget::Public, Privilege::Execute, DefaultPrivObjectType::Functions) => {
false
}
_ => true,
}
})
.collect()
}
#[must_use]
pub fn strip_owner_self_grants(grants: Vec<Grant>, owner: &Identifier) -> Vec<Grant> {
grants
.into_iter()
.filter(|g| match &g.grantee {
GrantTarget::Role(name) => name != owner,
GrantTarget::Public => true,
})
.collect()
}
pub fn decode_aclitem_array(items: &[String]) -> Result<Vec<Grant>, CatalogError> {
let mut out = Vec::with_capacity(items.len());
for raw in items {
out.extend(decode_one(raw)?);
}
Ok(out)
}
fn decode_one(raw: &str) -> Result<Vec<Grant>, CatalogError> {
let body = raw
.split('/')
.next()
.ok_or_else(|| CatalogError::BadColumnType {
query: crate::catalog::CatalogQuery::Schemas,
column: "acl".to_string(),
message: format!("malformed aclitem {raw:?}"),
})?;
let (grantee_str, privs) = body
.split_once('=')
.ok_or_else(|| CatalogError::BadColumnType {
query: crate::catalog::CatalogQuery::Schemas,
column: "acl".to_string(),
message: format!("malformed aclitem {raw:?}"),
})?;
let grantee = if grantee_str.is_empty() {
GrantTarget::Public
} else {
let trimmed = grantee_str
.strip_prefix('"')
.and_then(|s| s.strip_suffix('"'))
.unwrap_or(grantee_str);
GrantTarget::Role(Identifier::from_unquoted(trimmed).map_err(|e| {
CatalogError::BadColumnType {
query: crate::catalog::CatalogQuery::Schemas,
column: "acl".to_string(),
message: format!("aclitem grantee {grantee_str:?}: {e}"),
}
})?)
};
let mut out = Vec::new();
let mut chars = privs.chars().peekable();
while let Some(c) = chars.next() {
let parsed_privilege = match c {
'r' => Privilege::Select,
'w' => Privilege::Update,
'a' => Privilege::Insert,
'd' => Privilege::Delete,
'D' => Privilege::Truncate,
'x' => Privilege::References,
't' => Privilege::Trigger,
'X' => Privilege::Execute,
'U' => Privilege::Usage,
'C' => Privilege::Create,
_ => {
if chars.peek() == Some(&'*') {
chars.next();
}
continue;
}
};
let with_grant_option = chars.peek() == Some(&'*');
if with_grant_option {
chars.next();
}
out.push(Grant {
grantee: grantee.clone(),
privilege: parsed_privilege,
with_grant_option,
columns: None,
});
}
Ok(out)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn public_select() {
let g = decode_one("=r/owner").unwrap();
assert_eq!(g.len(), 1);
assert!(matches!(g[0].grantee, GrantTarget::Public));
assert_eq!(g[0].privilege, Privilege::Select);
assert!(!g[0].with_grant_option);
}
#[test]
fn role_multiple_privileges() {
let g = decode_one("alice=arwd/owner").unwrap();
assert_eq!(g.len(), 4);
let privs: Vec<Privilege> = g.iter().map(|x| x.privilege).collect();
assert!(privs.contains(&Privilege::Insert));
assert!(privs.contains(&Privilege::Select));
assert!(privs.contains(&Privilege::Update));
assert!(privs.contains(&Privilege::Delete));
}
#[test]
fn with_grant_option_flag() {
let g = decode_one("alice=r*/owner").unwrap();
assert_eq!(g.len(), 1);
assert!(g[0].with_grant_option);
}
#[test]
fn unmanaged_privileges_skipped() {
let g = decode_one("alice=Tr/owner").unwrap();
assert_eq!(g.len(), 1);
assert_eq!(g[0].privilege, Privilege::Select);
}
#[test]
fn malformed_aclitem_errors() {
assert!(decode_one("no_equals_sign").is_err());
}
#[test]
fn array_decode_combines() {
let arr = vec!["alice=r/o".to_string(), "=a/o".to_string()];
let g = decode_aclitem_array(&arr).unwrap();
assert_eq!(g.len(), 2);
}
#[test]
fn strip_owner_self_grants_removes_owner_entries() {
let owner = Identifier::from_unquoted("app_owner").unwrap();
let grants = vec![
Grant {
grantee: GrantTarget::Role(Identifier::from_unquoted("app_owner").unwrap()),
privilege: Privilege::Select,
with_grant_option: false,
columns: None,
},
Grant {
grantee: GrantTarget::Role(Identifier::from_unquoted("readers").unwrap()),
privilege: Privilege::Select,
with_grant_option: false,
columns: None,
},
];
let filtered = strip_owner_self_grants(grants, &owner);
assert_eq!(filtered.len(), 1);
assert!(matches!(&filtered[0].grantee, GrantTarget::Role(r) if r.as_str() == "readers"));
}
#[test]
fn strip_owner_self_grants_keeps_public() {
let owner = Identifier::from_unquoted("app_owner").unwrap();
let grants = vec![Grant {
grantee: GrantTarget::Public,
privilege: Privilege::Select,
with_grant_option: false,
columns: None,
}];
let filtered = strip_owner_self_grants(grants, &owner);
assert_eq!(filtered.len(), 1, "PUBLIC grants are not owner self-grants");
}
#[test]
fn all_managed_privilege_letters() {
let g = decode_one("alice=rwadDxtXUC/owner").unwrap();
assert_eq!(g.len(), 10, "all 10 managed privilege letters");
}
#[test]
fn strip_public_implicit_grants_removes_types_usage() {
use crate::ir::default_privileges::DefaultPrivObjectType;
let grants = vec![
Grant {
grantee: GrantTarget::Public,
privilege: Privilege::Usage,
with_grant_option: false,
columns: None,
},
Grant {
grantee: GrantTarget::Role(Identifier::from_unquoted("readers").unwrap()),
privilege: Privilege::Usage,
with_grant_option: false,
columns: None,
},
];
let filtered = strip_public_implicit_grants(grants, DefaultPrivObjectType::Types);
assert_eq!(filtered.len(), 1);
assert!(matches!(&filtered[0].grantee, GrantTarget::Role(r) if r.as_str() == "readers"));
}
#[test]
fn strip_public_implicit_grants_removes_functions_execute() {
use crate::ir::default_privileges::DefaultPrivObjectType;
let grants = vec![
Grant {
grantee: GrantTarget::Public,
privilege: Privilege::Execute,
with_grant_option: false,
columns: None,
},
Grant {
grantee: GrantTarget::Role(Identifier::from_unquoted("app").unwrap()),
privilege: Privilege::Execute,
with_grant_option: false,
columns: None,
},
];
let filtered = strip_public_implicit_grants(grants, DefaultPrivObjectType::Functions);
assert_eq!(filtered.len(), 1);
assert!(matches!(&filtered[0].grantee, GrantTarget::Role(r) if r.as_str() == "app"));
}
#[test]
fn strip_public_implicit_grants_keeps_wgo_variants() {
use crate::ir::default_privileges::DefaultPrivObjectType;
let grants = vec![Grant {
grantee: GrantTarget::Public,
privilege: Privilege::Usage,
with_grant_option: true, columns: None,
}];
let filtered = strip_public_implicit_grants(grants, DefaultPrivObjectType::Types);
assert_eq!(filtered.len(), 1, "WGO Public/Usage on TYPES must be kept");
}
#[test]
fn strip_public_implicit_grants_leaves_tables_untouched() {
use crate::ir::default_privileges::DefaultPrivObjectType;
let grants = vec![Grant {
grantee: GrantTarget::Public,
privilege: Privilege::Select,
with_grant_option: false,
columns: None,
}];
let filtered = strip_public_implicit_grants(grants, DefaultPrivObjectType::Tables);
assert_eq!(filtered.len(), 1, "Tables have no implicit PUBLIC grants");
}
#[test]
fn unmanaged_with_grant_option_skipped_cleanly() {
let g = decode_one("alice=T*r/owner").unwrap();
assert_eq!(g.len(), 1);
assert_eq!(g[0].privilege, Privilege::Select);
}
}