use serde_json::{Map, Value};
use sha2::{Digest, Sha256};
use super::{Policy, Refusal, Request};
use crate::record::procedure_org;
#[derive(Debug, PartialEq, Eq)]
enum Grant<'a> {
Realm(&'a str),
Org(&'a str, &'a str),
Proc(&'a str, &'a str),
}
impl<'a> Grant<'a> {
fn realm(&self) -> &'a str {
match self {
Grant::Realm(realm) | Grant::Org(realm, _) | Grant::Proc(realm, _) => realm,
}
}
}
fn grant(with: &str) -> Result<Grant<'_>, Refusal> {
let canonical = |realm| match canonical_realm_name(realm) {
true => Ok(()),
false => Err(Refusal::RealmNameNotCanonical),
};
if let Some(realm) = with.strip_prefix("mri:realm:") {
canonical(realm)?;
return Ok(Grant::Realm(realm));
}
if let Some(rest) = with.strip_prefix("mri:org:") {
let (realm, org) = rest.split_once('/').ok_or(Refusal::Malformed)?;
if org.is_empty() {
return Err(Refusal::Malformed);
}
canonical(realm)?;
return Ok(Grant::Org(realm, org));
}
if let Some(rest) = with.strip_prefix("mri:proc:") {
let (realm, procedure) = rest.split_once('/').ok_or(Refusal::Malformed)?;
canonical(realm)?;
org_of(procedure)?;
return Ok(Grant::Proc(realm, procedure));
}
Err(Refusal::Malformed)
}
fn org_of(procedure: &str) -> Result<&str, Refusal> {
match procedure_org(procedure) {
Ok(Some(org)) => Ok(org),
_ => Err(Refusal::ProcedureWithoutOrg),
}
}
pub fn covers(parent: &str, child: &str) -> bool {
let (Ok(p), Ok(c)) = (grant(parent), grant(child)) else {
return false;
};
match (p, c) {
(Grant::Realm(realm), c) => c.realm() == realm,
(Grant::Org(realm, org), Grant::Org(c_realm, c_org)) => c_realm == realm && c_org == org,
(Grant::Org(realm, org), Grant::Proc(c_realm, procedure)) => {
c_realm == realm && org_of(procedure) == Ok(org)
}
(Grant::Proc(realm, procedure), Grant::Proc(c_realm, c_procedure)) => {
c_realm == realm && c_procedure == procedure
}
_ => false,
}
}
fn canonical_realm_name(name: &str) -> bool {
!name.is_empty()
&& name.split('.').all(|segment| {
!segment.is_empty()
&& segment
.bytes()
.all(|b| b.is_ascii_lowercase() || b.is_ascii_digit() || b == b'-' || b == b'_')
})
}
fn realm_id(realm: &str) -> [u8; 32] {
Sha256::digest(realm.as_bytes()).into()
}
fn with_and_can(entry: &Value) -> Option<(&str, &Value)> {
let cap = entry.as_object()?;
Some((cap.get("with")?.as_str()?, cap.get("can")?))
}
pub(super) fn grants(
p: &Policy,
request: Option<&Request>,
claims: &Map<String, Value>,
) -> Result<Vec<Value>, Refusal> {
let caps = claims
.get("cap")
.and_then(Value::as_array)
.map(Vec::as_slice)
.unwrap_or(&[]);
if let Some(r) = request {
return requested(p, r, caps);
}
match p {
Policy::UcanRequired { .. } => Ok(caps.to_vec()),
Policy::RealmMemberRequired { can, .. } => caps
.iter()
.find(|entry| {
entry.as_object().and_then(|c| c.get("can")) == Some(&Value::String(can.clone()))
})
.map(|cap| vec![cap.clone()])
.ok_or_else(|| why_not_granted(caps, None)),
}
}
fn requested(p: &Policy, r: &Request, caps: &[Value]) -> Result<Vec<Value>, Refusal> {
org_of(&r.procedure)?;
let answers = |entry: &Value| {
let Some((with, can)) = with_and_can(entry) else {
return false;
};
let can_matches = match p {
Policy::RealmMemberRequired { can: wanted, .. } => {
can == &Value::String(wanted.clone())
}
Policy::UcanRequired { .. } => true,
};
let Ok(g) = grant(with) else {
return false;
};
can_matches
&& realm_id(g.realm()) == r.realm
&& covers(with, &format!("mri:proc:{}/{}", g.realm(), r.procedure))
};
caps.iter()
.find(|entry| answers(entry))
.map(|cap| vec![cap.clone()])
.ok_or_else(|| why_not_granted(caps, Some(&r.realm)))
}
fn why_not_granted(caps: &[Value], realm: Option<&[u8; 32]>) -> Refusal {
let first = caps
.iter()
.find_map(|entry| entry.as_object()?.get("with")?.as_str());
let Some(with) = first else {
return Refusal::MissingCapability;
};
match (grant(with), realm) {
(Err(Refusal::Malformed), _) => Refusal::MissingCapability,
(Err(refusal), _) => refusal,
(Ok(_), None) => Refusal::MissingCapability,
(Ok(g), Some(realm)) if realm_id(g.realm()) == *realm => Refusal::MissingCapability,
(Ok(_), Some(_)) => Refusal::WrongRealm,
}
}
pub(super) fn narrowed(parent_caps: &[Value], children: &[Value]) -> Result<Vec<Value>, Refusal> {
children
.iter()
.map(|child| covering(parent_caps, child))
.collect()
}
fn covering(caps: &[Value], child: &Value) -> Result<Value, Refusal> {
let child_cap = child.as_object().ok_or(Refusal::Malformed)?;
let (Some(child_with), Some(child_can)) = (child_cap.get("with"), child_cap.get("can")) else {
return Err(Refusal::Malformed);
};
let found = caps
.iter()
.find(|entry| match (with_and_can(entry), child_with.as_str()) {
(Some((with, can)), Some(child_with)) => can == child_can && covers(with, child_with),
_ => false,
});
found
.cloned()
.ok_or_else(|| why_not_narrowed(caps, child_with))
}
fn why_not_narrowed(caps: &[Value], child_with: &Value) -> Refusal {
let Some((with, _)) = caps.first().and_then(with_and_can) else {
return Refusal::GrantsMoreThanProof;
};
match child_with.as_str() {
Some(child) if same_realm(with, child) && covers(with, child) => Refusal::CanChanged,
Some(child) if same_realm(with, child) => Refusal::GrantsMoreThanProof,
_ => Refusal::WrongRealm,
}
}
fn same_realm(with: &str, other: &str) -> bool {
matches!((grant(with), grant(other)), (Ok(a), Ok(b)) if a.realm() == b.realm())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_realm_name_is_dotted_lowercase_segments() {
for good in ["io.macula", "a", "a-b_c.0"] {
assert!(canonical_realm_name(good), "{good}");
}
for bad in [
"",
".",
"io..macula",
"io.macula.",
"IO.macula",
"io macula",
"é",
] {
assert!(!canonical_realm_name(bad), "{bad}");
}
}
#[test]
fn a_with_is_one_of_three_mris() {
assert_eq!(grant("mri:realm:io.macula"), Ok(Grant::Realm("io.macula")));
assert_eq!(
grant("mri:org:io.macula/acme"),
Ok(Grant::Org("io.macula", "acme"))
);
assert_eq!(
grant("mri:proc:io.macula/acme/count_v1"),
Ok(Grant::Proc("io.macula", "acme/count_v1"))
);
assert_eq!(grant("mri:org:io.macula/"), Err(Refusal::Malformed));
assert_eq!(grant("mri:org:io.macula"), Err(Refusal::Malformed));
assert_eq!(grant("mri:realm:IO"), Err(Refusal::RealmNameNotCanonical));
assert_eq!(
grant("mri:proc:io.macula/count"),
Err(Refusal::ProcedureWithoutOrg)
);
assert_eq!(grant("https://x"), Err(Refusal::Malformed));
}
}