1use std::{path::Path, sync::Arc};
2
3use kcode_k1_access_driver::K1AccessDriver;
4use kcode_k1_groups::K1Groups;
5use kcode_k1_peering::K1Peering;
6use kcode_k1_txn_ordering::K1TxnOrdering;
7
8pub use kcode_k1_access_types::{
9 AccessCheck, AccessContext, AccessId, AccessPolicy, AccessRevision, Authority,
10 FilteredAuthorities, GroupId, ModelId, ProfileId, SubsystemId, Target, TxId, UserId,
11 ViewerSubject,
12};
13pub use kcode_k1_groups::{ALL_MODELS, ALL_MODELS_MEMBER, ALL_USERS, LOCAL_MODELS, SentinelGroup};
14
15const ACCESS_UNAVAILABLE: &str = "access is unavailable";
16const ACCESS_GROUP_UNAVAILABLE: &str = "access group is unavailable";
17const FILTERED_AUTHORITY: &str = "access authority is filtered";
18
19pub struct K1Access {
20 driver: K1AccessDriver,
21 groups: Arc<K1Groups>,
22}
23
24impl K1Access {
25 pub fn open(
26 root: &Path,
27 ordering: Arc<K1TxnOrdering>,
28 peering: Arc<K1Peering>,
29 groups: Arc<K1Groups>,
30 ) -> Result<Self, String> {
31 Ok(Self {
32 driver: K1AccessDriver::open(root, ordering, peering)?,
33 groups,
34 })
35 }
36
37 pub fn create(
38 &self,
39 context: &AccessContext,
40 target: Target,
41 profile_id: ProfileId,
42 policy: AccessPolicy,
43 ) -> Result<AccessRevision, String> {
44 require_visible_authority(context, &policy)?;
45 self.driver.create(target, profile_id, policy)
46 }
47
48 pub fn profile_id(&self, access_id: AccessId) -> Result<Option<ProfileId>, String> {
53 self.driver.profile_id(access_id)
54 }
55
56 pub fn controller_profile_ids(
57 &self,
58 context: &AccessContext,
59 access_ids: &[AccessId],
60 ) -> Result<Vec<Option<ProfileId>>, String> {
61 if access_ids.is_empty() {
62 return Ok(Vec::new());
63 }
64 let memberships = self.groups.memberships(context.user(), context.model())?;
65 self.driver
66 .controller_profile_ids(context, access_ids, memberships.user_groups())
67 }
68
69 pub fn replace_policy(
70 &self,
71 context: &AccessContext,
72 access_id: AccessId,
73 editors: Vec<Authority>,
74 viewers: Vec<ViewerSubject>,
75 ) -> Result<AccessRevision, String> {
76 let memberships = self.groups.memberships(context.user(), context.model())?;
77 let witness = self
78 .driver
79 .edit_witness(context, access_id, memberships.user_groups())?;
80 let witness = witness.ok_or_else(|| ACCESS_UNAVAILABLE.to_owned())?;
81 let (editors, viewers) = normalize_replacement(context.user(), editors, viewers)?;
82 self.driver.replace(
83 access_id,
84 context.user(),
85 memberships.revision(),
86 witness,
87 editors,
88 viewers,
89 )
90 }
91
92 pub fn check(
93 &self,
94 context: &AccessContext,
95 access_id: AccessId,
96 expected_subsystem: SubsystemId,
97 ) -> Result<AccessCheck, String> {
98 if self
99 .driver
100 .discovery_missing(context, access_id, expected_subsystem)?
101 {
102 self.driver.ensure_discovery(access_id)?;
103 }
104 let memberships = self.groups.memberships(context.user(), context.model())?;
105 self.driver.check(
106 context,
107 access_id,
108 expected_subsystem,
109 memberships.user_groups(),
110 memberships.model_groups(),
111 memberships.revision(),
112 )
113 }
114
115 pub fn check_many(
116 &self,
117 context: &AccessContext,
118 access_ids: &[AccessId],
119 expected_subsystem: SubsystemId,
120 ) -> Result<Vec<AccessCheck>, String> {
121 if access_ids.is_empty() {
122 return Ok(Vec::new());
123 }
124 let memberships = self.groups.memberships(context.user(), context.model())?;
125 self.driver.check_many(
126 context,
127 access_ids,
128 expected_subsystem,
129 memberships.user_groups(),
130 memberships.model_groups(),
131 memberships.revision(),
132 )
133 }
134
135 pub fn resolve_visible_targets(
136 &self,
137 context: &AccessContext,
138 targets: &[Target],
139 expected_subsystem: SubsystemId,
140 ) -> Result<Vec<Option<AccessId>>, String> {
141 if targets.is_empty() {
142 return Ok(Vec::new());
143 }
144 let memberships = self.groups.memberships(context.user(), context.model())?;
145 self.driver.resolve_visible_targets(
146 context,
147 targets,
148 expected_subsystem,
149 memberships.user_groups(),
150 memberships.model_groups(),
151 )
152 }
153
154 pub fn list_user(
155 &self,
156 context: &AccessContext,
157 expected_subsystem: SubsystemId,
158 ) -> Result<Vec<AccessId>, String> {
159 self.driver.discovered_for_user(context, expected_subsystem)
160 }
161
162 pub fn list_group(
163 &self,
164 context: &AccessContext,
165 group: GroupId,
166 expected_subsystem: SubsystemId,
167 ) -> Result<Vec<AccessId>, String> {
168 let groups = self.groups.groups_for_user(context.user())?;
169 require_group(&groups, group)?;
170 self.driver
171 .discovered_for_group(context, group, expected_subsystem)
172 }
173
174 pub fn list_user_group_targets(
175 &self,
176 context: &AccessContext,
177 expected_subsystem: SubsystemId,
178 ) -> Result<Vec<(AccessId, Target)>, String> {
179 let memberships = self.groups.memberships(context.user(), context.model())?;
180 self.driver.discovered_targets_for_user_and_groups(
181 context,
182 memberships.user_groups(),
183 expected_subsystem,
184 )
185 }
186}
187
188fn require_visible_authority(context: &AccessContext, policy: &AccessPolicy) -> Result<(), String> {
189 if context.filter().contains(policy.authority()) {
190 Err(FILTERED_AUTHORITY.to_owned())
191 } else {
192 Ok(())
193 }
194}
195
196fn normalize_replacement(
197 actor: UserId,
198 mut editors: Vec<Authority>,
199 mut viewers: Vec<ViewerSubject>,
200) -> Result<(Vec<Authority>, Vec<ViewerSubject>), String> {
201 AccessPolicy::new(Authority::User(actor), editors.clone(), viewers.clone())?;
202 editors.sort_unstable();
203 editors.dedup();
204 viewers.sort_unstable();
205 viewers.dedup();
206 viewers.retain(|viewer| match viewer {
207 ViewerSubject::User(user) => editors.binary_search(&Authority::User(*user)).is_err(),
208 ViewerSubject::Group(group) => editors.binary_search(&Authority::Group(*group)).is_err(),
209 ViewerSubject::Model(_) => true,
210 });
211 Ok((editors, viewers))
212}
213
214fn require_group(groups: &[GroupId], group: GroupId) -> Result<(), String> {
215 if groups.contains(&group) {
216 Ok(())
217 } else {
218 Err(ACCESS_GROUP_UNAVAILABLE.to_owned())
219 }
220}
221
222#[cfg(test)]
223mod tests {
224 use super::*;
225
226 fn txid(byte: u8) -> TxId {
227 TxId::from_bytes([byte; 12])
228 }
229
230 fn user(byte: u8) -> UserId {
231 UserId::from_tx_id(txid(byte))
232 }
233
234 fn group(byte: u8) -> GroupId {
235 GroupId::new(txid(byte))
236 }
237
238 fn model(byte: u8) -> ModelId {
239 ModelId::from_bytes([byte; 32])
240 }
241
242 #[test]
243 fn filtered_create_authority_is_rejected() {
244 let authority = Authority::Group(group(7));
245 let context = AccessContext::new(
246 user(1),
247 model(2),
248 FilteredAuthorities::new(vec![authority]).unwrap(),
249 )
250 .unwrap();
251 let policy = AccessPolicy::new(authority, Vec::new(), Vec::new()).unwrap();
252 assert_eq!(
253 require_visible_authority(&context, &policy),
254 Err(FILTERED_AUTHORITY.to_owned())
255 );
256 }
257
258 #[test]
259 fn replacement_lists_are_canonical_and_editor_viewers_are_removed() {
260 let editor_user = user(3);
261 let editor_group = group(4);
262 let visible_model = model(5);
263 let (editors, viewers) = normalize_replacement(
264 user(1),
265 vec![
266 Authority::Group(editor_group),
267 Authority::User(editor_user),
268 Authority::Group(editor_group),
269 ],
270 vec![
271 ViewerSubject::Model(visible_model),
272 ViewerSubject::User(editor_user),
273 ViewerSubject::Group(editor_group),
274 ViewerSubject::Model(visible_model),
275 ],
276 )
277 .unwrap();
278 assert_eq!(
279 editors,
280 vec![Authority::User(editor_user), Authority::Group(editor_group)]
281 );
282 assert_eq!(viewers, vec![ViewerSubject::Model(visible_model)]);
283 }
284
285 #[test]
286 fn replacement_rejects_sentinel_editors_and_all_models_member() {
287 assert!(
288 normalize_replacement(user(1), vec![Authority::Group(ALL_USERS)], Vec::new()).is_err()
289 );
290 assert!(
291 normalize_replacement(
292 user(1),
293 Vec::new(),
294 vec![ViewerSubject::Model(ALL_MODELS_MEMBER)]
295 )
296 .is_err()
297 );
298 }
299
300 #[test]
301 fn requested_group_requires_current_membership() {
302 let requested = group(1);
303 assert_eq!(require_group(&[requested], requested), Ok(()));
304 assert_eq!(
305 require_group(&[group(2), group(3)], requested),
306 Err(ACCESS_GROUP_UNAVAILABLE.to_owned())
307 );
308 }
309}