Skip to main content

kcode_k1_access_profile_values/
lib.rs

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}