1use std::collections::BTreeMap;
7use std::rc::Rc;
8
9use ravenrs_criteria::{Criteria, Environment, Scope, Value};
10
11use crate::{EntityInfo, FieldKind, Object};
12
13mod audit;
14mod directory;
15mod password;
16mod records;
17mod space;
18
19pub use audit::{AuditMode, AuditSettings, AuditTrail, AuditedObjectSpace};
20pub use directory::{
21 AuthError, Authenticator, Credentials, MIN_PASSWORD_CHARS, PasswordError, SecurityContext, SpaceMaker,
22 UserDirectory,
23};
24pub use password::{hash_password, verify_password};
25pub use records::{
26 ADMINISTRATORS_ROLE, SECURITY_ACTION_PERMISSION, SECURITY_AUDIT_ENTRY, SECURITY_MEMBER_PERMISSION,
27 SECURITY_NAVIGATION_PERMISSION, SECURITY_OBJECT_PERMISSION, SECURITY_ROLE, SECURITY_TYPE_PERMISSION,
28 SECURITY_USER, SECURITY_USER_ROLE, audit_entity, security_entities,
29};
30pub use space::SecuredObjectSpace;
31
32#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
34#[non_exhaustive]
35#[cfg_attr(feature = "live", ravenrs_live::expose(part = "variants"))]
36pub enum Operation {
37 Read,
39 Write,
41 Create,
43 Delete,
45}
46
47#[derive(Clone, Copy, Debug, PartialEq, Eq)]
49#[cfg_attr(feature = "live", ravenrs_live::expose(part = "variants"))]
50pub enum Access {
51 Allow,
53 Deny,
55}
56
57#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
59#[cfg_attr(feature = "live", ravenrs_live::expose(part = "variants"))]
60pub enum PermissionPolicy {
61 #[default]
63 DenyAll,
64 ReadOnlyAll,
66 AllowAll,
68}
69
70#[cfg_attr(feature = "live", ravenrs_live::expose)]
71impl PermissionPolicy {
72 fn allows(self, operation: Operation) -> bool {
73 match self {
74 Self::DenyAll => false,
75 Self::ReadOnlyAll => operation == Operation::Read,
76 Self::AllowAll => true,
77 }
78 }
79}
80
81#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
83#[cfg_attr(feature = "live", ravenrs_live::expose(part = "variants"))]
84pub enum RoleMerge {
85 #[default]
88 AnyRole,
89 AllRoles,
91}
92
93#[derive(Clone, Debug)]
96struct Condition {
97 criteria: Option<Criteria>,
98}
99
100#[cfg_attr(feature = "live", ravenrs_live::expose)]
101impl Condition {
102 fn new(source: &str) -> Self {
103 Self { criteria: Criteria::parse(source).ok() }
104 }
105
106 fn holds(&self, record: &Object, access: Access, environment: &Environment) -> bool {
108 let denies = access == Access::Deny;
109 match &self.criteria {
110 Some(criteria) => match Scope::new(environment).row(record).evaluate(criteria) {
111 Ok(Value::Bool(holds)) => holds,
112 _ => denies,
113 },
114 None => denies,
115 }
116 }
117}
118
119#[derive(Clone, Debug)]
120struct TypePermission {
121 entity: String,
122 operation: Operation,
123 access: Access,
124}
125
126#[derive(Clone, Debug)]
127struct ObjectPermission {
128 entity: String,
129 condition: Condition,
130 operation: Operation,
131 access: Access,
132}
133
134#[derive(Clone, Debug)]
135struct MemberPermission {
136 entity: String,
137 fields: Vec<String>,
138 condition: Option<Condition>,
139 operation: Operation,
140 access: Access,
141}
142
143#[derive(Clone, Debug, Default)]
145#[cfg_attr(feature = "live", ravenrs_live::expose(part = "fields"))]
146pub struct Role {
147 name: String,
148 policy: PermissionPolicy,
149 administrative: bool,
150 edit_model: bool,
151 types: Vec<TypePermission>,
152 objects: Vec<ObjectPermission>,
153 members: Vec<MemberPermission>,
154 navigation: BTreeMap<String, Access>,
155 actions: BTreeMap<String, Access>,
156}
157
158#[cfg_attr(feature = "live", ravenrs_live::expose)]
159impl Role {
160 pub fn new(name: impl Into<String>) -> Self {
162 Self { name: name.into(), ..Self::default() }
163 }
164
165 pub fn name(&self) -> &str {
167 &self.name
168 }
169
170 pub fn policy(mut self, policy: PermissionPolicy) -> Self {
172 self.policy = policy;
173 self
174 }
175
176 pub fn administrative(mut self) -> Self {
178 self.administrative = true;
179 self.edit_model = true;
180 self
181 }
182
183 pub fn is_administrative(&self) -> bool {
185 self.administrative
186 }
187
188 pub fn can_edit_model(mut self) -> Self {
190 self.edit_model = true;
191 self
192 }
193
194 pub fn type_permission(
196 mut self,
197 entity: impl Into<String>,
198 operations: impl IntoIterator<Item = Operation>,
199 access: Access,
200 ) -> Self {
201 let entity = entity.into();
202 self.types.extend(operations.into_iter().map(|operation| TypePermission {
203 entity: entity.clone(),
204 operation,
205 access,
206 }));
207 self
208 }
209
210 pub fn object_permission(
213 mut self,
214 entity: impl Into<String>,
215 criteria: &str,
216 operations: impl IntoIterator<Item = Operation>,
217 access: Access,
218 ) -> Self {
219 let entity = entity.into();
220 self.objects.extend(operations.into_iter().map(|operation| ObjectPermission {
221 entity: entity.clone(),
222 condition: Condition::new(criteria),
223 operation,
224 access,
225 }));
226 self
227 }
228
229 pub fn member_permission(
232 mut self,
233 entity: impl Into<String>,
234 fields: impl IntoIterator<Item = impl Into<String>>,
235 criteria: Option<&str>,
236 operations: impl IntoIterator<Item = Operation>,
237 access: Access,
238 ) -> Self {
239 let entity = entity.into();
240 let fields: Vec<String> = fields.into_iter().map(Into::into).collect();
241 self.members.extend(operations.into_iter().map(|operation| MemberPermission {
242 entity: entity.clone(),
243 fields: fields.clone(),
244 condition: criteria.map(Condition::new),
245 operation,
246 access,
247 }));
248 self
249 }
250
251 pub fn navigation_permission(mut self, item: impl Into<String>, access: Access) -> Self {
254 self.navigation.insert(item.into(), access);
255 self
256 }
257
258 pub fn action_permission(mut self, action: impl Into<String>, access: Access) -> Self {
260 self.actions.insert(action.into(), access);
261 self
262 }
263
264 fn type_says(&self, operation: Operation, entity: &str) -> Option<Access> {
265 strongest(
266 self.types.iter().filter(|p| p.operation == operation && p.entity == entity).map(|p| p.access),
267 )
268 }
269
270 fn objects_say(
273 &self,
274 operation: Operation,
275 entity: &str,
276 record: Option<&Object>,
277 environment: &Environment,
278 ) -> Option<Access> {
279 strongest(
280 self.objects
281 .iter()
282 .filter(|p| p.operation == operation && p.entity == entity)
283 .filter(|p| match record {
284 Some(record) => p.condition.holds(record, p.access, environment),
285 None => p.access == Access::Allow && p.condition.criteria.is_some(),
286 })
287 .map(|p| p.access),
288 )
289 }
290
291 fn members_say(
292 &self,
293 operation: Operation,
294 entity: &str,
295 field: &str,
296 record: Option<&Object>,
297 environment: &Environment,
298 ) -> Option<Access> {
299 let about = || {
300 self.members
301 .iter()
302 .filter(|p| p.operation == operation && p.entity == entity)
303 .filter(|p| p.fields.iter().any(|name| name == field))
304 };
305 let by_criteria = strongest(
306 about()
307 .filter_map(|p| p.condition.as_ref().map(|condition| (p, condition)))
308 .filter(|(p, condition)| match record {
309 Some(record) => condition.holds(record, p.access, environment),
310 None => p.access == Access::Allow && condition.criteria.is_some(),
311 })
312 .map(|(p, _)| p.access),
313 );
314 by_criteria.or_else(|| strongest(about().filter(|p| p.condition.is_none()).map(|p| p.access)))
315 }
316
317 fn owner_says(&self, operation: Operation, entity: &str, entities: &[EntityInfo]) -> Option<Access> {
321 let asked = if operation == Operation::Read { Operation::Read } else { Operation::Write };
322 strongest(entities.iter().flat_map(|owner| {
323 owner
324 .fields
325 .iter()
326 .filter(move |field| {
327 field.aggregated
328 && matches!(&field.kind, FieldKind::Collection(target) if target == entity)
329 })
330 .filter_map(move |field| {
331 let conditional =
332 self.objects.iter().any(|p| p.entity == owner.name && p.operation == asked)
333 || self.members.iter().any(|p| {
334 p.operation == asked
335 && p.entity == owner.name
336 && p.condition.is_some()
337 && p.fields.contains(&field.name)
338 });
339 if conditional {
340 return Some(Access::Deny);
341 }
342 strongest(
343 self.members
344 .iter()
345 .filter(|p| {
346 p.operation == asked
347 && p.entity == owner.name
348 && p.condition.is_none()
349 && p.fields.contains(&field.name)
350 })
351 .map(|p| p.access),
352 )
353 .or_else(|| self.type_says(asked, &owner.name))
354 })
355 }))
356 }
357
358 fn can(
359 &self,
360 operation: Operation,
361 entity: &str,
362 record: Option<&Object>,
363 entities: &[EntityInfo],
364 environment: &Environment,
365 ) -> bool {
366 if self.administrative {
367 return true;
368 }
369 let said = self
370 .objects_say(operation, entity, record, environment)
371 .or_else(|| self.type_says(operation, entity))
372 .or_else(|| self.owner_says(operation, entity, entities));
373 said.map_or_else(|| self.policy.allows(operation), |access| access == Access::Allow)
374 }
375
376 fn can_field(
377 &self,
378 operation: Operation,
379 entity: &str,
380 field: &str,
381 record: Option<&Object>,
382 entities: &[EntityInfo],
383 environment: &Environment,
384 ) -> bool {
385 if self.administrative {
386 return true;
387 }
388 match self.members_say(operation, entity, field, record, environment) {
389 Some(access) => access == Access::Allow,
390 None => self.can(operation, entity, record, entities, environment),
391 }
392 }
393
394 fn depends_on_record(&self, entity: &str) -> bool {
395 !self.administrative
396 && (self.objects.iter().any(|p| p.entity == entity)
397 || self.members.iter().any(|p| p.entity == entity && p.condition.is_some()))
398 }
399
400 fn merged_with(mut self, code: &Role) -> Self {
402 self.administrative |= code.administrative;
403 self.edit_model |= code.edit_model;
404 self.types.extend(code.types.iter().cloned());
405 self.objects.extend(code.objects.iter().cloned());
406 self.members.extend(code.members.iter().cloned());
407 for (item, access) in &code.navigation {
408 self.navigation.entry(item.clone()).or_insert(*access);
409 }
410 for (action, access) in &code.actions {
411 self.actions.entry(action.clone()).or_insert(*access);
412 }
413 if self.policy == PermissionPolicy::DenyAll {
414 self.policy = code.policy;
415 }
416 self
417 }
418}
419
420fn strongest(said: impl Iterator<Item = Access>) -> Option<Access> {
422 let mut found = None;
423 for access in said {
424 if access == Access::Deny {
425 return Some(Access::Deny);
426 }
427 found = Some(access);
428 }
429 found
430}
431
432#[derive(Clone, Debug)]
434#[non_exhaustive]
435#[cfg_attr(feature = "live", ravenrs_live::expose(part = "fields"))]
436pub struct Identity {
437 pub key: Value,
439 pub name: String,
441 pub roles: Vec<Role>,
443 pub must_change_password: bool,
445}
446
447#[cfg_attr(feature = "live", ravenrs_live::expose)]
448impl Identity {
449 pub fn new(key: impl Into<Value>, name: impl Into<String>, roles: Vec<Role>) -> Self {
451 Self { key: key.into(), name: name.into(), roles, must_change_password: false }
452 }
453}
454
455#[derive(Debug)]
457#[cfg_attr(feature = "live", ravenrs_live::expose(part = "fields"))]
458pub struct Permissions {
459 identity: Identity,
460 merge: RoleMerge,
461 entities: Rc<Vec<EntityInfo>>,
462 environment: Environment,
463}
464
465#[cfg_attr(feature = "live", ravenrs_live::expose)]
466impl Permissions {
467 pub fn new(identity: Identity, entities: Vec<EntityInfo>, merge: RoleMerge) -> Self {
469 let environment = crate::environment()
470 .parameter("CurrentUserId", identity.key.clone())
471 .parameter("CurrentUserName", identity.name.clone());
472 Self { identity, merge, entities: Rc::new(entities), environment }
473 }
474
475 pub fn identity(&self) -> &Identity {
477 &self.identity
478 }
479
480 pub fn environment(&self) -> &Environment {
482 &self.environment
483 }
484
485 pub(crate) fn entity(&self, name: &str) -> Option<&EntityInfo> {
486 self.entities.iter().find(|entity| entity.name == name)
487 }
488
489 fn agreed(&self, ask: impl Fn(&Role) -> bool) -> bool {
490 let roles = &self.identity.roles;
491 match self.merge {
492 RoleMerge::AnyRole => roles.iter().any(ask),
493 RoleMerge::AllRoles => !roles.is_empty() && roles.iter().all(ask),
494 }
495 }
496
497 pub fn is_administrator(&self) -> bool {
499 self.identity.roles.iter().any(|role| role.administrative)
500 }
501
502 pub fn can_edit_model(&self) -> bool {
504 self.identity.roles.iter().any(|role| role.edit_model)
505 }
506
507 fn built_in(&self, operation: Operation, entity: &str) -> Option<bool> {
510 if entity == SECURITY_AUDIT_ENTRY {
511 return Some(operation == Operation::Read && self.is_administrator());
512 }
513 records::is_security_entity(entity).then(|| self.is_administrator())
514 }
515
516 pub fn can(&self, operation: Operation, entity: &str, record: Option<&Object>) -> bool {
518 if let Some(answer) = self.built_in(operation, entity) {
519 return answer;
520 }
521 self.agreed(|role| role.can(operation, entity, record, &self.entities, &self.environment))
522 }
523
524 pub fn can_field(
526 &self,
527 operation: Operation,
528 entity: &str,
529 field: &str,
530 record: Option<&Object>,
531 ) -> bool {
532 self.can_field_within(operation, entity, field, record, 0)
533 }
534
535 fn can_field_within(
536 &self,
537 operation: Operation,
538 entity: &str,
539 field: &str,
540 record: Option<&Object>,
541 depth: usize,
542 ) -> bool {
543 if let Some(answer) = self.built_in(operation, entity)
544 && !answer
545 {
546 return false;
547 }
548 let info = self.entity(entity).and_then(|info| info.field_named(field));
549 if info.is_some_and(|info| info.secret) {
550 return false;
551 }
552 let allowed = self.agreed(|role| {
553 role.can_field(operation, entity, field, record, &self.entities, &self.environment)
554 });
555 if !allowed {
556 return false;
557 }
558 match info.and_then(|info| info.calculated.as_deref()) {
560 Some(expression) if operation == Operation::Read => {
561 depth < 8
562 && Criteria::parse(expression).is_ok_and(|criteria| {
563 criteria.properties().iter().all(|path| {
564 let head = path.split('.').next().unwrap_or(path).trim_start_matches('^');
565 head == field
566 || self.can_field_within(Operation::Read, entity, head, record, depth + 1)
567 })
568 })
569 }
570 _ => true,
571 }
572 }
573
574 pub(crate) fn depends_on_record(&self, entity: &str) -> bool {
576 self.identity.roles.iter().any(|role| role.depends_on_record(entity))
577 }
578
579 pub fn can_navigate(&self, group: Option<&str>, item: &str) -> bool {
582 if self.identity.roles.iter().any(|role| role.administrative) {
583 return true;
584 }
585 self.agreed(|role| {
586 let said =
587 role.navigation.get(item).or_else(|| group.and_then(|group| role.navigation.get(group)));
588 said != Some(&Access::Deny)
589 })
590 }
591
592 pub fn can_use_action(&self, action: &str) -> bool {
594 if self.is_administrator() {
595 return true;
596 }
597 !self.identity.roles.iter().any(|role| role.actions.get(action) == Some(&Access::Deny))
598 }
599}