Skip to content

Commit 2a127f9

Browse files
Merge pull request #36 from Spacecraft-Software/identity-field-selectors
feat(vault): granular identity field selectors (CLI + TUI per-field)
2 parents 28a314d + f57646c commit 2a127f9

7 files changed

Lines changed: 163 additions & 7 deletions

File tree

CHANGELOG.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,15 @@ range may break in any release.
1010

1111
### Added
1212

13+
- **Granular identity field selectors.** 16 new `--field` selectors expose every
14+
identity field individually — `identity-{title,first-name,middle-name,
15+
last-name,username,company,ssn,passport,license,address1,address2,address3,
16+
city,state,postal,country}` — alongside the existing composed `identity-name`
17+
/ `identity-address`. `vault get … --field identity-ssn` now works, and the
18+
TUI identity detail pane shows the full granular set so per-field reveal/copy
19+
reaches every field (SSN/passport/license masked, revealed on demand like the
20+
card CVV). Same proto → agent → CLI → TUI shape as the `card-*` selectors.
21+
1322
- **TUI per-field reveal/copy in the detail pane.** The detail pane is now
1423
focusable (`Tab` cycles folders → items → detail): with it focused, `j`/`k`
1524
move a field cursor and `Space`/`c` reveal/copy the **selected** field — so the

README.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,10 @@ Card (type 3) and identity (type 4) ciphers are readable from the CLI via
100100

101101
```sh
102102
vault get visa --field card-number # also: card-cardholder, card-brand, card-expiry, card-code
103-
vault get me --field identity-email # also: identity-name, identity-phone, identity-address
103+
vault get me --field identity-email # composed: identity-name, identity-address; plus
104+
# granular identity-{title,first-name,middle-name,
105+
# last-name,username,company,ssn,passport,license,
106+
# address1,address2,address3,city,state,postal,country}
104107
```
105108

106109
In the TUI, selecting a card or identity shows its fields in the detail pane:

crates/vault-agent/src/state.rs

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -727,6 +727,7 @@ impl AgentState {
727727
/// reliable path when several items share a name. When `id` is `None`, it
728728
/// falls back to a case-insensitive match on `query` and returns the first
729729
/// hit (the long-standing CLI behavior). `query` is also the error label.
730+
#[allow(clippy::too_many_lines)] // flat per-field dispatch (one arm per Field) reads best in one match
730731
pub fn get_item(&self, id: Option<&str>, query: &str, field: Field) -> Result<Item, IpcError> {
731732
let v = self.vault.as_ref().ok_or(IpcError::Locked)?;
732733
let query_lower = query.to_lowercase();
@@ -783,7 +784,23 @@ impl AgentState {
783784
Field::IdentityName
784785
| Field::IdentityEmail
785786
| Field::IdentityPhone
786-
| Field::IdentityAddress => DecryptOptions {
787+
| Field::IdentityAddress
788+
| Field::IdentityTitle
789+
| Field::IdentityFirstName
790+
| Field::IdentityMiddleName
791+
| Field::IdentityLastName
792+
| Field::IdentityUsername
793+
| Field::IdentityCompany
794+
| Field::IdentitySsn
795+
| Field::IdentityPassport
796+
| Field::IdentityLicense
797+
| Field::IdentityAddress1
798+
| Field::IdentityAddress2
799+
| Field::IdentityAddress3
800+
| Field::IdentityCity
801+
| Field::IdentityState
802+
| Field::IdentityPostal
803+
| Field::IdentityCountry => DecryptOptions {
787804
identity: true,
788805
..DecryptOptions::default()
789806
},
@@ -814,6 +831,30 @@ impl AgentState {
814831
Field::IdentityEmail => plain.identity.as_ref().and_then(|i| i.email.clone()),
815832
Field::IdentityPhone => plain.identity.as_ref().and_then(|i| i.phone.clone()),
816833
Field::IdentityAddress => plain.identity.as_ref().and_then(identity_address),
834+
Field::IdentityTitle => plain.identity.as_ref().and_then(|i| i.title.clone()),
835+
Field::IdentityFirstName => plain.identity.as_ref().and_then(|i| i.first_name.clone()),
836+
Field::IdentityMiddleName => {
837+
plain.identity.as_ref().and_then(|i| i.middle_name.clone())
838+
}
839+
Field::IdentityLastName => plain.identity.as_ref().and_then(|i| i.last_name.clone()),
840+
Field::IdentityUsername => plain.identity.as_ref().and_then(|i| i.username.clone()),
841+
Field::IdentityCompany => plain.identity.as_ref().and_then(|i| i.company.clone()),
842+
Field::IdentitySsn => plain.identity.as_ref().and_then(|i| i.ssn.clone()),
843+
Field::IdentityPassport => plain
844+
.identity
845+
.as_ref()
846+
.and_then(|i| i.passport_number.clone()),
847+
Field::IdentityLicense => plain
848+
.identity
849+
.as_ref()
850+
.and_then(|i| i.license_number.clone()),
851+
Field::IdentityAddress1 => plain.identity.as_ref().and_then(|i| i.address1.clone()),
852+
Field::IdentityAddress2 => plain.identity.as_ref().and_then(|i| i.address2.clone()),
853+
Field::IdentityAddress3 => plain.identity.as_ref().and_then(|i| i.address3.clone()),
854+
Field::IdentityCity => plain.identity.as_ref().and_then(|i| i.city.clone()),
855+
Field::IdentityState => plain.identity.as_ref().and_then(|i| i.state.clone()),
856+
Field::IdentityPostal => plain.identity.as_ref().and_then(|i| i.postal_code.clone()),
857+
Field::IdentityCountry => plain.identity.as_ref().and_then(|i| i.country.clone()),
817858
};
818859
let value = value.ok_or_else(|| IpcError::NoSuchField {
819860
item: name.clone(),

crates/vault-cli/src/main.rs

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -455,6 +455,22 @@ enum FieldArg {
455455
IdentityEmail,
456456
IdentityPhone,
457457
IdentityAddress,
458+
IdentityTitle,
459+
IdentityFirstName,
460+
IdentityMiddleName,
461+
IdentityLastName,
462+
IdentityUsername,
463+
IdentityCompany,
464+
IdentitySsn,
465+
IdentityPassport,
466+
IdentityLicense,
467+
IdentityAddress1,
468+
IdentityAddress2,
469+
IdentityAddress3,
470+
IdentityCity,
471+
IdentityState,
472+
IdentityPostal,
473+
IdentityCountry,
458474
}
459475

460476
impl From<FieldArg> for Field {
@@ -474,6 +490,22 @@ impl From<FieldArg> for Field {
474490
FieldArg::IdentityEmail => Self::IdentityEmail,
475491
FieldArg::IdentityPhone => Self::IdentityPhone,
476492
FieldArg::IdentityAddress => Self::IdentityAddress,
493+
FieldArg::IdentityTitle => Self::IdentityTitle,
494+
FieldArg::IdentityFirstName => Self::IdentityFirstName,
495+
FieldArg::IdentityMiddleName => Self::IdentityMiddleName,
496+
FieldArg::IdentityLastName => Self::IdentityLastName,
497+
FieldArg::IdentityUsername => Self::IdentityUsername,
498+
FieldArg::IdentityCompany => Self::IdentityCompany,
499+
FieldArg::IdentitySsn => Self::IdentitySsn,
500+
FieldArg::IdentityPassport => Self::IdentityPassport,
501+
FieldArg::IdentityLicense => Self::IdentityLicense,
502+
FieldArg::IdentityAddress1 => Self::IdentityAddress1,
503+
FieldArg::IdentityAddress2 => Self::IdentityAddress2,
504+
FieldArg::IdentityAddress3 => Self::IdentityAddress3,
505+
FieldArg::IdentityCity => Self::IdentityCity,
506+
FieldArg::IdentityState => Self::IdentityState,
507+
FieldArg::IdentityPostal => Self::IdentityPostal,
508+
FieldArg::IdentityCountry => Self::IdentityCountry,
477509
}
478510
}
479511
}

crates/vault-ipc/src/proto.rs

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -551,6 +551,38 @@ pub enum Field {
551551
IdentityPhone,
552552
/// `Identity` address lines + city/state/postal/country.
553553
IdentityAddress,
554+
/// `Identity.Title`.
555+
IdentityTitle,
556+
/// `Identity.FirstName`.
557+
IdentityFirstName,
558+
/// `Identity.MiddleName`.
559+
IdentityMiddleName,
560+
/// `Identity.LastName`.
561+
IdentityLastName,
562+
/// `Identity.Username`.
563+
IdentityUsername,
564+
/// `Identity.Company`.
565+
IdentityCompany,
566+
/// `Identity.SSN` (sensitive).
567+
IdentitySsn,
568+
/// `Identity.PassportNumber` (sensitive).
569+
IdentityPassport,
570+
/// `Identity.LicenseNumber` (sensitive).
571+
IdentityLicense,
572+
/// `Identity.Address1`.
573+
IdentityAddress1,
574+
/// `Identity.Address2`.
575+
IdentityAddress2,
576+
/// `Identity.Address3`.
577+
IdentityAddress3,
578+
/// `Identity.City`.
579+
IdentityCity,
580+
/// `Identity.State`.
581+
IdentityState,
582+
/// `Identity.PostalCode`.
583+
IdentityPostal,
584+
/// `Identity.Country`.
585+
IdentityCountry,
554586
}
555587

556588
/// Re-export the full cipher payload from `vault-core` when needed.

crates/vault-tui/src/app.rs

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -410,10 +410,24 @@ pub const fn detail_fields(cipher_type: u8) -> &'static [DetailField] {
410410
df("CVV", Field::CardCode, true),
411411
];
412412
const IDENTITY: &[DetailField] = &[
413-
df("Person", Field::IdentityName, false),
413+
df("Title", Field::IdentityTitle, false),
414+
df("First", Field::IdentityFirstName, false),
415+
df("Middle", Field::IdentityMiddleName, false),
416+
df("Last", Field::IdentityLastName, false),
417+
df("IdUser", Field::IdentityUsername, false),
418+
df("Company", Field::IdentityCompany, false),
414419
df("Email", Field::IdentityEmail, false),
415420
df("Phone", Field::IdentityPhone, false),
416-
df("Address", Field::IdentityAddress, false),
421+
df("Addr1", Field::IdentityAddress1, false),
422+
df("Addr2", Field::IdentityAddress2, false),
423+
df("Addr3", Field::IdentityAddress3, false),
424+
df("City", Field::IdentityCity, false),
425+
df("State", Field::IdentityState, false),
426+
df("Postal", Field::IdentityPostal, false),
427+
df("Country", Field::IdentityCountry, false),
428+
df("SSN", Field::IdentitySsn, true),
429+
df("Passport", Field::IdentityPassport, true),
430+
df("License", Field::IdentityLicense, true),
417431
];
418432
match cipher_type {
419433
3 => CARD,
@@ -2201,7 +2215,18 @@ mod tests {
22012215
.expect("card has a CVV field");
22022216
assert_eq!(cvv.field, Field::CardCode);
22032217
assert!(cvv.masked, "CVV is masked until revealed");
2204-
assert_eq!(detail_fields(4).len(), 4);
2218+
// Identity exposes its full granular set; the three sensitive fields are
2219+
// masked.
2220+
let id = detail_fields(4);
2221+
assert_eq!(id.len(), 18);
2222+
for label in ["SSN", "Passport", "License"] {
2223+
let f = id.iter().find(|f| f.label == label).expect("field present");
2224+
assert!(f.masked, "{label} must be masked");
2225+
}
2226+
assert_eq!(
2227+
id.iter().find(|f| f.label == "SSN").map(|f| f.field),
2228+
Some(Field::IdentitySsn)
2229+
);
22052230
}
22062231

22072232
#[test]

crates/vault-tui/src/main.rs

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -794,11 +794,25 @@ async fn ensure_detail(state: &mut App, socket: &Path) {
794794
("Brand", Field::CardBrand),
795795
("Exp", Field::CardExpiry),
796796
],
797+
// The non-sensitive identity fields (granular, matching the detail
798+
// pane's field cursor). SSN/passport/license reveal on demand, so they
799+
// aren't fetched here.
797800
4 => &[
798-
("Person", Field::IdentityName),
801+
("Title", Field::IdentityTitle),
802+
("First", Field::IdentityFirstName),
803+
("Middle", Field::IdentityMiddleName),
804+
("Last", Field::IdentityLastName),
805+
("IdUser", Field::IdentityUsername),
806+
("Company", Field::IdentityCompany),
799807
("Email", Field::IdentityEmail),
800808
("Phone", Field::IdentityPhone),
801-
("Address", Field::IdentityAddress),
809+
("Addr1", Field::IdentityAddress1),
810+
("Addr2", Field::IdentityAddress2),
811+
("Addr3", Field::IdentityAddress3),
812+
("City", Field::IdentityCity),
813+
("State", Field::IdentityState),
814+
("Postal", Field::IdentityPostal),
815+
("Country", Field::IdentityCountry),
802816
],
803817
_ => {
804818
state.detail = None;

0 commit comments

Comments
 (0)