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