Add resource secret epoch metadata
This commit is contained in:
parent
ce260625d6
commit
a05112e6a2
12 changed files with 287 additions and 13 deletions
|
|
@ -125,6 +125,9 @@ Roadmap items should be actionable and checkable:
|
||||||
replication and prefix-capability enforcement are still roadmap work.
|
replication and prefix-capability enforcement are still roadmap work.
|
||||||
- Document resources can be registered locally with empty JSON state and
|
- Document resources can be registered locally with empty JSON state and
|
||||||
local-only status. Automerge editing/state and sync are still roadmap work.
|
local-only status. Automerge editing/state and sync are still roadmap work.
|
||||||
|
- Resource secret epoch metadata can be created, rotated, and listed locally.
|
||||||
|
Payload encryption, key envelopes, and bearer invite enforcement are still
|
||||||
|
roadmap work.
|
||||||
- Signed peer-card LAN discovery payloads, peer auth over Iroh, cr-sqlite,
|
- Signed peer-card LAN discovery payloads, peer auth over Iroh, cr-sqlite,
|
||||||
iroh-docs, iroh-gossip, iroh-blobs, Automerge sync, real auth enforcement,
|
iroh-docs, iroh-gossip, iroh-blobs, Automerge sync, real auth enforcement,
|
||||||
OpenSSH KRL generation, and Keyhive/BeeKEM-style authorization are future
|
OpenSSH KRL generation, and Keyhive/BeeKEM-style authorization are future
|
||||||
|
|
|
||||||
2
Cargo.lock
generated
2
Cargo.lock
generated
|
|
@ -1121,6 +1121,7 @@ dependencies = [
|
||||||
"geth-keychain",
|
"geth-keychain",
|
||||||
"geth-kv",
|
"geth-kv",
|
||||||
"geth-resource",
|
"geth-resource",
|
||||||
|
"geth-secrets",
|
||||||
"geth-ssh-identity",
|
"geth-ssh-identity",
|
||||||
"geth-types",
|
"geth-types",
|
||||||
"serde",
|
"serde",
|
||||||
|
|
@ -1221,6 +1222,7 @@ dependencies = [
|
||||||
"geth-keychain",
|
"geth-keychain",
|
||||||
"geth-kv",
|
"geth-kv",
|
||||||
"geth-resource",
|
"geth-resource",
|
||||||
|
"geth-secrets",
|
||||||
"geth-ssh-identity",
|
"geth-ssh-identity",
|
||||||
"geth-store",
|
"geth-store",
|
||||||
"geth-types",
|
"geth-types",
|
||||||
|
|
|
||||||
|
|
@ -76,6 +76,9 @@ The bootstrap implementation provides:
|
||||||
- `geth resource create <kind> <name>`
|
- `geth resource create <kind> <name>`
|
||||||
- `geth keychain init [--admin-key <path>]`
|
- `geth keychain init [--admin-key <path>]`
|
||||||
- `geth keychain status`
|
- `geth keychain status`
|
||||||
|
- `geth secret status`
|
||||||
|
- `geth secret create <resource>`
|
||||||
|
- `geth secret rotate <resource>`
|
||||||
- `geth auth explain <subject> <resource> <capability>`
|
- `geth auth explain <subject> <resource> <capability>`
|
||||||
- `geth auth grant <subject> <resource> <capability> [--grant-id <id>]`
|
- `geth auth grant <subject> <resource> <capability> [--grant-id <id>]`
|
||||||
- `geth auth revoke <resource> <grant-id>`
|
- `geth auth revoke <resource> <grant-id>`
|
||||||
|
|
@ -95,8 +98,7 @@ The bootstrap implementation provides:
|
||||||
- `geth ssh revocation list`
|
- `geth ssh revocation list`
|
||||||
- `geth ssh revocation export --out <path>`
|
- `geth ssh revocation export --out <path>`
|
||||||
|
|
||||||
Other command groups exist as explicit stubs: `pipe`, `pubsub`, `secret`, and
|
Other command groups exist as explicit stubs: `pipe`, `pubsub`, and `ssh`.
|
||||||
`ssh`.
|
|
||||||
|
|
||||||
## Resource Modules
|
## Resource Modules
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -161,6 +161,8 @@ pub enum AuthCommand {
|
||||||
#[derive(Debug, Subcommand)]
|
#[derive(Debug, Subcommand)]
|
||||||
pub enum SecretCommand {
|
pub enum SecretCommand {
|
||||||
Status,
|
Status,
|
||||||
|
Create { resource: String },
|
||||||
|
Rotate { resource: String },
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Subcommand)]
|
#[derive(Debug, Subcommand)]
|
||||||
|
|
@ -390,9 +392,10 @@ fn request_for_command(command: Command) -> Result<ControlRequest> {
|
||||||
Command::Auth {
|
Command::Auth {
|
||||||
command: AuthCommand::Revoke { resource, grant_id },
|
command: AuthCommand::Revoke { resource, grant_id },
|
||||||
} => ControlRequest::AuthRevoke { resource, grant_id },
|
} => ControlRequest::AuthRevoke { resource, grant_id },
|
||||||
Command::Secret { command } => ControlRequest::ModuleStub {
|
Command::Secret { command } => match command {
|
||||||
module: "secret".to_owned(),
|
SecretCommand::Status => ControlRequest::SecretStatus,
|
||||||
command: format!("{command:?}"),
|
SecretCommand::Create { resource } => ControlRequest::SecretCreate { resource },
|
||||||
|
SecretCommand::Rotate { resource } => ControlRequest::SecretRotate { resource },
|
||||||
},
|
},
|
||||||
Command::Cas { command } => match command {
|
Command::Cas { command } => match command {
|
||||||
CasCommand::Add { path } => ControlRequest::CasAdd { path },
|
CasCommand::Add { path } => ControlRequest::CasAdd { path },
|
||||||
|
|
@ -636,6 +639,20 @@ fn print_response(response: ControlResponse, json: bool) -> Result<()> {
|
||||||
println!("recorded keychain op: {}", op.id);
|
println!("recorded keychain op: {}", op.id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
ControlResponse::SecretStatus { secrets } => {
|
||||||
|
if secrets.is_empty() {
|
||||||
|
println!("no resource secrets");
|
||||||
|
} else {
|
||||||
|
for secret in secrets {
|
||||||
|
println!("{}\t{}\tepoch {}", secret.id, secret.resource, secret.epoch);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ControlResponse::SecretCreated { secret } => {
|
||||||
|
println!("resource secret: {}", secret.id);
|
||||||
|
println!("resource: {}", secret.resource);
|
||||||
|
println!("epoch: {}", secret.epoch);
|
||||||
|
}
|
||||||
ControlResponse::AuthExplain(explain) => {
|
ControlResponse::AuthExplain(explain) => {
|
||||||
println!("allowed: {}", explain.allowed);
|
println!("allowed: {}", explain.allowed);
|
||||||
println!("subject: {}", explain.subject);
|
println!("subject: {}", explain.subject);
|
||||||
|
|
|
||||||
|
|
@ -15,5 +15,6 @@ geth-document = { path = "../geth-document" }
|
||||||
geth-keychain = { path = "../geth-keychain" }
|
geth-keychain = { path = "../geth-keychain" }
|
||||||
geth-kv = { path = "../geth-kv" }
|
geth-kv = { path = "../geth-kv" }
|
||||||
geth-resource = { path = "../geth-resource" }
|
geth-resource = { path = "../geth-resource" }
|
||||||
|
geth-secrets = { path = "../geth-secrets" }
|
||||||
geth-ssh-identity = { path = "../geth-ssh-identity" }
|
geth-ssh-identity = { path = "../geth-ssh-identity" }
|
||||||
geth-types = { path = "../geth-types" }
|
geth-types = { path = "../geth-types" }
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ use geth_document::DocumentResource;
|
||||||
use geth_keychain::KeychainOp;
|
use geth_keychain::KeychainOp;
|
||||||
use geth_kv::{KvEntry, KvResource};
|
use geth_kv::{KvEntry, KvResource};
|
||||||
use geth_resource::ResourceDescriptor;
|
use geth_resource::ResourceDescriptor;
|
||||||
|
use geth_secrets::ResourceMasterSecret;
|
||||||
use geth_ssh_identity::{
|
use geth_ssh_identity::{
|
||||||
SshCertApproval, SshCertRequest, SshCertificateRecord, SshRevocationEntry,
|
SshCertApproval, SshCertRequest, SshCertificateRecord, SshRevocationEntry,
|
||||||
};
|
};
|
||||||
|
|
@ -48,6 +49,13 @@ pub enum ControlRequest {
|
||||||
admin_key_path: Option<PathBuf>,
|
admin_key_path: Option<PathBuf>,
|
||||||
},
|
},
|
||||||
KeychainStatus,
|
KeychainStatus,
|
||||||
|
SecretStatus,
|
||||||
|
SecretCreate {
|
||||||
|
resource: String,
|
||||||
|
},
|
||||||
|
SecretRotate {
|
||||||
|
resource: String,
|
||||||
|
},
|
||||||
AuthExplain {
|
AuthExplain {
|
||||||
subject: String,
|
subject: String,
|
||||||
resource: String,
|
resource: String,
|
||||||
|
|
@ -167,6 +175,12 @@ pub enum ControlResponse {
|
||||||
KeychainInitialized {
|
KeychainInitialized {
|
||||||
ops: Vec<KeychainOp>,
|
ops: Vec<KeychainOp>,
|
||||||
},
|
},
|
||||||
|
SecretStatus {
|
||||||
|
secrets: Vec<ResourceMasterSecret>,
|
||||||
|
},
|
||||||
|
SecretCreated {
|
||||||
|
secret: ResourceMasterSecret,
|
||||||
|
},
|
||||||
AuthExplain(AuthExplanation),
|
AuthExplain(AuthExplanation),
|
||||||
AuthOpRecorded {
|
AuthOpRecorded {
|
||||||
op: AuthOp,
|
op: AuthOp,
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,7 @@ geth-iroh = { path = "../geth-iroh" }
|
||||||
geth-keychain = { path = "../geth-keychain" }
|
geth-keychain = { path = "../geth-keychain" }
|
||||||
geth-kv = { path = "../geth-kv" }
|
geth-kv = { path = "../geth-kv" }
|
||||||
geth-resource = { path = "../geth-resource" }
|
geth-resource = { path = "../geth-resource" }
|
||||||
|
geth-secrets = { path = "../geth-secrets" }
|
||||||
geth-ssh-identity = { path = "../geth-ssh-identity" }
|
geth-ssh-identity = { path = "../geth-ssh-identity" }
|
||||||
geth-store = { path = "../geth-store" }
|
geth-store = { path = "../geth-store" }
|
||||||
geth-types = { path = "../geth-types" }
|
geth-types = { path = "../geth-types" }
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@ use geth_iroh::{EndpointStatus, GethIrohConfig, GethIrohEndpoint, GethRelayMode}
|
||||||
use geth_keychain::{KeychainOp, KeychainOpKind};
|
use geth_keychain::{KeychainOp, KeychainOpKind};
|
||||||
use geth_kv::{KvEntry, KvResource};
|
use geth_kv::{KvEntry, KvResource};
|
||||||
use geth_resource::ResourceDescriptor;
|
use geth_resource::ResourceDescriptor;
|
||||||
|
use geth_secrets::ResourceMasterSecret;
|
||||||
use geth_ssh_identity::{
|
use geth_ssh_identity::{
|
||||||
SshCertApproval, SshCertKind, SshCertRequest, SshCertRequestStatus, SshCertificateRecord,
|
SshCertApproval, SshCertKind, SshCertRequest, SshCertRequestStatus, SshCertificateRecord,
|
||||||
SshRevocationEntry, SshRevocationKind, build_ssh_cert_sign_command, cert_request_id,
|
SshRevocationEntry, SshRevocationKind, build_ssh_cert_sign_command, cert_request_id,
|
||||||
|
|
@ -21,7 +22,8 @@ use geth_ssh_identity::{
|
||||||
};
|
};
|
||||||
use geth_store::{
|
use geth_store::{
|
||||||
Store, StoredAuthOp, StoredDbResource, StoredDocumentResource, StoredKeychainOp, StoredKvEntry,
|
Store, StoredAuthOp, StoredDbResource, StoredDocumentResource, StoredKeychainOp, StoredKvEntry,
|
||||||
StoredKvStore, StoredResource, StoredSshCertRequest, StoredSshCertificate, StoredSshRevocation,
|
StoredKvStore, StoredResource, StoredResourceSecret, StoredSshCertRequest,
|
||||||
|
StoredSshCertificate, StoredSshRevocation,
|
||||||
};
|
};
|
||||||
use geth_types::{
|
use geth_types::{
|
||||||
AuthOpId, Capability, KeyId, NodeId, PrincipalId, ResourceId, ResourceKind, ResourceName,
|
AuthOpId, Capability, KeyId, NodeId, PrincipalId, ResourceId, ResourceKind, ResourceName,
|
||||||
|
|
@ -67,6 +69,8 @@ pub enum NodeError {
|
||||||
InvalidDocumentName(String),
|
InvalidDocumentName(String),
|
||||||
#[error("document not found: {0}")]
|
#[error("document not found: {0}")]
|
||||||
DocumentNotFound(String),
|
DocumentNotFound(String),
|
||||||
|
#[error("resource not found: {0}")]
|
||||||
|
ResourceNotFound(String),
|
||||||
#[error("invalid ssh certificate kind: {0}")]
|
#[error("invalid ssh certificate kind: {0}")]
|
||||||
InvalidSshCertKind(String),
|
InvalidSshCertKind(String),
|
||||||
#[error("invalid ssh certificate request status: {0}")]
|
#[error("invalid ssh certificate request status: {0}")]
|
||||||
|
|
@ -335,6 +339,27 @@ pub fn handle_request(
|
||||||
nodes: view.nodes.len(),
|
nodes: view.nodes.len(),
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
ControlRequest::SecretStatus => Ok(ControlResponse::SecretStatus {
|
||||||
|
secrets: store
|
||||||
|
.list_resource_secrets()?
|
||||||
|
.into_iter()
|
||||||
|
.map(resource_secret_from_stored)
|
||||||
|
.collect(),
|
||||||
|
}),
|
||||||
|
ControlRequest::SecretCreate { resource } => {
|
||||||
|
ensure_resource_exists(&store, &resource)?;
|
||||||
|
let secret = create_resource_secret(&store, &resource, 1)?;
|
||||||
|
Ok(ControlResponse::SecretCreated { secret })
|
||||||
|
}
|
||||||
|
ControlRequest::SecretRotate { resource } => {
|
||||||
|
ensure_resource_exists(&store, &resource)?;
|
||||||
|
let next_epoch = store
|
||||||
|
.latest_resource_secret(&resource)?
|
||||||
|
.map(|secret| secret.epoch + 1)
|
||||||
|
.unwrap_or(1);
|
||||||
|
let secret = create_resource_secret(&store, &resource, next_epoch)?;
|
||||||
|
Ok(ControlResponse::SecretCreated { secret })
|
||||||
|
}
|
||||||
ControlRequest::AuthExplain {
|
ControlRequest::AuthExplain {
|
||||||
subject,
|
subject,
|
||||||
resource,
|
resource,
|
||||||
|
|
@ -764,6 +789,48 @@ fn document_resource_from_stored(stored: &StoredDocumentResource) -> DocumentRes
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn ensure_resource_exists(store: &Store, resource_id: &str) -> Result<(), NodeError> {
|
||||||
|
if store
|
||||||
|
.list_resources()?
|
||||||
|
.into_iter()
|
||||||
|
.any(|resource| resource.resource_id == resource_id)
|
||||||
|
{
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
Err(NodeError::ResourceNotFound(resource_id.to_owned()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn create_resource_secret(
|
||||||
|
store: &Store,
|
||||||
|
resource_id: &str,
|
||||||
|
epoch: u64,
|
||||||
|
) -> Result<ResourceMasterSecret, NodeError> {
|
||||||
|
let created_at = UnixMillis(geth_store::now_ms());
|
||||||
|
let secret_id = format!(
|
||||||
|
"secret:{}",
|
||||||
|
geth_crypto::blake3_hex(format!("{resource_id}\0{epoch}\0{}", created_at.0).as_bytes())
|
||||||
|
);
|
||||||
|
let stored = StoredResourceSecret {
|
||||||
|
secret_id,
|
||||||
|
resource_id: resource_id.to_owned(),
|
||||||
|
epoch,
|
||||||
|
status: "active".to_owned(),
|
||||||
|
created_at_ms: created_at.0,
|
||||||
|
};
|
||||||
|
store.insert_resource_secret(&stored)?;
|
||||||
|
Ok(resource_secret_from_stored(stored))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn resource_secret_from_stored(stored: StoredResourceSecret) -> ResourceMasterSecret {
|
||||||
|
ResourceMasterSecret {
|
||||||
|
id: stored.secret_id.into(),
|
||||||
|
resource: stored.resource_id.into(),
|
||||||
|
epoch: stored.epoch,
|
||||||
|
created_at: UnixMillis(stored.created_at_ms),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn store_auth_op(store: &Store, op: &AuthOp) -> Result<(), NodeError> {
|
fn store_auth_op(store: &Store, op: &AuthOp) -> Result<(), NodeError> {
|
||||||
store.insert_auth_op(&StoredAuthOp {
|
store.insert_auth_op(&StoredAuthOp {
|
||||||
op_id: op.id.to_string(),
|
op_id: op.id.to_string(),
|
||||||
|
|
|
||||||
|
|
@ -382,6 +382,65 @@ impl Store {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn insert_resource_secret(&self, secret: &StoredResourceSecret) -> Result<(), StoreError> {
|
||||||
|
self.conn.execute(
|
||||||
|
r#"INSERT OR REPLACE INTO resource_secrets(
|
||||||
|
secret_id, resource_id, epoch, status, created_at_ms
|
||||||
|
) VALUES (?1, ?2, ?3, ?4, ?5)"#,
|
||||||
|
params![
|
||||||
|
secret.secret_id,
|
||||||
|
secret.resource_id,
|
||||||
|
secret.epoch as i64,
|
||||||
|
secret.status,
|
||||||
|
secret.created_at_ms
|
||||||
|
],
|
||||||
|
)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn list_resource_secrets(&self) -> Result<Vec<StoredResourceSecret>, StoreError> {
|
||||||
|
let mut stmt = self.conn.prepare(
|
||||||
|
r#"SELECT secret_id, resource_id, epoch, status, created_at_ms
|
||||||
|
FROM resource_secrets ORDER BY resource_id, epoch, secret_id"#,
|
||||||
|
)?;
|
||||||
|
let rows = stmt.query_map([], |row| {
|
||||||
|
Ok(StoredResourceSecret {
|
||||||
|
secret_id: row.get(0)?,
|
||||||
|
resource_id: row.get(1)?,
|
||||||
|
epoch: row.get::<_, i64>(2)? as u64,
|
||||||
|
status: row.get(3)?,
|
||||||
|
created_at_ms: row.get(4)?,
|
||||||
|
})
|
||||||
|
})?;
|
||||||
|
rows.collect::<Result<Vec<_>, _>>()
|
||||||
|
.map_err(StoreError::from)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn latest_resource_secret(
|
||||||
|
&self,
|
||||||
|
resource_id: &str,
|
||||||
|
) -> Result<Option<StoredResourceSecret>, StoreError> {
|
||||||
|
let mut stmt = self.conn.prepare(
|
||||||
|
r#"SELECT secret_id, resource_id, epoch, status, created_at_ms
|
||||||
|
FROM resource_secrets
|
||||||
|
WHERE resource_id = ?1
|
||||||
|
ORDER BY epoch DESC, created_at_ms DESC, secret_id DESC
|
||||||
|
LIMIT 1"#,
|
||||||
|
)?;
|
||||||
|
let mut rows = stmt.query(params![resource_id])?;
|
||||||
|
if let Some(row) = rows.next()? {
|
||||||
|
Ok(Some(StoredResourceSecret {
|
||||||
|
secret_id: row.get(0)?,
|
||||||
|
resource_id: row.get(1)?,
|
||||||
|
epoch: row.get::<_, i64>(2)? as u64,
|
||||||
|
status: row.get(3)?,
|
||||||
|
created_at_ms: row.get(4)?,
|
||||||
|
}))
|
||||||
|
} else {
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn record_cas_object(
|
pub fn record_cas_object(
|
||||||
&self,
|
&self,
|
||||||
hash: &str,
|
hash: &str,
|
||||||
|
|
@ -753,6 +812,15 @@ pub struct StoredDocumentResource {
|
||||||
pub updated_at_ms: i64,
|
pub updated_at_ms: i64,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||||
|
pub struct StoredResourceSecret {
|
||||||
|
pub secret_id: String,
|
||||||
|
pub resource_id: String,
|
||||||
|
pub epoch: u64,
|
||||||
|
pub status: String,
|
||||||
|
pub created_at_ms: i64,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||||
pub struct CasObject {
|
pub struct CasObject {
|
||||||
pub hash: String,
|
pub hash: String,
|
||||||
|
|
@ -1072,4 +1140,39 @@ mod tests {
|
||||||
Some(document)
|
Some(document)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn resource_secret_epochs_roundtrip() {
|
||||||
|
let store = Store::open_memory().expect("open");
|
||||||
|
let first = StoredResourceSecret {
|
||||||
|
secret_id: "secret:1".to_owned(),
|
||||||
|
resource_id: "resource:kv:prefs".to_owned(),
|
||||||
|
epoch: 1,
|
||||||
|
status: "active".to_owned(),
|
||||||
|
created_at_ms: 1,
|
||||||
|
};
|
||||||
|
let second = StoredResourceSecret {
|
||||||
|
secret_id: "secret:2".to_owned(),
|
||||||
|
resource_id: "resource:kv:prefs".to_owned(),
|
||||||
|
epoch: 2,
|
||||||
|
status: "active".to_owned(),
|
||||||
|
created_at_ms: 2,
|
||||||
|
};
|
||||||
|
|
||||||
|
store.insert_resource_secret(&first).expect("insert first");
|
||||||
|
store
|
||||||
|
.insert_resource_secret(&second)
|
||||||
|
.expect("insert second");
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
store
|
||||||
|
.latest_resource_secret("resource:kv:prefs")
|
||||||
|
.expect("latest"),
|
||||||
|
Some(second.clone())
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
store.list_resource_secrets().expect("list"),
|
||||||
|
vec![first, second]
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -609,6 +609,64 @@ fn document_create_and_status_use_local_store() {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn secret_create_rotate_and_status_track_resource_epochs() {
|
||||||
|
let home = tempfile::tempdir().expect("tempdir");
|
||||||
|
let paths = geth_config::GethPaths::from_home(home.path());
|
||||||
|
let node = geth_node::init_node(&paths).expect("init node");
|
||||||
|
|
||||||
|
let first = geth_node::handle_request(
|
||||||
|
&node,
|
||||||
|
geth_control::ControlRequest::SecretCreate {
|
||||||
|
resource: "resource:cas:local".to_owned(),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.expect("create secret");
|
||||||
|
match first {
|
||||||
|
geth_control::ControlResponse::SecretCreated { secret } => {
|
||||||
|
assert_eq!(secret.resource.to_string(), "resource:cas:local");
|
||||||
|
assert_eq!(secret.epoch, 1);
|
||||||
|
}
|
||||||
|
other => panic!("unexpected response: {other:?}"),
|
||||||
|
}
|
||||||
|
|
||||||
|
let second = geth_node::handle_request(
|
||||||
|
&node,
|
||||||
|
geth_control::ControlRequest::SecretRotate {
|
||||||
|
resource: "resource:cas:local".to_owned(),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.expect("rotate secret");
|
||||||
|
match second {
|
||||||
|
geth_control::ControlResponse::SecretCreated { secret } => {
|
||||||
|
assert_eq!(secret.resource.to_string(), "resource:cas:local");
|
||||||
|
assert_eq!(secret.epoch, 2);
|
||||||
|
}
|
||||||
|
other => panic!("unexpected response: {other:?}"),
|
||||||
|
}
|
||||||
|
|
||||||
|
let status = geth_node::handle_request(&node, geth_control::ControlRequest::SecretStatus)
|
||||||
|
.expect("secret status");
|
||||||
|
match status {
|
||||||
|
geth_control::ControlResponse::SecretStatus { secrets } => {
|
||||||
|
assert_eq!(secrets.len(), 2);
|
||||||
|
assert_eq!(secrets[0].epoch, 1);
|
||||||
|
assert_eq!(secrets[1].epoch, 2);
|
||||||
|
}
|
||||||
|
other => panic!("unexpected response: {other:?}"),
|
||||||
|
}
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
geth_node::handle_request(
|
||||||
|
&node,
|
||||||
|
geth_control::ControlRequest::SecretCreate {
|
||||||
|
resource: "resource:missing".to_owned(),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.is_err()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn ssh_cert_request_approval_and_revocation_export_use_local_state() {
|
fn ssh_cert_request_approval_and_revocation_export_use_local_state() {
|
||||||
let home = tempfile::tempdir().expect("tempdir");
|
let home = tempfile::tempdir().expect("tempdir");
|
||||||
|
|
|
||||||
|
|
@ -139,7 +139,9 @@ for CLI/control output, but it is not the signed representation.
|
||||||
|
|
||||||
The payload access plane is `geth-secrets`: resource master secrets, epochs,
|
The payload access plane is `geth-secrets`: resource master secrets, epochs,
|
||||||
key envelopes, bearer secrets, and rotation. Revocation for private data is
|
key envelopes, bearer secrets, and rotation. Revocation for private data is
|
||||||
modeled initially as secret epoch rotation.
|
modeled initially as secret epoch rotation. The daemon persists resource secret
|
||||||
|
epoch metadata through `secret create/rotate/status`, but it does not yet store
|
||||||
|
payload key material, encrypt resource data, or distribute key envelopes.
|
||||||
|
|
||||||
## Multi-User Direction
|
## Multi-User Direction
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -174,12 +174,16 @@ resource-scoped capability decisions.
|
||||||
- `[ ]` Future completion requires signed-op validation before accepting
|
- `[ ]` Future completion requires signed-op validation before accepting
|
||||||
replicated auth ops.
|
replicated auth ops.
|
||||||
|
|
||||||
- `[ ]` Resource secrets and bearer invites.
|
- `[~]` Resource secrets and bearer invites.
|
||||||
Acceptance criteria:
|
Acceptance criteria:
|
||||||
- Bearer secrets grant only resource-scoped capabilities.
|
- `[x]` `geth secret create <resource>` records resource secret epoch 1
|
||||||
- Bearer principals cannot mutate trust graph state by default.
|
metadata.
|
||||||
- Secret epoch rotation is represented in durable metadata.
|
- `[x]` `geth secret rotate <resource>` records the next resource secret
|
||||||
- Tests verify bearer access does not imply node identity.
|
epoch.
|
||||||
|
- `[x]` Secret epoch rotation is represented in durable metadata.
|
||||||
|
- `[ ]` Bearer secrets grant only resource-scoped capabilities.
|
||||||
|
- `[ ]` Bearer principals cannot mutate trust graph state by default.
|
||||||
|
- `[ ]` Tests verify bearer access does not imply node identity.
|
||||||
|
|
||||||
- `[~]` SSH certificate and revocation lifecycle.
|
- `[~]` SSH certificate and revocation lifecycle.
|
||||||
Acceptance criteria:
|
Acceptance criteria:
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue