kcode_k1_access_discovery/
lib.rs1use std::collections::{BTreeSet, HashSet};
2use std::path::Path;
3
4use access_types_v2::{AccessId as DiscoveryAccessId, TxId as DiscoveryTxId};
5use kcode_k1_access_discovery_store::DiscoveryStore;
6use kcode_k1_access_store::StoredAccess;
7use kcode_k1_access_types::{
8 AccessContext, AccessId, AccessPolicy, Authority, GroupId, SubsystemId, Target, TxId, UserId,
9 ViewerSubject,
10};
11
12pub struct AccessDiscovery {
13 store: DiscoveryStore,
14}
15
16impl AccessDiscovery {
17 pub fn open(root: &Path) -> Result<Self, String> {
18 Ok(Self {
19 store: DiscoveryStore::open(root)?,
20 })
21 }
22
23 pub fn checkpoint(&self) -> Result<Option<TxId>, String> {
24 self.store.checkpoint()
25 }
26
27 pub fn complete(&self, records: &[StoredAccess]) -> Result<bool, String> {
28 for record in records {
29 let (users, groups) = subjects(record.policy());
30 if self
31 .store
32 .contains_missing(&users, &groups, discovery_id(record.access_id()))?
33 {
34 return Ok(false);
35 }
36 }
37 Ok(true)
38 }
39
40 pub fn commit(
41 &self,
42 callback_txid: TxId,
43 discovery: Option<(AccessId, &AccessPolicy)>,
44 ) -> Result<(), String> {
45 match discovery {
46 Some((access_id, policy)) => {
47 let (users, groups) = subjects(policy);
48 self.store
49 .commit(callback_txid, &users, &groups, discovery_id(access_id))
50 .map(|_| ())
51 }
52 None => self
53 .store
54 .commit(
55 callback_txid,
56 &[],
57 &[],
58 DiscoveryAccessId::new(DiscoveryTxId::from_bytes(*callback_txid.as_bytes())),
59 )
60 .map(|_| ()),
61 }
62 }
63
64 pub fn missing(
65 &self,
66 context: &AccessContext,
67 records: &[StoredAccess],
68 access_id: AccessId,
69 expected_subsystem: SubsystemId,
70 ) -> Result<bool, String> {
71 let Some(record) = visible_record(context, records, access_id, expected_subsystem) else {
72 return Ok(false);
73 };
74 let (users, groups) = subjects(record.policy());
75 self.store
76 .contains_missing(&users, &groups, discovery_id(access_id))
77 }
78
79 pub fn list_user(
80 &self,
81 context: &AccessContext,
82 records: &[StoredAccess],
83 subsystem: SubsystemId,
84 ) -> Result<Vec<AccessId>, String> {
85 Ok(self
86 .store
87 .list_user(context.user())?
88 .into_iter()
89 .map(current_id)
90 .filter(|access_id| visible_record(context, records, *access_id, subsystem).is_some())
91 .collect())
92 }
93
94 pub fn list_group(
95 &self,
96 context: &AccessContext,
97 records: &[StoredAccess],
98 group: GroupId,
99 subsystem: SubsystemId,
100 ) -> Result<Vec<AccessId>, String> {
101 Ok(self
102 .store
103 .list_group(group)?
104 .into_iter()
105 .map(current_id)
106 .filter(|access_id| visible_record(context, records, *access_id, subsystem).is_some())
107 .collect())
108 }
109
110 pub fn list_user_groups(
111 &self,
112 context: &AccessContext,
113 records: &[StoredAccess],
114 groups: &[GroupId],
115 subsystem: SubsystemId,
116 ) -> Result<Vec<(AccessId, Target)>, String> {
117 let mut discovered = self.store.list_user(context.user())?;
118 for group in groups.iter().copied().collect::<BTreeSet<_>>() {
119 discovered.extend(self.store.list_group(group)?);
120 }
121
122 let mut seen = HashSet::new();
123 let mut visible = Vec::new();
124 for access_id in discovered.into_iter().map(current_id) {
125 let Some(record) = visible_record(context, records, access_id, subsystem) else {
126 continue;
127 };
128 if seen.insert(access_id) {
129 visible.push((access_id, record.target().clone()));
130 }
131 }
132 Ok(visible)
133 }
134
135 pub fn discard(&self) -> Result<(), String> {
136 self.store.discard()
137 }
138}
139
140fn subjects(policy: &AccessPolicy) -> (Vec<UserId>, Vec<GroupId>) {
141 let mut users = Vec::with_capacity(1 + policy.editors().len() + policy.viewers().len());
142 let mut groups = Vec::with_capacity(1 + policy.editors().len() + policy.viewers().len());
143 push_authority(&mut users, &mut groups, policy.authority());
144 for authority in policy.editors() {
145 push_authority(&mut users, &mut groups, *authority);
146 }
147 for viewer in policy.viewers() {
148 match viewer {
149 ViewerSubject::User(user) => users.push(*user),
150 ViewerSubject::Group(group) => groups.push(*group),
151 ViewerSubject::Model(_) => {}
152 }
153 }
154 (users, groups)
155}
156
157fn push_authority(users: &mut Vec<UserId>, groups: &mut Vec<GroupId>, authority: Authority) {
158 match authority {
159 Authority::User(user) => users.push(user),
160 Authority::Group(group) => groups.push(group),
161 }
162}
163
164fn discovery_id(access_id: AccessId) -> DiscoveryAccessId {
165 DiscoveryAccessId::new(DiscoveryTxId::from_bytes(*access_id.txid().as_bytes()))
166}
167
168fn current_id(access_id: DiscoveryAccessId) -> AccessId {
169 AccessId::new(TxId::from_bytes(*access_id.txid().as_bytes()))
170}
171
172fn visible_record<'a>(
173 context: &AccessContext,
174 records: &'a [StoredAccess],
175 access_id: AccessId,
176 subsystem: SubsystemId,
177) -> Option<&'a StoredAccess> {
178 records
179 .iter()
180 .find(|record| record.access_id() == access_id)
181 .filter(|record| record.target().subsystem() == subsystem)
182 .filter(|record| !context.filter().contains(record.policy().authority()))
183}