docs(org): complete A5 living-docs sweep (item CRUD merged) + dead_code cleanup
Extends the A5 pre-stage now that dev-b's full B-stream (item CRUD + all 19 org subcommands) merged to main (7392795). Living docs: - FORMATS/CRYPTO/SECURITY/DESIGN: flip the item-CRUD "pending Dev-B" markers to shipped; SECURITY audit vocabulary moves item-* actions to live. - crates/relicario-cli/ARCHITECTURE.md: full 19-subcommand surface (12 admin + 7 item CRUD), accurate OrgAddKind scope (Login/SecureNote/Identity). - STATUS.md: enterprise-org-vault landed section (merged7392795) + tracked follow-ups + honest known-limitations; correct spec citation. - ROADMAP.md: backend-complete row + phase-2 follow-ups. - CHANGELOG.md: finalize the enterprise-org-vault Unreleased section (item CRUD into Added; Card/Key/Document/Totp + extension + phase-2 into Deferred). Code (PM-directed dead_code fixes): wire device::current_device_seed by removing the identical duplicate private fn in org_session.rs (de-dup); #[allow(dead_code)] + justification on org_session org_meta_path/load_meta (API completeness, no command consumes org.json yet). Also silence a 3rd pre-existing test-only warning (unused relicario() helper in tests/org_init_signing.rs). Honest deferrals kept explicit throughout: Card/Key/Document/Totp org add/edit parity, extension org switch/read (Dev-D) + writes, phase-2 (SSO/LDAP, read audit, per-collection subkeys, HTTP plane). Full workspace cargo test green, zero warnings. All cited code constants pinned file:line. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TJo44YM3UbBjro2fG6NrKy
This commit is contained in:
@@ -74,26 +74,43 @@ under `src/commands/`. Each source file has one job.
|
||||
- **`src/org_session.rs`** — `UnlockedOrgVault`, the org-vault analogue of
|
||||
`session.rs`. Holds the org master key in `Zeroizing<[u8; 32]>` for one CLI
|
||||
invocation, recovered by unwrapping `keys/<member-id>.enc` with the device
|
||||
ed25519 seed (`relicario_core::unwrap_org_key`). Owns the **collection-scoped**
|
||||
`item_path` (`items/<collection-slug>/<id>.enc` — the leading slug is what the
|
||||
pre-receive hook authorizes against, never decrypting), fingerprint-based
|
||||
member matching (`relicario_core::fingerprint`, tolerant of OpenSSH
|
||||
whitespace/comment differences), `atomic_write`, and `org_git_run`. Note
|
||||
`org_git_run` runs **bare git** — unlike `helpers::git_run` it does NOT inject
|
||||
ed25519 seed. `open_org_vault` calls `crate::device::current_device_seed()`
|
||||
directly (`device.rs`) — a duplicate private fn that previously existed in
|
||||
`org_session.rs` was removed during the A5 sweep (implementations were
|
||||
identical). Owns the **collection-scoped** `item_path`
|
||||
(`items/<collection-slug>/<id>.enc` — the leading slug is what the pre-receive
|
||||
hook authorizes against, never decrypting), fingerprint-based member matching
|
||||
(`relicario_core::fingerprint`, tolerant of OpenSSH whitespace/comment
|
||||
differences), `atomic_write`, and `org_git_run`. Note `org_git_run` runs
|
||||
**bare git** — unlike `helpers::git_run` it does NOT inject
|
||||
`commit.gpgsign=false`, because org commits MUST be signed (the hook verifies
|
||||
every commit's signature); signing config is established by
|
||||
`configure_git_signing` during `org init`.
|
||||
|
||||
- **`src/commands/org.rs`** — the `relicario org` subcommand surface. Merged:
|
||||
`init`, `add-member` / `remove-member` / `set-role` (owner-only escalation
|
||||
guard), `create-collection` / `grant` / `revoke`, `rotate-key`
|
||||
(`run_rotate_key`, `commands/org.rs:332` — fresh key, re-wrap for all members,
|
||||
re-encrypt every item blob + manifest under the new key, concurrent-rotation
|
||||
abort), and `status` / `audit` (verified-signer attribution + `TAMPERED`
|
||||
flag). **TODO (pending Dev-B B9–B14):** the item-CRUD commands (`org add` /
|
||||
`get` / `list` / `edit` / `rm` / `restore` / `purge`) and the final
|
||||
`Commands::Org` wiring in `main.rs`. `device.rs` gains
|
||||
`current_device_seed` / `current_device_pubkey` helpers for the ECIES unwrap.
|
||||
- **`src/commands/org.rs`** — the `relicario org` subcommand surface. Full
|
||||
19-subcommand surface is merged and wired via `Commands::Org` in `main.rs`.
|
||||
|
||||
*Admin / lifecycle (12):* `init` (structure + wrap + `configure_git_signing` +
|
||||
signed bootstrap commit), `add-member` / `remove-member` / `set-role`
|
||||
(owner-only escalation guard), `create-collection` / `grant` / `revoke`,
|
||||
`rotate-key` (`run_rotate_key`, `commands/org.rs:332` — fresh key, re-wrap for
|
||||
all members, re-encrypt every item blob + manifest under the new key,
|
||||
concurrent-rotation abort), `transfer-ownership`, `delete-org`, `status` /
|
||||
`audit` (verified-signer attribution + `TAMPERED` flag).
|
||||
|
||||
*Item CRUD (7):* `org add` creates typed items via `OrgAddKind`
|
||||
(`commands/org.rs:749`) — **Login / SecureNote / Identity only**; Card /
|
||||
SshKey / Document / Totp creation is a deferred follow-up. `get` / `list` can
|
||||
display any item type if present. `org get <query> [--show]` masks secrets
|
||||
unless `--show`; `org list [--trashed]` filters by the caller's collection
|
||||
grants; `org edit <query>` is flag-driven (blank flags keep current values);
|
||||
`org rm` soft-deletes, `org restore` undoes, `org purge` permanently removes
|
||||
the encrypted blob. All item ops are collection-scoped and grant-enforced. The
|
||||
audit trail emits `item-create` / `item-update` / `item-delete` /
|
||||
`item-restore` / `item-purge`.
|
||||
|
||||
Deferred: Card / SshKey / Document / Totp `org add` / `edit` parity;
|
||||
extension org reads and writes (Dev-D).
|
||||
|
||||
- **`src/helpers.rs`** (`helpers.rs:1-101`) — pure, no-state plumbing:
|
||||
`find_vault_dir_from` (`helpers.rs:14-28`) walks up parent directories
|
||||
|
||||
@@ -39,8 +39,14 @@ impl UnlockedOrgVault {
|
||||
}
|
||||
pub fn members_path(&self) -> PathBuf { self.root.join("members.json") }
|
||||
pub fn collections_path(&self) -> PathBuf { self.root.join("collections.json") }
|
||||
// OrgMeta accessors — part of the UnlockedOrgVault path/loader API surface
|
||||
// (parallel to members_path/collections_path + load_members), retained for
|
||||
// completeness. No command consumes org.json yet; surfacing the org
|
||||
// name/id in `org status` is a tracked follow-up, so allow until then.
|
||||
#[allow(dead_code)]
|
||||
pub fn org_meta_path(&self) -> PathBuf { self.root.join("org.json") }
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn load_meta(&self) -> Result<OrgMeta> {
|
||||
let s = fs::read_to_string(self.org_meta_path()).context("read org.json")?;
|
||||
Ok(serde_json::from_str(&s).context("parse org.json")?)
|
||||
@@ -185,7 +191,7 @@ pub fn open_org_vault(dir_flag: Option<&std::path::Path>) -> Result<UnlockedOrgV
|
||||
fs::read(&key_path).with_context(|| format!("read {}", key_path.display()))?;
|
||||
|
||||
// Recover the device ed25519 seed and unwrap.
|
||||
let seed = current_device_seed()?;
|
||||
let seed = crate::device::current_device_seed()?;
|
||||
let org_key = relicario_core::unwrap_org_key(&wrapped, &seed)?;
|
||||
|
||||
Ok(UnlockedOrgVault { root, org_key })
|
||||
@@ -202,27 +208,6 @@ fn current_device_fingerprint() -> Result<String> {
|
||||
}
|
||||
|
||||
/// Recover the active device's ed25519 seed (the 32-byte private scalar source)
|
||||
/// from its OpenSSH `signing.key`, for ECIES unwrap.
|
||||
fn current_device_seed() -> Result<Zeroizing<[u8; 32]>> {
|
||||
let name = crate::device::current_device()?
|
||||
.ok_or_else(|| anyhow::anyhow!("no active device — run `relicario device add` first"))?;
|
||||
let key_pem = crate::device::load_signing_key(&name)?;
|
||||
let private = ssh_key::PrivateKey::from_openssh(key_pem.as_str())
|
||||
.map_err(|e| anyhow::anyhow!("parse device signing key: {e}"))?;
|
||||
let ed = private
|
||||
.key_data()
|
||||
.ed25519()
|
||||
.ok_or_else(|| anyhow::anyhow!("device signing key is not ed25519"))?;
|
||||
// Ed25519PrivateKey derefs to its 32-byte seed.
|
||||
let seed_bytes: &[u8] = ed.private.as_ref();
|
||||
if seed_bytes.len() != 32 {
|
||||
anyhow::bail!("ed25519 seed has wrong length: {}", seed_bytes.len());
|
||||
}
|
||||
let mut seed = Zeroizing::new([0u8; 32]);
|
||||
seed.copy_from_slice(seed_bytes);
|
||||
Ok(seed)
|
||||
}
|
||||
|
||||
pub(crate) fn atomic_write(path: &Path, data: &[u8]) -> Result<()> {
|
||||
let mut tmp = path.as_os_str().to_owned();
|
||||
tmp.push(".tmp");
|
||||
|
||||
@@ -3,6 +3,10 @@ use std::path::Path;
|
||||
use std::process::Command;
|
||||
use tempfile::TempDir;
|
||||
|
||||
// Base runner kept as the documented counterpart to relicario_with_git_identity
|
||||
// below (every test in this file needs the git identity, so only the _with_
|
||||
// variant is currently called).
|
||||
#[allow(dead_code)]
|
||||
fn relicario(config_home: &Path, args: &[&str]) -> std::process::Output {
|
||||
Command::new(env!("CARGO_BIN_EXE_relicario"))
|
||||
.env("XDG_CONFIG_HOME", config_home)
|
||||
|
||||
Reference in New Issue
Block a user