use std::cmp::Ordering;
pub use kcode_k1_access_types::{Authorizations, OwnerSubject, RequestPrincipal, ViewerSubject};
pub use kcode_k1_groups::{GroupId, ModelId, TxId, UserId};
use kcode_k1_groups::ALL_MODELS;
#[derive(Copy, Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct ProfileId(TxId);
impl ProfileId {
pub const fn new(txid: TxId) -> Self {
Self(txid)
}
pub const fn txid(self) -> TxId {
self.0
}
}
#[derive(Copy, Clone, Debug, Eq, Hash, PartialEq)]
pub enum ProfileOwner {
RequestUser,
User(UserId),
Group(GroupId),
}
impl ProfileOwner {
const fn sort_tag(self) -> u8 {
match self {
Self::RequestUser => 0,
Self::User(_) => 1,
Self::Group(_) => 2,
}
}
}
impl Ord for ProfileOwner {
fn cmp(&self, other: &Self) -> Ordering {
self.sort_tag()
.cmp(&other.sort_tag())
.then_with(|| match (self, other) {
(Self::RequestUser, Self::RequestUser) => Ordering::Equal,
(Self::User(left), Self::User(right)) => {
left.as_tx_id().as_bytes().cmp(right.as_tx_id().as_bytes())
}
(Self::Group(left), Self::Group(right)) => {
left.txid().as_bytes().cmp(right.txid().as_bytes())
}
_ => unreachable!("equal owner sort tags have equal variants"),
})
}
}
impl PartialOrd for ProfileOwner {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
#[derive(Copy, Clone, Debug, Eq, Hash, PartialEq)]
pub enum ProfileViewer {
RequestUser,
RequestModel,
User(UserId),
Group(GroupId),
Model(ModelId),
}
impl ProfileViewer {
const fn sort_tag(self) -> u8 {
match self {
Self::RequestUser => 0,
Self::RequestModel => 1,
Self::User(_) => 2,
Self::Group(_) => 3,
Self::Model(_) => 4,
}
}
}
impl Ord for ProfileViewer {
fn cmp(&self, other: &Self) -> Ordering {
self.sort_tag()
.cmp(&other.sort_tag())
.then_with(|| match (self, other) {
(Self::RequestUser, Self::RequestUser)
| (Self::RequestModel, Self::RequestModel) => Ordering::Equal,
(Self::User(left), Self::User(right)) => {
left.as_tx_id().as_bytes().cmp(right.as_tx_id().as_bytes())
}
(Self::Group(left), Self::Group(right)) => {
left.txid().as_bytes().cmp(right.txid().as_bytes())
}
(Self::Model(left), Self::Model(right)) => left.as_bytes().cmp(right.as_bytes()),
_ => unreachable!("equal viewer sort tags have equal variants"),
})
}
}
impl PartialOrd for ProfileViewer {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct AuthorizationProfile {
owners: Vec<ProfileOwner>,
viewers: Vec<ProfileViewer>,
}
impl AuthorizationProfile {
pub fn new(
mut owners: Vec<ProfileOwner>,
mut viewers: Vec<ProfileViewer>,
) -> Result<Self, String> {
if owners.is_empty() {
return Err("authorization profile requires at least one owner".to_owned());
}
owners.sort_unstable();
owners.dedup();
viewers.sort_unstable();
viewers.dedup();
Ok(Self { owners, viewers })
}
pub fn owners(&self) -> &[ProfileOwner] {
&self.owners
}
pub fn viewers(&self) -> &[ProfileViewer] {
&self.viewers
}
pub fn resolve(&self, principal: RequestPrincipal) -> Result<Authorizations, String> {
let owners = self
.owners
.iter()
.map(|owner| match *owner {
ProfileOwner::RequestUser => OwnerSubject::User(principal.user()),
ProfileOwner::User(user) => OwnerSubject::User(user),
ProfileOwner::Group(group) => OwnerSubject::Group(group),
})
.collect();
let viewers = self
.viewers
.iter()
.map(|viewer| match *viewer {
ProfileViewer::RequestUser => ViewerSubject::User(principal.user()),
ProfileViewer::RequestModel => ViewerSubject::Model(principal.model()),
ProfileViewer::User(user) => ViewerSubject::User(user),
ProfileViewer::Group(group) => ViewerSubject::Group(group),
ProfileViewer::Model(model) => ViewerSubject::Model(model),
})
.collect();
Authorizations::new(owners, viewers)
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum ProfileSelection {
BuiltIn,
Saved(ProfileId),
Inline(AuthorizationProfile),
}
#[derive(Copy, Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub enum ProfileSource {
BuiltIn,
Saved(ProfileId),
Inline,
}
#[derive(Copy, Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct ProfileRevision {
profile_id: ProfileId,
txid: TxId,
}
impl ProfileRevision {
pub const fn new(profile_id: ProfileId, txid: TxId) -> Self {
Self { profile_id, txid }
}
pub const fn profile_id(&self) -> ProfileId {
self.profile_id
}
pub const fn txid(&self) -> TxId {
self.txid
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct ResolvedProfile {
authorizations: Authorizations,
source: ProfileSource,
saved_revision: Option<ProfileRevision>,
}
impl ResolvedProfile {
pub fn new(
authorizations: Authorizations,
source: ProfileSource,
saved_revision: Option<ProfileRevision>,
) -> Result<Self, String> {
match (source, saved_revision) {
(ProfileSource::Saved(profile_id), Some(revision))
if revision.profile_id() == profile_id => {}
(ProfileSource::Saved(_), None) => {
return Err("saved profile requires a revision".to_owned());
}
(ProfileSource::Saved(_), Some(_)) => {
return Err("saved profile revision does not match profile ID".to_owned());
}
(ProfileSource::BuiltIn | ProfileSource::Inline, Some(_)) => {
return Err("only a saved profile may have a revision".to_owned());
}
(ProfileSource::BuiltIn | ProfileSource::Inline, None) => {}
}
Ok(Self {
authorizations,
source,
saved_revision,
})
}
pub fn authorizations(&self) -> &Authorizations {
&self.authorizations
}
pub const fn source(&self) -> ProfileSource {
self.source
}
pub const fn saved_revision(&self) -> Option<ProfileRevision> {
self.saved_revision
}
pub fn into_authorizations(self) -> Authorizations {
self.authorizations
}
}
pub fn built_in_profile() -> AuthorizationProfile {
AuthorizationProfile::new(
vec![ProfileOwner::RequestUser],
vec![ProfileViewer::Group(ALL_MODELS)],
)
.expect("built-in profile is valid")
}
pub fn resolve_built_in(principal: RequestPrincipal) -> Result<ResolvedProfile, String> {
let authorizations = built_in_profile().resolve(principal)?;
ResolvedProfile::new(authorizations, ProfileSource::BuiltIn, None)
}
#[cfg(test)]
mod tests {
use super::*;
fn tx(byte: u8) -> TxId {
TxId::from_bytes([byte; 12])
}
fn user(byte: u8) -> UserId {
UserId::from_tx_id(tx(byte))
}
fn group(byte: u8) -> GroupId {
GroupId::new(tx(byte))
}
fn model(byte: u8) -> ModelId {
ModelId::from_bytes([byte; 32])
}
#[test]
fn profile_ids_revisions_and_selections_preserve_values() {
let profile_id = ProfileId::new(tx(1));
let revision = ProfileRevision::new(profile_id, tx(2));
assert_eq!(profile_id.txid(), tx(1));
assert_eq!(revision.profile_id(), profile_id);
assert_eq!(revision.txid(), tx(2));
let inline = AuthorizationProfile::new(vec![ProfileOwner::RequestUser], Vec::new())
.expect("profile");
let selections = [
ProfileSelection::BuiltIn,
ProfileSelection::Saved(profile_id),
ProfileSelection::Inline(inline),
];
assert!(matches!(&selections[0], ProfileSelection::BuiltIn));
assert!(matches!(&selections[1], ProfileSelection::Saved(id) if *id == profile_id));
assert!(matches!(&selections[2], ProfileSelection::Inline(_)));
}
#[test]
fn authorization_profiles_require_owners_and_normalize_subjects() {
assert_eq!(
AuthorizationProfile::new(Vec::new(), Vec::new()).unwrap_err(),
"authorization profile requires at least one owner"
);
let profile = AuthorizationProfile::new(
vec![
ProfileOwner::Group(group(2)),
ProfileOwner::User(user(2)),
ProfileOwner::RequestUser,
ProfileOwner::Group(group(1)),
ProfileOwner::User(user(1)),
ProfileOwner::User(user(2)),
],
vec![
ProfileViewer::Model(model(2)),
ProfileViewer::Group(group(2)),
ProfileViewer::RequestModel,
ProfileViewer::User(user(2)),
ProfileViewer::RequestUser,
ProfileViewer::Model(model(1)),
ProfileViewer::User(user(1)),
ProfileViewer::Group(group(1)),
ProfileViewer::Model(model(2)),
],
)
.expect("profile");
assert_eq!(
profile.owners(),
&[
ProfileOwner::RequestUser,
ProfileOwner::User(user(1)),
ProfileOwner::User(user(2)),
ProfileOwner::Group(group(1)),
ProfileOwner::Group(group(2)),
]
);
assert_eq!(
profile.viewers(),
&[
ProfileViewer::RequestUser,
ProfileViewer::RequestModel,
ProfileViewer::User(user(1)),
ProfileViewer::User(user(2)),
ProfileViewer::Group(group(1)),
ProfileViewer::Group(group(2)),
ProfileViewer::Model(model(1)),
ProfileViewer::Model(model(2)),
]
);
}
#[test]
fn resolution_substitutes_the_request_principal_and_normalizes_grants() {
let profile = AuthorizationProfile::new(
vec![ProfileOwner::RequestUser, ProfileOwner::Group(group(3))],
vec![
ProfileViewer::RequestUser,
ProfileViewer::RequestModel,
ProfileViewer::User(user(4)),
ProfileViewer::Group(group(3)),
ProfileViewer::Model(model(5)),
],
)
.expect("profile");
let resolved = profile
.resolve(RequestPrincipal::new(user(1), model(2)))
.expect("resolution");
assert_eq!(
resolved.owners(),
&[OwnerSubject::User(user(1)), OwnerSubject::Group(group(3)),]
);
assert_eq!(
resolved.viewers(),
&[
ViewerSubject::User(user(4)),
ViewerSubject::Model(model(2)),
ViewerSubject::Model(model(5)),
]
);
}
#[test]
fn resolved_profiles_enforce_saved_revision_invariants() {
let profile_id = ProfileId::new(tx(1));
let revision = ProfileRevision::new(profile_id, tx(3));
let make = || {
Authorizations::new(
vec![OwnerSubject::User(user(1))],
vec![ViewerSubject::Model(model(1))],
)
.expect("authorizations")
};
let authorizations = make();
let resolved = ResolvedProfile::new(
authorizations.clone(),
ProfileSource::Saved(profile_id),
Some(revision),
)
.expect("resolved profile");
assert_eq!(resolved.authorizations(), &authorizations);
assert_eq!(resolved.source(), ProfileSource::Saved(profile_id));
assert_eq!(resolved.saved_revision(), Some(revision));
assert_eq!(resolved.into_authorizations(), authorizations);
assert_eq!(
ResolvedProfile::new(make(), ProfileSource::Saved(profile_id), None).unwrap_err(),
"saved profile requires a revision"
);
assert_eq!(
ResolvedProfile::new(
make(),
ProfileSource::Saved(ProfileId::new(tx(2))),
Some(revision),
)
.unwrap_err(),
"saved profile revision does not match profile ID"
);
assert_eq!(
ResolvedProfile::new(make(), ProfileSource::BuiltIn, Some(revision)).unwrap_err(),
"only a saved profile may have a revision"
);
assert!(ResolvedProfile::new(make(), ProfileSource::Inline, None).is_ok());
}
#[test]
fn built_in_profile_uses_request_user_and_all_models() {
let profile = built_in_profile();
assert_eq!(profile.owners(), &[ProfileOwner::RequestUser]);
assert_eq!(profile.viewers(), &[ProfileViewer::Group(ALL_MODELS)]);
let resolved = resolve_built_in(RequestPrincipal::new(user(1), model(2)))
.expect("built-in resolution");
assert_eq!(resolved.source(), ProfileSource::BuiltIn);
assert_eq!(resolved.saved_revision(), None);
assert_eq!(
resolved.authorizations().owners(),
&[OwnerSubject::User(user(1))]
);
assert_eq!(
resolved.authorizations().viewers(),
&[ViewerSubject::Group(ALL_MODELS)]
);
}
}