use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use crate::acl::ApproveScope;
use super::create::CreateAclResultBody;
use super::entry::{Approve, StepUp};
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
pub struct UpdateAclBody {
#[serde(rename = "subject")]
pub did: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub label: Option<String>,
#[serde(rename = "scopes", default, skip_serializing_if = "Option::is_none")]
pub allowed_contexts: Option<Vec<String>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub expires_at: Option<DateTime<Utc>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub reason: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub step_up: Option<StepUp>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub approve: Option<Approve>,
#[serde(
rename = "allowedKeys",
default,
deserialize_with = "double_option",
skip_serializing_if = "Option::is_none"
)]
pub allowed_keys: Option<Option<Vec<String>>>,
}
fn double_option<'de, T, D>(de: D) -> Result<Option<Option<T>>, D::Error>
where
T: Deserialize<'de>,
D: serde::Deserializer<'de>,
{
Deserialize::deserialize(de).map(Some)
}
impl UpdateAclBody {
pub fn step_up_approver(&self) -> Option<String> {
self.step_up.as_ref().and_then(|s| s.approver.clone())
}
pub fn step_up_require(&self) -> Option<String> {
self.step_up.as_ref().and_then(|s| s.require.clone())
}
pub fn approve_scope(&self) -> Option<ApproveScope> {
self.approve.as_ref().map(Approve::to_scope)
}
}
pub type UpdateAclResultBody = CreateAclResultBody;
#[cfg(test)]
mod tests {
use super::*;
fn body(json: serde_json::Value) -> UpdateAclBody {
serde_json::from_value(json).unwrap()
}
#[test]
fn allowed_keys_round_trips_as_camel_case() {
let b = UpdateAclBody {
did: "did:key:zA".into(),
label: None,
allowed_contexts: None,
expires_at: None,
reason: None,
step_up: None,
approve: None,
allowed_keys: Some(Some(vec!["key-1".into(), "key-2".into()])),
};
let v = serde_json::to_value(&b).unwrap();
assert!(v.get("allowedKeys").is_some(), "{v}");
assert!(v.get("allowed_keys").is_none(), "one spelling only: {v}");
let back: UpdateAclBody = serde_json::from_value(v).unwrap();
assert_eq!(
back.allowed_keys,
Some(Some(vec!["key-1".to_string(), "key-2".to_string()]))
);
let b = body(serde_json::json!({
"subject": "did:key:zA", "allowed_keys": ["key-1"]
}));
assert_eq!(b.allowed_keys, None);
}
#[test]
fn allowed_keys_distinguishes_absent_null_and_empty() {
let absent = body(serde_json::json!({ "subject": "did:key:zA" }));
assert_eq!(absent.allowed_keys, None, "omitted = leave unchanged");
let null = body(serde_json::json!({ "subject": "did:key:zA", "allowedKeys": null }));
assert_eq!(null.allowed_keys, Some(None), "null = clear the filter");
let empty = body(serde_json::json!({ "subject": "did:key:zA", "allowedKeys": [] }));
assert_eq!(
empty.allowed_keys,
Some(Some(vec![])),
"empty array = authorized on NO keys — not a wildcard, not a clear"
);
}
#[test]
fn allowed_keys_clear_serializes_as_explicit_null() {
let b = UpdateAclBody {
did: "did:key:zA".into(),
label: None,
allowed_contexts: None,
expires_at: None,
reason: None,
step_up: None,
approve: None,
allowed_keys: Some(None),
};
let v = serde_json::to_value(&b).unwrap();
assert!(v.get("allowedKeys").is_some_and(|k| k.is_null()), "{v}");
let untouched = UpdateAclBody {
allowed_keys: None,
..b
};
let v = serde_json::to_value(&untouched).unwrap();
assert!(v.get("allowedKeys").is_none(), "omitted stays omitted: {v}");
}
}