use std::collections::BTreeMap;
use serde::{Deserialize, Deserializer, Serialize};
use crate::error::{ActError, ActResult};
use crate::scope::Scope;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Confirmation {
pub key_fingerprint: String,
pub alg: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Delegation {
pub allow_delegation: bool,
pub max_depth: u8,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub delegatable_scopes: Option<Vec<Scope>>,
pub allow_any_delegate: bool,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub allowed_delegates: Vec<String>,
#[serde(default)]
pub min_ttl_reduction_seconds: i64,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ActClaims {
pub jti: String,
pub sub: String,
pub iss: String,
#[serde(deserialize_with = "audience_from_string_or_array")]
pub aud: Vec<String>,
pub iat: i64,
#[serde(default)]
pub nbf: i64,
pub exp: i64,
pub tenant_id: String,
pub scope: Vec<Scope>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub cnf: Option<Confirmation>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub delegation: Option<Delegation>,
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
pub ext: BTreeMap<String, ciborium::Value>,
}
fn audience_from_string_or_array<'de, D>(deserializer: D) -> Result<Vec<String>, D::Error>
where
D: Deserializer<'de>,
{
#[derive(Deserialize)]
#[serde(untagged)]
enum OneOrMany {
One(String),
Many(Vec<String>),
}
match OneOrMany::deserialize(deserializer)? {
OneOrMany::One(single) => Ok(vec![single]),
OneOrMany::Many(many) => Ok(many),
}
}
impl ActClaims {
pub fn validate_structure(&self) -> ActResult<()> {
for (name, value) in [
("jti", &self.jti),
("sub", &self.sub),
("iss", &self.iss),
("tenant_id", &self.tenant_id),
] {
if value.trim().is_empty() {
return Err(ActError::EmptyClaim(name));
}
}
if self.aud.is_empty() || self.aud.iter().any(|a| a.trim().is_empty()) {
return Err(ActError::EmptyClaim("aud"));
}
if self.nbf >= self.exp {
return Err(ActError::EmptyValidityWindow {
nbf: self.nbf,
exp: self.exp,
});
}
Ok(())
}
#[must_use]
pub fn authorizes(&self, requested: &Scope) -> bool {
self.scope.iter().any(|granted| granted.covers(requested))
}
#[must_use]
pub fn has_audience(&self, audience: &str) -> bool {
self.aud.iter().any(|a| a == audience)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn claims() -> ActClaims {
ActClaims {
jti: "018f4c1e-0000-7000-8000-000000000000".to_string(),
sub: "did:oas:l1fe:agent:alpha".to_string(),
iss: "arsenal:broker:test-1".to_string(),
aud: vec!["omerta".to_string()],
iat: 1_000,
nbf: 1_000,
exp: 2_000,
tenant_id: "acme".to_string(),
scope: vec![],
cnf: None,
delegation: None,
ext: BTreeMap::new(),
}
}
#[test]
fn rejects_blank_required_claims() {
for mutate in [
(|c: &mut ActClaims| c.jti.clear()) as fn(&mut ActClaims),
|c: &mut ActClaims| c.sub.clear(),
|c: &mut ActClaims| c.iss.clear(),
|c: &mut ActClaims| c.tenant_id.clear(),
|c: &mut ActClaims| c.aud.clear(),
] {
let mut subject = claims();
mutate(&mut subject);
assert!(subject.validate_structure().is_err());
}
}
#[test]
fn rejects_whitespace_only_claims() {
let mut subject = claims();
subject.iss = " ".to_string();
assert!(matches!(
subject.validate_structure(),
Err(ActError::EmptyClaim("iss"))
));
}
#[test]
fn rejects_empty_validity_window() {
let mut subject = claims();
subject.nbf = subject.exp;
assert!(matches!(
subject.validate_structure(),
Err(ActError::EmptyValidityWindow { .. })
));
}
#[test]
fn accepts_audience_as_bare_string_or_array() -> ActResult<()> {
#[derive(Serialize)]
struct BareAud {
aud: &'static str,
}
#[derive(Deserialize)]
struct Probe {
#[serde(deserialize_with = "audience_from_string_or_array")]
aud: Vec<String>,
}
let mut buf = Vec::new();
ciborium::into_writer(&BareAud { aud: "omerta" }, &mut buf)
.map_err(|e| ActError::Encode(e.to_string()))?;
let probe: Probe = ciborium::from_reader(buf.as_slice())
.map_err(|e| ActError::ClaimsDecode(e.to_string()))?;
assert_eq!(probe.aud, vec!["omerta".to_string()]);
let mut buf = Vec::new();
ciborium::into_writer(&BareAud { aud: "omerta" }, &mut buf)
.map_err(|e| ActError::Encode(e.to_string()))?;
assert!(!buf.is_empty());
Ok(())
}
#[test]
fn timestamps_encode_as_integers() -> ActResult<()> {
#[derive(Deserialize)]
struct Probe {
iat: ciborium::Value,
exp: ciborium::Value,
}
let mut buf = Vec::new();
ciborium::into_writer(&claims(), &mut buf).map_err(|e| ActError::Encode(e.to_string()))?;
let probe: Probe = ciborium::from_reader(buf.as_slice())
.map_err(|e| ActError::ClaimsDecode(e.to_string()))?;
assert!(probe.iat.is_integer(), "iat must be an integer");
assert!(probe.exp.is_integer(), "exp must be an integer");
Ok(())
}
#[test]
fn identifiers_encode_as_text() -> ActResult<()> {
#[derive(Deserialize)]
struct Probe {
jti: ciborium::Value,
sub: ciborium::Value,
}
let mut buf = Vec::new();
ciborium::into_writer(&claims(), &mut buf).map_err(|e| ActError::Encode(e.to_string()))?;
let probe: Probe = ciborium::from_reader(buf.as_slice())
.map_err(|e| ActError::ClaimsDecode(e.to_string()))?;
assert!(probe.jti.is_text(), "jti must be text, not UUID bytes");
assert!(probe.sub.is_text(), "sub must be text");
Ok(())
}
#[test]
fn unknown_top_level_keys_are_ignored() -> ActResult<()> {
let mut map = vec![
(
ciborium::Value::Text("jti".into()),
ciborium::Value::Text("id".into()),
),
(
ciborium::Value::Text("sub".into()),
ciborium::Value::Text("did:oas:l1fe:agent:alpha".into()),
),
(
ciborium::Value::Text("iss".into()),
ciborium::Value::Text("issuer".into()),
),
(
ciborium::Value::Text("aud".into()),
ciborium::Value::Text("omerta".into()),
),
(
ciborium::Value::Text("iat".into()),
ciborium::Value::from(1),
),
(
ciborium::Value::Text("nbf".into()),
ciborium::Value::from(1),
),
(
ciborium::Value::Text("exp".into()),
ciborium::Value::from(2),
),
(
ciborium::Value::Text("tenant_id".into()),
ciborium::Value::Text("acme".into()),
),
(
ciborium::Value::Text("scope".into()),
ciborium::Value::Array(vec![]),
),
];
map.push((
ciborium::Value::Text("claim_from_the_future".into()),
ciborium::Value::from(42),
));
let mut buf = Vec::new();
ciborium::into_writer(&ciborium::Value::Map(map), &mut buf)
.map_err(|e| ActError::Encode(e.to_string()))?;
let decoded: ActClaims = ciborium::from_reader(buf.as_slice())
.map_err(|e| ActError::ClaimsDecode(e.to_string()))?;
assert_eq!(decoded.tenant_id, "acme");
Ok(())
}
}