use serde::{Deserialize, Serialize};
use crate::wac::client::{ClientConditionBody, ClientConditionEvaluator};
use crate::wac::document::AclDocument;
use crate::wac::evaluator::GroupMembership;
use crate::wac::issuer::{IssuerConditionBody, IssuerConditionEvaluator};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum ConditionOutcome {
Satisfied,
NotApplicable,
Denied,
}
#[derive(Debug, Clone)]
pub enum Condition {
Client(ClientConditionBody),
Issuer(IssuerConditionBody),
Unknown {
type_iri: String,
},
}
impl Condition {
pub fn type_iri(&self) -> &str {
match self {
Condition::Client(_) => "acl:ClientCondition",
Condition::Issuer(_) => "acl:IssuerCondition",
Condition::Unknown { type_iri } => type_iri.as_str(),
}
}
}
impl Serialize for Condition {
fn serialize<S: serde::Serializer>(&self, ser: S) -> Result<S::Ok, S::Error> {
use serde::ser::SerializeMap;
match self {
Condition::Client(body) => {
let mut m = ser.serialize_map(None)?;
m.serialize_entry("@type", "acl:ClientCondition")?;
if let Some(v) = &body.client {
m.serialize_entry("acl:client", v)?;
}
if let Some(v) = &body.client_group {
m.serialize_entry("acl:clientGroup", v)?;
}
if let Some(v) = &body.client_class {
m.serialize_entry("acl:clientClass", v)?;
}
m.end()
}
Condition::Issuer(body) => {
let mut m = ser.serialize_map(None)?;
m.serialize_entry("@type", "acl:IssuerCondition")?;
if let Some(v) = &body.issuer {
m.serialize_entry("acl:issuer", v)?;
}
if let Some(v) = &body.issuer_group {
m.serialize_entry("acl:issuerGroup", v)?;
}
if let Some(v) = &body.issuer_class {
m.serialize_entry("acl:issuerClass", v)?;
}
m.end()
}
Condition::Unknown { type_iri } => {
let mut m = ser.serialize_map(Some(1))?;
m.serialize_entry("@type", type_iri)?;
m.end()
}
}
}
}
impl<'de> Deserialize<'de> for Condition {
fn deserialize<D: serde::Deserializer<'de>>(de: D) -> Result<Self, D::Error> {
let raw: serde_json::Value = Deserialize::deserialize(de)?;
let obj = raw.as_object().ok_or_else(|| {
serde::de::Error::custom("acl:condition entry must be a JSON object")
})?;
let type_iri_value = obj
.get("@type")
.ok_or_else(|| serde::de::Error::custom("acl:condition missing @type"))?;
let type_iri_str = type_iri_value.as_str().ok_or_else(|| {
serde::de::Error::custom("acl:condition @type must be a string")
})?;
let matches_client = matches!(
type_iri_str,
"acl:ClientCondition"
| "http://www.w3.org/ns/auth/acl#ClientCondition"
| "https://www.w3.org/ns/auth/acl#ClientCondition"
);
let matches_issuer = matches!(
type_iri_str,
"acl:IssuerCondition"
| "http://www.w3.org/ns/auth/acl#IssuerCondition"
| "https://www.w3.org/ns/auth/acl#IssuerCondition"
);
if matches_client {
let body =
ClientConditionBody::deserialize(raw).map_err(serde::de::Error::custom)?;
Ok(Condition::Client(body))
} else if matches_issuer {
let body =
IssuerConditionBody::deserialize(raw).map_err(serde::de::Error::custom)?;
Ok(Condition::Issuer(body))
} else {
Ok(Condition::Unknown {
type_iri: type_iri_str.to_string(),
})
}
}
}
#[derive(Debug, Clone, Copy)]
pub struct RequestContext<'a> {
pub web_id: Option<&'a str>,
pub client_id: Option<&'a str>,
pub issuer: Option<&'a str>,
}
pub trait ConditionDispatcher: Send + Sync {
fn dispatch(
&self,
cond: &Condition,
ctx: &RequestContext<'_>,
groups: &dyn GroupMembership,
) -> ConditionOutcome;
}
#[derive(Default)]
pub struct ConditionRegistry {
client_eval: Option<ClientConditionEvaluator>,
issuer_eval: Option<IssuerConditionEvaluator>,
}
impl ConditionRegistry {
pub fn new() -> Self {
Self::default()
}
pub fn with_client(mut self, e: ClientConditionEvaluator) -> Self {
self.client_eval = Some(e);
self
}
pub fn with_issuer(mut self, e: IssuerConditionEvaluator) -> Self {
self.issuer_eval = Some(e);
self
}
pub fn default_with_client_and_issuer() -> Self {
Self::new()
.with_client(ClientConditionEvaluator)
.with_issuer(IssuerConditionEvaluator)
}
pub fn supported_iris(&self) -> Vec<&'static str> {
let mut s: Vec<&'static str> = Vec::new();
if self.client_eval.is_some() {
s.push("acl:ClientCondition");
}
if self.issuer_eval.is_some() {
s.push("acl:IssuerCondition");
}
s
}
}
impl ConditionDispatcher for ConditionRegistry {
fn dispatch(
&self,
cond: &Condition,
ctx: &RequestContext<'_>,
groups: &dyn GroupMembership,
) -> ConditionOutcome {
match cond {
Condition::Client(body) => match &self.client_eval {
Some(e) => e.evaluate(body, ctx, groups),
None => ConditionOutcome::NotApplicable,
},
Condition::Issuer(body) => match &self.issuer_eval {
Some(e) => e.evaluate(body, ctx, groups),
None => ConditionOutcome::NotApplicable,
},
Condition::Unknown { .. } => ConditionOutcome::NotApplicable,
}
}
}
pub struct EmptyDispatcher;
impl ConditionDispatcher for EmptyDispatcher {
fn dispatch(
&self,
_cond: &Condition,
_ctx: &RequestContext<'_>,
_groups: &dyn GroupMembership,
) -> ConditionOutcome {
ConditionOutcome::NotApplicable
}
}
#[derive(Debug, thiserror::Error)]
#[error("unsupported acl:condition type: {iri}")]
pub struct UnsupportedCondition {
pub iri: String,
}
pub fn validate_for_write(
doc: &AclDocument,
_registry: &ConditionRegistry,
) -> Result<(), UnsupportedCondition> {
let Some(graph) = &doc.graph else {
return Ok(());
};
for auth in graph {
if let Some(conds) = &auth.condition {
for c in conds {
if let Condition::Unknown { type_iri } = c {
return Err(UnsupportedCondition {
iri: type_iri.clone(),
});
}
}
}
}
Ok(())
}
pub fn validate_acl_document(doc: &AclDocument) -> Result<(), UnsupportedCondition> {
validate_for_write(doc, &ConditionRegistry::default_with_client_and_issuer())
}