1use corium_core::{EntityId, Keyword, KeywordInterner, Partition, Value};
11use corium_db::{Db, FIRST_USER_ID};
12use corium_forms::schemaform::schema_from_edn;
13use corium_forms::txforms::tx_items_from_edn;
14use corium_query::edn::{Edn, read_all};
15
16use crate::model::{ObjectRef, SubjectRef};
17use crate::schema;
18
19#[derive(Debug, thiserror::Error)]
21pub enum BootstrapError {
22 #[error("cannot read policy EDN: {0}")]
24 Edn(#[from] corium_query::edn::EdnError),
25 #[error("bad policy form: {0}")]
27 Form(#[from] corium_forms::txforms::TxFormError),
28 #[error("cannot apply policy transaction: {0}")]
30 Transact(#[from] corium_tx::TxError),
31}
32
33pub struct PolicyBuilder {
35 db: Db,
36 interner: KeywordInterner,
37 next_user: u64,
38}
39
40impl Default for PolicyBuilder {
41 fn default() -> Self {
42 Self::new()
43 }
44}
45
46impl PolicyBuilder {
47 #[must_use]
52 pub fn new() -> Self {
53 let (schema, idents) =
54 schema_from_edn(&schema::schema_forms()).expect("reserved schema is well formed");
55 let interner = KeywordInterner::default();
56 Self {
57 db: Db::new(schema).with_naming(idents, interner.clone()),
58 interner,
59 next_user: FIRST_USER_ID,
60 }
61 }
62
63 pub fn transact_edn(&mut self, text: &str) -> Result<&mut Self, BootstrapError> {
69 let forms = read_all(text)?;
70 let forms = match forms.as_slice() {
71 [Edn::Vector(items) | Edn::List(items)] => items.clone(),
72 _ => forms,
73 };
74 self.transact(&forms)
75 }
76
77 pub fn transact(&mut self, forms: &[Edn]) -> Result<&mut Self, BootstrapError> {
83 let items = tx_items_from_edn(&self.db, &mut self.interner, forms)?;
84 let t = self.db.basis_t() + 1;
85 let tx = EntityId::new(Partition::Tx as u32, t);
86 let prepared = corium_tx::prepare(&self.db, items, tx, self.next_user)?;
87 self.next_user = prepared
88 .tempids
89 .values()
90 .filter(|entity| entity.partition() == Partition::User as u32)
91 .map(|entity| entity.sequence() + 1)
92 .max()
93 .unwrap_or(self.next_user)
94 .max(self.next_user);
95 self.db = self
96 .db
97 .clone()
98 .with_naming(self.db.idents().clone(), self.interner.clone())
99 .with_transaction(t, &prepared.datoms);
100 Ok(self)
101 }
102
103 #[must_use]
105 pub fn db(&self) -> Db {
106 self.db.clone()
107 }
108}
109
110pub fn policy_db(text: &str) -> Result<Db, BootstrapError> {
115 let mut builder = PolicyBuilder::new();
116 builder.transact_edn(text)?;
117 Ok(builder.db())
118}
119
120#[must_use]
122pub fn tuple_form(subject: &str, relation: &str, object: &str) -> Edn {
123 entity(
127 "tuple",
128 &[
129 (
130 schema::TUPLE_SUBJECT,
131 SubjectRef::parse(subject).to_string(),
132 ),
133 (schema::TUPLE_RELATION, relation.to_owned()),
134 (schema::TUPLE_OBJECT, ObjectRef::parse(object).to_string()),
135 ],
136 )
137}
138
139#[must_use]
141pub fn principal_form(id: &str, provider: Option<&str>, roles: &[String]) -> Edn {
142 let mut pairs = vec![(Edn::keyword(schema::PRINCIPAL_ID), Edn::Str(id.to_owned()))];
143 if let Some(provider) = provider {
144 pairs.push((
145 Edn::keyword(schema::PRINCIPAL_PROVIDER),
146 Edn::Str(provider.to_owned()),
147 ));
148 }
149 if !roles.is_empty() {
150 pairs.push((
151 Edn::keyword(schema::PRINCIPAL_ROLE),
152 Edn::Vector(roles.iter().cloned().map(Edn::Str).collect()),
153 ));
154 }
155 map_with_id(format!("principal-{id}"), pairs)
156}
157
158#[must_use]
160pub fn permission_form(object_type: &str, action: &str, relations: &[String]) -> Edn {
161 map_with_id(
162 format!("permission-{object_type}-{action}"),
163 vec![
164 (
165 Edn::keyword(schema::PERMISSION_OBJECT_TYPE),
166 Edn::Str(object_type.to_owned()),
167 ),
168 (
169 Edn::keyword(schema::PERMISSION_ACTION),
170 Edn::Str(action.to_owned()),
171 ),
172 (
173 Edn::keyword(schema::PERMISSION_RELATION),
174 Edn::Vector(relations.iter().cloned().map(Edn::Str).collect()),
175 ),
176 ],
177 )
178}
179
180#[must_use]
183pub fn find_tuple(db: &Db, subject: &str, relation: &str, object: &str) -> Option<EntityId> {
184 let subject = SubjectRef::parse(subject).to_string();
185 let object = ObjectRef::parse(object).to_string();
186 let attribute = |ident: &str| db.idents().entid(&Keyword::parse(ident));
187 let (subject_attr, relation_attr, object_attr) = (
188 attribute(schema::TUPLE_SUBJECT)?,
189 attribute(schema::TUPLE_RELATION)?,
190 attribute(schema::TUPLE_OBJECT)?,
191 );
192 let matches = |entity: EntityId, attribute, expected: &str| {
193 db.values(entity, attribute)
194 .iter()
195 .any(|value| matches!(value, Value::Str(text) if &**text == expected))
196 };
197 db.datoms_for_attribute(subject_attr)
198 .filter(|datom| matches!(&datom.v, Value::Str(text) if **text == subject))
199 .map(|datom| datom.e)
200 .find(|entity| {
201 matches(*entity, relation_attr, relation) && matches(*entity, object_attr, &object)
202 })
203}
204
205#[must_use]
207pub fn retract_entity_form(entity: EntityId) -> Edn {
208 Edn::Vector(vec![
209 Edn::keyword("db/retractEntity"),
210 Edn::Long(i64::try_from(entity.raw()).unwrap_or_default()),
211 ])
212}
213
214fn entity(prefix: &str, attributes: &[(&str, String)]) -> Edn {
215 let pairs = attributes
216 .iter()
217 .map(|(attribute, value)| (Edn::keyword(attribute), Edn::Str(value.clone())))
218 .collect();
219 let id = format!(
220 "{prefix}-{}",
221 attributes
222 .iter()
223 .map(|(_, value)| value.as_str())
224 .collect::<Vec<_>>()
225 .join("-")
226 );
227 map_with_id(id, pairs)
228}
229
230fn map_with_id(temp_id: String, mut pairs: Vec<(Edn, Edn)>) -> Edn {
231 pairs.push((Edn::keyword("db/id"), Edn::Str(temp_id)));
232 pairs.sort();
233 Edn::Map(pairs)
234}
235
236#[cfg(test)]
237mod tests {
238 use super::*;
239 use crate::policy::Policy;
240
241 #[test]
242 fn builds_and_edits_a_policy_database() {
243 let mut builder = PolicyBuilder::new();
244 builder
245 .transact(&[
246 tuple_form("alice", "writer", "database:music"),
247 permission_form("database", "write", &["writer".to_owned()]),
248 ])
249 .expect("policy applies");
250 let db = builder.db();
251 let policy = Policy::compile(&db).expect("policy compiles");
252 assert_eq!(policy.stats().tuples, 1);
253 assert_eq!(policy.stats().permissions, 1);
254
255 let entity = find_tuple(&db, "alice", "writer", "database:music").expect("tuple is stored");
256 builder
257 .transact(&[retract_entity_form(entity)])
258 .expect("retraction applies");
259 let policy = Policy::compile(&builder.db()).expect("policy compiles");
260 assert_eq!(policy.stats().tuples, 0);
261 }
262
263 #[test]
264 fn normalizes_operator_shorthand() {
265 let Edn::Map(pairs) = tuple_form("alice", "member", "group:eng") else {
266 panic!("tuple form is a map");
267 };
268 let subject = pairs
269 .iter()
270 .find(|(key, _)| *key == Edn::keyword(schema::TUPLE_SUBJECT))
271 .map(|(_, value)| value.clone());
272 assert_eq!(subject, Some(Edn::Str("user:alice".to_owned())));
273 }
274}