1use anyhow::{Context, Result, anyhow};
2use auths_core::crypto::signer::decrypt_keypair;
3use auths_id::attestation::create::create_signed_attestation;
4use auths_id::attestation::revoke::create_signed_revocation;
5use auths_id::identity::initialize::initialize_registry_identity;
6use auths_id::identity::resolve::DidResolver;
7use chrono::{DateTime, Utc};
8use clap::{ArgAction, Parser, Subcommand};
9use serde_json;
10use std::fs;
11use std::path::PathBuf;
12use std::sync::Arc;
13
14use auths_core::signing::{PassphraseProvider, StorageSigner};
15use auths_core::storage::keychain::{KeyAlias, get_platform_keychain};
16use auths_id::{
17 attestation::{export::AttestationSink, group::AttestationGroup, verify::verify_with_resolver},
18 identity::resolve::DefaultDidResolver,
19 storage::git_refs::AttestationMetadata,
20 storage::{
21 attestation::AttestationSource,
22 identity::IdentityStorage,
23 layout::{self, StorageLayoutConfig},
24 },
25};
26
27use auths_sdk::workflows::org::{Role, member_role_order};
28use auths_storage::git::{
29 GitRegistryBackend, RegistryAttestationStorage, RegistryConfig, RegistryIdentityStorage,
30};
31use auths_verifier::types::DeviceDID;
32use auths_verifier::{Capability, Ed25519PublicKey, Prefix};
33
34use clap::ValueEnum;
35
36#[derive(Debug, Clone, Copy, ValueEnum, PartialEq, Eq)]
40pub enum CliRole {
41 Admin,
42 Member,
43 Readonly,
44}
45
46impl From<CliRole> for Role {
47 fn from(r: CliRole) -> Self {
48 match r {
49 CliRole::Admin => Role::Admin,
50 CliRole::Member => Role::Member,
51 CliRole::Readonly => Role::Readonly,
52 }
53 }
54}
55
56#[derive(Parser, Debug, Clone)]
58pub struct OrgCommand {
59 #[clap(subcommand)]
60 pub subcommand: OrgSubcommand,
61
62 #[command(flatten)]
63 pub overrides: crate::commands::registry_overrides::RegistryOverrides,
64}
65
66#[derive(Subcommand, Debug, Clone)]
68pub enum OrgSubcommand {
69 Init {
71 #[arg(long)]
73 name: String,
74
75 #[arg(long)]
77 local_key_alias: Option<String>,
78
79 #[arg(long)]
81 metadata_file: Option<PathBuf>,
82 },
83 Attest {
84 #[arg(long)]
85 subject: String,
86 #[arg(long)]
87 payload_file: PathBuf,
88 #[arg(long)]
89 note: Option<String>,
90 #[arg(long)]
91 expires_at: Option<String>,
92 #[arg(long)]
93 signer_alias: Option<String>,
94 },
95 Revoke {
96 #[arg(long)]
97 subject: String,
98 #[arg(long)]
99 note: Option<String>,
100 #[arg(long)]
101 signer_alias: Option<String>,
102 },
103 Show {
104 #[arg(long)]
105 subject: String,
106 #[arg(long, action = ArgAction::SetTrue)]
107 include_revoked: bool,
108 },
109 List {
110 #[arg(long, action = ArgAction::SetTrue)]
111 include_revoked: bool,
112 },
113 AddMember {
115 #[arg(long)]
117 org: String,
118
119 #[arg(long)]
121 member: String,
122
123 #[arg(long, value_enum)]
125 role: CliRole,
126
127 #[arg(long, value_delimiter = ',')]
129 capabilities: Option<Vec<String>>,
130
131 #[arg(long)]
133 signer_alias: Option<String>,
134
135 #[arg(long)]
137 note: Option<String>,
138 },
139
140 RevokeMember {
142 #[arg(long)]
144 org: String,
145
146 #[arg(long)]
148 member: String,
149
150 #[arg(long)]
152 note: Option<String>,
153
154 #[arg(long)]
156 signer_alias: Option<String>,
157 },
158
159 ListMembers {
161 #[arg(long)]
163 org: String,
164
165 #[arg(long, action = ArgAction::SetTrue)]
167 include_revoked: bool,
168 },
169}
170
171pub fn handle_org(
173 cmd: OrgCommand,
174 repo_opt: Option<PathBuf>,
175 identity_ref_override: Option<String>,
176 identity_blob_name_override: Option<String>,
177 attestation_prefix_override: Option<String>,
178 attestation_blob_name_override: Option<String>,
179 passphrase_provider: Arc<dyn PassphraseProvider + Send + Sync>,
180) -> Result<()> {
181 let repo_path = layout::resolve_repo_path(repo_opt)?;
182
183 let mut config = StorageLayoutConfig::default();
184 if let Some(r) = identity_ref_override {
185 config.identity_ref = r.into();
186 }
187 if let Some(b) = identity_blob_name_override {
188 config.identity_blob_name = b.into();
189 }
190 if let Some(p) = attestation_prefix_override {
191 config.device_attestation_prefix = p.into();
192 }
193 if let Some(b) = attestation_blob_name_override {
194 config.attestation_blob_name = b.into();
195 }
196
197 let _attestation_storage = RegistryAttestationStorage::new(repo_path.clone());
198 let resolver: DefaultDidResolver = DefaultDidResolver::with_repo(&repo_path);
199
200 match cmd.subcommand {
201 OrgSubcommand::Init {
202 name,
203 local_key_alias,
204 metadata_file,
205 } => {
206 let key_alias = local_key_alias.unwrap_or_else(|| {
208 format!(
209 "org-{}",
210 name.chars()
211 .filter(|c| c.is_alphanumeric())
212 .take(20)
213 .collect::<String>()
214 .to_lowercase()
215 )
216 });
217
218 println!("šļø Initializing new organization identity...");
219 println!(" Organization Name: {}", name);
220 println!(" Repository path: {:?}", repo_path);
221 println!(" Local Key Alias: {}", key_alias);
222 println!(" Using Identity Ref: '{}'", config.identity_ref);
223
224 use crate::factories::storage::{ensure_git_repo, open_git_repo};
226
227 let identity_storage_check = RegistryIdentityStorage::new(repo_path.clone());
228 if repo_path.exists() {
229 match open_git_repo(&repo_path) {
230 Ok(_) => {
231 println!(" Git repository found.");
232 if identity_storage_check.load_identity().is_ok() {
233 return Err(anyhow!(
234 "An identity already exists at {:?}. Aborting.",
235 repo_path
236 ));
237 }
238 }
239 Err(_) => {
240 println!(" Path exists but is not a Git repo. Initializing...");
241 ensure_git_repo(&repo_path)
242 .context("Failed to initialize Git repository")?;
243 }
244 }
245 } else {
246 println!(" Creating Git repo directory...");
247 ensure_git_repo(&repo_path)
248 .context("Failed to create and initialize Git repository")?;
249 }
250
251 let mut metadata_json = serde_json::json!({
253 "type": "org",
254 "name": name,
255 "created_at": Utc::now().to_rfc3339()
256 });
257
258 if let Some(ref mf) = metadata_file
260 && mf.exists()
261 {
262 let metadata_content = fs::read_to_string(mf)
263 .with_context(|| format!("Failed to read metadata file: {:?}", mf))?;
264 let additional: serde_json::Value = serde_json::from_str(&metadata_content)
265 .with_context(|| format!("Invalid JSON in metadata file: {:?}", mf))?;
266
267 if let (Some(base), Some(add)) =
269 (metadata_json.as_object_mut(), additional.as_object())
270 {
271 for (k, v) in add {
272 if k != "type" && k != "name" {
273 base.insert(k.clone(), v.clone());
274 }
275 }
276 }
277 println!(" Merged additional metadata from {:?}", mf);
278 }
279
280 println!(
281 " Org metadata: {}",
282 serde_json::to_string(&metadata_json)?
283 );
284
285 println!(" Creating KERI-based organization identity (did:keri)...");
287
288 let backend = std::sync::Arc::new(GitRegistryBackend::from_config_unchecked(
289 RegistryConfig::single_tenant(&repo_path),
290 ));
291 let key_alias = KeyAlias::new_unchecked(key_alias);
292 let (controller_did, alias) = initialize_registry_identity(
293 backend,
294 &key_alias,
295 passphrase_provider.as_ref(),
296 &get_platform_keychain()?,
297 None,
298 )
299 .context("Failed to initialize org identity")?;
300
301 println!(" Creating admin attestation for organization creator...");
303
304 let identity_storage = RegistryIdentityStorage::new(repo_path.clone());
305 let managed_identity = identity_storage
306 .load_identity()
307 .context("Failed to load newly created org identity")?;
308 let rid = managed_identity.storage_id;
309
310 let org_resolved = resolver.resolve(controller_did.as_str()).with_context(|| {
312 format!(
313 "Failed to resolve public key for org identity: {}",
314 controller_did
315 )
316 })?;
317 let org_pk_bytes = *org_resolved.public_key();
318
319 let now = Utc::now();
320 let admin_capabilities = vec![
321 Capability::sign_commit(),
322 Capability::sign_release(),
323 Capability::manage_members(),
324 Capability::rotate_keys(),
325 ];
326
327 let meta = AttestationMetadata {
328 note: Some(format!("Organization '{}' root admin", name)),
329 timestamp: Some(now),
330 expires_at: None, };
332
333 let signer = StorageSigner::new(get_platform_keychain()?);
334 let org_did = DeviceDID::new(controller_did.to_string());
335
336 let attestation = create_signed_attestation(
337 now,
338 &rid,
339 &controller_did,
340 &org_did,
341 org_pk_bytes.as_bytes(),
342 Some(serde_json::json!({
343 "org_role": "admin",
344 "org_name": name
345 })),
346 &meta,
347 &signer,
348 passphrase_provider.as_ref(),
349 Some(&alias),
350 None, admin_capabilities,
352 Some(Role::Admin),
353 None, )
355 .context("Failed to create admin attestation")?;
356
357 let attestation_storage = RegistryAttestationStorage::new(repo_path.clone());
359 attestation_storage
360 .export(&auths_verifier::VerifiedAttestation::dangerous_from_unchecked(attestation))
361 .context("Failed to export admin attestation to Git")?;
362
363 println!("\nā
Organization identity initialized successfully!");
364 println!(" Org Identity ID: {}", controller_did);
365 println!(" Org Name: {}", name);
366 println!(" Repo Path: {:?}", repo_path);
367 println!(" Key Alias: {}", alias);
368 println!(" Admin Role: Granted with all capabilities");
369
370 if let Some(did_prefix) = controller_did.as_str().strip_prefix("did:keri:") {
371 println!(
372 " KEL Ref: '{}'",
373 layout::keri_kel_ref(&Prefix::new_unchecked(did_prefix.to_string()))
374 );
375 }
376
377 println!(" Identity Ref: '{}'", config.identity_ref);
378 println!(
379 " Member Ref: '{}'",
380 config.org_member_ref(controller_did.as_str(), &org_did)
381 );
382 println!("\nš Store your key passphrase securely.");
383 println!(
384 " You can now add members with: auths org add-member --org {} --member <identity-id> --role <role>",
385 controller_did
386 );
387
388 Ok(())
389 }
390
391 OrgSubcommand::Attest {
392 subject, payload_file, note, expires_at, signer_alias, } => {
398 let signer_alias = signer_alias
399 .ok_or_else(|| anyhow!("Signer key alias must be provided with --signer-alias"))?;
400 let signer_alias = KeyAlias::new_unchecked(signer_alias);
401
402 let identity_storage = RegistryIdentityStorage::new(repo_path.clone());
403 let managed_identity = identity_storage
404 .load_identity()
405 .context("Failed to load org identity from Git repository")?;
406 let controller_did = managed_identity.controller_did;
407 let rid = managed_identity.storage_id;
408
409 let payload_str = fs::read_to_string(&payload_file)
410 .with_context(|| format!("Failed to read payload file {:?}", payload_file))?;
411 let payload: serde_json::Value =
412 serde_json::from_str(&payload_str).context("Invalid JSON in payload file")?;
413
414 let key_storage = get_platform_keychain()?;
415 let (stored_did, encrypted_key) = key_storage
416 .load_key(&signer_alias)
417 .with_context(|| format!("Failed to load signer key '{}'", signer_alias))?;
418
419 if stored_did != controller_did {
420 return Err(anyhow!(
421 "Signer key alias '{}' belongs to DID '{}', but loaded org identity is '{}'",
422 signer_alias,
423 stored_did,
424 controller_did
425 ));
426 }
427
428 let passphrase = passphrase_provider.get_passphrase(&format!(
429 "Enter passphrase for org identity key '{}':",
430 signer_alias
431 ))?;
432 let _pkcs8_bytes = decrypt_keypair(&encrypted_key, &passphrase)
433 .context("Failed to decrypt signer key (invalid passphrase?)")?;
434
435 let subject_did = DeviceDID::new(subject.clone());
436
437 let device_resolved = resolver.resolve(&subject).with_context(|| {
439 format!("Failed to resolve public key for subject: {}", subject)
440 })?;
441 let device_pk_bytes = *device_resolved.public_key();
442
443 let now = Utc::now();
444 let meta = AttestationMetadata {
445 note,
446 timestamp: Some(now),
447 expires_at: expires_at
448 .as_deref()
449 .map(DateTime::parse_from_rfc3339)
450 .transpose()
451 .map_err(|e| anyhow!("Invalid RFC3339 datetime string: {}", e))?
452 .map(|dt| dt.with_timezone(&Utc)),
453 };
454
455 let signer = StorageSigner::new(key_storage);
456 let attestation = create_signed_attestation(
457 now,
458 &rid,
459 &controller_did,
460 &subject_did,
461 device_pk_bytes.as_bytes(),
462 Some(payload),
463 &meta,
464 &signer,
465 passphrase_provider.as_ref(),
466 Some(&signer_alias),
467 None, vec![],
469 None,
470 None,
471 )
472 .context("Failed to create signed attestation object")?;
473
474 let attestation_storage = RegistryAttestationStorage::new(repo_path.clone());
475 attestation_storage
476 .export(&auths_verifier::VerifiedAttestation::dangerous_from_unchecked(attestation))
477 .context("Failed to export attestation to Git")?;
478
479 println!(
480 "\nā
Org attestation created successfully from '{}' ā '{}'",
481 controller_did, subject_did
482 );
483
484 Ok(())
485 }
486
487 OrgSubcommand::Revoke {
488 subject,
489 note,
490 signer_alias,
491 } => {
492 println!("š Revoking org authorization for subject: {subject}");
493 println!(" Using Repository: {:?}", repo_path);
494 println!(" Using Identity Ref: '{}'", config.identity_ref);
495 println!(
496 " Using Attestation Prefix: '{}'",
497 config.device_attestation_prefix
498 );
499
500 let signer_alias = signer_alias
501 .ok_or_else(|| anyhow!("Signer key alias must be provided for revocation"))?;
502 let signer_alias = KeyAlias::new_unchecked(signer_alias);
503
504 let identity_storage = RegistryIdentityStorage::new(repo_path.clone());
505 let managed_identity = identity_storage
506 .load_identity()
507 .context("Failed to load identity from Git repository")?;
508 let controller_did = managed_identity.controller_did;
509 let rid = managed_identity.storage_id;
510
511 let encrypted_key = get_platform_keychain()?
512 .load_key(&signer_alias)
513 .context("Failed to load signer key")?
514 .1;
515 let pass = passphrase_provider.get_passphrase(&format!(
516 "Enter passphrase for identity key '{}':",
517 signer_alias
518 ))?;
519 let _pkcs8_bytes =
520 decrypt_keypair(&encrypted_key, &pass).context("Failed to decrypt identity key")?;
521
522 let subject_did = DeviceDID::new(subject.clone());
524 let now = Utc::now();
525
526 let attestation_storage = RegistryAttestationStorage::new(repo_path.clone());
528 let existing = attestation_storage
529 .load_attestations_for_device(&subject_did)
530 .context("Failed to load attestations for subject")?;
531 let device_public_key = existing
532 .iter()
533 .find(|a| !a.device_public_key.is_zero())
534 .map(|a| a.device_public_key)
535 .unwrap_or_else(|| Ed25519PublicKey::from_bytes([0u8; 32]));
536
537 println!("š Creating signed revocation...");
538 let signer = StorageSigner::new(get_platform_keychain()?);
539 let attestation = create_signed_revocation(
540 &rid,
541 &controller_did,
542 &subject_did,
543 device_public_key.as_bytes(),
544 note,
545 None,
546 now,
547 &signer,
548 passphrase_provider.as_ref(),
549 &signer_alias,
550 )
551 .context("Failed to create revocation")?;
552
553 println!("š¾ Writing revocation to Git...");
554 attestation_storage
555 .export(&auths_verifier::VerifiedAttestation::dangerous_from_unchecked(attestation))
556 .context("Failed to write revocation")?;
557
558 println!("\nā
Revoked authorization for subject {subject}");
559
560 Ok(())
561 }
562
563 OrgSubcommand::Show {
564 subject,
565 include_revoked,
566 } => {
567 let attestation_storage = RegistryAttestationStorage::new(repo_path.clone());
568 let resolver = DefaultDidResolver::with_repo(&repo_path);
569 let group = AttestationGroup::from_list(attestation_storage.load_all_attestations()?);
570
571 let subject_did = DeviceDID(subject.clone());
572 if let Some(list) = group.by_device.get(subject_did.as_str()) {
573 for (i, att) in list.iter().enumerate() {
574 if !include_revoked
575 && (att.is_revoked() || att.expires_at.is_some_and(|e| Utc::now() > e))
576 {
577 continue;
578 }
579
580 let status = match verify_with_resolver(Utc::now(), &resolver, att) {
581 Ok(_) => "ā
valid",
582 Err(e) if e.to_string().contains("revoked") => "š revoked",
583 Err(e) if e.to_string().contains("expired") => "ā expired",
584 Err(_) => "ā invalid",
585 };
586
587 println!(
588 "{i}. [{}] @ {}",
589 status,
590 att.timestamp.unwrap_or(Utc::now())
591 );
592 if let Some(note) = &att.note {
593 println!(" š {}", note);
594 }
595 if let Some(payload) = &att.payload {
596 println!(" š¦ {}", serde_json::to_string_pretty(payload)?);
597 }
598 }
599 } else {
600 println!("No authorizations found for subject: {}", subject);
601 }
602
603 Ok(())
604 }
605
606 OrgSubcommand::List { include_revoked } => {
607 let attestation_storage = RegistryAttestationStorage::new(repo_path.clone());
608 let resolver = DefaultDidResolver::with_repo(&repo_path);
609 let group = AttestationGroup::from_list(attestation_storage.load_all_attestations()?);
610
611 for (subject, list) in group.by_device.iter() {
612 let latest = list.last().unwrap();
613 if !include_revoked
614 && (latest.is_revoked() || latest.expires_at.is_some_and(|e| Utc::now() > e))
615 {
616 continue;
617 }
618
619 let status = match verify_with_resolver(Utc::now(), &resolver, latest) {
620 Ok(_) => "ā
valid",
621 Err(e) if e.to_string().contains("revoked") => "š revoked",
622 Err(e) if e.to_string().contains("expired") => "ā expired",
623 Err(_) => "ā invalid",
624 };
625
626 println!("- {} [{}]", subject, status);
627 }
628
629 Ok(())
630 }
631
632 OrgSubcommand::AddMember {
633 org,
634 member,
635 role: cli_role,
636 capabilities,
637 signer_alias,
638 note,
639 } => {
640 let role = Role::from(cli_role);
641 println!("š„ Adding member to organization...");
642 println!(" Org: {}", org);
643 println!(" Member: {}", member);
644 println!(" Role: {}", role);
645
646 let identity_storage = RegistryIdentityStorage::new(repo_path.clone());
648 let managed_identity = identity_storage
649 .load_identity()
650 .context("Failed to load identity. Are you running this from an org repository?")?;
651 let invoker_did = managed_identity.controller_did.clone();
652 let rid = managed_identity.storage_id;
653
654 let signer_alias = KeyAlias::new_unchecked(signer_alias.unwrap_or_else(|| {
656 format!(
658 "org-{}",
659 org.chars()
660 .filter(|c| c.is_alphanumeric())
661 .take(20)
662 .collect::<String>()
663 .to_lowercase()
664 )
665 }));
666
667 let attestation_storage = RegistryAttestationStorage::new(repo_path.clone());
670 let invoker_did_device = DeviceDID::new(invoker_did.to_string());
671 let invoker_attestations = attestation_storage.load_all_attestations()?;
672
673 let invoker_has_manage_members = invoker_attestations.iter().any(|att| {
675 att.subject.as_str() == invoker_did_device.as_str()
676 && !att.is_revoked()
677 && att.capabilities.contains(&Capability::manage_members())
678 });
679
680 if !invoker_has_manage_members {
681 return Err(anyhow!(
682 "You don't have ManageMembers capability for org '{}'. Only org admins can add members.",
683 org
684 ));
685 }
686
687 let key_storage = get_platform_keychain()?;
689 let (stored_did, encrypted_key) = key_storage
690 .load_key(&signer_alias)
691 .with_context(|| format!("Failed to load signer key '{}'", signer_alias))?;
692
693 if stored_did != invoker_did {
694 return Err(anyhow!(
695 "Signer key alias '{}' belongs to DID '{}', but loaded identity is '{}'",
696 signer_alias,
697 stored_did,
698 invoker_did
699 ));
700 }
701
702 let passphrase = passphrase_provider
703 .get_passphrase(&format!("Enter passphrase for org key '{}':", signer_alias))?;
704 let _pkcs8_bytes = decrypt_keypair(&encrypted_key, &passphrase)
705 .context("Failed to decrypt signer key (invalid passphrase?)")?;
706
707 let member_did = DeviceDID::new(member.clone());
709 let member_resolved = resolver
710 .resolve(&member)
711 .with_context(|| format!("Failed to resolve public key for member: {}", member))?;
712 let member_pk_bytes = *member_resolved.public_key();
713
714 let member_capabilities = if let Some(cap_strs) = capabilities {
716 cap_strs
717 .iter()
718 .map(|s| {
719 s.parse::<Capability>().unwrap_or_else(|e| {
720 eprintln!("error: {e}");
721 std::process::exit(2);
722 })
723 })
724 .collect()
725 } else {
726 role.default_capabilities()
727 };
728
729 println!(
730 " Capabilities: {:?}",
731 member_capabilities
732 .iter()
733 .map(|c| format!("{:?}", c))
734 .collect::<Vec<_>>()
735 .join(", ")
736 );
737
738 let now = Utc::now();
740 let meta = AttestationMetadata {
741 note: note.or_else(|| Some(format!("Added as {} by {}", role, invoker_did))),
742 timestamp: Some(now),
743 expires_at: None, };
745
746 let signer = StorageSigner::new(key_storage);
747 let attestation = create_signed_attestation(
748 now,
749 &rid,
750 &invoker_did,
751 &member_did,
752 member_pk_bytes.as_bytes(),
753 Some(serde_json::json!({
754 "org_role": role.to_string(),
755 "org_did": org
756 })),
757 &meta,
758 &signer,
759 passphrase_provider.as_ref(),
760 Some(&signer_alias),
761 None, member_capabilities.clone(),
763 Some(role),
764 Some(invoker_did.clone()),
765 )
766 .context("Failed to create member attestation")?;
767
768 let attestation_storage = RegistryAttestationStorage::new(repo_path.clone());
770 attestation_storage
771 .export(&auths_verifier::VerifiedAttestation::dangerous_from_unchecked(attestation))
772 .context("Failed to export member attestation to Git")?;
773
774 println!("\nā
Member added successfully!");
775 println!(" Member ID: {}", member);
776 println!(" Role: {}", role);
777 println!(
778 " Capabilities: {}",
779 member_capabilities
780 .iter()
781 .map(|c| format!("{:?}", c))
782 .collect::<Vec<_>>()
783 .join(", ")
784 );
785 println!(" Delegated by: {}", invoker_did);
786 println!(
787 " Stored at: {}",
788 config.org_member_ref(&org, &member_did)
789 );
790
791 Ok(())
792 }
793
794 OrgSubcommand::RevokeMember {
795 org,
796 member,
797 note,
798 signer_alias,
799 } => {
800 println!("š Revoking member from organization...");
801 println!(" Org: {}", org);
802 println!(" Member: {}", member);
803
804 let identity_storage = RegistryIdentityStorage::new(repo_path.clone());
806 let managed_identity = identity_storage
807 .load_identity()
808 .context("Failed to load identity. Are you running this from an org repository?")?;
809 let invoker_did = managed_identity.controller_did.clone();
810 let rid = managed_identity.storage_id;
811
812 let signer_alias = KeyAlias::new_unchecked(signer_alias.unwrap_or_else(|| {
814 format!(
815 "org-{}",
816 org.chars()
817 .filter(|c| c.is_alphanumeric())
818 .take(20)
819 .collect::<String>()
820 .to_lowercase()
821 )
822 }));
823
824 let attestation_storage = RegistryAttestationStorage::new(repo_path.clone());
826 let invoker_did_device = DeviceDID::new(invoker_did.to_string());
827 let all_attestations = attestation_storage.load_all_attestations()?;
828
829 let invoker_has_manage_members = all_attestations.iter().any(|att| {
831 att.subject.as_str() == invoker_did_device.as_str()
832 && !att.is_revoked()
833 && att.capabilities.contains(&Capability::manage_members())
834 });
835
836 if !invoker_has_manage_members {
837 return Err(anyhow!(
838 "You don't have ManageMembers capability for org '{}'. Only org admins can revoke members.",
839 org
840 ));
841 }
842
843 let member_did = DeviceDID::new(member.clone());
845 let member_attestation = all_attestations
846 .iter()
847 .find(|att| att.subject.as_str() == member_did.as_str());
848
849 match member_attestation {
850 None => {
851 return Err(anyhow!(
852 "Member '{}' is not a member of org '{}'. Cannot revoke.",
853 member,
854 org
855 ));
856 }
857 Some(att) if att.is_revoked() => {
858 return Err(anyhow!(
859 "Member '{}' is already revoked from org '{}'.",
860 member,
861 org
862 ));
863 }
864 Some(_) => {} }
866
867 let key_storage = get_platform_keychain()?;
869 let (stored_did, encrypted_key) = key_storage
870 .load_key(&signer_alias)
871 .with_context(|| format!("Failed to load signer key '{}'", signer_alias))?;
872
873 if stored_did != invoker_did {
874 return Err(anyhow!(
875 "Signer key alias '{}' belongs to DID '{}', but loaded identity is '{}'",
876 signer_alias,
877 stored_did,
878 invoker_did
879 ));
880 }
881
882 let passphrase = passphrase_provider
883 .get_passphrase(&format!("Enter passphrase for org key '{}':", signer_alias))?;
884 let _pkcs8_bytes = decrypt_keypair(&encrypted_key, &passphrase)
885 .context("Failed to decrypt signer key (invalid passphrase?)")?;
886
887 let attestation_storage = RegistryAttestationStorage::new(repo_path.clone());
889 let existing = attestation_storage
890 .load_attestations_for_device(&member_did)
891 .context("Failed to load attestations for member")?;
892 let member_public_key = existing
893 .iter()
894 .find(|a| !a.device_public_key.is_zero())
895 .map(|a| a.device_public_key)
896 .unwrap_or_else(|| Ed25519PublicKey::from_bytes([0u8; 32]));
897
898 let now = Utc::now();
900 let signer = StorageSigner::new(key_storage);
901
902 println!("š Creating signed revocation...");
903 let revocation = create_signed_revocation(
904 &rid,
905 &invoker_did,
906 &member_did,
907 member_public_key.as_bytes(),
908 note.clone(),
909 None, now,
911 &signer,
912 passphrase_provider.as_ref(),
913 &signer_alias,
914 )
915 .context("Failed to create revocation")?;
916
917 println!("š¾ Writing revocation to Git...");
919 attestation_storage
920 .export(&auths_verifier::VerifiedAttestation::dangerous_from_unchecked(revocation))
921 .context("Failed to export revocation to Git")?;
922
923 println!("\nā
Member revoked successfully!");
924 println!(" Member ID: {}", member);
925 println!(" Revoked by: {}", invoker_did);
926 if let Some(n) = note {
927 println!(" Note: {}", n);
928 }
929 println!(
930 " Stored at: {}",
931 config.org_member_ref(&org, &member_did)
932 );
933
934 Ok(())
935 }
936
937 OrgSubcommand::ListMembers {
938 org,
939 include_revoked,
940 } => {
941 println!("š Listing members of organization: {}", org);
942
943 let attestation_storage = RegistryAttestationStorage::new(repo_path.clone());
945 let all_attestations = attestation_storage.load_all_attestations()?;
946
947 #[allow(clippy::type_complexity)]
949 let mut members: Vec<(
950 String,
951 Option<Role>,
952 Option<String>,
953 bool,
954 Vec<Capability>,
955 )> = Vec::new();
956
957 for att in &all_attestations {
958 if att.is_revoked() && !include_revoked {
960 continue;
961 }
962
963 if att.expires_at.is_some_and(|e| Utc::now() > e) && !include_revoked {
965 continue;
966 }
967
968 members.push((
969 att.subject.to_string(),
970 att.role,
971 att.delegated_by.as_ref().map(|d| d.to_string()),
972 att.is_revoked(),
973 att.capabilities.clone(),
974 ));
975 }
976
977 if members.is_empty() {
978 println!("\nNo members found for organization.");
979 return Ok(());
980 }
981
982 members.sort_by(|a, b| {
983 member_role_order(&a.1)
984 .cmp(&member_role_order(&b.1))
985 .then_with(|| a.0.cmp(&b.0))
986 });
987
988 println!("\nOrg: {}", org);
989 println!("\nMembers ({} total):", members.len());
990 println!("āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā");
991
992 for (member_did, role, delegated_by, revoked, capabilities) in &members {
993 let role_str = role.as_ref().map(|r| r.as_str()).unwrap_or("unknown");
994 let status = if *revoked { " (revoked)" } else { "" };
995
996 let prefix = if delegated_by.is_none() {
998 "āā "
999 } else {
1000 "ā āā "
1001 };
1002
1003 let caps: Vec<String> = capabilities.iter().map(|c| format!("{:?}", c)).collect();
1005 let caps_str = if caps.is_empty() {
1006 String::new()
1007 } else {
1008 format!(" [{}]", caps.join(", "))
1009 };
1010
1011 println!(
1012 "{}{} [{}]{}{}",
1013 prefix, member_did, role_str, status, caps_str
1014 );
1015
1016 if let Some(delegator) = delegated_by {
1017 println!("ā delegated by: {}", delegator);
1018 }
1019 }
1020
1021 println!("āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā");
1022
1023 if !include_revoked {
1024 let revoked_count = all_attestations.iter().filter(|a| a.is_revoked()).count();
1025 if revoked_count > 0 {
1026 println!(
1027 "\n({} revoked member(s) hidden. Use --include-revoked to show.)",
1028 revoked_count
1029 );
1030 }
1031 }
1032
1033 Ok(())
1034 }
1035 }
1036}
1037
1038use crate::commands::executable::ExecutableCommand;
1039use crate::config::CliConfig;
1040
1041impl ExecutableCommand for OrgCommand {
1042 fn execute(&self, ctx: &CliConfig) -> Result<()> {
1043 handle_org(
1044 self.clone(),
1045 ctx.repo_path.clone(),
1046 self.overrides.identity_ref.clone(),
1047 self.overrides.identity_blob.clone(),
1048 self.overrides.attestation_prefix.clone(),
1049 self.overrides.attestation_blob.clone(),
1050 ctx.passphrase_provider.clone(),
1051 )
1052 }
1053}