use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum SideEffectLevel {
None,
Mutating,
Destructive,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum Discloses {
None,
Metadata,
Secret,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub struct Exposure {
pub discloses: Discloses,
#[serde(rename = "actsAsSubject")]
pub acts_as_subject: bool,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub struct TaskClass {
#[serde(rename = "sideEffects")]
pub side_effects: SideEffectLevel,
pub exposure: Exposure,
}
impl TaskClass {
pub const fn new(
side_effects: SideEffectLevel,
discloses: Discloses,
acts_as_subject: bool,
) -> Self {
Self {
side_effects,
exposure: Exposure {
discloses,
acts_as_subject,
},
}
}
pub const fn floor() -> Self {
Self::new(SideEffectLevel::Mutating, Discloses::Secret, true)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PolicyRequest {
#[serde(rename = "typeUri")]
pub type_uri: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub kind: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub subject: Option<String>,
#[serde(
rename = "payloadDigest",
default,
skip_serializing_if = "Option::is_none"
)]
pub payload_digest: Option<String>,
#[serde(rename = "sideEffects")]
pub side_effects: SideEffectLevel,
pub exposure: Exposure,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Consumer {
pub did: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub kind: Option<serde_json::Value>,
#[serde(rename = "deviceId", default, skip_serializing_if = "Option::is_none")]
pub device_id: Option<String>,
#[serde(
rename = "lastUserVerificationAt",
default,
skip_serializing_if = "Option::is_none"
)]
pub last_user_verification_at: Option<String>,
#[serde(
rename = "networkClass",
default,
skip_serializing_if = "Option::is_none"
)]
pub network_class: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub acr: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub amr: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PolicyInput {
pub request: PolicyRequest,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub site: Option<serde_json::Value>,
#[serde(rename = "contextId")]
pub context_id: String,
pub consumer: Consumer,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub enum Disposition {
Allow,
Deny,
RequireStepUp,
RequireConsent,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct StepUp {
pub method: String,
#[serde(
rename = "ttlSeconds",
default,
skip_serializing_if = "Option::is_none"
)]
pub ttl_seconds: Option<u32>,
}
fn default_min_approvals() -> u32 {
1
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RequireConsent {
#[serde(rename = "approverSet")]
pub approver_set: String,
#[serde(rename = "excludeRequester", default)]
pub exclude_requester: bool,
#[serde(rename = "minApprovals", default = "default_min_approvals")]
pub min_approvals: u32,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PolicyDecision {
pub decision: Disposition,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub mode: Option<String>,
#[serde(rename = "stepUp", default, skip_serializing_if = "Option::is_none")]
pub step_up: Option<StepUp>,
#[serde(
rename = "requireConsent",
default,
skip_serializing_if = "Option::is_none"
)]
pub require_consent: Option<RequireConsent>,
#[serde(
rename = "ttlSecondsCap",
default,
skip_serializing_if = "Option::is_none"
)]
pub ttl_seconds_cap: Option<u32>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub explanation: Option<String>,
}
impl PolicyDecision {
pub fn default_deny(reason: impl Into<String>) -> Self {
Self {
decision: Disposition::Deny,
mode: None,
step_up: None,
require_consent: None,
ttl_seconds_cap: None,
explanation: Some(reason.into()),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PolicyModule {
pub id: String,
pub name: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
pub module: String,
#[serde(rename = "appliesTo", default)]
pub applies_to: Vec<String>,
#[serde(default)]
pub priority: i32,
#[serde(default = "default_enabled")]
pub enabled: bool,
#[serde(default)]
pub version: u64,
#[serde(rename = "createdAt")]
pub created_at: String,
#[serde(rename = "updatedAt")]
pub updated_at: String,
#[serde(default, skip_serializing_if = "serde_json::Value::is_null")]
pub ext: serde_json::Value,
}
fn default_enabled() -> bool {
true
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn side_effect_level_wire_casing() {
assert_eq!(
serde_json::to_value(SideEffectLevel::Destructive).unwrap(),
serde_json::json!("destructive")
);
}
#[test]
fn disposition_wire_casing() {
assert_eq!(
serde_json::to_value(Disposition::RequireConsent).unwrap(),
serde_json::json!("requireConsent")
);
assert_eq!(
serde_json::to_value(Disposition::RequireStepUp).unwrap(),
serde_json::json!("requireStepUp")
);
}
#[test]
fn policy_input_omits_absent_site_and_optionals() {
let input = PolicyInput {
request: PolicyRequest {
type_uri: "https://trusttasks.org/spec/did-management/did/delete/0.1".into(),
kind: None,
subject: Some("did:webvh:abc".into()),
payload_digest: None,
side_effects: SideEffectLevel::Destructive,
exposure: Exposure {
discloses: Discloses::None,
acts_as_subject: false,
},
},
site: None,
context_id: "ctx1".into(),
consumer: Consumer {
did: "did:key:zTest".into(),
kind: None,
device_id: Some("dev-1".into()),
last_user_verification_at: None,
network_class: None,
acr: None,
amr: vec![],
},
};
let v = serde_json::to_value(&input).unwrap();
assert!(v.get("site").is_none(), "absent site must be omitted");
assert_eq!(v["request"]["sideEffects"], "destructive");
assert_eq!(v["request"]["exposure"]["actsAsSubject"], false);
assert_eq!(v["consumer"]["deviceId"], "dev-1");
assert!(v["request"].get("payloadDigest").is_none());
}
#[test]
fn decision_round_trips_from_rego_shape() {
let j = serde_json::json!({
"decision": "requireConsent",
"requireConsent": { "approverSet": "operators", "excludeRequester": true }
});
let d: PolicyDecision = serde_json::from_value(j).unwrap();
assert_eq!(d.decision, Disposition::RequireConsent);
let rc = d.require_consent.unwrap();
assert!(rc.exclude_requester);
assert_eq!(rc.min_approvals, 1, "minApprovals defaults to 1");
}
}