auths_sdk/workflows/
org.rs1use std::ops::ControlFlow;
4
5use auths_core::ports::clock::ClockProvider;
6use auths_core::ports::id::UuidProvider;
7use auths_id::ports::registry::RegistryBackend;
8use auths_verifier::Capability;
9use auths_verifier::core::ResourceId;
10pub use auths_verifier::core::Role;
11use auths_verifier::core::{Attestation, Ed25519PublicKey, Ed25519Signature};
12use auths_verifier::types::{DeviceDID, IdentityDID};
13
14use crate::error::OrgError;
15
16pub fn member_role_order(role: &Option<Role>) -> u8 {
26 match role {
27 Some(Role::Admin) => 0,
28 Some(Role::Member) => 1,
29 Some(Role::Readonly) => 2,
30 None => 3,
31 }
32}
33
34pub(crate) fn find_admin(
50 backend: &dyn RegistryBackend,
51 org_prefix: &str,
52 public_key_hex: &str,
53) -> Result<Attestation, OrgError> {
54 let signer_bytes = hex::decode(public_key_hex)
55 .map_err(|e| OrgError::InvalidPublicKey(format!("hex decode failed: {e}")))?;
56
57 let mut found: Option<Attestation> = None;
58
59 backend
60 .visit_org_member_attestations(org_prefix, &mut |entry| {
61 if let Ok(att) = &entry.attestation
62 && att.device_public_key.as_bytes().as_slice() == signer_bytes.as_slice()
63 && !att.is_revoked()
64 && att.capabilities.contains(&Capability::manage_members())
65 {
66 found = Some(att.clone());
67 return ControlFlow::Break(());
68 }
69 ControlFlow::Continue(())
70 })
71 .map_err(|e| OrgError::Storage(e.to_string()))?;
72
73 found.ok_or_else(|| OrgError::AdminNotFound {
74 org: org_prefix.to_owned(),
75 })
76}
77
78pub(crate) fn find_member(
92 backend: &dyn RegistryBackend,
93 org_prefix: &str,
94 member_did: &str,
95) -> Result<Option<Attestation>, OrgError> {
96 let mut found: Option<Attestation> = None;
97
98 backend
99 .visit_org_member_attestations(org_prefix, &mut |entry| {
100 if entry.did.to_string() == member_did
101 && let Ok(att) = &entry.attestation
102 {
103 found = Some(att.clone());
104 return ControlFlow::Break(());
105 }
106 ControlFlow::Continue(())
107 })
108 .map_err(|e| OrgError::Storage(e.to_string()))?;
109
110 Ok(found)
111}
112
113fn parse_capabilities(raw: &[String]) -> Result<Vec<Capability>, OrgError> {
116 raw.iter()
117 .map(|s| {
118 Capability::try_from(s.clone()).map_err(|e| OrgError::InvalidCapability {
119 cap: s.clone(),
120 reason: e.to_string(),
121 })
122 })
123 .collect()
124}
125
126pub struct AddMemberCommand {
130 pub org_prefix: String,
132 pub member_did: String,
134 pub role: Role,
136 pub capabilities: Vec<String>,
138 pub public_key_hex: String,
140}
141
142pub struct RevokeMemberCommand {
144 pub org_prefix: String,
146 pub member_did: String,
148 pub public_key_hex: String,
150}
151
152pub struct UpdateCapabilitiesCommand {
154 pub org_prefix: String,
156 pub member_did: String,
158 pub capabilities: Vec<String>,
160 pub public_key_hex: String,
162}
163
164pub fn add_organization_member(
183 backend: &dyn RegistryBackend,
184 clock: &dyn ClockProvider,
185 id_provider: &dyn UuidProvider,
186 cmd: AddMemberCommand,
187) -> Result<Attestation, OrgError> {
188 let admin_att = find_admin(backend, &cmd.org_prefix, &cmd.public_key_hex)?;
189 let parsed_caps = parse_capabilities(&cmd.capabilities)?;
190
191 let member = Attestation {
192 version: 1,
193 rid: ResourceId::new(id_provider.new_id().to_string()),
194 issuer: admin_att.issuer.clone(),
195 subject: DeviceDID::new(&cmd.member_did),
196 device_public_key: Ed25519PublicKey::from_bytes([0u8; 32]),
197 identity_signature: Ed25519Signature::empty(),
198 device_signature: Ed25519Signature::empty(),
199 revoked_at: None,
200 expires_at: None,
201 timestamp: Some(clock.now()),
202 note: None,
203 payload: None,
204 role: Some(cmd.role),
205 capabilities: parsed_caps,
206 delegated_by: Some(IdentityDID::new(admin_att.subject.to_string())),
207 signer_type: None,
208 };
209
210 backend
211 .store_org_member(&cmd.org_prefix, &member)
212 .map_err(|e| OrgError::Storage(e.to_string()))?;
213
214 Ok(member)
215}
216
217pub fn revoke_organization_member(
232 backend: &dyn RegistryBackend,
233 clock: &dyn ClockProvider,
234 cmd: RevokeMemberCommand,
235) -> Result<Attestation, OrgError> {
236 find_admin(backend, &cmd.org_prefix, &cmd.public_key_hex)?;
237
238 let existing = find_member(backend, &cmd.org_prefix, &cmd.member_did)?.ok_or_else(|| {
239 OrgError::MemberNotFound {
240 org: cmd.org_prefix.clone(),
241 did: cmd.member_did.clone(),
242 }
243 })?;
244
245 if existing.is_revoked() {
246 return Err(OrgError::AlreadyRevoked {
247 did: cmd.member_did.clone(),
248 });
249 }
250
251 let now = clock.now();
252 let mut revoked = existing;
253 revoked.revoked_at = Some(now);
254 revoked.timestamp = Some(now);
255 revoked.note = Some("Revoked by admin".to_owned());
256
257 backend
258 .store_org_member(&cmd.org_prefix, &revoked)
259 .map_err(|e| OrgError::Storage(e.to_string()))?;
260
261 Ok(revoked)
262}
263
264pub fn update_member_capabilities(
279 backend: &dyn RegistryBackend,
280 clock: &dyn ClockProvider,
281 cmd: UpdateCapabilitiesCommand,
282) -> Result<Attestation, OrgError> {
283 find_admin(backend, &cmd.org_prefix, &cmd.public_key_hex)?;
284
285 let existing = find_member(backend, &cmd.org_prefix, &cmd.member_did)?.ok_or_else(|| {
286 OrgError::MemberNotFound {
287 org: cmd.org_prefix.clone(),
288 did: cmd.member_did.clone(),
289 }
290 })?;
291
292 if existing.is_revoked() {
293 return Err(OrgError::AlreadyRevoked {
294 did: cmd.member_did.clone(),
295 });
296 }
297
298 let parsed_caps = parse_capabilities(&cmd.capabilities)?;
299 let mut updated = existing;
300 updated.capabilities = parsed_caps;
301 updated.timestamp = Some(clock.now());
302
303 backend
304 .store_org_member(&cmd.org_prefix, &updated)
305 .map_err(|e| OrgError::Storage(e.to_string()))?;
306
307 Ok(updated)
308}