use std::ops::ControlFlow;
use std::sync::Arc;
use auths_core::storage::keychain::{KeyAlias, extract_public_key_bytes};
use auths_id::keri::Event;
use auths_id::keri::delegation::{
anchor_received_dip, incept_delegated_device, list_delegated_devices, mark_agent_scope,
read_agent_scope, revoke_delegated_device,
};
use auths_id::keri::parse_did_keri;
use auths_id::keri::types::Prefix;
use auths_id::policy::{EvalContext, context_from_delegated_member};
use auths_keri::{AgentScope, DipEvent};
use auths_verifier::core::Role;
use chrono::{DateTime, Utc};
use crate::audit::emit_audit;
use crate::context::AuthsContext;
use crate::domains::org::error::OrgError;
use crate::domains::org::offboarding::{
OffboardingRecord, SignedOffboardingRecord, find_revocation_event, sign_offboarding_record,
store_offboarding_record,
};
const ROLE_MARKER_PREFIX: &str = "role:";
fn role_marker(role: Role) -> String {
format!("{ROLE_MARKER_PREFIX}{}", role.as_str())
}
fn parse_role(s: &str) -> Option<Role> {
match s {
"admin" => Some(Role::Admin),
"member" => Some(Role::Member),
"readonly" => Some(Role::Readonly),
_ => None,
}
}
fn split_role_and_caps(scope: Option<&AgentScope>) -> (Option<Role>, Vec<String>) {
let Some(scope) = scope else {
return (None, Vec::new());
};
let mut role = None;
let mut caps = Vec::new();
for cap in &scope.capabilities {
match cap.strip_prefix(ROLE_MARKER_PREFIX) {
Some(r) => role = parse_role(r),
None => caps.push(cap.clone()),
}
}
(role, caps)
}
fn collect_kel(ctx: &AuthsContext, prefix: &Prefix) -> Vec<Event> {
let mut events = Vec::new();
let _ = ctx.registry.visit_events(prefix, 0, &mut |e| {
events.push(e.clone());
ControlFlow::Continue(())
});
events
}
pub(crate) fn ensure_single_sig_org(
ctx: &AuthsContext,
org_prefix: &Prefix,
) -> Result<(), OrgError> {
let state = ctx
.registry
.get_key_state(org_prefix)
.map_err(OrgError::Storage)?;
if state.threshold.simple_value() == Some(1) {
Ok(())
} else {
Err(OrgError::OrgThresholdDelegationUnsupported {
org: org_prefix.as_str().to_string(),
})
}
}
#[derive(Debug, Clone)]
pub struct OrgMemberResult {
pub member_did: String,
pub member_prefix: String,
}
#[allow(clippy::too_many_arguments)]
pub fn add_member(
ctx: &AuthsContext,
org_prefix: &Prefix,
org_alias: &KeyAlias,
member_alias: &KeyAlias,
member_curve: auths_crypto::CurveType,
role: Role,
capabilities: &[String],
expires_at: Option<i64>,
) -> Result<OrgMemberResult, OrgError> {
ensure_single_sig_org(ctx, org_prefix)?;
if ctx.key_storage.load_key(member_alias).is_ok() {
return Err(OrgError::MemberKeyExists {
alias: member_alias.as_str().to_string(),
});
}
let (_pk, org_curve) = extract_public_key_bytes(
ctx.key_storage.as_ref(),
org_alias,
ctx.passphrase_provider.as_ref(),
)
.map_err(OrgError::CryptoError)?;
let member = incept_delegated_device(
Arc::clone(&ctx.registry),
org_prefix,
org_alias,
org_curve,
member_alias,
member_curve,
ctx.passphrase_provider.as_ref(),
ctx.key_storage.as_ref(),
)
.map_err(OrgError::Delegation)?;
let mut scope_caps = vec![role_marker(role)];
scope_caps.extend(capabilities.iter().cloned());
mark_agent_scope(
ctx.registry.as_ref(),
org_prefix,
org_alias,
org_curve,
&member.device_prefix,
&AgentScope {
capabilities: scope_caps,
expires_at,
},
ctx.passphrase_provider.as_ref(),
ctx.key_storage.as_ref(),
)
.map_err(OrgError::Delegation)?;
Ok(OrgMemberResult {
member_did: member.device_did.as_str().to_string(),
member_prefix: member.device_prefix.as_str().to_string(),
})
}
#[allow(clippy::too_many_arguments)]
pub fn add_existing_member(
ctx: &AuthsContext,
org_prefix: &Prefix,
org_alias: &KeyAlias,
member_dip: &DipEvent,
member_attachment: &[u8],
role: Role,
capabilities: &[String],
expires_at: Option<i64>,
) -> Result<OrgMemberResult, OrgError> {
ensure_single_sig_org(ctx, org_prefix)?;
let member_prefix = member_dip.i.clone();
let member_did = format!("did:keri:{}", member_prefix.as_str());
if member_dip.di.as_str() != org_prefix.as_str() {
return Err(OrgError::MemberNotDelegable {
did: member_did,
org: org_prefix.as_str().to_string(),
});
}
if resolve_member_authority(ctx, org_prefix, &member_prefix)?.is_some() {
return Ok(OrgMemberResult {
member_did,
member_prefix: member_prefix.as_str().to_string(),
});
}
let (_pk, org_curve) = extract_public_key_bytes(
ctx.key_storage.as_ref(),
org_alias,
ctx.passphrase_provider.as_ref(),
)
.map_err(OrgError::CryptoError)?;
anchor_received_dip(
ctx.registry.as_ref(),
org_prefix,
org_alias,
org_curve,
member_dip,
member_attachment,
ctx.passphrase_provider.as_ref(),
ctx.key_storage.as_ref(),
)
.map_err(OrgError::Delegation)?;
let mut scope_caps = vec![role_marker(role)];
scope_caps.extend(capabilities.iter().cloned());
mark_agent_scope(
ctx.registry.as_ref(),
org_prefix,
org_alias,
org_curve,
&member_prefix,
&AgentScope {
capabilities: scope_caps,
expires_at,
},
ctx.passphrase_provider.as_ref(),
ctx.key_storage.as_ref(),
)
.map_err(OrgError::Delegation)?;
Ok(OrgMemberResult {
member_did,
member_prefix: member_prefix.as_str().to_string(),
})
}
pub fn revoke_member(
ctx: &AuthsContext,
org_prefix: &Prefix,
org_alias: &KeyAlias,
member_did: &str,
reason: Option<String>,
) -> Result<Option<SignedOffboardingRecord>, OrgError> {
let member_prefix = parse_did_keri(member_did).map_err(|_| OrgError::MemberNotFound {
org: org_prefix.as_str().to_string(),
did: member_did.to_string(),
})?;
let authority =
resolve_member_authority(ctx, org_prefix, &member_prefix)?.ok_or_else(|| {
OrgError::MemberNotFound {
org: org_prefix.as_str().to_string(),
did: member_did.to_string(),
}
})?;
if authority.revoked {
return Ok(None);
}
let (_pk, org_curve) = extract_public_key_bytes(
ctx.key_storage.as_ref(),
org_alias,
ctx.passphrase_provider.as_ref(),
)
.map_err(OrgError::CryptoError)?;
revoke_delegated_device(
ctx.registry.as_ref(),
org_prefix,
org_alias,
org_curve,
&member_prefix,
ctx.passphrase_provider.as_ref(),
ctx.key_storage.as_ref(),
)
.map_err(OrgError::Delegation)?;
let org_kel = collect_kel(ctx, org_prefix);
let (revocation_seal_said, revoked_at_seq) = find_revocation_event(&org_kel, &member_prefix)
.ok_or_else(|| {
OrgError::Signing("revocation event not found after anchoring".to_string())
})?;
let org_did = format!("did:keri:{}", org_prefix.as_str());
let record = OffboardingRecord {
org_did: org_did.clone(),
member_did: member_did.to_string(),
revoked_at_seq,
revocation_seal_said,
reason,
operator_did: org_did,
prior_role: authority.role.map(|r| r.as_str().to_string()),
prior_caps: authority.capabilities,
recorded_at: ctx.clock.now().to_rfc3339(),
};
let signed = sign_offboarding_record(ctx, org_alias, org_curve, record)?;
store_offboarding_record(ctx, org_prefix, &member_prefix, &signed)?;
emit_audit(
ctx,
&signed.record.operator_did,
"org:offboard-member",
"Success",
);
Ok(Some(signed))
}
#[derive(Debug, Clone)]
pub struct OrgMemberAuthority {
pub member_did: String,
pub member_prefix: String,
pub delegated_by_org: String,
pub revoked: bool,
pub role: Option<Role>,
pub capabilities: Vec<String>,
pub expires_at: Option<i64>,
}
pub fn resolve_member_authority(
ctx: &AuthsContext,
org_prefix: &Prefix,
member_prefix: &Prefix,
) -> Result<Option<OrgMemberAuthority>, OrgError> {
let delegated =
list_delegated_devices(ctx.registry.as_ref(), org_prefix).map_err(OrgError::Delegation)?;
let Some(info) = delegated
.into_iter()
.find(|d| d.device_prefix.as_str() == member_prefix.as_str())
else {
return Ok(None);
};
let org_kel = collect_kel(ctx, org_prefix);
let scope = read_agent_scope(&org_kel, member_prefix);
let (role, capabilities) = split_role_and_caps(scope.as_ref());
Ok(Some(OrgMemberAuthority {
member_did: format!("did:keri:{}", member_prefix.as_str()),
member_prefix: member_prefix.as_str().to_string(),
delegated_by_org: format!("did:keri:{}", org_prefix.as_str()),
revoked: info.revoked,
role,
capabilities,
expires_at: scope.and_then(|s| s.expires_at),
}))
}
pub fn list_members(
ctx: &AuthsContext,
org_prefix: &Prefix,
) -> Result<Vec<OrgMemberAuthority>, OrgError> {
let delegated =
list_delegated_devices(ctx.registry.as_ref(), org_prefix).map_err(OrgError::Delegation)?;
let org_kel = collect_kel(ctx, org_prefix);
let org_did = format!("did:keri:{}", org_prefix.as_str());
Ok(delegated
.into_iter()
.map(|info| {
let scope = read_agent_scope(&org_kel, &info.device_prefix);
let (role, capabilities) = split_role_and_caps(scope.as_ref());
OrgMemberAuthority {
member_did: format!("did:keri:{}", info.device_prefix.as_str()),
member_prefix: info.device_prefix.as_str().to_string(),
delegated_by_org: org_did.clone(),
revoked: info.revoked,
role,
capabilities,
expires_at: scope.and_then(|s| s.expires_at),
}
})
.collect())
}
pub fn member_policy_context(
ctx: &AuthsContext,
org_prefix: &Prefix,
member_prefix: &Prefix,
now: DateTime<Utc>,
) -> Result<EvalContext, OrgError> {
let org_did = format!("did:keri:{}", org_prefix.as_str());
let member_did = format!("did:keri:{}", member_prefix.as_str());
match resolve_member_authority(ctx, org_prefix, member_prefix)? {
Some(auth) => {
let expires_at = auth.expires_at.and_then(|s| DateTime::from_timestamp(s, 0));
context_from_delegated_member(
&org_did,
&member_did,
auth.revoked,
auth.role.as_ref().map(Role::as_str),
&auth.capabilities,
expires_at,
now,
)
.map_err(|e| OrgError::InvalidDid(e.to_string()))
}
None => EvalContext::try_from_strings(now, &org_did, &member_did)
.map(|c| c.revoked(true))
.map_err(|e| OrgError::InvalidDid(e.to_string())),
}
}