use crate::ids::{numeric_id, string_id, validation_error};
use serde::{Deserialize, Serialize};
use std::collections::{BTreeMap, BTreeSet};
use std::fmt;
use thiserror::Error;
const MAX_PRINCIPAL_ID_BYTES: usize = 256;
pub const MAX_ACCESS_GRANT_ENTRIES: usize = 1_000;
pub const MAX_ACCESS_GRANTS_PRINCIPAL_BYTES: usize = 65_536;
validation_error!(
PrincipalIdValidationError,
"invalid principal_id {value:?}: {reason}"
);
validation_error!(
SubjectIdValidationError,
"invalid subject_id {value:?}: {reason}"
);
string_id! {
SubjectId,
error = SubjectIdValidationError,
validate = validate_subject_id,
schema(
description = "Stable opaque subject id containing 1 to 256 visible ASCII characters other than the comma.",
pattern = r"^[\x21-\x2B\x2D-\x7E]{1,256}$",
example = "usr_8f3c"
)
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Subject {
pub principal_scope: PrincipalScope,
pub subject_id: SubjectId,
pub principals: PrincipalSet,
}
validation_error!(
PrincipalScopeValidationError,
"invalid principal_scope {value:?}: {reason}"
);
string_id! {
PrincipalId,
error = PrincipalIdValidationError,
validate = validate_principal_id,
schema(
description = "Stable opaque principal id containing 1 to 256 visible ASCII characters other than the comma.",
pattern = r"^[\x21-\x2B\x2D-\x7E]{1,256}$",
example = "prn_8f3c"
)
}
string_id! {
PrincipalScope,
error = PrincipalScopeValidationError,
validate = validate_principal_scope,
schema(
description = "Opaque identity-domain id containing 1 to 256 visible ASCII characters other than the comma.",
pattern = r"^[\x21-\x2B\x2D-\x7E]{1,256}$",
example = "org_acme"
)
}
numeric_id! {
AccessRevisionNo,
public_ordinal,
schema_description = "Monotonic per-inode access revision. It increases with every accepted access update."
}
fn visible_ascii_reason(value: &str) -> Option<&'static str> {
if value.is_empty() {
return Some("must not be empty");
}
if value.len() > MAX_PRINCIPAL_ID_BYTES {
return Some("must be 256 bytes or fewer");
}
if !value.bytes().all(|byte| (0x21..=0x7e).contains(&byte)) {
return Some("must contain only visible ASCII characters");
}
if value.contains(',') {
return Some("must not contain a comma, which separates ids on the wire");
}
None
}
fn validate_principal_id(value: &str) -> Result<(), PrincipalIdValidationError> {
visible_ascii_reason(value).map_or(Ok(()), |reason| {
Err(PrincipalIdValidationError::new(value, reason))
})
}
fn validate_subject_id(value: &str) -> Result<(), SubjectIdValidationError> {
visible_ascii_reason(value).map_or(Ok(()), |reason| {
Err(SubjectIdValidationError::new(value, reason))
})
}
fn validate_principal_scope(value: &str) -> Result<(), PrincipalScopeValidationError> {
visible_ascii_reason(value).map_or(Ok(()), |reason| {
Err(PrincipalScopeValidationError::new(value, reason))
})
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
#[serde(rename_all = "snake_case")]
pub enum AccessRight {
Read,
History,
Write,
Create,
Remove,
Share,
Manage,
Admin,
}
impl AccessRight {
pub const ALL: [Self; 8] = [
Self::Read,
Self::History,
Self::Write,
Self::Create,
Self::Remove,
Self::Share,
Self::Manage,
Self::Admin,
];
pub const fn as_str(self) -> &'static str {
match self {
Self::Read => "read",
Self::History => "history",
Self::Write => "write",
Self::Create => "create",
Self::Remove => "remove",
Self::Share => "share",
Self::Manage => "manage",
Self::Admin => "admin",
}
}
const fn bit(self) -> u8 {
1 << (self as u8)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
#[cfg_attr(feature = "openapi", schema(value_type = Vec<AccessRight>))]
pub struct AccessRights(u8);
impl AccessRights {
pub const EMPTY: Self = Self(0);
pub const ALL: Self = Self(((1_u16 << AccessRight::ALL.len()) - 1) as u8);
pub const ADMIN: Self = Self(AccessRight::Admin.bit());
pub fn contains(self, right: AccessRight) -> bool {
self.0 & right.bit() != 0
}
pub fn insert(&mut self, right: AccessRight) {
self.0 |= right.bit();
}
pub fn union(self, other: Self) -> Self {
Self(self.0 | other.0)
}
pub fn difference(self, other: Self) -> Self {
Self(self.0 & !other.0)
}
pub fn is_subset_of(self, other: Self) -> bool {
self.0 & !other.0 == 0
}
pub fn is_empty(self) -> bool {
self.0 == 0
}
pub fn iter(self) -> impl Iterator<Item = AccessRight> {
AccessRight::ALL
.into_iter()
.filter(move |right| self.contains(*right))
}
}
impl FromIterator<AccessRight> for AccessRights {
fn from_iter<I: IntoIterator<Item = AccessRight>>(rights: I) -> Self {
rights.into_iter().fold(Self::EMPTY, |mut set, right| {
set.insert(right);
set
})
}
}
impl Serialize for AccessRights {
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
serializer.collect_seq(self.iter())
}
}
impl<'de> Deserialize<'de> for AccessRights {
fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
let names = Vec::<AccessRight>::deserialize(deserializer)?;
let mut rights = Self::EMPTY;
for right in names {
if rights.contains(right) {
return Err(serde::de::Error::custom(format!(
"duplicate right `{}`",
right.as_str()
)));
}
rights.insert(right);
}
Ok(rights)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize)]
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
#[cfg_attr(feature = "openapi", schema(value_type = std::collections::BTreeMap<String, AccessRights>))]
#[serde(transparent)]
pub struct AccessGrants(BTreeMap<PrincipalId, AccessRights>);
impl AccessGrants {
pub fn new(entries: BTreeMap<PrincipalId, AccessRights>) -> Result<Self, AccessGrantsError> {
if entries.len() > MAX_ACCESS_GRANT_ENTRIES {
return Err(AccessGrantsError::TooManyEntries {
entries: entries.len(),
});
}
if let Some((principal_id, _)) = entries.iter().find(|(_, rights)| rights.is_empty()) {
return Err(AccessGrantsError::EmptyRights {
principal_id: principal_id.clone(),
});
}
let principal_bytes = entries.keys().map(|id| id.as_str().len()).sum::<usize>();
if principal_bytes > MAX_ACCESS_GRANTS_PRINCIPAL_BYTES {
return Err(AccessGrantsError::TooManyPrincipalBytes { principal_bytes });
}
Ok(Self(entries))
}
pub fn get(&self, principal_id: &PrincipalId) -> AccessRights {
self.0.get(principal_id).copied().unwrap_or_default()
}
pub fn iter(&self) -> impl Iterator<Item = (&PrincipalId, AccessRights)> {
self.0.iter().map(|(id, rights)| (id, *rights))
}
pub fn len(&self) -> usize {
self.0.len()
}
pub fn is_empty(&self) -> bool {
self.0.is_empty()
}
pub fn as_map(&self) -> &BTreeMap<PrincipalId, AccessRights> {
&self.0
}
pub fn logical_bytes(&self) -> usize {
self.0
.iter()
.map(|(id, rights)| id.as_str().len() + rights.iter().count())
.sum()
}
}
impl TryFrom<BTreeMap<PrincipalId, AccessRights>> for AccessGrants {
type Error = AccessGrantsError;
fn try_from(entries: BTreeMap<PrincipalId, AccessRights>) -> Result<Self, Self::Error> {
Self::new(entries)
}
}
impl<'de> Deserialize<'de> for AccessGrants {
fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
struct AccessGrantsVisitor;
impl<'de> serde::de::Visitor<'de> for AccessGrantsVisitor {
type Value = AccessGrants;
fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str("a map from principal ids to access rights")
}
fn visit_map<A: serde::de::MapAccess<'de>>(
self,
mut map: A,
) -> Result<Self::Value, A::Error> {
let mut entries: BTreeMap<PrincipalId, AccessRights> = BTreeMap::new();
while let Some((principal_id, rights)) = map.next_entry()? {
match entries.entry(principal_id) {
std::collections::btree_map::Entry::Vacant(entry) => {
entry.insert(rights);
}
std::collections::btree_map::Entry::Occupied(entry) => {
return Err(serde::de::Error::custom(
AccessGrantsError::DuplicatePrincipal {
principal_id: entry.key().clone(),
},
));
}
}
}
AccessGrants::new(entries).map_err(serde::de::Error::custom)
}
}
deserializer.deserialize_map(AccessGrantsVisitor)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Error)]
pub enum AccessGrantsError {
#[error("access grants name principal `{principal_id}` more than once")]
DuplicatePrincipal {
principal_id: PrincipalId,
},
#[error("access grants name {entries} principals, which exceeds the maximum of {MAX_ACCESS_GRANT_ENTRIES}")]
TooManyEntries {
entries: usize,
},
#[error("access grants hold {principal_bytes} bytes of principal ids, which exceeds the maximum of {MAX_ACCESS_GRANTS_PRINCIPAL_BYTES} bytes")]
TooManyPrincipalBytes {
principal_bytes: usize,
},
#[error("access grant for `{principal_id}` carries no rights")]
EmptyRights {
principal_id: PrincipalId,
},
}
pub const MAX_SUBJECT_PRINCIPALS: usize = 64;
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct PrincipalSet(BTreeSet<PrincipalId>);
impl PrincipalSet {
pub fn new(principals: BTreeSet<PrincipalId>) -> Result<Self, PrincipalSetError> {
if principals.len() > MAX_SUBJECT_PRINCIPALS {
return Err(PrincipalSetError::TooManyPrincipals {
principals: principals.len(),
});
}
Ok(Self(principals))
}
pub fn iter(&self) -> impl Iterator<Item = &PrincipalId> {
self.0.iter()
}
pub fn contains(&self, principal_id: &PrincipalId) -> bool {
self.0.contains(principal_id)
}
pub fn len(&self) -> usize {
self.0.len()
}
pub fn is_empty(&self) -> bool {
self.0.is_empty()
}
}
#[derive(Debug, Clone, PartialEq, Eq, Error)]
pub enum PrincipalSetError {
#[error("principal set names {principals} principals, which exceeds the maximum of {MAX_SUBJECT_PRINCIPALS}")]
TooManyPrincipals {
principals: usize,
},
}
#[cfg(test)]
mod tests {
use super::{
AccessGrants, AccessGrantsError, AccessRight, AccessRights, PrincipalId,
MAX_ACCESS_GRANTS_PRINCIPAL_BYTES, MAX_ACCESS_GRANT_ENTRIES,
};
#[test]
fn ids_never_contain_the_wire_separator() {
assert!(PrincipalId::parse("visitor,prn_root").is_err());
assert!(super::SubjectId::parse("usr,ada").is_err());
assert!(super::PrincipalScope::parse("org,demo").is_err());
assert!(PrincipalId::parse("visitor").is_ok());
}
use std::collections::BTreeMap;
#[test]
fn access_rights_encode_in_declaration_order_and_decode_any_order() {
let rights: AccessRights = AccessRight::ALL.into_iter().rev().collect();
let encoded = serde_json::to_string(&rights).expect("serialize rights");
assert_eq!(
encoded,
r#"["read","history","write","create","remove","share","manage","admin"]"#
);
assert_eq!(
serde_json::from_str::<AccessRights>(&encoded).expect("decode encoded rights"),
AccessRights::ALL
);
assert_eq!(
serde_json::from_str::<AccessRights>(r#"["manage","read"]"#)
.expect("decode rights in another order"),
[AccessRight::Read, AccessRight::Manage]
.into_iter()
.collect()
);
assert!(serde_json::from_str::<AccessRights>(r#"["read","read"]"#).is_err());
assert!(serde_json::from_str::<AccessRights>(r#"["owner"]"#).is_err());
let empty = serde_json::to_string(&AccessRights::EMPTY).expect("serialize empty rights");
assert_eq!(empty, "[]");
assert_eq!(
serde_json::from_str::<AccessRights>(&empty).expect("empty rights"),
AccessRights::EMPTY
);
}
#[test]
fn access_grants_json_rejects_a_repeated_principal() {
let error =
serde_json::from_str::<AccessGrants>(r#"{"viewer":["read"],"viewer":["manage"]}"#)
.expect_err("repeated principal");
assert!(
error
.to_string()
.contains("access grants name principal `viewer` more than once"),
"{error}"
);
}
#[test]
fn access_grants_cbor_rejects_a_repeated_principal() {
let mut encoded = Vec::new();
ciborium::ser::into_writer(
&ciborium::Value::Map(vec![
(
ciborium::Value::Text("viewer".to_owned()),
ciborium::Value::Array(vec![ciborium::Value::Text("read".to_owned())]),
),
(
ciborium::Value::Text("viewer".to_owned()),
ciborium::Value::Array(vec![ciborium::Value::Text("manage".to_owned())]),
),
]),
&mut encoded,
)
.expect("encode repeated principal");
let error = ciborium::de::from_reader::<AccessGrants, _>(encoded.as_slice())
.expect_err("repeated principal");
assert!(
error
.to_string()
.contains("access grants name principal `viewer` more than once"),
"{error}"
);
}
#[test]
fn access_grants_reject_empty_rights_and_oversized_maps() {
let principal = PrincipalId::parse("prn_ada").expect("principal");
let read: AccessRights = [AccessRight::Read].into_iter().collect();
let long_id_count = MAX_ACCESS_GRANTS_PRINCIPAL_BYTES / 256 + 1;
assert!(long_id_count < MAX_ACCESS_GRANT_ENTRIES);
for (entries, expected) in [
(
BTreeMap::from([(principal.clone(), AccessRights::EMPTY)]),
AccessGrantsError::EmptyRights {
principal_id: principal,
},
),
(
(0..=MAX_ACCESS_GRANT_ENTRIES)
.map(|index| {
(
PrincipalId::parse(format!("prn_{index}")).expect("principal"),
read,
)
})
.collect(),
AccessGrantsError::TooManyEntries {
entries: MAX_ACCESS_GRANT_ENTRIES + 1,
},
),
(
(0..long_id_count)
.map(|index| {
(
PrincipalId::parse(format!("{index:0256}")).expect("principal"),
read,
)
})
.collect(),
AccessGrantsError::TooManyPrincipalBytes {
principal_bytes: long_id_count * 256,
},
),
] {
assert_eq!(
AccessGrants::new(entries).expect_err("invalid grants"),
expected
);
}
}
}