Add resource-scoped bearer access metadata
This commit is contained in:
parent
a05112e6a2
commit
d072843cac
10 changed files with 344 additions and 11 deletions
|
|
@ -126,8 +126,9 @@ Roadmap items should be actionable and checkable:
|
||||||
- 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.
|
- Resource secret epoch metadata can be created, rotated, and listed locally.
|
||||||
Payload encryption, key envelopes, and bearer invite enforcement are still
|
Bearer access metadata can be created/listed/revoked as resource-scoped auth
|
||||||
roadmap work.
|
ops and must not allow trust graph mutation capabilities. Payload encryption,
|
||||||
|
key envelopes, and bearer challenge-response 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
|
||||||
|
|
|
||||||
|
|
@ -79,6 +79,9 @@ The bootstrap implementation provides:
|
||||||
- `geth secret status`
|
- `geth secret status`
|
||||||
- `geth secret create <resource>`
|
- `geth secret create <resource>`
|
||||||
- `geth secret rotate <resource>`
|
- `geth secret rotate <resource>`
|
||||||
|
- `geth secret bearer create <resource> --capability <capability>`
|
||||||
|
- `geth secret bearer list`
|
||||||
|
- `geth secret bearer revoke <resource> <secret>`
|
||||||
- `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>`
|
||||||
|
|
|
||||||
|
|
@ -161,8 +161,32 @@ pub enum AuthCommand {
|
||||||
#[derive(Debug, Subcommand)]
|
#[derive(Debug, Subcommand)]
|
||||||
pub enum SecretCommand {
|
pub enum SecretCommand {
|
||||||
Status,
|
Status,
|
||||||
Create { resource: String },
|
Create {
|
||||||
Rotate { resource: String },
|
resource: String,
|
||||||
|
},
|
||||||
|
Rotate {
|
||||||
|
resource: String,
|
||||||
|
},
|
||||||
|
Bearer {
|
||||||
|
#[command(subcommand)]
|
||||||
|
command: SecretBearerCommand,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Subcommand)]
|
||||||
|
pub enum SecretBearerCommand {
|
||||||
|
Create {
|
||||||
|
resource: String,
|
||||||
|
#[arg(long = "capability", required = true)]
|
||||||
|
capabilities: Vec<String>,
|
||||||
|
#[arg(long)]
|
||||||
|
expires_at_ms: Option<i64>,
|
||||||
|
},
|
||||||
|
List,
|
||||||
|
Revoke {
|
||||||
|
resource: String,
|
||||||
|
secret: String,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Subcommand)]
|
#[derive(Debug, Subcommand)]
|
||||||
|
|
@ -396,6 +420,21 @@ fn request_for_command(command: Command) -> Result<ControlRequest> {
|
||||||
SecretCommand::Status => ControlRequest::SecretStatus,
|
SecretCommand::Status => ControlRequest::SecretStatus,
|
||||||
SecretCommand::Create { resource } => ControlRequest::SecretCreate { resource },
|
SecretCommand::Create { resource } => ControlRequest::SecretCreate { resource },
|
||||||
SecretCommand::Rotate { resource } => ControlRequest::SecretRotate { resource },
|
SecretCommand::Rotate { resource } => ControlRequest::SecretRotate { resource },
|
||||||
|
SecretCommand::Bearer { command } => match command {
|
||||||
|
SecretBearerCommand::Create {
|
||||||
|
resource,
|
||||||
|
capabilities,
|
||||||
|
expires_at_ms,
|
||||||
|
} => ControlRequest::SecretBearerCreate {
|
||||||
|
resource,
|
||||||
|
capabilities,
|
||||||
|
expires_at_ms,
|
||||||
|
},
|
||||||
|
SecretBearerCommand::List => ControlRequest::SecretBearerList,
|
||||||
|
SecretBearerCommand::Revoke { resource, secret } => {
|
||||||
|
ControlRequest::SecretBearerRevoke { resource, secret }
|
||||||
|
}
|
||||||
|
},
|
||||||
},
|
},
|
||||||
Command::Cas { command } => match command {
|
Command::Cas { command } => match command {
|
||||||
CasCommand::Add { path } => ControlRequest::CasAdd { path },
|
CasCommand::Add { path } => ControlRequest::CasAdd { path },
|
||||||
|
|
@ -653,6 +692,46 @@ fn print_response(response: ControlResponse, json: bool) -> Result<()> {
|
||||||
println!("resource: {}", secret.resource);
|
println!("resource: {}", secret.resource);
|
||||||
println!("epoch: {}", secret.epoch);
|
println!("epoch: {}", secret.epoch);
|
||||||
}
|
}
|
||||||
|
ControlResponse::SecretBearerCreated { access } => {
|
||||||
|
println!("bearer secret: {}", access.secret);
|
||||||
|
println!("resource: {}", access.resource);
|
||||||
|
println!(
|
||||||
|
"capabilities: {}",
|
||||||
|
access
|
||||||
|
.capabilities
|
||||||
|
.iter()
|
||||||
|
.map(ToString::to_string)
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join(",")
|
||||||
|
);
|
||||||
|
if let Some(expires_at) = access.expires_at {
|
||||||
|
println!("expires_at_ms: {}", expires_at.0);
|
||||||
|
}
|
||||||
|
println!("may_delegate: {}", access.may_delegate);
|
||||||
|
}
|
||||||
|
ControlResponse::SecretBearerList { access } => {
|
||||||
|
if access.is_empty() {
|
||||||
|
println!("no bearer access");
|
||||||
|
} else {
|
||||||
|
for item in access {
|
||||||
|
println!(
|
||||||
|
"{}\t{}\t{}\tmay_delegate={}",
|
||||||
|
item.secret,
|
||||||
|
item.resource,
|
||||||
|
item.capabilities
|
||||||
|
.iter()
|
||||||
|
.map(ToString::to_string)
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join(","),
|
||||||
|
item.may_delegate
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ControlResponse::SecretBearerRevoked { resource, secret } => {
|
||||||
|
println!("revoked bearer secret: {secret}");
|
||||||
|
println!("resource: {resource}");
|
||||||
|
}
|
||||||
ControlResponse::AuthExplain(explain) => {
|
ControlResponse::AuthExplain(explain) => {
|
||||||
println!("allowed: {}", explain.allowed);
|
println!("allowed: {}", explain.allowed);
|
||||||
println!("subject: {}", explain.subject);
|
println!("subject: {}", explain.subject);
|
||||||
|
|
|
||||||
|
|
@ -4,7 +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_secrets::{BearerAccess, ResourceMasterSecret};
|
||||||
use geth_ssh_identity::{
|
use geth_ssh_identity::{
|
||||||
SshCertApproval, SshCertRequest, SshCertificateRecord, SshRevocationEntry,
|
SshCertApproval, SshCertRequest, SshCertificateRecord, SshRevocationEntry,
|
||||||
};
|
};
|
||||||
|
|
@ -56,6 +56,16 @@ pub enum ControlRequest {
|
||||||
SecretRotate {
|
SecretRotate {
|
||||||
resource: String,
|
resource: String,
|
||||||
},
|
},
|
||||||
|
SecretBearerCreate {
|
||||||
|
resource: String,
|
||||||
|
capabilities: Vec<String>,
|
||||||
|
expires_at_ms: Option<i64>,
|
||||||
|
},
|
||||||
|
SecretBearerList,
|
||||||
|
SecretBearerRevoke {
|
||||||
|
resource: String,
|
||||||
|
secret: String,
|
||||||
|
},
|
||||||
AuthExplain {
|
AuthExplain {
|
||||||
subject: String,
|
subject: String,
|
||||||
resource: String,
|
resource: String,
|
||||||
|
|
@ -181,6 +191,16 @@ pub enum ControlResponse {
|
||||||
SecretCreated {
|
SecretCreated {
|
||||||
secret: ResourceMasterSecret,
|
secret: ResourceMasterSecret,
|
||||||
},
|
},
|
||||||
|
SecretBearerCreated {
|
||||||
|
access: BearerAccess,
|
||||||
|
},
|
||||||
|
SecretBearerList {
|
||||||
|
access: Vec<BearerAccess>,
|
||||||
|
},
|
||||||
|
SecretBearerRevoked {
|
||||||
|
resource: String,
|
||||||
|
secret: String,
|
||||||
|
},
|
||||||
AuthExplain(AuthExplanation),
|
AuthExplain(AuthExplanation),
|
||||||
AuthOpRecorded {
|
AuthOpRecorded {
|
||||||
op: AuthOp,
|
op: AuthOp,
|
||||||
|
|
|
||||||
|
|
@ -14,7 +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_secrets::{BearerAccess, 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,
|
||||||
|
|
@ -71,6 +71,8 @@ pub enum NodeError {
|
||||||
DocumentNotFound(String),
|
DocumentNotFound(String),
|
||||||
#[error("resource not found: {0}")]
|
#[error("resource not found: {0}")]
|
||||||
ResourceNotFound(String),
|
ResourceNotFound(String),
|
||||||
|
#[error("secrets error: {0}")]
|
||||||
|
Secrets(#[from] geth_secrets::SecretsError),
|
||||||
#[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}")]
|
||||||
|
|
@ -360,6 +362,73 @@ pub fn handle_request(
|
||||||
let secret = create_resource_secret(&store, &resource, next_epoch)?;
|
let secret = create_resource_secret(&store, &resource, next_epoch)?;
|
||||||
Ok(ControlResponse::SecretCreated { secret })
|
Ok(ControlResponse::SecretCreated { secret })
|
||||||
}
|
}
|
||||||
|
ControlRequest::SecretBearerCreate {
|
||||||
|
resource,
|
||||||
|
capabilities,
|
||||||
|
expires_at_ms,
|
||||||
|
} => {
|
||||||
|
ensure_resource_exists(&store, &resource)?;
|
||||||
|
let capabilities = capabilities
|
||||||
|
.into_iter()
|
||||||
|
.map(Capability::new)
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
geth_secrets::validate_bearer_capabilities(&capabilities)?;
|
||||||
|
let created_at = UnixMillis(geth_store::now_ms());
|
||||||
|
let secret = geth_types::SecretId::new(format!(
|
||||||
|
"bearer:{}",
|
||||||
|
geth_crypto::blake3_hex(
|
||||||
|
format!(
|
||||||
|
"{resource}\0{}\0{}",
|
||||||
|
capabilities
|
||||||
|
.iter()
|
||||||
|
.map(ToString::to_string)
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join(","),
|
||||||
|
created_at.0
|
||||||
|
)
|
||||||
|
.as_bytes()
|
||||||
|
)
|
||||||
|
));
|
||||||
|
let access = BearerAccess::resource_scoped(
|
||||||
|
secret.clone(),
|
||||||
|
ResourceId::new(resource.clone()),
|
||||||
|
capabilities.clone(),
|
||||||
|
);
|
||||||
|
let op = AuthOp {
|
||||||
|
id: generated_auth_op_id("bearer-create", &resource, secret.as_str(), created_at),
|
||||||
|
resource: ResourceId::new(resource),
|
||||||
|
created_at,
|
||||||
|
kind: AuthOpKind::BearerAccessCreate {
|
||||||
|
secret,
|
||||||
|
capabilities,
|
||||||
|
expires_at: expires_at_ms.map(UnixMillis),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
store_auth_op(&store, &op)?;
|
||||||
|
Ok(ControlResponse::SecretBearerCreated {
|
||||||
|
access: BearerAccess {
|
||||||
|
expires_at: expires_at_ms.map(UnixMillis),
|
||||||
|
..access
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
ControlRequest::SecretBearerList => Ok(ControlResponse::SecretBearerList {
|
||||||
|
access: load_bearer_access(&store)?,
|
||||||
|
}),
|
||||||
|
ControlRequest::SecretBearerRevoke { resource, secret } => {
|
||||||
|
ensure_resource_exists(&store, &resource)?;
|
||||||
|
let created_at = UnixMillis(geth_store::now_ms());
|
||||||
|
let op = AuthOp {
|
||||||
|
id: generated_auth_op_id("bearer-revoke", &resource, &secret, created_at),
|
||||||
|
resource: ResourceId::new(resource.clone()),
|
||||||
|
created_at,
|
||||||
|
kind: AuthOpKind::BearerAccessRevoke {
|
||||||
|
secret: secret.clone().into(),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
store_auth_op(&store, &op)?;
|
||||||
|
Ok(ControlResponse::SecretBearerRevoked { resource, secret })
|
||||||
|
}
|
||||||
ControlRequest::AuthExplain {
|
ControlRequest::AuthExplain {
|
||||||
subject,
|
subject,
|
||||||
resource,
|
resource,
|
||||||
|
|
@ -831,6 +900,26 @@ fn resource_secret_from_stored(stored: StoredResourceSecret) -> ResourceMasterSe
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn load_bearer_access(store: &Store) -> Result<Vec<BearerAccess>, NodeError> {
|
||||||
|
let ops = store
|
||||||
|
.list_auth_ops()?
|
||||||
|
.into_iter()
|
||||||
|
.map(|stored| serde_json::from_str(&stored.op_json).map_err(NodeError::from))
|
||||||
|
.collect::<Result<Vec<AuthOp>, NodeError>>()?;
|
||||||
|
let view = geth_auth::reduce_auth_ops(&ops);
|
||||||
|
Ok(view
|
||||||
|
.bearer_access
|
||||||
|
.into_values()
|
||||||
|
.map(|record| BearerAccess {
|
||||||
|
secret: record.secret,
|
||||||
|
resource: record.resource,
|
||||||
|
capabilities: record.capabilities,
|
||||||
|
expires_at: record.expires_at,
|
||||||
|
may_delegate: false,
|
||||||
|
})
|
||||||
|
.collect())
|
||||||
|
}
|
||||||
|
|
||||||
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(),
|
||||||
|
|
|
||||||
|
|
@ -45,3 +45,55 @@ impl BearerAccess {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, thiserror::Error)]
|
||||||
|
pub enum SecretsError {
|
||||||
|
#[error("bearer access must grant at least one capability")]
|
||||||
|
EmptyBearerCapabilities,
|
||||||
|
#[error("capability is not allowed for bearer access: {0}")]
|
||||||
|
ForbiddenBearerCapability(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn validate_bearer_capabilities(capabilities: &[Capability]) -> Result<(), SecretsError> {
|
||||||
|
if capabilities.is_empty() {
|
||||||
|
return Err(SecretsError::EmptyBearerCapabilities);
|
||||||
|
}
|
||||||
|
for capability in capabilities {
|
||||||
|
let capability = capability.as_str();
|
||||||
|
if matches!(
|
||||||
|
capability,
|
||||||
|
"auth.delegate"
|
||||||
|
| "auth.revoke"
|
||||||
|
| "trust.modify"
|
||||||
|
| "ssh_proxy.admin_shell"
|
||||||
|
| "node.enroll"
|
||||||
|
) {
|
||||||
|
return Err(SecretsError::ForbiddenBearerCapability(
|
||||||
|
capability.to_owned(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn bearer_capabilities_reject_trust_mutation() {
|
||||||
|
assert!(validate_bearer_capabilities(&["kv.read".into(), "kv.write".into()]).is_ok());
|
||||||
|
assert!(matches!(
|
||||||
|
validate_bearer_capabilities(&[]),
|
||||||
|
Err(SecretsError::EmptyBearerCapabilities)
|
||||||
|
));
|
||||||
|
assert!(matches!(
|
||||||
|
validate_bearer_capabilities(&["auth.delegate".into()]),
|
||||||
|
Err(SecretsError::ForbiddenBearerCapability(_))
|
||||||
|
));
|
||||||
|
assert!(matches!(
|
||||||
|
validate_bearer_capabilities(&["node.enroll".into()]),
|
||||||
|
Err(SecretsError::ForbiddenBearerCapability(_))
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -582,6 +582,23 @@ impl Store {
|
||||||
.map_err(StoreError::from)
|
.map_err(StoreError::from)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn list_auth_ops(&self) -> Result<Vec<StoredAuthOp>, StoreError> {
|
||||||
|
let mut stmt = self.conn.prepare(
|
||||||
|
r#"SELECT op_id, resource_id, op_json, created_at_ms
|
||||||
|
FROM auth_ops ORDER BY resource_id, created_at_ms, op_id"#,
|
||||||
|
)?;
|
||||||
|
let rows = stmt.query_map([], |row| {
|
||||||
|
Ok(StoredAuthOp {
|
||||||
|
op_id: row.get(0)?,
|
||||||
|
resource_id: row.get(1)?,
|
||||||
|
op_json: row.get(2)?,
|
||||||
|
created_at_ms: row.get(3)?,
|
||||||
|
})
|
||||||
|
})?;
|
||||||
|
rows.collect::<Result<Vec<_>, _>>()
|
||||||
|
.map_err(StoreError::from)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn insert_keychain_op(&self, op: &StoredKeychainOp) -> Result<(), StoreError> {
|
pub fn insert_keychain_op(&self, op: &StoredKeychainOp) -> Result<(), StoreError> {
|
||||||
self.conn.execute(
|
self.conn.execute(
|
||||||
r#"INSERT OR REPLACE INTO keychain_ops(op_id, op_json, created_at_ms)
|
r#"INSERT OR REPLACE INTO keychain_ops(op_id, op_json, created_at_ms)
|
||||||
|
|
@ -995,6 +1012,7 @@ mod tests {
|
||||||
.expect("list auth ops"),
|
.expect("list auth ops"),
|
||||||
vec![first]
|
vec![first]
|
||||||
);
|
);
|
||||||
|
assert_eq!(store.list_auth_ops().expect("list all auth ops").len(), 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
|
||||||
|
|
@ -667,6 +667,73 @@ fn secret_create_rotate_and_status_track_resource_epochs() {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn bearer_access_create_list_revoke_uses_resource_scoped_auth_ops() {
|
||||||
|
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 response = geth_node::handle_request(
|
||||||
|
&node,
|
||||||
|
geth_control::ControlRequest::SecretBearerCreate {
|
||||||
|
resource: "resource:cas:local".to_owned(),
|
||||||
|
capabilities: vec!["cas.fetch".to_owned(), "cas.pin".to_owned()],
|
||||||
|
expires_at_ms: Some(1234),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.expect("create bearer");
|
||||||
|
let secret = match response {
|
||||||
|
geth_control::ControlResponse::SecretBearerCreated { access } => {
|
||||||
|
assert_eq!(access.resource.to_string(), "resource:cas:local");
|
||||||
|
assert_eq!(access.capabilities.len(), 2);
|
||||||
|
assert_eq!(access.expires_at.map(|expires_at| expires_at.0), Some(1234));
|
||||||
|
assert!(!access.may_delegate);
|
||||||
|
access.secret.to_string()
|
||||||
|
}
|
||||||
|
other => panic!("unexpected response: {other:?}"),
|
||||||
|
};
|
||||||
|
|
||||||
|
let response = geth_node::handle_request(&node, geth_control::ControlRequest::SecretBearerList)
|
||||||
|
.expect("list bearer");
|
||||||
|
match response {
|
||||||
|
geth_control::ControlResponse::SecretBearerList { access } => {
|
||||||
|
assert_eq!(access.len(), 1);
|
||||||
|
assert_eq!(access[0].secret.to_string(), secret);
|
||||||
|
assert!(!access[0].may_delegate);
|
||||||
|
}
|
||||||
|
other => panic!("unexpected response: {other:?}"),
|
||||||
|
}
|
||||||
|
|
||||||
|
geth_node::handle_request(
|
||||||
|
&node,
|
||||||
|
geth_control::ControlRequest::SecretBearerRevoke {
|
||||||
|
resource: "resource:cas:local".to_owned(),
|
||||||
|
secret: secret.clone(),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.expect("revoke bearer");
|
||||||
|
let response = geth_node::handle_request(&node, geth_control::ControlRequest::SecretBearerList)
|
||||||
|
.expect("list revoked bearer");
|
||||||
|
match response {
|
||||||
|
geth_control::ControlResponse::SecretBearerList { access } => {
|
||||||
|
assert!(access.is_empty());
|
||||||
|
}
|
||||||
|
other => panic!("unexpected response: {other:?}"),
|
||||||
|
}
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
geth_node::handle_request(
|
||||||
|
&node,
|
||||||
|
geth_control::ControlRequest::SecretBearerCreate {
|
||||||
|
resource: "resource:cas:local".to_owned(),
|
||||||
|
capabilities: vec!["auth.delegate".to_owned()],
|
||||||
|
expires_at_ms: None,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.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");
|
||||||
|
|
|
||||||
|
|
@ -140,8 +140,10 @@ 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. The daemon persists resource secret
|
modeled initially as secret epoch rotation. The daemon persists resource secret
|
||||||
epoch metadata through `secret create/rotate/status`, but it does not yet store
|
epoch metadata through `secret create/rotate/status`. Bearer access is recorded
|
||||||
payload key material, encrypt resource data, or distribute key envelopes.
|
as resource-scoped auth operations and rejects trust-mutation capabilities such
|
||||||
|
as `auth.delegate`, `auth.revoke`, and `node.enroll`. The daemon does not yet
|
||||||
|
store payload key material, encrypt resource data, or distribute key envelopes.
|
||||||
|
|
||||||
## Multi-User Direction
|
## Multi-User Direction
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -181,9 +181,11 @@ resource-scoped capability decisions.
|
||||||
- `[x]` `geth secret rotate <resource>` records the next resource secret
|
- `[x]` `geth secret rotate <resource>` records the next resource secret
|
||||||
epoch.
|
epoch.
|
||||||
- `[x]` Secret epoch rotation is represented in durable metadata.
|
- `[x]` Secret epoch rotation is represented in durable metadata.
|
||||||
- `[ ]` Bearer secrets grant only resource-scoped capabilities.
|
- `[x]` Bearer secrets grant only resource-scoped capabilities.
|
||||||
- `[ ]` Bearer principals cannot mutate trust graph state by default.
|
- `[x]` Bearer principals cannot mutate trust graph state by default.
|
||||||
- `[ ]` Tests verify bearer access does not imply node identity.
|
- `[x]` Tests verify bearer access does not imply node identity.
|
||||||
|
- `[ ]` Future completion requires bearer challenge-response proof instead
|
||||||
|
of metadata-only local records.
|
||||||
|
|
||||||
- `[~]` SSH certificate and revocation lifecycle.
|
- `[~]` SSH certificate and revocation lifecycle.
|
||||||
Acceptance criteria:
|
Acceptance criteria:
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue