use chrono::Utc;
use serde::{Deserialize, Serialize};
use trust_tasks_rs::{
handlers::InMemoryHandler, ErrorPayload, ErrorResponse, RejectReason, TransportHandler,
TrustTask, TrustTaskCode, TypeUri,
};
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
struct AclChangeRole {
subject: String,
from_role: String,
to_role: String,
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
struct ChangeRoleResponse {
entry: AclEntry,
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
struct AclEntry {
subject: String,
role: String,
updated_at: chrono::DateTime<Utc>,
updated_by: String,
}
const CHANGING_AUTHORITY: &str = "did:web:org.example";
const MAINTAINER: &str = "did:web:maintainer.example";
const CURRENT_ROLE: &str = "member";
#[allow(clippy::result_large_err)]
fn handle(
request: TrustTask<AclChangeRole>,
handler: &impl TransportHandler,
) -> Result<TrustTask<ChangeRoleResponse>, ErrorResponse> {
let new_id = |stem: &str| format!("urn:example:{}-{}", stem, request.id);
let _resolved = handler
.resolve_parties(&request)
.map_err(|e| request.reject_with(new_id("err"), RejectReason::from(e)))?;
request
.validate_basic(Utc::now(), MAINTAINER)
.map_err(|reason| request.reject_with(new_id("err"), reason))?;
if request.payload.from_role != CURRENT_ROLE {
return Err(request.reject_with(
new_id("err"),
ErrorPayload::new(
TrustTaskCode::new_extended("acl/change-role", "stateMismatch")
.expect("a well-formed §8.5 extended code"),
)
.with_message("subject's current role does not match payload.fromRole")
.with_retryable(true)
.with_details(serde_json::json!({ "currentRole": CURRENT_ROLE })),
));
}
Ok(request.respond_with(
new_id("resp"),
ChangeRoleResponse {
entry: AclEntry {
subject: request.payload.subject.clone(),
role: request.payload.to_role.clone(),
updated_at: Utc::now(),
updated_by: CHANGING_AUTHORITY.into(),
},
},
))
}
fn main() {
let producer = InMemoryHandler::new()
.with_local(CHANGING_AUTHORITY)
.with_peer(MAINTAINER);
let consumer = InMemoryHandler::new()
.with_local(MAINTAINER)
.with_peer(CHANGING_AUTHORITY);
let change = |from: &str, to: &str| AclChangeRole {
subject: "did:web:bob.example".into(),
from_role: from.into(),
to_role: to.into(),
};
let mut request = TrustTask::new(
"req-001",
TypeUri::canonical("acl/change-role", 0, 1).unwrap(),
change("member", "moderator"),
);
request.issuer = Some(CHANGING_AUTHORITY.into());
request.recipient = Some(MAINTAINER.into());
request.issued_at = Some(Utc::now());
producer.prepare_outbound(&mut request);
println!(
"REQUEST (wire form, in-band parties stripped since transport conveys them):\n{}",
serde_json::to_string_pretty(&request).unwrap()
);
match handle(request, &consumer) {
Ok(response) => println!(
"\nSUCCESS RESPONSE:\n{}",
serde_json::to_string_pretty(&response).unwrap()
),
Err(err) => println!(
"\nERROR RESPONSE:\n{}",
serde_json::to_string_pretty(&err).unwrap()
),
}
let mut stale = TrustTask::new(
"req-002",
TypeUri::canonical("acl/change-role", 0, 1).unwrap(),
change("moderator", "admin"),
);
stale.issuer = Some(CHANGING_AUTHORITY.into());
stale.recipient = Some(MAINTAINER.into());
stale.issued_at = Some(Utc::now());
match handle(stale, &consumer) {
Ok(_) => unreachable!("a fromRole that disagrees with the ACL must be rejected"),
Err(err) => println!(
"\nERROR RESPONSE (stale state):\n{}",
serde_json::to_string_pretty(&err).unwrap()
),
}
let mut tampered = TrustTask::new(
"req-003",
TypeUri::canonical("acl/change-role", 0, 1).unwrap(),
change("member", "moderator"),
);
tampered.issuer = Some("did:web:attacker.example".into());
match handle(tampered, &consumer) {
Ok(_) => unreachable!("identity mismatch must be rejected"),
Err(err) => println!(
"\nERROR RESPONSE (identity mismatch):\n{}",
serde_json::to_string_pretty(&err).unwrap()
),
}
}