Skip to main content

auths_cli/commands/
org.rs

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/// CLI-level role wrapper that derives `ValueEnum` for argument parsing.
37///
38/// Converts to `auths_sdk::workflows::org::Role` at the CLI boundary.
39#[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/// The `org` subcommand, handling member authorizations.
57#[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/// Subcommands for managing authorizations issued by this identity.
67#[derive(Subcommand, Debug, Clone)]
68pub enum OrgSubcommand {
69    /// Initialize a new organization identity
70    Init {
71        /// Organization name
72        #[arg(long)]
73        name: String,
74
75        /// Alias for the local signing key (auto-generated if not provided)
76        #[arg(long)]
77        local_key_alias: Option<String>,
78
79        /// Optional metadata file (if provided, merged with org metadata)
80        #[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    /// Add a member to an organization
114    AddMember {
115        /// Organization identity ID
116        #[arg(long)]
117        org: String,
118
119        /// Member identity ID to add
120        #[arg(long)]
121        member: String,
122
123        /// Role to assign (admin, member, readonly)
124        #[arg(long, value_enum)]
125        role: CliRole,
126
127        /// Override default capabilities (comma-separated)
128        #[arg(long, value_delimiter = ',')]
129        capabilities: Option<Vec<String>>,
130
131        /// Alias of the signing key in keychain
132        #[arg(long)]
133        signer_alias: Option<String>,
134
135        /// Optional note for the authorization
136        #[arg(long)]
137        note: Option<String>,
138    },
139
140    /// Revoke a member from an organization
141    RevokeMember {
142        /// Organization identity ID
143        #[arg(long)]
144        org: String,
145
146        /// Member identity ID to revoke
147        #[arg(long)]
148        member: String,
149
150        /// Reason for revocation
151        #[arg(long)]
152        note: Option<String>,
153
154        /// Alias of the signing key in keychain
155        #[arg(long)]
156        signer_alias: Option<String>,
157    },
158
159    /// List members of an organization
160    ListMembers {
161        /// Organization identity ID
162        #[arg(long)]
163        org: String,
164
165        /// Include revoked members
166        #[arg(long, action = ArgAction::SetTrue)]
167        include_revoked: bool,
168    },
169}
170
171/// Handles `org` commands for issuing or revoking member authorizations.
172pub 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            // Generate a key alias if not provided
207            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            // --- Ensure Git repo exists ---
225            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            // --- Build org metadata ---
252            let mut metadata_json = serde_json::json!({
253                "type": "org",
254                "name": name,
255                "created_at": Utc::now().to_rfc3339()
256            });
257
258            // Merge with additional metadata file if provided
259            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                // Merge additional metadata (preserving type and name)
268                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            // --- Generate KERI Identity ---
286            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            // --- Create admin self-attestation ---
302            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            // Resolve the org's own public key for self-attestation
311            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, // Admin attestation doesn't expire
331            };
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, // Self-attestation, no device signature
351                admin_capabilities,
352                Some(Role::Admin),
353                None, // Root admin has no delegator
354            )
355            .context("Failed to create admin attestation")?;
356
357            // Export to Git at the org member ref path
358            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,      // The subject DID (String)
393            payload_file, // Path to the JSON payload
394            note,         // Optional note (String)
395            expires_at,   // Optional RFC3339 expiration string
396            signer_alias, // Alias of the org's signing key in keychain
397        } => {
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            // --- Resolve device public key using the custom resolver IF did:key ---
438            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, // No device signature for org attestations
468                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            // Allow both did:key and did:keri as subject input
523            let subject_did = DeviceDID::new(subject.clone());
524            let now = Utc::now();
525
526            // Look up the subject's public key from existing attestations
527            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            // Load invoker's identity and key
647            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            // Determine signer alias
655            let signer_alias = KeyAlias::new_unchecked(signer_alias.unwrap_or_else(|| {
656                // Try to derive alias from org name in identity metadata
657                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            // Verify invoker has ManageMembers capability
668            // First, load the invoker's own org attestation to check capabilities
669            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            // Find invoker's attestation for this org
674            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            // Load signer key and verify passphrase
688            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            // Resolve member's public key
708            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            // Determine capabilities: use override if provided, otherwise use role defaults
715            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            // Create the attestation
739            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, // Member attestations don't expire by default
744            };
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, // No device signature for org membership attestations
762                member_capabilities.clone(),
763                Some(role),
764                Some(invoker_did.clone()),
765            )
766            .context("Failed to create member attestation")?;
767
768            // Export to Git at the org member ref path
769            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            // Load invoker's identity and key
805            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            // Determine signer alias
813            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            // Verify invoker has ManageMembers capability
825            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            // Find invoker's attestation for this org
830            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            // Check if member exists and is not already revoked
844            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(_) => {} // Member exists and is active, proceed
865            }
866
867            // Load signer key and verify passphrase
868            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            // Look up the member's public key from existing attestations
888            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            // Create revocation
899            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, // No expiration for revocations
910                now,
911                &signer,
912                passphrase_provider.as_ref(),
913                &signer_alias,
914            )
915            .context("Failed to create revocation")?;
916
917            // Export to Git
918            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            // Load all attestations
944            let attestation_storage = RegistryAttestationStorage::new(repo_path.clone());
945            let all_attestations = attestation_storage.load_all_attestations()?;
946
947            // Build member list with delegation info
948            #[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                // Skip if revoked and not including revoked
959                if att.is_revoked() && !include_revoked {
960                    continue;
961                }
962
963                // Skip expired attestations
964                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                // Determine tree prefix based on delegator
997                let prefix = if delegated_by.is_none() {
998                    "ā”œā”€ "
999                } else {
1000                    "│  └─ "
1001                };
1002
1003                // Format capabilities
1004                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}