use std::fmt;
use corium_protocol::authz::{Action, ActionClass};
pub const WILDCARD: &str = "*";
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct ObjectRef {
pub kind: String,
pub id: String,
}
impl ObjectRef {
pub fn new(kind: impl Into<String>, id: impl Into<String>) -> Self {
Self {
kind: kind.into(),
id: id.into(),
}
}
#[must_use]
pub fn parse(text: &str) -> Self {
match text.split_once(':') {
Some((kind, id)) if !kind.is_empty() && !id.is_empty() => Self::new(kind, id),
_ => Self::new("user", text),
}
}
#[must_use]
pub fn wildcard_of_type(&self) -> Self {
Self::new(self.kind.clone(), WILDCARD)
}
#[must_use]
pub fn is_wildcard(&self) -> bool {
self.id == WILDCARD
}
}
impl fmt::Display for ObjectRef {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(formatter, "{}:{}", self.kind, self.id)
}
}
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct SubjectRef {
pub object: ObjectRef,
pub relation: Option<String>,
}
impl SubjectRef {
#[must_use]
pub fn parse(text: &str) -> Self {
match text.split_once('#') {
Some((object, relation)) if !relation.is_empty() => Self {
object: ObjectRef::parse(object),
relation: Some(relation.to_owned()),
},
_ => Self {
object: ObjectRef::parse(text),
relation: None,
},
}
}
}
impl fmt::Display for SubjectRef {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match &self.relation {
Some(relation) => write!(formatter, "{}#{relation}", self.object),
None => write!(formatter, "{}", self.object),
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Tuple {
pub subject: SubjectRef,
pub relation: String,
pub object: ObjectRef,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Rewrite {
pub relation: String,
pub via_relation: String,
pub on_relation: String,
pub object_type: Option<String>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Permission {
pub object_type: String,
pub action: String,
pub relations: Vec<String>,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum FilterKind {
AttributeAllowlist,
AttributeDenylist,
}
impl FilterKind {
#[must_use]
pub fn parse(text: &str) -> Option<Self> {
match text {
"attribute-allowlist" | "allowlist" => Some(Self::AttributeAllowlist),
"attribute-denylist" | "denylist" => Some(Self::AttributeDenylist),
_ => None,
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ViewDef {
pub name: String,
pub kind: FilterKind,
pub attributes: Vec<String>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Binding {
pub relation: String,
pub object: ObjectRef,
pub view: Option<String>,
pub unfiltered: bool,
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct PrincipalDef {
pub id: String,
pub provider: Option<String>,
pub roles: Vec<String>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ObjectDef {
pub object: ObjectRef,
pub database: Option<String>,
}
#[must_use]
pub fn action_name(action: Action) -> &'static str {
match action {
Action::Query => "query",
Action::Pull => "pull",
Action::Datoms => "datoms",
Action::TxRange => "tx-range",
Action::Subscribe => "subscribe",
Action::Inspect => "inspect",
Action::Transact => "transact",
Action::CreateDatabase => "create-database",
Action::DeleteDatabase => "delete-database",
Action::ForkDatabase => "fork-database",
Action::ListDatabases => "list-databases",
Action::GarbageCollect => "garbage-collect",
Action::ManageIndex => "manage-index",
}
}
#[must_use]
pub fn action_from_name(name: &str) -> Option<Action> {
const ACTIONS: [Action; 13] = [
Action::Query,
Action::Pull,
Action::Datoms,
Action::TxRange,
Action::Subscribe,
Action::Inspect,
Action::Transact,
Action::CreateDatabase,
Action::DeleteDatabase,
Action::ForkDatabase,
Action::ListDatabases,
Action::GarbageCollect,
Action::ManageIndex,
];
ACTIONS
.into_iter()
.find(|action| action_name(*action) == name)
}
#[must_use]
pub fn action_names() -> Vec<&'static str> {
[
Action::Query,
Action::Pull,
Action::Datoms,
Action::TxRange,
Action::Subscribe,
Action::Inspect,
Action::Transact,
Action::CreateDatabase,
Action::DeleteDatabase,
Action::ForkDatabase,
Action::ListDatabases,
Action::GarbageCollect,
Action::ManageIndex,
]
.into_iter()
.map(action_name)
.collect()
}
#[must_use]
pub fn action_class_name(class: ActionClass) -> &'static str {
match class {
ActionClass::Read => "read",
ActionClass::Write => "write",
ActionClass::Admin => "admin",
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn object_refs_round_trip() {
let reference = ObjectRef::parse("database:music");
assert_eq!(reference, ObjectRef::new("database", "music"));
assert_eq!(reference.to_string(), "database:music");
assert_eq!(
reference.wildcard_of_type(),
ObjectRef::new("database", "*")
);
assert!(reference.wildcard_of_type().is_wildcard());
assert_eq!(ObjectRef::parse("alice"), ObjectRef::new("user", "alice"));
assert_eq!(
ObjectRef::parse("user:oidc:alice"),
ObjectRef::new("user", "oidc:alice")
);
}
#[test]
fn subject_refs_carry_usersets() {
let plain = SubjectRef::parse("group:eng");
assert_eq!(plain.relation, None);
let userset = SubjectRef::parse("group:eng#member");
assert_eq!(userset.object, ObjectRef::new("group", "eng"));
assert_eq!(userset.relation.as_deref(), Some("member"));
assert_eq!(userset.to_string(), "group:eng#member");
}
#[test]
fn action_names_are_stable() {
assert_eq!(action_name(Action::Query), "query");
assert_eq!(action_name(Action::CreateDatabase), "create-database");
assert_eq!(action_class_name(ActionClass::Admin), "admin");
}
#[test]
fn action_names_round_trip() {
for name in action_names() {
let action = action_from_name(name).expect("every listed name parses");
assert_eq!(action_name(action), name);
}
assert_eq!(action_names().len(), 13);
assert!(action_from_name("nonsense").is_none());
}
}