1use std::collections::{BTreeMap, BTreeSet};
14use std::sync::Arc;
15
16use corium_core::{AttrId, Keyword, Value};
17use corium_db::Db;
18use corium_protocol::authz::{Action, ActionClass, ViewFilter};
19
20use crate::model::{
21 Binding, FilterKind, ObjectDef, ObjectRef, Permission, PrincipalDef, Rewrite, SubjectRef,
22 Tuple, ViewDef, WILDCARD, action_class_name, action_name,
23};
24use crate::schema;
25use crate::view;
26
27#[derive(Clone, Debug, thiserror::Error, PartialEq, Eq)]
29pub enum PolicyError {
30 #[error("database is not an authorization database: {0} is not installed")]
32 NotAnAuthzDatabase(String),
33 #[error("{entity} at entity {eid} is missing {attribute}")]
35 Incomplete {
36 entity: &'static str,
38 eid: String,
40 attribute: &'static str,
42 },
43 #[error("{attribute} at entity {eid} is not a {expected}")]
45 BadValue {
46 attribute: &'static str,
48 eid: String,
50 expected: &'static str,
52 },
53 #[error("binding {relation} on {object} names undefined view {view:?}")]
56 UndefinedView {
57 relation: String,
59 object: String,
61 view: String,
63 },
64 #[error("view {view:?} has unsupported filter type {kind:?}")]
66 UnsupportedFilter {
67 view: String,
69 kind: String,
71 },
72}
73
74#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
76pub struct PolicyStats {
77 pub principals: usize,
79 pub objects: usize,
81 pub tuples: usize,
83 pub permissions: usize,
85 pub rewrites: usize,
87 pub views: usize,
89 pub bindings: usize,
91}
92
93#[derive(Clone)]
95pub struct Policy {
96 basis_t: u64,
97 by_object: BTreeMap<(ObjectRef, String), Vec<SubjectRef>>,
99 rewrites: BTreeMap<String, Vec<Rewrite>>,
101 permissions: BTreeMap<(String, String), BTreeSet<String>>,
103 views: BTreeMap<String, Arc<dyn ViewFilter>>,
105 bindings: BTreeMap<(String, ObjectRef), Binding>,
107 principals: BTreeMap<String, PrincipalDef>,
109 objects_by_database: BTreeMap<String, Vec<ObjectRef>>,
111 stats: PolicyStats,
112}
113
114impl std::fmt::Debug for Policy {
115 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
116 formatter
117 .debug_struct("Policy")
118 .field("basis_t", &self.basis_t)
119 .field("stats", &self.stats)
120 .finish_non_exhaustive()
121 }
122}
123
124impl Policy {
125 #[must_use]
128 pub fn empty() -> Self {
129 Self {
130 basis_t: 0,
131 by_object: BTreeMap::new(),
132 rewrites: BTreeMap::new(),
133 permissions: BTreeMap::new(),
134 views: BTreeMap::new(),
135 bindings: BTreeMap::new(),
136 principals: BTreeMap::new(),
137 objects_by_database: BTreeMap::new(),
138 stats: PolicyStats::default(),
139 }
140 }
141
142 pub fn compile(db: &Db) -> Result<Self, PolicyError> {
149 let reader = Reader::new(db)?;
150 let mut policy = Self::empty();
151 policy.basis_t = db.basis_t();
152 policy.read_principals(&reader)?;
153 policy.read_objects(&reader)?;
154 policy.read_tuples(&reader)?;
155 policy.read_permissions(&reader)?;
156 policy.read_rewrites(&reader)?;
157 policy.read_views(&reader)?;
160 policy.read_bindings(&reader)?;
161 policy.stats.principals = policy.principals.len();
162 Ok(policy)
163 }
164
165 fn read_principals(&mut self, reader: &Reader) -> Result<(), PolicyError> {
166 for (eid, values) in reader.rows(schema::PRINCIPAL_ID) {
167 let id = reader.one_string("principal", eid, schema::PRINCIPAL_ID, values)?;
168 let provider = reader
169 .value(eid, schema::PRINCIPAL_PROVIDER)
170 .map(|values| {
171 reader.one_string("principal", eid, schema::PRINCIPAL_PROVIDER, values)
172 })
173 .transpose()?
174 .filter(|provider| provider != WILDCARD);
175 let roles = reader.strings(eid, schema::PRINCIPAL_ROLE);
176 self.principals.insert(
177 id.clone(),
178 PrincipalDef {
179 id,
180 provider,
181 roles,
182 },
183 );
184 }
185 Ok(())
186 }
187
188 fn read_objects(&mut self, reader: &Reader) -> Result<(), PolicyError> {
189 for (eid, values) in reader.rows(schema::OBJECT_ID) {
190 let id = reader.one_string("object", eid, schema::OBJECT_ID, values)?;
191 let kind = reader.required_string("object", eid, schema::OBJECT_TYPE)?;
192 let database = reader
193 .value(eid, schema::OBJECT_DATABASE)
194 .map(|values| reader.one_string("object", eid, schema::OBJECT_DATABASE, values))
195 .transpose()?;
196 let definition = ObjectDef {
197 object: ObjectRef::new(kind, id),
198 database,
199 };
200 if let Some(database) = &definition.database {
201 self.objects_by_database
202 .entry(database.clone())
203 .or_default()
204 .push(definition.object.clone());
205 }
206 self.stats.objects += 1;
207 }
208 Ok(())
209 }
210
211 fn read_tuples(&mut self, reader: &Reader) -> Result<(), PolicyError> {
212 for (eid, values) in reader.rows(schema::TUPLE_RELATION) {
213 let relation = reader.one_string("tuple", eid, schema::TUPLE_RELATION, values)?;
214 let subject = reader.required_string("tuple", eid, schema::TUPLE_SUBJECT)?;
215 let object = reader.required_string("tuple", eid, schema::TUPLE_OBJECT)?;
216 let tuple = Tuple {
217 subject: SubjectRef::parse(&subject),
218 relation,
219 object: ObjectRef::parse(&object),
220 };
221 self.by_object
222 .entry((tuple.object.clone(), tuple.relation.clone()))
223 .or_default()
224 .push(tuple.subject);
225 self.stats.tuples += 1;
226 }
227 Ok(())
228 }
229
230 fn read_permissions(&mut self, reader: &Reader) -> Result<(), PolicyError> {
231 for (eid, values) in reader.rows(schema::PERMISSION_ACTION) {
232 let action = reader.one_string("permission", eid, schema::PERMISSION_ACTION, values)?;
233 let object_type = reader
234 .value(eid, schema::PERMISSION_OBJECT_TYPE)
235 .map(|values| {
236 reader.one_string("permission", eid, schema::PERMISSION_OBJECT_TYPE, values)
237 })
238 .transpose()?
239 .unwrap_or_else(|| WILDCARD.to_owned());
240 let relations = reader.strings(eid, schema::PERMISSION_RELATION);
241 if relations.is_empty() {
242 return Err(PolicyError::Incomplete {
243 entity: "permission",
244 eid: format!("{eid:?}"),
245 attribute: schema::PERMISSION_RELATION,
246 });
247 }
248 let permission = Permission {
249 object_type,
250 action,
251 relations,
252 };
253 self.permissions
254 .entry((permission.object_type.clone(), permission.action.clone()))
255 .or_default()
256 .extend(permission.relations);
257 self.stats.permissions += 1;
258 }
259 Ok(())
260 }
261
262 fn read_rewrites(&mut self, reader: &Reader) -> Result<(), PolicyError> {
263 for (eid, values) in reader.rows(schema::REWRITE_RELATION) {
264 let relation = reader.one_string("rewrite", eid, schema::REWRITE_RELATION, values)?;
265 let via_relation =
266 reader.required_string("rewrite", eid, schema::REWRITE_VIA_RELATION)?;
267 let on_relation =
268 reader.required_string("rewrite", eid, schema::REWRITE_ON_RELATION)?;
269 let object_type = reader
270 .value(eid, schema::REWRITE_OBJECT_TYPE)
271 .map(|values| {
272 reader.one_string("rewrite", eid, schema::REWRITE_OBJECT_TYPE, values)
273 })
274 .transpose()?
275 .filter(|kind| kind != WILDCARD);
276 self.rewrites
277 .entry(relation.clone())
278 .or_default()
279 .push(Rewrite {
280 relation,
281 via_relation,
282 on_relation,
283 object_type,
284 });
285 self.stats.rewrites += 1;
286 }
287 Ok(())
288 }
289
290 fn read_views(&mut self, reader: &Reader) -> Result<(), PolicyError> {
291 for (eid, values) in reader.rows(schema::VIEW_NAME) {
292 let name = reader.one_string("view", eid, schema::VIEW_NAME, values)?;
293 let kind_text = reader.required_string("view", eid, schema::VIEW_FILTER_TYPE)?;
294 let kind =
295 FilterKind::parse(&kind_text).ok_or_else(|| PolicyError::UnsupportedFilter {
296 view: name.clone(),
297 kind: kind_text,
298 })?;
299 let definition = ViewDef {
300 name: name.clone(),
301 kind,
302 attributes: reader.strings(eid, schema::VIEW_ATTRIBUTE),
303 };
304 self.views.insert(name, view::build(&definition));
305 self.stats.views += 1;
306 }
307 Ok(())
308 }
309
310 fn read_bindings(&mut self, reader: &Reader) -> Result<(), PolicyError> {
311 for (eid, values) in reader.rows(schema::BINDING_RELATION) {
312 let relation = reader.one_string("binding", eid, schema::BINDING_RELATION, values)?;
313 let object = ObjectRef::parse(&reader.required_string(
314 "binding",
315 eid,
316 schema::BINDING_OBJECT,
317 )?);
318 let view_name = reader
319 .value(eid, schema::BINDING_VIEW)
320 .map(|values| reader.one_string("binding", eid, schema::BINDING_VIEW, values))
321 .transpose()?;
322 let unfiltered = reader
323 .value(eid, schema::BINDING_UNFILTERED)
324 .and_then(|values| values.first())
325 .is_some_and(|value| matches!(value, Value::Bool(true)));
326 if let Some(name) = &view_name
327 && !self.views.contains_key(name)
328 {
329 return Err(PolicyError::UndefinedView {
330 relation,
331 object: object.to_string(),
332 view: name.clone(),
333 });
334 }
335 self.bindings.insert(
336 (relation.clone(), object.clone()),
337 Binding {
338 relation,
339 object,
340 view: view_name,
341 unfiltered,
342 },
343 );
344 self.stats.bindings += 1;
345 }
346 Ok(())
347 }
348
349 #[must_use]
351 pub const fn basis_t(&self) -> u64 {
352 self.basis_t
353 }
354
355 #[must_use]
357 pub const fn stats(&self) -> PolicyStats {
358 self.stats
359 }
360
361 #[must_use]
363 pub fn principal(&self, id: &str) -> Option<&PrincipalDef> {
364 self.principals.get(id)
365 }
366
367 #[must_use]
369 pub fn objects_for_database(&self, name: &str) -> &[ObjectRef] {
370 self.objects_by_database
371 .get(name)
372 .map_or(&[], Vec::as_slice)
373 }
374
375 #[must_use]
381 pub fn relations_for(&self, object_type: &str, action: Action) -> BTreeSet<String> {
382 let names = [
383 action_name(action),
384 action_class_name(ActionClass::of(action)),
385 WILDCARD,
386 ];
387 let mut relations = BTreeSet::new();
388 for name in names {
389 for kind in [object_type, WILDCARD] {
390 if let Some(found) = self.permissions.get(&(kind.to_owned(), name.to_owned())) {
391 relations.extend(found.iter().cloned());
392 }
393 }
394 }
395 relations
396 }
397
398 pub(crate) fn subjects_for(&self, object: &ObjectRef, relation: &str) -> Vec<&SubjectRef> {
401 let mut subjects = Vec::new();
402 for key in [object.clone(), object.wildcard_of_type()] {
403 if let Some(found) = self.by_object.get(&(key, relation.to_owned())) {
404 subjects.extend(found.iter());
405 }
406 if object.is_wildcard() {
407 break;
408 }
409 }
410 subjects
411 }
412
413 pub(crate) fn rewrites_for(&self, relation: &str, object_type: &str) -> Vec<&Rewrite> {
415 self.rewrites.get(relation).map_or_else(Vec::new, |rules| {
416 rules
417 .iter()
418 .filter(|rule| {
419 rule.object_type
420 .as_ref()
421 .is_none_or(|kind| kind == object_type)
422 })
423 .collect()
424 })
425 }
426
427 pub(crate) fn binding_for(&self, relation: &str, object: &ObjectRef) -> Option<&Binding> {
430 self.bindings
431 .get(&(relation.to_owned(), object.clone()))
432 .or_else(|| {
433 self.bindings
434 .get(&(relation.to_owned(), object.wildcard_of_type()))
435 })
436 }
437
438 pub(crate) fn view(&self, name: &str) -> Option<&Arc<dyn ViewFilter>> {
440 self.views.get(name)
441 }
442}
443
444struct Reader {
446 rows: BTreeMap<&'static str, BTreeMap<corium_core::EntityId, Vec<Value>>>,
448 interner: corium_core::KeywordInterner,
449}
450
451impl Reader {
452 fn new(db: &Db) -> Result<Self, PolicyError> {
453 let mut rows: BTreeMap<&'static str, BTreeMap<corium_core::EntityId, Vec<Value>>> =
454 BTreeMap::new();
455 let mut installed = 0usize;
456 for attribute in schema::ATTRIBUTES {
457 let Some(id) = attribute_id(db, attribute) else {
458 continue;
459 };
460 installed += 1;
461 let mut by_entity: BTreeMap<corium_core::EntityId, Vec<Value>> = BTreeMap::new();
462 for datom in db.datoms_for_attribute(id) {
463 by_entity.entry(datom.e).or_default().push(datom.v.clone());
464 }
465 rows.insert(attribute, by_entity);
466 }
467 if installed == 0 {
471 return Err(PolicyError::NotAnAuthzDatabase(
472 schema::TUPLE_SUBJECT.to_owned(),
473 ));
474 }
475 Ok(Self {
476 rows,
477 interner: db.interner().clone(),
478 })
479 }
480
481 fn rows(&self, attribute: &'static str) -> Vec<(corium_core::EntityId, &Vec<Value>)> {
483 self.rows.get(attribute).map_or_else(Vec::new, |by_entity| {
484 by_entity
485 .iter()
486 .map(|(eid, values)| (*eid, values))
487 .collect()
488 })
489 }
490
491 fn value(&self, eid: corium_core::EntityId, attribute: &'static str) -> Option<&Vec<Value>> {
492 self.rows.get(attribute).and_then(|rows| rows.get(&eid))
493 }
494
495 fn one_string(
496 &self,
497 entity: &'static str,
498 eid: corium_core::EntityId,
499 attribute: &'static str,
500 values: &[Value],
501 ) -> Result<String, PolicyError> {
502 let value = values.first().ok_or_else(|| PolicyError::Incomplete {
503 entity,
504 eid: format!("{eid:?}"),
505 attribute,
506 })?;
507 self.text(value).ok_or_else(|| PolicyError::BadValue {
508 attribute,
509 eid: format!("{eid:?}"),
510 expected: "string",
511 })
512 }
513
514 fn required_string(
515 &self,
516 entity: &'static str,
517 eid: corium_core::EntityId,
518 attribute: &'static str,
519 ) -> Result<String, PolicyError> {
520 let values = self.value(eid, attribute).ok_or(PolicyError::Incomplete {
521 entity,
522 eid: format!("{eid:?}"),
523 attribute,
524 })?;
525 self.one_string(entity, eid, attribute, values)
526 }
527
528 fn strings(&self, eid: corium_core::EntityId, attribute: &'static str) -> Vec<String> {
529 self.value(eid, attribute).map_or_else(Vec::new, |values| {
530 values.iter().filter_map(|value| self.text(value)).collect()
531 })
532 }
533
534 fn text(&self, value: &Value) -> Option<String> {
537 match value {
538 Value::Str(text) => Some(text.to_string()),
539 Value::Keyword(id) => self
540 .interner
541 .resolve(*id)
542 .map(std::string::ToString::to_string),
543 _ => None,
544 }
545 }
546}
547
548fn attribute_id(db: &Db, attribute: &str) -> Option<AttrId> {
549 db.idents().entid(&Keyword::parse(attribute))
550}