1use std::{
2 collections::HashMap,
3 path::Path,
4 sync::{LockResult, Mutex, RwLock, RwLockReadGuard},
5};
6
7pub use kcode_k1_access_format::{AccessAction, OwnerWitness};
8pub use kcode_k1_access_types::{
9 AccessCheck, AccessId, AccessRevision, Authorizations, GroupId, ModelId, OwnerSubject,
10 RequestPrincipal, SubsystemId, Target, TxId, UserId, ViewerSubject,
11};
12pub use kcode_k1_txn_ordering::K1TxnOrdering;
13
14use kcode_k1_access_discovery_store::DiscoveryStore;
15use kcode_k1_access_store::{Store, StoreMutation, StoredAccess};
16use kcode_k1_transaction::Transaction;
17
18#[derive(Clone, Debug, Eq, PartialEq)]
19pub enum ApplyOutcome {
20 Applied(AccessRevision),
21 Unchanged(AccessRevision),
22 Rejected(String),
23}
24
25pub struct Projection {
26 store: Store,
27 discovery: DiscoveryStore,
28 state: RwLock<State>,
29 apply: Mutex<()>,
30}
31struct State {
32 available: bool,
33 objects: HashMap<AccessId, StoredAccess>,
34 targets: HashMap<Target, AccessId>,
35}
36struct Prepared {
37 outcome: ApplyOutcome,
38 mutation: StoreMutation,
39 delta: Delta,
40 discovery: Option<(AccessId, Authorizations)>,
41}
42enum Delta {
43 None,
44 Create(AccessId, Target, StoredAccess),
45 Replace(AccessId, StoredAccess),
46}
47
48fn locked<T>(result: LockResult<T>, message: &str) -> Result<T, String> {
49 result.map_err(|_| message.to_owned())
50}
51fn subjects(authorizations: &Authorizations) -> (Vec<UserId>, Vec<GroupId>) {
52 let mut users = Vec::new();
53 let mut groups = Vec::new();
54 for owner in authorizations.owners() {
55 match owner {
56 OwnerSubject::User(user) => users.push(*user),
57 OwnerSubject::Group(group) => groups.push(*group),
58 }
59 }
60 for viewer in authorizations.viewers() {
61 match viewer {
62 ViewerSubject::User(user) => users.push(*user),
63 ViewerSubject::Group(group) => groups.push(*group),
64 ViewerSubject::Model(_) => {}
65 }
66 }
67 (users, groups)
68}
69
70impl State {
71 fn finish(&mut self, delta: Delta) -> Result<(), String> {
72 let failure = match delta {
73 Delta::None => None,
74 Delta::Create(access_id, target, stored) => {
75 if self.objects.contains_key(&access_id) || self.targets.contains_key(&target) {
76 Some("projection create contradiction after commit")
77 } else {
78 self.targets.insert(target, access_id);
79 self.objects.insert(access_id, stored);
80 None
81 }
82 }
83 Delta::Replace(access_id, stored) => {
84 let consistent = self.targets.get(stored.target()) == Some(&access_id)
85 && self
86 .objects
87 .get(&access_id)
88 .is_some_and(|row| row.target() == stored.target());
89 if !consistent {
90 Some("projection replace contradiction after commit")
91 } else if let Some(row) = self.objects.get_mut(&access_id) {
92 *row = stored;
93 None
94 } else {
95 Some("projection replace disappeared after commit")
96 }
97 }
98 };
99 if let Some(failure) = failure {
100 self.available = false;
101 return Err(failure.to_owned());
102 }
103 Ok(())
104 }
105}
106
107impl Projection {
108 pub fn open(root: &Path, ordering: &K1TxnOrdering) -> Result<(Self, Option<TxId>), String> {
109 let store = Store::open(root)?;
110 let discovery = DiscoveryStore::open(root)?;
111 let (cursor, rows) = store.snapshot()?.into_parts();
112 let subsystem = SubsystemId::from_str("k1-access-subsystem")?;
113 if !Self::valid_snapshot(ordering, subsystem, cursor, &rows)?
114 || discovery.checkpoint()? != cursor
115 {
116 return Self::reset(store, discovery, root);
117 }
118 for row in &rows {
119 let (users, groups) = subjects(row.authorizations());
120 if discovery.contains_missing(&users, &groups, row.access_id())? {
121 return Self::reset(store, discovery, root);
122 }
123 }
124 let mut objects = HashMap::new();
125 let mut targets = HashMap::new();
126 objects
127 .try_reserve(rows.len())
128 .map_err(|error| format!("unable to reserve access index: {error}"))?;
129 targets
130 .try_reserve(rows.len())
131 .map_err(|error| format!("unable to reserve target index: {error}"))?;
132 for row in rows {
133 let access_id = row.access_id();
134 let target = row.target().clone();
135 if objects.insert(access_id, row).is_some()
136 || targets.insert(target, access_id).is_some()
137 {
138 return Self::reset(store, discovery, root);
139 }
140 }
141 Ok((Self::new(store, discovery, objects, targets), cursor))
142 }
143 fn new(
144 store: Store,
145 discovery: DiscoveryStore,
146 objects: HashMap<AccessId, StoredAccess>,
147 targets: HashMap<Target, AccessId>,
148 ) -> Self {
149 Self {
150 store,
151 discovery,
152 state: RwLock::new(State {
153 available: true,
154 objects,
155 targets,
156 }),
157 apply: Mutex::new(()),
158 }
159 }
160 fn reset(
161 store: Store,
162 discovery: DiscoveryStore,
163 root: &Path,
164 ) -> Result<(Self, Option<TxId>), String> {
165 store.clear()?;
166 discovery.discard()?;
167 let discovery = DiscoveryStore::open(root)?;
168 Ok((
169 Self::new(store, discovery, HashMap::new(), HashMap::new()),
170 None,
171 ))
172 }
173 fn valid_snapshot(
174 ordering: &K1TxnOrdering,
175 subsystem: SubsystemId,
176 cursor: Option<TxId>,
177 rows: &[StoredAccess],
178 ) -> Result<bool, String> {
179 if !rows.is_empty() && cursor.is_none() {
180 return Ok(false);
181 }
182 if let Some(cursor) = cursor
183 && Self::load_action(ordering, subsystem, cursor)?.is_none()
184 {
185 return Ok(false);
186 }
187 for row in rows {
188 let access_id = row.access_id();
189 let Some(create) = Self::load_action(ordering, subsystem, access_id.txid())? else {
190 return Ok(false);
191 };
192 let valid = match create {
193 AccessAction::Create {
194 target,
195 authorizations,
196 } if &target == row.target() => {
197 if row.revision() == access_id.txid() {
198 &authorizations == row.authorizations()
199 } else {
200 matches!(Self::load_action(ordering, subsystem, row.revision())?, Some(AccessAction::Replace { access_id: replaced, authorizations, .. }) if replaced == access_id && &authorizations == row.authorizations())
201 }
202 }
203 _ => false,
204 };
205 if !valid {
206 return Ok(false);
207 }
208 }
209 Ok(true)
210 }
211 fn load_action(
212 ordering: &K1TxnOrdering,
213 subsystem: SubsystemId,
214 txid: TxId,
215 ) -> Result<Option<AccessAction>, String> {
216 let Some(bytes) = ordering
217 .get_txn(txid)
218 .map_err(|error| format!("KTO transaction lookup failed: {error}"))?
219 else {
220 return Ok(None);
221 };
222 let Ok(transaction) = Transaction::parse(&bytes) else {
223 return Ok(None);
224 };
225 if transaction.subsystem() != subsystem {
226 return Ok(None);
227 }
228 Ok(kcode_k1_access_format::decode(transaction.payload())
229 .ok()
230 .map(|(_, action)| action))
231 }
232 pub fn apply(&self, callback_txid: TxId, action: AccessAction) -> Result<ApplyOutcome, String> {
233 let _lane = locked(self.apply.lock(), "projection apply lock poisoned")?;
234 let prepared = {
235 let mut state = locked(self.state.write(), "projection state lock poisoned")?;
236 if !state.available {
237 return Err("projection unavailable".to_owned());
238 }
239 Self::prepare(&mut state, callback_txid, action)?
240 };
241 if let Err(error) = self.store.commit(callback_txid, &prepared.mutation) {
242 self.unavailable();
243 return Err(error);
244 }
245 let committed = if let Some((access_id, authorizations)) = &prepared.discovery {
246 let (users, groups) = subjects(authorizations);
247 self.discovery
248 .commit(callback_txid, &users, &groups, *access_id)
249 } else {
250 self.discovery
251 .commit(callback_txid, &[], &[], AccessId::new(callback_txid))
252 };
253 if let Err(error) = committed {
254 self.unavailable();
255 return Err(error);
256 }
257 let mut state = locked(
258 self.state.write(),
259 "projection state lock poisoned after commit",
260 )?;
261 if !state.available {
262 return Err("projection became unavailable after commit".to_owned());
263 }
264 state.finish(prepared.delta)?;
265 Ok(prepared.outcome)
266 }
267 fn unavailable(&self) {
268 if let Ok(mut state) = self.state.write() {
269 state.available = false;
270 }
271 }
272 fn prepare(state: &mut State, txid: TxId, action: AccessAction) -> Result<Prepared, String> {
273 match action {
274 AccessAction::Create {
275 target,
276 authorizations,
277 } => {
278 let access_id = AccessId::new(txid);
279 if state.objects.contains_key(&access_id) {
280 return Ok(Self::rejected("access ID already exists"));
281 }
282 if state.targets.contains_key(&target) {
283 return Ok(Self::rejected("target already exists"));
284 }
285 state
286 .objects
287 .try_reserve(1)
288 .map_err(|error| format!("unable to reserve access index: {error}"))?;
289 state
290 .targets
291 .try_reserve(1)
292 .map_err(|error| format!("unable to reserve target index: {error}"))?;
293 let stored =
294 StoredAccess::new(access_id, target.clone(), txid, authorizations.clone());
295 Ok(Prepared {
296 outcome: ApplyOutcome::Applied(AccessRevision::new(access_id, txid)),
297 mutation: StoreMutation::Create(stored.clone()),
298 delta: Delta::Create(access_id, target, stored),
299 discovery: Some((access_id, authorizations)),
300 })
301 }
302 AccessAction::Replace {
303 access_id,
304 actor,
305 witness,
306 authorizations,
307 ..
308 } => {
309 let Some(current) = state.objects.get(&access_id) else {
310 return Ok(Self::rejected("unknown access ID"));
311 };
312 if !Self::valid_witness(current.authorizations(), actor, &witness) {
313 return Ok(Self::rejected("owner witness is invalid"));
314 }
315 if current.authorizations() == &authorizations {
316 return Ok(Prepared {
317 outcome: ApplyOutcome::Unchanged(AccessRevision::new(
318 access_id,
319 current.revision(),
320 )),
321 mutation: StoreMutation::CursorOnly,
322 delta: Delta::None,
323 discovery: Some((access_id, authorizations)),
324 });
325 }
326 let stored = StoredAccess::new(
327 access_id,
328 current.target().clone(),
329 txid,
330 authorizations.clone(),
331 );
332 Ok(Prepared {
333 outcome: ApplyOutcome::Applied(AccessRevision::new(access_id, txid)),
334 mutation: StoreMutation::Replace {
335 access_id,
336 revision: txid,
337 authorizations: authorizations.clone(),
338 },
339 delta: Delta::Replace(access_id, stored),
340 discovery: Some((access_id, authorizations)),
341 })
342 }
343 AccessAction::EnsureDiscovery { access_id } => {
344 let Some(stored) = state.objects.get(&access_id) else {
345 return Ok(Self::rejected("unknown access ID"));
346 };
347 Ok(Prepared {
348 outcome: ApplyOutcome::Unchanged(AccessRevision::new(
349 access_id,
350 stored.revision(),
351 )),
352 mutation: StoreMutation::CursorOnly,
353 delta: Delta::None,
354 discovery: Some((access_id, stored.authorizations().clone())),
355 })
356 }
357 }
358 }
359 fn rejected(reason: &str) -> Prepared {
360 Prepared {
361 outcome: ApplyOutcome::Rejected(reason.to_owned()),
362 mutation: StoreMutation::CursorOnly,
363 delta: Delta::None,
364 discovery: None,
365 }
366 }
367 fn valid_witness(auth: &Authorizations, actor: UserId, witness: &OwnerWitness) -> bool {
368 auth.owners().iter().any(|owner| match (owner, witness) {
369 (OwnerSubject::User(owner), OwnerWitness::User) => *owner == actor,
370 (OwnerSubject::Group(owner), OwnerWitness::Group(group)) => owner == group,
371 _ => false,
372 })
373 }
374 fn readable(&self) -> Result<RwLockReadGuard<'_, State>, String> {
375 let state = locked(self.state.read(), "projection state lock poisoned")?;
376 if state.available {
377 Ok(state)
378 } else {
379 Err("projection unavailable".to_owned())
380 }
381 }
382 pub fn discovery_missing(
383 &self,
384 access_id: AccessId,
385 expected_subsystem: SubsystemId,
386 ) -> Result<bool, String> {
387 let state = self.readable()?;
388 let Some(stored) = state.objects.get(&access_id) else {
389 return Ok(false);
390 };
391 if stored.target().subsystem() != expected_subsystem {
392 return Ok(false);
393 }
394 let (users, groups) = subjects(stored.authorizations());
395 self.discovery.contains_missing(&users, &groups, access_id)
396 }
397 pub fn discovered_for_user(
398 &self,
399 user: UserId,
400 subsystem: SubsystemId,
401 ) -> Result<Vec<AccessId>, String> {
402 self.discovered(self.discovery.list_user(user)?, subsystem)
403 }
404 pub fn discovered_for_group(
405 &self,
406 group: GroupId,
407 subsystem: SubsystemId,
408 ) -> Result<Vec<AccessId>, String> {
409 self.discovered(self.discovery.list_group(group)?, subsystem)
410 }
411 fn discovered(
412 &self,
413 ids: Vec<AccessId>,
414 subsystem: SubsystemId,
415 ) -> Result<Vec<AccessId>, String> {
416 let state = self.readable()?;
417 Ok(ids
418 .into_iter()
419 .filter(|id| {
420 state
421 .objects
422 .get(id)
423 .is_some_and(|stored| stored.target().subsystem() == subsystem)
424 })
425 .collect())
426 }
427 pub fn owner_witness(
428 &self,
429 access_id: AccessId,
430 user: UserId,
431 user_groups: &[GroupId],
432 ) -> Result<Option<OwnerWitness>, String> {
433 let state = self.readable()?;
434 let Some(stored) = state.objects.get(&access_id) else {
435 return Ok(None);
436 };
437 let owners = stored.authorizations().owners();
438 if owners
439 .iter()
440 .any(|owner| matches!(owner, OwnerSubject::User(owner) if *owner == user))
441 {
442 return Ok(Some(OwnerWitness::User));
443 }
444 Ok(owners
445 .iter()
446 .filter_map(|owner| match owner {
447 OwnerSubject::Group(group) if user_groups.contains(group) => Some(*group),
448 _ => None,
449 })
450 .min()
451 .map(OwnerWitness::Group))
452 }
453 pub fn check_user(
454 &self,
455 user: UserId,
456 access_id: AccessId,
457 expected_subsystem: SubsystemId,
458 user_groups: &[GroupId],
459 groups_revision: Option<TxId>,
460 ) -> Result<AccessCheck, String> {
461 let state = self.readable()?;
462 let Some(stored) = state.objects.get(&access_id) else {
463 return Self::hidden_check();
464 };
465 if stored.target().subsystem() != expected_subsystem {
466 return Self::hidden_check();
467 }
468 let auth = stored.authorizations();
469 let can_manage = Self::user_owner(auth, user, user_groups);
470 let can_view = can_manage
471 || auth.viewers().iter().any(|viewer| match viewer {
472 ViewerSubject::User(viewer) => *viewer == user,
473 ViewerSubject::Group(group) => user_groups.contains(group),
474 ViewerSubject::Model(_) => false,
475 });
476 AccessCheck::new(
477 can_view,
478 can_manage,
479 can_view.then(|| stored.target().clone()),
480 can_view.then_some(stored.revision()),
481 can_view.then_some(groups_revision).flatten(),
482 )
483 }
484 pub fn check(
485 &self,
486 principal: RequestPrincipal,
487 access_id: AccessId,
488 expected_subsystem: SubsystemId,
489 user_groups: &[GroupId],
490 model_groups: &[GroupId],
491 groups_revision: Option<TxId>,
492 ) -> Result<AccessCheck, String> {
493 let state = self.readable()?;
494 let Some(stored) = state.objects.get(&access_id) else {
495 return Self::hidden_check();
496 };
497 if stored.target().subsystem() != expected_subsystem {
498 return Self::hidden_check();
499 }
500 let auth = stored.authorizations();
501 let user_owner = Self::user_owner(auth, principal.user(), user_groups);
502 let user_view = user_owner
503 || auth.viewers().iter().any(|viewer| match viewer {
504 ViewerSubject::User(user) => *user == principal.user(),
505 ViewerSubject::Group(group) => user_groups.contains(group),
506 ViewerSubject::Model(_) => false,
507 });
508 let model_view = auth.owners().iter().any(|owner| match owner {
509 OwnerSubject::Group(group) => model_groups.contains(group),
510 OwnerSubject::User(_) => false,
511 }) || auth.viewers().iter().any(|viewer| match viewer {
512 ViewerSubject::Model(model) => *model == principal.model(),
513 ViewerSubject::Group(group) => model_groups.contains(group),
514 ViewerSubject::User(_) => false,
515 });
516 let can_manage = user_owner;
517 let can_view = user_view && model_view;
518 let evidence = can_view || can_manage;
519 AccessCheck::new(
520 can_view,
521 can_manage,
522 can_view.then(|| stored.target().clone()),
523 evidence.then_some(stored.revision()),
524 evidence.then_some(groups_revision).flatten(),
525 )
526 }
527 fn user_owner(auth: &Authorizations, user: UserId, groups: &[GroupId]) -> bool {
528 auth.owners().iter().any(|owner| match owner {
529 OwnerSubject::User(owner) => *owner == user,
530 OwnerSubject::Group(group) => groups.contains(group),
531 })
532 }
533 fn hidden_check() -> Result<AccessCheck, String> {
534 AccessCheck::new(false, false, None, None, None)
535 }
536 pub fn clear(&self) -> Result<(), String> {
537 let _lane = locked(self.apply.lock(), "projection apply lock poisoned")?;
538 self.unavailable();
539 self.store.clear().and(self.discovery.discard())
540 }
541}
542
543#[cfg(test)]
544mod tests {
545 use super::*;
546 use std::{
547 fs,
548 sync::atomic::{AtomicU64, Ordering},
549 };
550 static NEXT: AtomicU64 = AtomicU64::new(0);
551 fn root() -> std::path::PathBuf {
552 let root = std::env::temp_dir().join(format!(
553 "access-projection-{}-{}",
554 std::process::id(),
555 NEXT.fetch_add(1, Ordering::Relaxed)
556 ));
557 fs::create_dir(&root).unwrap();
558 root
559 }
560 fn tx(value: u8) -> TxId {
561 TxId::from_bytes([value; 12])
562 }
563 fn user(value: u8) -> UserId {
564 UserId::from_tx_id(tx(value))
565 }
566 fn group(value: u8) -> GroupId {
567 GroupId::new(tx(value))
568 }
569 fn target(name: &str) -> Target {
570 Target::new(SubsystemId::from_str(name).unwrap(), vec![1])
571 }
572 fn auth() -> Authorizations {
573 Authorizations::new(
574 vec![OwnerSubject::User(user(1)), OwnerSubject::Group(group(2))],
575 vec![
576 ViewerSubject::User(user(3)),
577 ViewerSubject::Group(group(4)),
578 ViewerSubject::Model(ModelId::from_bytes([5; 32])),
579 ],
580 )
581 .unwrap()
582 }
583 #[test]
584 fn create_replace_ensure_queries_and_authorization_fan_out_without_duplicates()
585 -> Result<(), String> {
586 let root = root();
587 let ordering = K1TxnOrdering::open(&root.join("ordering"))?;
588 let (projection, _) = Projection::open(&root.join("projection"), &ordering)?;
589 let access_id = AccessId::new(tx(6));
590 let subsystem = SubsystemId::from_str("one")?;
591 assert_eq!(
592 projection.apply(
593 tx(6),
594 AccessAction::Create {
595 target: target("one"),
596 authorizations: auth()
597 }
598 )?,
599 ApplyOutcome::Applied(AccessRevision::new(access_id, tx(6)))
600 );
601 assert_eq!(
602 projection.discovered_for_user(user(1), subsystem)?,
603 vec![access_id]
604 );
605 assert_eq!(
606 projection.discovered_for_group(group(4), subsystem)?,
607 vec![access_id]
608 );
609 assert!(!projection.discovery_missing(access_id, subsystem)?);
610 assert_eq!(
611 projection.apply(tx(7), AccessAction::EnsureDiscovery { access_id })?,
612 ApplyOutcome::Unchanged(AccessRevision::new(access_id, tx(6)))
613 );
614 assert_eq!(
615 projection.discovered_for_user(user(1), subsystem)?,
616 vec![access_id]
617 );
618 let replacement = Authorizations::new(
619 vec![OwnerSubject::User(user(3))],
620 vec![ViewerSubject::Group(group(2))],
621 )?;
622 assert_eq!(
623 projection.apply(
624 tx(8),
625 AccessAction::Replace {
626 access_id,
627 actor: user(1),
628 groups_revision: None,
629 witness: OwnerWitness::User,
630 authorizations: replacement
631 }
632 )?,
633 ApplyOutcome::Applied(AccessRevision::new(access_id, tx(8)))
634 );
635 assert_eq!(
636 projection.discovered_for_user(user(3), subsystem)?,
637 vec![access_id]
638 );
639 assert_eq!(
640 projection.discovered_for_group(group(2), subsystem)?,
641 vec![access_id]
642 );
643 assert!(
644 projection
645 .discovered_for_user(user(1), SubsystemId::from_str("two")?)?
646 .is_empty()
647 );
648 assert_eq!(
649 projection.apply(
650 tx(9),
651 AccessAction::EnsureDiscovery {
652 access_id: AccessId::new(tx(9))
653 }
654 )?,
655 ApplyOutcome::Rejected("unknown access ID".to_owned())
656 );
657 let view = projection.check(
658 RequestPrincipal::new(user(1), ModelId::from_bytes([5; 32])),
659 access_id,
660 subsystem,
661 &[group(2)],
662 &[group(2)],
663 None,
664 )?;
665 assert!(view.can_view() && !view.can_manage());
666 assert_eq!(
667 projection.owner_witness(access_id, user(3), &[group(2)])?,
668 Some(OwnerWitness::User)
669 );
670 fs::remove_dir_all(root).map_err(|error| error.to_string())
671 }
672 #[test]
673 fn check_user_grants_human_subjects_and_hides_model_only_access() -> Result<(), String> {
674 let root = root();
675 let ordering = K1TxnOrdering::open(&root.join("ordering"))?;
676 let (projection, _) = Projection::open(&root.join("projection"), &ordering)?;
677 let subsystem = SubsystemId::from_str("http")?;
678 let groups_revision = Some(tx(99));
679 let model_only = AccessId::new(tx(10));
680 projection.apply(
681 tx(10),
682 AccessAction::Create {
683 target: target("http"),
684 authorizations: Authorizations::new(
685 vec![OwnerSubject::User(user(9))],
686 vec![ViewerSubject::Model(ModelId::from_bytes([8; 32]))],
687 )?,
688 },
689 )?;
690 assert_eq!(
691 projection.check_user(user(1), model_only, subsystem, &[], groups_revision)?,
692 AccessCheck::new(false, false, None, None, None)?
693 );
694 let model_group_only = AccessId::new(tx(11));
695 projection.apply(
696 tx(11),
697 AccessAction::Create {
698 target: target("http-model-group"),
699 authorizations: Authorizations::new(
700 vec![OwnerSubject::User(user(9))],
701 vec![ViewerSubject::Group(group(8))],
702 )?,
703 },
704 )?;
705 assert_eq!(
706 projection.check_user(
707 user(1),
708 model_group_only,
709 SubsystemId::from_str("http-model-group")?,
710 &[],
711 groups_revision
712 )?,
713 AccessCheck::new(false, false, None, None, None)?
714 );
715 let user_owner = AccessId::new(tx(12));
716 projection.apply(
717 tx(12),
718 AccessAction::Create {
719 target: target("http-user-owner"),
720 authorizations: Authorizations::new(
721 vec![OwnerSubject::User(user(1))],
722 vec![ViewerSubject::Model(ModelId::from_bytes([8; 32]))],
723 )?,
724 },
725 )?;
726 assert_eq!(
727 projection.check_user(
728 user(1),
729 user_owner,
730 SubsystemId::from_str("http-user-owner")?,
731 &[],
732 groups_revision
733 )?,
734 AccessCheck::new(
735 true,
736 true,
737 Some(target("http-user-owner")),
738 Some(tx(12)),
739 groups_revision
740 )?
741 );
742 let group_viewer = AccessId::new(tx(13));
743 projection.apply(
744 tx(13),
745 AccessAction::Create {
746 target: target("http-group-viewer"),
747 authorizations: Authorizations::new(
748 vec![OwnerSubject::User(user(9))],
749 vec![ViewerSubject::Group(group(2))],
750 )?,
751 },
752 )?;
753 assert_eq!(
754 projection.check_user(
755 user(1),
756 group_viewer,
757 SubsystemId::from_str("http-group-viewer")?,
758 &[group(2)],
759 groups_revision
760 )?,
761 AccessCheck::new(
762 true,
763 false,
764 Some(target("http-group-viewer")),
765 Some(tx(13)),
766 groups_revision
767 )?
768 );
769 assert_eq!(
770 projection.check_user(
771 user(1),
772 group_viewer,
773 SubsystemId::from_str("wrong")?,
774 &[group(2)],
775 groups_revision
776 )?,
777 AccessCheck::new(false, false, None, None, None)?
778 );
779 fs::remove_dir_all(root).map_err(|error| error.to_string())
780 }
781 #[test]
782 fn mismatched_discovery_checkpoint_resets_both_derived_stores() -> Result<(), String> {
783 let root = root();
784 let projection_root = root.join("projection");
785 let ordering = K1TxnOrdering::open(&root.join("ordering"))?;
786 kcode_k1_access_discovery_store::DiscoveryStore::open(&projection_root)?.commit(
787 tx(1),
788 &[user(2)],
789 &[],
790 AccessId::new(tx(3)),
791 )?;
792 let (projection, cursor) = Projection::open(&projection_root, &ordering)?;
793 assert_eq!(cursor, None);
794 assert!(
795 projection
796 .discovered_for_user(user(2), SubsystemId::from_str("one")?)?
797 .is_empty()
798 );
799 projection.clear()?;
800 fs::remove_dir_all(root).map_err(|error| error.to_string())
801 }
802}