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