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