phonograph/interim-server/src/pg_acls.rs

177 lines
5.2 KiB
Rust
Raw Normal View History

2025-05-26 22:08:21 -07:00
use nom::{
branch::alt,
bytes::complete::{is_not, tag, take_till},
character::char,
combinator::{opt, value},
error::ParseError,
multi::{many0, many1},
sequence::delimited,
AsChar as _, IResult, Parser,
};
use sqlx::{
error::BoxDynError,
postgres::{PgHasArrayType, PgTypeInfo, PgValueRef},
Decode, Postgres,
};
/// This type will automatically decode Postgres "aclitem" values, provided that
/// the query is cast to a TEXT type and selected with type annotations. For
/// example:
/// ```sql
/// select datacl::text[] as "datacl: Vec<PgAclItem>" from pg_database;
/// ```
/// The TEXT cast is necessary because the aclitem type itself is incompatible
/// with binary value format, which makes it incompatible with SQLx.
#[derive(Clone, Debug, PartialEq)]
pub struct PgAclItem {
pub grantee: String,
pub privileges: Vec<PgPrivilege>,
pub grantor: String,
}
#[derive(Clone, Debug, PartialEq)]
pub struct PgPrivilege {
pub grant_option: bool,
pub privilege: PgPrivilegeType,
}
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
pub enum PgPrivilegeType {
Select,
Insert,
Update,
Delete,
Truncate,
References,
Trigger,
Create,
Connect,
Temporary,
Execute,
Usage,
Set,
AlterSystem,
Maintain,
}
2025-05-28 16:35:00 -07:00
impl PgPrivilegeType {
pub fn to_abbrev(&self) -> char {
match self {
Self::Select => 'r',
Self::Insert => 'a',
Self::Update => 'w',
Self::Delete => 'd',
Self::Truncate => 'D',
Self::References => 'x',
Self::Trigger => 't',
Self::Create => 'C',
Self::Connect => 'c',
Self::Temporary => 'T',
Self::Execute => 'X',
Self::Usage => 'U',
Self::Set => 's',
Self::AlterSystem => 'A',
Self::Maintain => 'm',
}
}
}
2025-05-26 22:08:21 -07:00
impl<'a> Decode<'a, Postgres> for PgAclItem {
fn decode(value: PgValueRef<'a>) -> Result<Self, BoxDynError> {
let acl_item_str = <&str as Decode<Postgres>>::decode(value)?;
let (remainder, acl_item) = parse_acl_item::<(_, nom::error::ErrorKind)>(acl_item_str)
.map_err(|err| err.to_owned())?;
assert_eq!(remainder, "");
Ok(acl_item)
}
}
impl sqlx::Type<Postgres> for PgAclItem {
fn type_info() -> PgTypeInfo {
PgTypeInfo::with_name("aclitem")
}
}
impl PgHasArrayType for PgAclItem {
fn array_type_info() -> PgTypeInfo {
PgTypeInfo::array_of("aclitem")
}
}
fn parse_acl_item<'a, E: ParseError<&'a str>>(input: &'a str) -> IResult<&'a str, PgAclItem, E> {
let (remainder, grantee) = parse_identifier(input)?;
let (remainder, _) = char('=').parse(remainder)?;
let (remainder, privileges) = parse_privileges(remainder)?;
let (remainder, _) = char('/').parse(remainder)?;
let (remainder, grantor) = parse_identifier(remainder)?;
Ok((
remainder,
PgAclItem {
grantee,
privileges,
grantor,
},
))
}
fn parse_identifier<'a, E: ParseError<&'a str>>(input: &'a str) -> IResult<&'a str, String, E> {
alt((
delimited(char('"'), parse_escaped_identifier, char('"')),
parse_plain_identifier,
))
.parse(input)
}
/// WARNING: This works correctly only for identifiers read from aclitem
/// strings, as they are quote-escaped whenever the identifier contains
/// characters other than alphanumerics and underscores.
fn parse_plain_identifier<'a, E: ParseError<&'a str>>(
input: &'a str,
) -> IResult<&'a str, String, E> {
take_till(|c: char| !c.is_alphanum() && c != '_')
.parse(input)
.map(|(remainder, parsed)| (remainder, parsed.to_owned()))
}
fn parse_escaped_identifier<'a, E: ParseError<&'a str>>(
input: &'a str,
) -> IResult<&'a str, String, E> {
let (remainder, parsed) = many1(alt((value("\"", tag("\"\"")), is_not("\"")))).parse(input)?;
Ok((remainder, parsed.join("")))
}
fn parse_privileges<'a, E: ParseError<&'a str>>(
input: &'a str,
) -> IResult<&'a str, Vec<PgPrivilege>, E> {
many0(parse_privilege).parse(input)
}
fn parse_privilege<'a, E: ParseError<&'a str>>(input: &'a str) -> IResult<&'a str, PgPrivilege, E> {
let (remainder, priv_type) = alt((
value(PgPrivilegeType::Select, char('r')),
value(PgPrivilegeType::Insert, char('a')),
value(PgPrivilegeType::Update, char('w')),
value(PgPrivilegeType::Delete, char('d')),
value(PgPrivilegeType::Truncate, char('D')),
value(PgPrivilegeType::References, char('x')),
value(PgPrivilegeType::Trigger, char('t')),
value(PgPrivilegeType::Create, char('C')),
value(PgPrivilegeType::Connect, char('c')),
value(PgPrivilegeType::Temporary, char('T')),
value(PgPrivilegeType::Execute, char('X')),
value(PgPrivilegeType::Usage, char('U')),
value(PgPrivilegeType::Set, char('s')),
value(PgPrivilegeType::AlterSystem, char('A')),
value(PgPrivilegeType::Maintain, char('m')),
))
.parse(input)?;
let (remainder, parsed_grant_option) = opt(char('*')).parse(remainder)?;
Ok((
remainder,
PgPrivilege {
grant_option: parsed_grant_option.is_some(),
privilege: priv_type,
},
))
}