mod pattern;
use crate::error::Error;
use crate::object_path::is_internal_path;
use serde::{Deserialize, Serialize};
use std::collections::{BTreeMap, BTreeSet};
pub use pattern::KeyPattern;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Principal {
Anyone,
SignedIn(Identity),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Identity {
ServiceToken,
User(UserIdentity),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct UserIdentity {
pub subject: String,
pub groups: BTreeSet<String>,
}
impl Principal {
pub fn service_token() -> Self {
Self::SignedIn(Identity::ServiceToken)
}
pub fn user(subject: impl Into<String>, groups: impl IntoIterator<Item = String>) -> Self {
Self::SignedIn(Identity::User(UserIdentity {
subject: subject.into(),
groups: groups.into_iter().collect(),
}))
}
pub fn is_anonymous(&self) -> bool {
matches!(self, Self::Anyone)
}
pub fn is_signed_in(&self) -> bool {
matches!(self, Self::SignedIn(_))
}
pub fn is_service_token(&self) -> bool {
matches!(self, Self::SignedIn(Identity::ServiceToken))
}
fn reach(&self) -> Reach {
match self {
Self::Anyone => Reach::Anonymous,
Self::SignedIn(Identity::User(_)) => Reach::User,
Self::SignedIn(Identity::ServiceToken) => Reach::ServiceToken,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Reach {
Anonymous,
User,
ServiceToken,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(try_from = "String", into = "String")]
pub enum Who {
Anyone,
SignedIn,
Group(String),
User(String),
}
impl Who {
pub fn matches(&self, principal: &Principal) -> bool {
match (self, principal) {
(Self::Anyone, Principal::Anyone) | (Self::SignedIn, Principal::SignedIn(_)) => true,
(Self::Group(group), Principal::SignedIn(Identity::User(user))) => {
user.groups.contains(group)
}
(Self::User(subject), Principal::SignedIn(Identity::User(user))) => {
user.subject == *subject
}
_ => false,
}
}
pub fn needs_identity(&self) -> bool {
matches!(self, Self::Group(_) | Self::User(_))
}
}
impl std::str::FromStr for Who {
type Err = Error;
fn from_str(source: &str) -> Result<Self, Self::Err> {
match source {
"anyone" => return Ok(Self::Anyone),
"signed-in" => return Ok(Self::SignedIn),
_ => {}
}
let named = |prefix: &str, build: fn(String) -> Self| {
source.strip_prefix(prefix).map(|name| {
if name.is_empty() {
Err(config(format!(
"`{source}` names nobody; write `{prefix}<name>`"
)))
} else {
Ok(build(name.to_string()))
}
})
};
named("group:", Self::Group)
.or_else(|| named("user:", Self::User))
.unwrap_or_else(|| {
Err(config(format!(
"unknown principal `{source}`; expected `anyone`, `signed-in`, \
`group:<name>` or `user:<name>`"
)))
})
}
}
impl std::fmt::Display for Who {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Anyone => f.write_str("anyone"),
Self::SignedIn => f.write_str("signed-in"),
Self::Group(name) => write!(f, "group:{name}"),
Self::User(name) => write!(f, "user:{name}"),
}
}
}
impl TryFrom<String> for Who {
type Error = Error;
fn try_from(source: String) -> Result<Self, Self::Error> {
source.parse()
}
}
impl From<Who> for String {
fn from(who: Who) -> Self {
who.to_string()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum Verb {
List,
Read,
Write,
Delete,
Search,
}
impl Verb {
pub const ALL: [Self; 5] = [
Self::List,
Self::Read,
Self::Write,
Self::Delete,
Self::Search,
];
pub const fn is_mutating(self) -> bool {
matches!(self, Self::Write | Self::Delete)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Effect {
Allow,
Deny,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(try_from = "RuleRepr", into = "RuleRepr")]
pub struct AccessRule {
pub who: Who,
pub effect: Effect,
pub verbs: BTreeSet<Verb>,
pub under: Vec<KeyPattern>,
}
#[derive(Serialize, Deserialize)]
struct RuleRepr {
who: Who,
#[serde(default, skip_serializing_if = "Option::is_none")]
may: Option<BTreeSet<Verb>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
may_not: Option<BTreeSet<Verb>>,
#[serde(
default = "whole_kb_patterns",
skip_serializing_if = "is_whole_kb_patterns"
)]
under: Vec<KeyPattern>,
}
impl TryFrom<RuleRepr> for AccessRule {
type Error = Error;
fn try_from(repr: RuleRepr) -> Result<Self, Self::Error> {
let (effect, verbs) = match (repr.may, repr.may_not) {
(Some(verbs), None) => (Effect::Allow, verbs),
(None, Some(verbs)) => (Effect::Deny, verbs),
(Some(_), Some(_)) => {
return Err(config(format!(
"an access rule for `{}` names both `may` and `may_not`; split it into two \
rules",
repr.who
)));
}
(None, None) => {
return Err(config(format!(
"an access rule for `{}` names neither `may` nor `may_not`",
repr.who
)));
}
};
Ok(Self {
who: repr.who,
effect,
verbs,
under: repr.under,
})
}
}
impl From<AccessRule> for RuleRepr {
fn from(rule: AccessRule) -> Self {
let (may, may_not) = match rule.effect {
Effect::Allow => (Some(rule.verbs), None),
Effect::Deny => (None, Some(rule.verbs)),
};
Self {
who: rule.who,
may,
may_not,
under: rule.under,
}
}
}
fn whole_kb_patterns() -> Vec<KeyPattern> {
vec![KeyPattern::whole_kb()]
}
fn is_whole_kb_patterns(patterns: &[KeyPattern]) -> bool {
matches!(patterns, [only] if only.is_whole_kb())
}
impl AccessRule {
pub fn new(who: Who, verbs: impl IntoIterator<Item = Verb>) -> Self {
Self {
who,
effect: Effect::Allow,
verbs: verbs.into_iter().collect(),
under: whole_kb_patterns(),
}
}
pub fn deny(who: Who, verbs: impl IntoIterator<Item = Verb>) -> Self {
Self {
who,
effect: Effect::Deny,
verbs: verbs.into_iter().collect(),
under: whole_kb_patterns(),
}
}
#[must_use]
pub fn under(mut self, patterns: impl IntoIterator<Item = KeyPattern>) -> Self {
self.under = patterns.into_iter().collect();
self
}
fn covers(&self, principal: &Principal, verb: Verb) -> bool {
self.who.matches(principal) && self.verbs.contains(&verb)
}
fn is_whole_kb(&self) -> bool {
self.under.iter().any(KeyPattern::is_whole_kb)
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(transparent)]
pub struct AccessPolicy(Vec<AccessRule>);
impl FromIterator<AccessRule> for AccessPolicy {
fn from_iter<T: IntoIterator<Item = AccessRule>>(iter: T) -> Self {
Self(iter.into_iter().collect())
}
}
impl AccessPolicy {
pub fn empty() -> Self {
Self(Vec::new())
}
pub fn signed_in_full() -> Self {
Self(vec![AccessRule::new(Who::SignedIn, Verb::ALL)])
}
pub fn is_empty(&self) -> bool {
self.0.is_empty()
}
pub fn rules(&self) -> &[AccessRule] {
&self.0
}
pub fn names_an_identity(&self) -> bool {
self.0.iter().any(|rule| rule.who.needs_identity())
}
pub fn validate(&self) -> Result<(), Error> {
for rule in &self.0 {
if rule.verbs.is_empty() {
return Err(config(format!(
"an access rule for `{}` names no verbs; remove it or name a verb",
rule.who
)));
}
if rule.under.is_empty() {
return Err(config(
"an access rule has an empty `under`; omit the field to grant the whole \
knowledge base, or name a pattern",
));
}
if rule.who == Who::Anyone
&& let Some(verb) = rule.verbs.iter().copied().find(|verb| verb.is_mutating())
{
let spelling = match rule.effect {
Effect::Allow => "grants",
Effect::Deny => "revokes",
};
return Err(config(format!(
"an access rule {spelling} `{}` for `anyone`; anonymous writes are never \
honoured, so this rule cannot mean what it says",
serde_verb(verb),
)));
}
if let Some(pattern) = rule
.under
.iter()
.find(|pattern| pattern.literal_prefix() == Some(".notedthat"))
{
return Err(config(format!(
"an access rule scopes `{}` to `{pattern}`; the `.notedthat` namespace is \
not addressable by a rule — the service token always reaches it and \
nobody else ever does",
rule.who,
)));
}
}
Ok(())
}
pub fn allows(&self, principal: &Principal, verb: Verb, key: &str) -> bool {
self.key_filter(principal, verb).allows(key)
}
pub fn grants_any(&self, principal: &Principal, verb: Verb) -> bool {
if principal.is_anonymous() && verb.is_mutating() {
return false;
}
let mut granted = false;
for rule in self.0.iter().filter(|rule| rule.covers(principal, verb)) {
match rule.effect {
Effect::Allow => granted = true,
Effect::Deny if rule.is_whole_kb() => return false,
Effect::Deny => {}
}
}
granted
}
pub fn visible_in_listing(&self, principal: &Principal) -> bool {
principal.is_service_token()
|| Verb::ALL
.iter()
.any(|verb| self.grants_any(principal, *verb))
}
pub fn key_filter(&self, principal: &Principal, verb: Verb) -> KeyFilter<'_> {
let reach = principal.reach();
if reach == Reach::Anonymous && verb.is_mutating() {
return KeyFilter {
reach,
allow: Vec::new(),
deny: Vec::new(),
};
}
let (mut allow, mut deny) = (Vec::new(), Vec::new());
for rule in self.0.iter().filter(|rule| rule.covers(principal, verb)) {
match rule.effect {
Effect::Allow => allow.extend(rule.under.iter()),
Effect::Deny => deny.extend(rule.under.iter()),
}
}
KeyFilter { reach, allow, deny }
}
}
#[derive(Debug, Clone)]
pub struct KeyFilter<'a> {
reach: Reach,
allow: Vec<&'a KeyPattern>,
deny: Vec<&'a KeyPattern>,
}
impl KeyFilter<'_> {
pub fn allows(&self, key: &str) -> bool {
if is_internal_path(key) {
return self.reach == Reach::ServiceToken;
}
self.allow.iter().any(|pattern| pattern.matches(key))
&& !self.deny.iter().any(|pattern| pattern.matches(key))
}
pub fn covers_whole_kb(&self) -> bool {
self.deny.is_empty() && self.allow.iter().any(|pattern| pattern.is_whole_kb())
}
pub fn is_allow_all(&self) -> bool {
self.reach == Reach::ServiceToken
&& self.deny.is_empty()
&& self.allow.iter().any(|pattern| pattern.is_whole_kb())
}
pub fn is_deny_all(&self) -> bool {
self.reach != Reach::ServiceToken
&& (self.allow.is_empty() || self.deny.iter().any(|pattern| pattern.is_whole_kb()))
}
pub fn literal_prefix_hint(&self) -> Option<&str> {
let mut shared: Option<&str> = None;
for pattern in &self.allow {
let prefix = pattern.literal_prefix()?;
match shared {
None => shared = Some(prefix),
Some(existing) if existing == prefix => {}
Some(_) => return None,
}
}
shared
}
}
pub fn signed_in_policies(
declared: &BTreeMap<String, crate::slug::KbSlug>,
) -> BTreeMap<String, std::sync::Arc<AccessPolicy>> {
declared
.keys()
.map(|slug| {
(
slug.clone(),
std::sync::Arc::new(AccessPolicy::signed_in_full()),
)
})
.collect()
}
fn config(message: impl Into<String>) -> Error {
Error::Config {
message: message.into(),
}
}
fn serde_verb(verb: Verb) -> &'static str {
match verb {
Verb::List => "list",
Verb::Read => "read",
Verb::Write => "write",
Verb::Delete => "delete",
Verb::Search => "search",
}
}