1use std::cmp::Ordering;
2
3pub use kcode_k1_access_types::{Authorizations, OwnerSubject, RequestPrincipal, ViewerSubject};
4pub use kcode_k1_groups::{GroupId, ModelId, TxId, UserId};
5
6use kcode_k1_groups::ALL_MODELS;
7
8#[derive(Copy, Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
9pub struct ProfileId(TxId);
10
11impl ProfileId {
12 pub const fn new(txid: TxId) -> Self {
13 Self(txid)
14 }
15
16 pub const fn txid(self) -> TxId {
17 self.0
18 }
19}
20
21#[derive(Copy, Clone, Debug, Eq, Hash, PartialEq)]
22pub enum ProfileOwner {
23 RequestUser,
24 User(UserId),
25 Group(GroupId),
26}
27
28impl ProfileOwner {
29 const fn sort_tag(self) -> u8 {
30 match self {
31 Self::RequestUser => 0,
32 Self::User(_) => 1,
33 Self::Group(_) => 2,
34 }
35 }
36}
37
38impl Ord for ProfileOwner {
39 fn cmp(&self, other: &Self) -> Ordering {
40 self.sort_tag()
41 .cmp(&other.sort_tag())
42 .then_with(|| match (self, other) {
43 (Self::RequestUser, Self::RequestUser) => Ordering::Equal,
44 (Self::User(left), Self::User(right)) => {
45 left.as_tx_id().as_bytes().cmp(right.as_tx_id().as_bytes())
46 }
47 (Self::Group(left), Self::Group(right)) => {
48 left.txid().as_bytes().cmp(right.txid().as_bytes())
49 }
50 _ => unreachable!("equal owner sort tags have equal variants"),
51 })
52 }
53}
54
55impl PartialOrd for ProfileOwner {
56 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
57 Some(self.cmp(other))
58 }
59}
60
61#[derive(Copy, Clone, Debug, Eq, Hash, PartialEq)]
62pub enum ProfileViewer {
63 RequestUser,
64 RequestModel,
65 User(UserId),
66 Group(GroupId),
67 Model(ModelId),
68}
69
70impl ProfileViewer {
71 const fn sort_tag(self) -> u8 {
72 match self {
73 Self::RequestUser => 0,
74 Self::RequestModel => 1,
75 Self::User(_) => 2,
76 Self::Group(_) => 3,
77 Self::Model(_) => 4,
78 }
79 }
80}
81
82impl Ord for ProfileViewer {
83 fn cmp(&self, other: &Self) -> Ordering {
84 self.sort_tag()
85 .cmp(&other.sort_tag())
86 .then_with(|| match (self, other) {
87 (Self::RequestUser, Self::RequestUser)
88 | (Self::RequestModel, Self::RequestModel) => Ordering::Equal,
89 (Self::User(left), Self::User(right)) => {
90 left.as_tx_id().as_bytes().cmp(right.as_tx_id().as_bytes())
91 }
92 (Self::Group(left), Self::Group(right)) => {
93 left.txid().as_bytes().cmp(right.txid().as_bytes())
94 }
95 (Self::Model(left), Self::Model(right)) => left.as_bytes().cmp(right.as_bytes()),
96 _ => unreachable!("equal viewer sort tags have equal variants"),
97 })
98 }
99}
100
101impl PartialOrd for ProfileViewer {
102 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
103 Some(self.cmp(other))
104 }
105}
106
107#[derive(Clone, Debug, Eq, PartialEq)]
108pub struct AuthorizationProfile {
109 owners: Vec<ProfileOwner>,
110 viewers: Vec<ProfileViewer>,
111}
112
113impl AuthorizationProfile {
114 pub fn new(
115 mut owners: Vec<ProfileOwner>,
116 mut viewers: Vec<ProfileViewer>,
117 ) -> Result<Self, String> {
118 if owners.is_empty() {
119 return Err("authorization profile requires at least one owner".to_owned());
120 }
121 owners.sort_unstable();
122 owners.dedup();
123 viewers.sort_unstable();
124 viewers.dedup();
125 Ok(Self { owners, viewers })
126 }
127
128 pub fn owners(&self) -> &[ProfileOwner] {
129 &self.owners
130 }
131
132 pub fn viewers(&self) -> &[ProfileViewer] {
133 &self.viewers
134 }
135
136 pub fn resolve(&self, principal: RequestPrincipal) -> Result<Authorizations, String> {
137 let owners = self
138 .owners
139 .iter()
140 .map(|owner| match *owner {
141 ProfileOwner::RequestUser => OwnerSubject::User(principal.user()),
142 ProfileOwner::User(user) => OwnerSubject::User(user),
143 ProfileOwner::Group(group) => OwnerSubject::Group(group),
144 })
145 .collect();
146 let viewers = self
147 .viewers
148 .iter()
149 .map(|viewer| match *viewer {
150 ProfileViewer::RequestUser => ViewerSubject::User(principal.user()),
151 ProfileViewer::RequestModel => ViewerSubject::Model(principal.model()),
152 ProfileViewer::User(user) => ViewerSubject::User(user),
153 ProfileViewer::Group(group) => ViewerSubject::Group(group),
154 ProfileViewer::Model(model) => ViewerSubject::Model(model),
155 })
156 .collect();
157 Authorizations::new(owners, viewers)
158 }
159}
160
161#[derive(Clone, Debug, Eq, PartialEq)]
162pub enum ProfileSelection {
163 BuiltIn,
164 Saved(ProfileId),
165 Inline(AuthorizationProfile),
166}
167
168#[derive(Copy, Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
169pub enum ProfileSource {
170 BuiltIn,
171 Saved(ProfileId),
172 Inline,
173}
174
175#[derive(Copy, Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
176pub struct ProfileRevision {
177 profile_id: ProfileId,
178 txid: TxId,
179}
180
181impl ProfileRevision {
182 pub const fn new(profile_id: ProfileId, txid: TxId) -> Self {
183 Self { profile_id, txid }
184 }
185
186 pub const fn profile_id(&self) -> ProfileId {
187 self.profile_id
188 }
189
190 pub const fn txid(&self) -> TxId {
191 self.txid
192 }
193}
194
195#[derive(Clone, Debug, Eq, PartialEq)]
196pub struct ResolvedProfile {
197 authorizations: Authorizations,
198 source: ProfileSource,
199 saved_revision: Option<ProfileRevision>,
200}
201
202impl ResolvedProfile {
203 pub fn new(
204 authorizations: Authorizations,
205 source: ProfileSource,
206 saved_revision: Option<ProfileRevision>,
207 ) -> Result<Self, String> {
208 match (source, saved_revision) {
209 (ProfileSource::Saved(profile_id), Some(revision))
210 if revision.profile_id() == profile_id => {}
211 (ProfileSource::Saved(_), None) => {
212 return Err("saved profile requires a revision".to_owned());
213 }
214 (ProfileSource::Saved(_), Some(_)) => {
215 return Err("saved profile revision does not match profile ID".to_owned());
216 }
217 (ProfileSource::BuiltIn | ProfileSource::Inline, Some(_)) => {
218 return Err("only a saved profile may have a revision".to_owned());
219 }
220 (ProfileSource::BuiltIn | ProfileSource::Inline, None) => {}
221 }
222 Ok(Self {
223 authorizations,
224 source,
225 saved_revision,
226 })
227 }
228
229 pub fn authorizations(&self) -> &Authorizations {
230 &self.authorizations
231 }
232
233 pub const fn source(&self) -> ProfileSource {
234 self.source
235 }
236
237 pub const fn saved_revision(&self) -> Option<ProfileRevision> {
238 self.saved_revision
239 }
240
241 pub fn into_authorizations(self) -> Authorizations {
242 self.authorizations
243 }
244}
245
246pub fn built_in_profile() -> AuthorizationProfile {
247 AuthorizationProfile::new(
248 vec![ProfileOwner::RequestUser],
249 vec![ProfileViewer::Group(ALL_MODELS)],
250 )
251 .expect("built-in profile is valid")
252}
253
254pub fn resolve_built_in(principal: RequestPrincipal) -> Result<ResolvedProfile, String> {
255 let authorizations = built_in_profile().resolve(principal)?;
256 ResolvedProfile::new(authorizations, ProfileSource::BuiltIn, None)
257}
258
259#[cfg(test)]
260mod tests {
261 use super::*;
262
263 fn tx(byte: u8) -> TxId {
264 TxId::from_bytes([byte; 12])
265 }
266
267 fn user(byte: u8) -> UserId {
268 UserId::from_tx_id(tx(byte))
269 }
270
271 fn group(byte: u8) -> GroupId {
272 GroupId::new(tx(byte))
273 }
274
275 fn model(byte: u8) -> ModelId {
276 ModelId::from_bytes([byte; 32])
277 }
278
279 #[test]
280 fn profile_ids_revisions_and_selections_preserve_values() {
281 let profile_id = ProfileId::new(tx(1));
282 let revision = ProfileRevision::new(profile_id, tx(2));
283 assert_eq!(profile_id.txid(), tx(1));
284 assert_eq!(revision.profile_id(), profile_id);
285 assert_eq!(revision.txid(), tx(2));
286 let inline = AuthorizationProfile::new(vec![ProfileOwner::RequestUser], Vec::new())
287 .expect("profile");
288 let selections = [
289 ProfileSelection::BuiltIn,
290 ProfileSelection::Saved(profile_id),
291 ProfileSelection::Inline(inline),
292 ];
293 assert!(matches!(&selections[0], ProfileSelection::BuiltIn));
294 assert!(matches!(&selections[1], ProfileSelection::Saved(id) if *id == profile_id));
295 assert!(matches!(&selections[2], ProfileSelection::Inline(_)));
296 }
297
298 #[test]
299 fn authorization_profiles_require_owners_and_normalize_subjects() {
300 assert_eq!(
301 AuthorizationProfile::new(Vec::new(), Vec::new()).unwrap_err(),
302 "authorization profile requires at least one owner"
303 );
304 let profile = AuthorizationProfile::new(
305 vec![
306 ProfileOwner::Group(group(2)),
307 ProfileOwner::User(user(2)),
308 ProfileOwner::RequestUser,
309 ProfileOwner::Group(group(1)),
310 ProfileOwner::User(user(1)),
311 ProfileOwner::User(user(2)),
312 ],
313 vec![
314 ProfileViewer::Model(model(2)),
315 ProfileViewer::Group(group(2)),
316 ProfileViewer::RequestModel,
317 ProfileViewer::User(user(2)),
318 ProfileViewer::RequestUser,
319 ProfileViewer::Model(model(1)),
320 ProfileViewer::User(user(1)),
321 ProfileViewer::Group(group(1)),
322 ProfileViewer::Model(model(2)),
323 ],
324 )
325 .expect("profile");
326 assert_eq!(
327 profile.owners(),
328 &[
329 ProfileOwner::RequestUser,
330 ProfileOwner::User(user(1)),
331 ProfileOwner::User(user(2)),
332 ProfileOwner::Group(group(1)),
333 ProfileOwner::Group(group(2)),
334 ]
335 );
336 assert_eq!(
337 profile.viewers(),
338 &[
339 ProfileViewer::RequestUser,
340 ProfileViewer::RequestModel,
341 ProfileViewer::User(user(1)),
342 ProfileViewer::User(user(2)),
343 ProfileViewer::Group(group(1)),
344 ProfileViewer::Group(group(2)),
345 ProfileViewer::Model(model(1)),
346 ProfileViewer::Model(model(2)),
347 ]
348 );
349 }
350
351 #[test]
352 fn resolution_substitutes_the_request_principal_and_normalizes_grants() {
353 let profile = AuthorizationProfile::new(
354 vec![ProfileOwner::RequestUser, ProfileOwner::Group(group(3))],
355 vec![
356 ProfileViewer::RequestUser,
357 ProfileViewer::RequestModel,
358 ProfileViewer::User(user(4)),
359 ProfileViewer::Group(group(3)),
360 ProfileViewer::Model(model(5)),
361 ],
362 )
363 .expect("profile");
364 let resolved = profile
365 .resolve(RequestPrincipal::new(user(1), model(2)))
366 .expect("resolution");
367 assert_eq!(
368 resolved.owners(),
369 &[OwnerSubject::User(user(1)), OwnerSubject::Group(group(3)),]
370 );
371 assert_eq!(
372 resolved.viewers(),
373 &[
374 ViewerSubject::User(user(4)),
375 ViewerSubject::Model(model(2)),
376 ViewerSubject::Model(model(5)),
377 ]
378 );
379 }
380
381 #[test]
382 fn resolved_profiles_enforce_saved_revision_invariants() {
383 let profile_id = ProfileId::new(tx(1));
384 let revision = ProfileRevision::new(profile_id, tx(3));
385 let make = || {
386 Authorizations::new(
387 vec![OwnerSubject::User(user(1))],
388 vec![ViewerSubject::Model(model(1))],
389 )
390 .expect("authorizations")
391 };
392 let authorizations = make();
393 let resolved = ResolvedProfile::new(
394 authorizations.clone(),
395 ProfileSource::Saved(profile_id),
396 Some(revision),
397 )
398 .expect("resolved profile");
399 assert_eq!(resolved.authorizations(), &authorizations);
400 assert_eq!(resolved.source(), ProfileSource::Saved(profile_id));
401 assert_eq!(resolved.saved_revision(), Some(revision));
402 assert_eq!(resolved.into_authorizations(), authorizations);
403 assert_eq!(
404 ResolvedProfile::new(make(), ProfileSource::Saved(profile_id), None).unwrap_err(),
405 "saved profile requires a revision"
406 );
407 assert_eq!(
408 ResolvedProfile::new(
409 make(),
410 ProfileSource::Saved(ProfileId::new(tx(2))),
411 Some(revision),
412 )
413 .unwrap_err(),
414 "saved profile revision does not match profile ID"
415 );
416 assert_eq!(
417 ResolvedProfile::new(make(), ProfileSource::BuiltIn, Some(revision)).unwrap_err(),
418 "only a saved profile may have a revision"
419 );
420 assert!(ResolvedProfile::new(make(), ProfileSource::Inline, None).is_ok());
421 }
422
423 #[test]
424 fn built_in_profile_uses_request_user_and_all_models() {
425 let profile = built_in_profile();
426 assert_eq!(profile.owners(), &[ProfileOwner::RequestUser]);
427 assert_eq!(profile.viewers(), &[ProfileViewer::Group(ALL_MODELS)]);
428 let resolved = resolve_built_in(RequestPrincipal::new(user(1), model(2)))
429 .expect("built-in resolution");
430 assert_eq!(resolved.source(), ProfileSource::BuiltIn);
431 assert_eq!(resolved.saved_revision(), None);
432 assert_eq!(
433 resolved.authorizations().owners(),
434 &[OwnerSubject::User(user(1))]
435 );
436 assert_eq!(
437 resolved.authorizations().viewers(),
438 &[ViewerSubject::Group(ALL_MODELS)]
439 );
440 }
441}