use corium_core::{EntityId, Keyword, KeywordInterner, Partition, Value};
use corium_db::{Db, FIRST_USER_ID};
use corium_forms::schemaform::schema_from_edn;
use corium_forms::txforms::tx_items_from_edn;
use corium_query::edn::{Edn, read_all};
use crate::model::{ObjectRef, SubjectRef};
use crate::schema;
#[derive(Debug, thiserror::Error)]
pub enum BootstrapError {
#[error("cannot read policy EDN: {0}")]
Edn(#[from] corium_query::edn::EdnError),
#[error("bad policy form: {0}")]
Form(#[from] corium_forms::txforms::TxFormError),
#[error("cannot apply policy transaction: {0}")]
Transact(#[from] corium_tx::TxError),
}
pub struct PolicyBuilder {
db: Db,
interner: KeywordInterner,
next_user: u64,
}
impl Default for PolicyBuilder {
fn default() -> Self {
Self::new()
}
}
impl PolicyBuilder {
#[must_use]
pub fn new() -> Self {
let (schema, idents) =
schema_from_edn(&schema::schema_forms()).expect("reserved schema is well formed");
let interner = KeywordInterner::default();
Self {
db: Db::new(schema).with_naming(idents, interner.clone()),
interner,
next_user: FIRST_USER_ID,
}
}
pub fn transact_edn(&mut self, text: &str) -> Result<&mut Self, BootstrapError> {
let forms = read_all(text)?;
let forms = match forms.as_slice() {
[Edn::Vector(items) | Edn::List(items)] => items.clone(),
_ => forms,
};
self.transact(&forms)
}
pub fn transact(&mut self, forms: &[Edn]) -> Result<&mut Self, BootstrapError> {
let items = tx_items_from_edn(&self.db, &mut self.interner, forms)?;
let t = self.db.basis_t() + 1;
let tx = EntityId::new(Partition::Tx as u32, t);
let prepared = corium_tx::prepare(&self.db, items, tx, self.next_user)?;
self.next_user = prepared
.tempids
.values()
.filter(|entity| entity.partition() == Partition::User as u32)
.map(|entity| entity.sequence() + 1)
.max()
.unwrap_or(self.next_user)
.max(self.next_user);
self.db = self
.db
.clone()
.with_naming(self.db.idents().clone(), self.interner.clone())
.with_transaction(t, &prepared.datoms);
Ok(self)
}
#[must_use]
pub fn db(&self) -> Db {
self.db.clone()
}
}
pub fn policy_db(text: &str) -> Result<Db, BootstrapError> {
let mut builder = PolicyBuilder::new();
builder.transact_edn(text)?;
Ok(builder.db())
}
#[must_use]
pub fn tuple_form(subject: &str, relation: &str, object: &str) -> Edn {
entity(
"tuple",
&[
(
schema::TUPLE_SUBJECT,
SubjectRef::parse(subject).to_string(),
),
(schema::TUPLE_RELATION, relation.to_owned()),
(schema::TUPLE_OBJECT, ObjectRef::parse(object).to_string()),
],
)
}
#[must_use]
pub fn principal_form(id: &str, provider: Option<&str>, roles: &[String]) -> Edn {
let mut pairs = vec![(Edn::keyword(schema::PRINCIPAL_ID), Edn::Str(id.to_owned()))];
if let Some(provider) = provider {
pairs.push((
Edn::keyword(schema::PRINCIPAL_PROVIDER),
Edn::Str(provider.to_owned()),
));
}
if !roles.is_empty() {
pairs.push((
Edn::keyword(schema::PRINCIPAL_ROLE),
Edn::Vector(roles.iter().cloned().map(Edn::Str).collect()),
));
}
map_with_id(format!("principal-{id}"), pairs)
}
#[must_use]
pub fn permission_form(object_type: &str, action: &str, relations: &[String]) -> Edn {
map_with_id(
format!("permission-{object_type}-{action}"),
vec![
(
Edn::keyword(schema::PERMISSION_OBJECT_TYPE),
Edn::Str(object_type.to_owned()),
),
(
Edn::keyword(schema::PERMISSION_ACTION),
Edn::Str(action.to_owned()),
),
(
Edn::keyword(schema::PERMISSION_RELATION),
Edn::Vector(relations.iter().cloned().map(Edn::Str).collect()),
),
],
)
}
#[must_use]
pub fn find_tuple(db: &Db, subject: &str, relation: &str, object: &str) -> Option<EntityId> {
let subject = SubjectRef::parse(subject).to_string();
let object = ObjectRef::parse(object).to_string();
let attribute = |ident: &str| db.idents().entid(&Keyword::parse(ident));
let (subject_attr, relation_attr, object_attr) = (
attribute(schema::TUPLE_SUBJECT)?,
attribute(schema::TUPLE_RELATION)?,
attribute(schema::TUPLE_OBJECT)?,
);
let matches = |entity: EntityId, attribute, expected: &str| {
db.values(entity, attribute)
.iter()
.any(|value| matches!(value, Value::Str(text) if &**text == expected))
};
db.datoms_for_attribute(subject_attr)
.filter(|datom| matches!(&datom.v, Value::Str(text) if **text == subject))
.map(|datom| datom.e)
.find(|entity| {
matches(*entity, relation_attr, relation) && matches(*entity, object_attr, &object)
})
}
#[must_use]
pub fn retract_entity_form(entity: EntityId) -> Edn {
Edn::Vector(vec![
Edn::keyword("db/retractEntity"),
Edn::Long(i64::try_from(entity.raw()).unwrap_or_default()),
])
}
fn entity(prefix: &str, attributes: &[(&str, String)]) -> Edn {
let pairs = attributes
.iter()
.map(|(attribute, value)| (Edn::keyword(attribute), Edn::Str(value.clone())))
.collect();
let id = format!(
"{prefix}-{}",
attributes
.iter()
.map(|(_, value)| value.as_str())
.collect::<Vec<_>>()
.join("-")
);
map_with_id(id, pairs)
}
fn map_with_id(temp_id: String, mut pairs: Vec<(Edn, Edn)>) -> Edn {
pairs.push((Edn::keyword("db/id"), Edn::Str(temp_id)));
pairs.sort();
Edn::Map(pairs)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::policy::Policy;
#[test]
fn builds_and_edits_a_policy_database() {
let mut builder = PolicyBuilder::new();
builder
.transact(&[
tuple_form("alice", "writer", "database:music"),
permission_form("database", "write", &["writer".to_owned()]),
])
.expect("policy applies");
let db = builder.db();
let policy = Policy::compile(&db).expect("policy compiles");
assert_eq!(policy.stats().tuples, 1);
assert_eq!(policy.stats().permissions, 1);
let entity = find_tuple(&db, "alice", "writer", "database:music").expect("tuple is stored");
builder
.transact(&[retract_entity_form(entity)])
.expect("retraction applies");
let policy = Policy::compile(&builder.db()).expect("policy compiles");
assert_eq!(policy.stats().tuples, 0);
}
#[test]
fn normalizes_operator_shorthand() {
let Edn::Map(pairs) = tuple_form("alice", "member", "group:eng") else {
panic!("tuple form is a map");
};
let subject = pairs
.iter()
.find(|(key, _)| *key == Edn::keyword(schema::TUPLE_SUBJECT))
.map(|(_, value)| value.clone());
assert_eq!(subject, Some(Edn::Str("user:alice".to_owned())));
}
}