use std::collections::BTreeSet;
use serde::{Deserialize, Serialize};
use crate::core::{Capability, Timestamp, format_timestamp};
#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
#[serde(transparent)]
pub struct Scope(BTreeSet<String>);
impl Scope {
#[must_use]
pub fn root() -> Self {
Self(BTreeSet::from(["*".to_owned()]))
}
#[must_use]
pub fn empty() -> Self {
Self(BTreeSet::new())
}
pub fn of<I, S>(patterns: I) -> Self
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
Self(patterns.into_iter().map(Into::into).collect())
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.0.is_empty()
}
pub fn patterns(&self) -> impl Iterator<Item = &str> {
self.0.iter().map(String::as_str)
}
fn pattern_covers(pattern: &str, capability: &str) -> bool {
if pattern == "*" {
return true;
}
let Some(prefix) = pattern.strip_suffix(".*") else {
return pattern == capability;
};
capability == prefix
|| (capability.starts_with(prefix)
&& capability.as_bytes().get(prefix.len()) == Some(&b'.'))
}
#[must_use]
pub fn permits(&self, capability: &Capability) -> bool {
self.0
.iter()
.any(|p| Self::pattern_covers(p, &capability.0))
}
#[must_use]
pub fn contains(&self, other: &Self) -> bool {
other.0.iter().all(|o| self.0.iter().any(|s| covers(s, o)))
}
}
fn covers(a: &str, b: &str) -> bool {
if a == "*" {
return true;
}
if b == "*" {
return false;
}
match (a.strip_suffix(".*"), b.strip_suffix(".*")) {
(Some(pa), Some(pb)) => pb == pa || (pb.starts_with(pa) && pb.as_bytes()[pa.len()] == b'.'),
(Some(_), None) => Scope::pattern_covers(a, b),
(None, Some(_)) => false,
(None, None) => a == b,
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Principal {
pub id: String,
pub scope: Scope,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub audience: Option<String>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
with = "time::serde::rfc3339::option"
)]
pub not_after: Option<Timestamp>,
}
impl Principal {
pub fn new(id: impl Into<String>, scope: Scope) -> Self {
Self {
id: id.into(),
scope,
audience: None,
not_after: None,
}
}
#[must_use]
pub fn for_audience(mut self, audience: impl Into<String>) -> Self {
self.audience = Some(audience.into());
self
}
#[must_use]
pub const fn until(mut self, not_after: Timestamp) -> Self {
self.not_after = Some(not_after);
self
}
}
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
pub enum DelegationError {
#[error(
"'{to}' would hold authority '{widened}' that its delegator '{from}' does not — \
delegation may only narrow"
)]
ScopeWidened {
from: String,
to: String,
widened: String,
},
#[error(
"'{to}' would stay valid until {delegate_until}, later than its delegator '{from}' \
({delegator_until}) — delegation may only narrow"
)]
ValidityWidened {
from: String,
to: String,
delegator_until: String,
delegate_until: String,
},
#[error(
"'{to}' names audience '{delegate_audience}' but its delegator '{from}' is bound to \
'{delegator_audience}' — delegation may only narrow"
)]
AudienceWidened {
from: String,
to: String,
delegator_audience: String,
delegate_audience: String,
},
#[error("delegation depth {depth} exceeds the limit of {max}")]
TooDeep { depth: usize, max: usize },
#[error("delegation chain is empty: there is no principal to act as")]
Empty,
#[error(
"delegation to '{subject}' expired at {not_after}; this admission is at {at} — \
obtain a fresh credential rather than retrying"
)]
Expired {
subject: String,
not_after: String,
at: String,
},
#[error(
"delegation to '{subject}' is bound to audience '{audience}' and cannot act on \
plane '{plane}'"
)]
WrongAudience {
subject: String,
audience: String,
plane: String,
},
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(into = "DelegationWire", try_from = "DelegationWire")]
pub struct Delegation {
root: Principal,
rest: Vec<Principal>,
}
#[derive(Serialize, Deserialize)]
struct DelegationWire {
links: Vec<Principal>,
}
impl From<Delegation> for DelegationWire {
fn from(chain: Delegation) -> Self {
Self {
links: chain.links().cloned().collect(),
}
}
}
impl TryFrom<DelegationWire> for Delegation {
type Error = DelegationError;
fn try_from(wire: DelegationWire) -> Result<Self, Self::Error> {
Self::rehydrate(wire.links)
}
}
pub const MAX_DELEGATION_DEPTH: usize = 3;
impl Delegation {
#[must_use]
pub fn root(owner: Principal) -> Self {
Self {
root: owner,
rest: Vec::new(),
}
}
pub fn delegate(&self, to: Principal) -> Result<Self, DelegationError> {
let from = self.subject();
if !from.scope.contains(&to.scope) {
let widened = to
.scope
.patterns()
.find(|p| !from.scope.contains(&Scope::of([*p])))
.unwrap_or("<unknown>")
.to_owned();
return Err(DelegationError::ScopeWidened {
from: from.id.clone(),
to: to.id,
widened,
});
}
if let (Some(delegator_until), Some(delegate_until)) = (self.not_after(), to.not_after)
&& delegate_until > delegator_until
{
return Err(DelegationError::ValidityWidened {
from: from.id.clone(),
to: to.id,
delegator_until: format_timestamp(delegator_until),
delegate_until: format_timestamp(delegate_until),
});
}
if let (Some(delegator_audience), Some(delegate_audience)) =
(self.audience(), to.audience.as_deref())
&& delegate_audience != delegator_audience
{
return Err(DelegationError::AudienceWidened {
from: from.id.clone(),
to: to.id,
delegator_audience: delegator_audience.to_owned(),
delegate_audience: delegate_audience.to_owned(),
});
}
if self.depth() + 1 > MAX_DELEGATION_DEPTH {
return Err(DelegationError::TooDeep {
depth: self.depth() + 1,
max: MAX_DELEGATION_DEPTH,
});
}
let mut next = self.clone();
next.rest.push(to);
Ok(next)
}
#[must_use]
pub const fn owner(&self) -> &Principal {
&self.root
}
#[must_use]
pub fn subject(&self) -> &Principal {
self.rest.last().unwrap_or(&self.root)
}
#[must_use]
pub const fn depth(&self) -> usize {
self.rest.len()
}
#[must_use]
pub fn effective_scope(&self) -> &Scope {
&self.subject().scope
}
pub fn links(&self) -> impl Iterator<Item = &Principal> {
std::iter::once(&self.root).chain(self.rest.iter())
}
#[must_use]
pub fn not_after(&self) -> Option<Timestamp> {
self.links().filter_map(|link| link.not_after).min()
}
#[must_use]
pub fn audience(&self) -> Option<&str> {
self.links().find_map(|link| link.audience.as_deref())
}
pub fn admissible(&self, plane: &str, at: Timestamp) -> Result<(), DelegationError> {
if let Some(not_after) = self.not_after()
&& at >= not_after
{
return Err(DelegationError::Expired {
subject: self.subject().id.clone(),
not_after: format_timestamp(not_after),
at: format_timestamp(at),
});
}
if let Some(audience) = self.audience()
&& audience != plane
{
return Err(DelegationError::WrongAudience {
subject: self.subject().id.clone(),
audience: audience.to_owned(),
plane: plane.to_owned(),
});
}
Ok(())
}
pub fn rehydrate(links: Vec<Principal>) -> Result<Self, DelegationError> {
let mut it = links.into_iter();
let root = it.next().ok_or(DelegationError::Empty)?;
let mut chain = Self::root(root);
for link in it {
chain = chain.delegate(link)?;
}
Ok(chain)
}
}
impl Delegation {
#[must_use]
pub fn as_context(&self) -> serde_json::Value {
serde_json::json!({
"owner": self.owner().id,
"subject": self.subject().id,
"delegation_depth": self.depth(),
"scope": self.effective_scope().patterns().collect::<Vec<_>>(),
})
}
}