Wire auth explain to local auth ops
This commit is contained in:
parent
187b99eb8d
commit
f54920bb65
9 changed files with 299 additions and 19 deletions
|
|
@ -111,7 +111,9 @@ Roadmap items should be actionable and checkable:
|
||||||
payloads. The keychain reducer builds an active identity view for admin keys,
|
payloads. The keychain reducer builds an active identity view for admin keys,
|
||||||
users, devices, nodes, agents, and endpoint bindings.
|
users, devices, nodes, agents, and endpoint bindings.
|
||||||
- The auth reducer builds a current permission view for resources, grants,
|
- The auth reducer builds a current permission view for resources, grants,
|
||||||
groups, and bearer access. Daemon-side enforcement is still roadmap work.
|
groups, and bearer access. The daemon persists local auth grant/revoke ops
|
||||||
|
and uses them for `auth explain`. Signature validation and daemon-side module
|
||||||
|
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
|
||||||
|
|
|
||||||
|
|
@ -76,6 +76,8 @@ The bootstrap implementation provides:
|
||||||
- `geth resource create <kind> <name>`
|
- `geth resource create <kind> <name>`
|
||||||
- `geth keychain status`
|
- `geth keychain status`
|
||||||
- `geth auth explain <subject> <resource> <capability>`
|
- `geth auth explain <subject> <resource> <capability>`
|
||||||
|
- `geth auth grant <subject> <resource> <capability> [--grant-id <id>]`
|
||||||
|
- `geth auth revoke <resource> <grant-id>`
|
||||||
- local filesystem CAS commands: `add`, `get`, `hash`, `has`, `list`
|
- local filesystem CAS commands: `add`, `get`, `hash`, `has`, `list`
|
||||||
- SSH certificate flow metadata:
|
- SSH certificate flow metadata:
|
||||||
- `geth ssh cert request --public-key <path> --principal <name>`
|
- `geth ssh cert request --public-key <path> --principal <name>`
|
||||||
|
|
|
||||||
|
|
@ -142,6 +142,17 @@ pub enum AuthCommand {
|
||||||
resource: String,
|
resource: String,
|
||||||
capability: String,
|
capability: String,
|
||||||
},
|
},
|
||||||
|
Grant {
|
||||||
|
subject: String,
|
||||||
|
resource: String,
|
||||||
|
capability: String,
|
||||||
|
#[arg(long)]
|
||||||
|
grant_id: Option<String>,
|
||||||
|
},
|
||||||
|
Revoke {
|
||||||
|
resource: String,
|
||||||
|
grant_id: String,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Subcommand)]
|
#[derive(Debug, Subcommand)]
|
||||||
|
|
@ -350,6 +361,23 @@ fn request_for_command(command: Command) -> Result<ControlRequest> {
|
||||||
resource,
|
resource,
|
||||||
capability,
|
capability,
|
||||||
},
|
},
|
||||||
|
Command::Auth {
|
||||||
|
command:
|
||||||
|
AuthCommand::Grant {
|
||||||
|
subject,
|
||||||
|
resource,
|
||||||
|
capability,
|
||||||
|
grant_id,
|
||||||
|
},
|
||||||
|
} => ControlRequest::AuthGrant {
|
||||||
|
subject,
|
||||||
|
resource,
|
||||||
|
capability,
|
||||||
|
grant_id,
|
||||||
|
},
|
||||||
|
Command::Auth {
|
||||||
|
command: AuthCommand::Revoke { resource, grant_id },
|
||||||
|
} => ControlRequest::AuthRevoke { resource, grant_id },
|
||||||
Command::Secret { command } => ControlRequest::ModuleStub {
|
Command::Secret { command } => ControlRequest::ModuleStub {
|
||||||
module: "secret".to_owned(),
|
module: "secret".to_owned(),
|
||||||
command: format!("{command:?}"),
|
command: format!("{command:?}"),
|
||||||
|
|
@ -575,6 +603,10 @@ fn print_response(response: ControlResponse, json: bool) -> Result<()> {
|
||||||
println!("reason: {}", explain.reason);
|
println!("reason: {}", explain.reason);
|
||||||
println!("evaluated_ops: {}", explain.evaluated_ops);
|
println!("evaluated_ops: {}", explain.evaluated_ops);
|
||||||
}
|
}
|
||||||
|
ControlResponse::AuthOpRecorded { op } => {
|
||||||
|
println!("recorded auth op: {}", op.id);
|
||||||
|
println!("resource: {}", op.resource);
|
||||||
|
}
|
||||||
ControlResponse::SshCertRequested { request } => {
|
ControlResponse::SshCertRequested { request } => {
|
||||||
println!("ssh cert request: {}", request.id);
|
println!("ssh cert request: {}", request.id);
|
||||||
println!("status: {}", request.status);
|
println!("status: {}", request.status);
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
use geth_auth::AuthExplanation;
|
use geth_auth::{AuthExplanation, AuthOp};
|
||||||
use geth_resource::ResourceDescriptor;
|
use geth_resource::ResourceDescriptor;
|
||||||
use geth_ssh_identity::{
|
use geth_ssh_identity::{
|
||||||
SshCertApproval, SshCertRequest, SshCertificateRecord, SshRevocationEntry,
|
SshCertApproval, SshCertRequest, SshCertificateRecord, SshRevocationEntry,
|
||||||
|
|
@ -37,6 +37,16 @@ pub enum ControlRequest {
|
||||||
resource: String,
|
resource: String,
|
||||||
capability: String,
|
capability: String,
|
||||||
},
|
},
|
||||||
|
AuthGrant {
|
||||||
|
subject: String,
|
||||||
|
resource: String,
|
||||||
|
capability: String,
|
||||||
|
grant_id: Option<String>,
|
||||||
|
},
|
||||||
|
AuthRevoke {
|
||||||
|
resource: String,
|
||||||
|
grant_id: String,
|
||||||
|
},
|
||||||
SshCertRequest {
|
SshCertRequest {
|
||||||
public_key_path: PathBuf,
|
public_key_path: PathBuf,
|
||||||
cert_kind: String,
|
cert_kind: String,
|
||||||
|
|
@ -105,6 +115,9 @@ pub enum ControlResponse {
|
||||||
},
|
},
|
||||||
KeychainStatus(KeychainStatusResponse),
|
KeychainStatus(KeychainStatusResponse),
|
||||||
AuthExplain(AuthExplanation),
|
AuthExplain(AuthExplanation),
|
||||||
|
AuthOpRecorded {
|
||||||
|
op: AuthOp,
|
||||||
|
},
|
||||||
SshCertRequested {
|
SshCertRequested {
|
||||||
request: SshCertRequest,
|
request: SshCertRequest,
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
pub mod service;
|
pub mod service;
|
||||||
|
|
||||||
use geth_auth::AuthExplanation;
|
use geth_auth::{AuthExplanation, AuthOp, AuthOpKind};
|
||||||
use geth_cas::{LocalCas, hash_path};
|
use geth_cas::{LocalCas, hash_path};
|
||||||
use geth_config::{GethConfig, GethPaths, RelayMode};
|
use geth_config::{GethConfig, GethPaths, RelayMode};
|
||||||
use geth_control::{
|
use geth_control::{
|
||||||
|
|
@ -16,10 +16,12 @@ use geth_ssh_identity::{
|
||||||
certificate_id, revocation_id, ssh_public_key_fingerprint,
|
certificate_id, revocation_id, ssh_public_key_fingerprint,
|
||||||
};
|
};
|
||||||
use geth_store::{
|
use geth_store::{
|
||||||
Store, StoredResource, StoredSshCertRequest, StoredSshCertificate, StoredSshRevocation,
|
Store, StoredAuthOp, StoredResource, StoredSshCertRequest, StoredSshCertificate,
|
||||||
|
StoredSshRevocation,
|
||||||
};
|
};
|
||||||
use geth_types::{
|
use geth_types::{
|
||||||
NodeId, ResourceId, ResourceKind, ResourceName, SshCertId, SshCertRequestId, UnixMillis,
|
AuthOpId, Capability, NodeId, PrincipalId, ResourceId, ResourceKind, ResourceName, SshCertId,
|
||||||
|
SshCertRequestId, UnixMillis,
|
||||||
};
|
};
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
|
use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
|
||||||
|
|
@ -249,7 +251,10 @@ pub fn handle_request(
|
||||||
resource,
|
resource,
|
||||||
capability,
|
capability,
|
||||||
} => {
|
} => {
|
||||||
if store.get_peer_card(&subject)?.is_some() {
|
let ops = load_auth_ops_for_resource(&store, &resource)?;
|
||||||
|
let discovered = store.get_peer_card(&subject)?.is_some();
|
||||||
|
if ops.is_empty() {
|
||||||
|
if discovered {
|
||||||
Ok(ControlResponse::AuthExplain(
|
Ok(ControlResponse::AuthExplain(
|
||||||
AuthExplanation::discovered_candidate(subject, resource, capability),
|
AuthExplanation::discovered_candidate(subject, resource, capability),
|
||||||
))
|
))
|
||||||
|
|
@ -258,6 +263,54 @@ pub fn handle_request(
|
||||||
subject, resource, capability,
|
subject, resource, capability,
|
||||||
)))
|
)))
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
let mut explanation = geth_auth::explain_auth_ops(
|
||||||
|
&ops,
|
||||||
|
PrincipalId::new(subject.clone()),
|
||||||
|
ResourceId::new(resource.clone()),
|
||||||
|
Capability::new(capability.clone()),
|
||||||
|
);
|
||||||
|
if discovered && !explanation.allowed {
|
||||||
|
explanation.reason = format!(
|
||||||
|
"subject is a discovered peer candidate only; discovery does not grant trust or authorization; {}",
|
||||||
|
explanation.reason
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Ok(ControlResponse::AuthExplain(explanation))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ControlRequest::AuthGrant {
|
||||||
|
subject,
|
||||||
|
resource,
|
||||||
|
capability,
|
||||||
|
grant_id,
|
||||||
|
} => {
|
||||||
|
let created_at = UnixMillis(geth_store::now_ms());
|
||||||
|
let grant_id =
|
||||||
|
grant_id.unwrap_or_else(|| generated_grant_id(&subject, &resource, &capability));
|
||||||
|
let op = AuthOp {
|
||||||
|
id: generated_auth_op_id("grant-create", &resource, &grant_id, created_at),
|
||||||
|
resource: ResourceId::new(resource),
|
||||||
|
created_at,
|
||||||
|
kind: AuthOpKind::GrantCreate {
|
||||||
|
grant_id,
|
||||||
|
principal: PrincipalId::new(subject),
|
||||||
|
capabilities: vec![Capability::new(capability)],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
store_auth_op(&store, &op)?;
|
||||||
|
Ok(ControlResponse::AuthOpRecorded { op })
|
||||||
|
}
|
||||||
|
ControlRequest::AuthRevoke { resource, grant_id } => {
|
||||||
|
let created_at = UnixMillis(geth_store::now_ms());
|
||||||
|
let op = AuthOp {
|
||||||
|
id: generated_auth_op_id("grant-revoke", &resource, &grant_id, created_at),
|
||||||
|
resource: ResourceId::new(resource),
|
||||||
|
created_at,
|
||||||
|
kind: AuthOpKind::GrantRevoke { grant_id },
|
||||||
|
};
|
||||||
|
store_auth_op(&store, &op)?;
|
||||||
|
Ok(ControlResponse::AuthOpRecorded { op })
|
||||||
}
|
}
|
||||||
ControlRequest::SshCertRequest {
|
ControlRequest::SshCertRequest {
|
||||||
public_key_path,
|
public_key_path,
|
||||||
|
|
@ -447,6 +500,45 @@ fn stored_resource_to_descriptor(stored: StoredResource) -> Result<ResourceDescr
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn store_auth_op(store: &Store, op: &AuthOp) -> Result<(), NodeError> {
|
||||||
|
store.insert_auth_op(&StoredAuthOp {
|
||||||
|
op_id: op.id.to_string(),
|
||||||
|
resource_id: op.resource.to_string(),
|
||||||
|
op_json: serde_json::to_string(op)?,
|
||||||
|
created_at_ms: op.created_at.0,
|
||||||
|
})?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn load_auth_ops_for_resource(store: &Store, resource: &str) -> Result<Vec<AuthOp>, NodeError> {
|
||||||
|
store
|
||||||
|
.list_auth_ops_for_resource(resource)?
|
||||||
|
.into_iter()
|
||||||
|
.map(|stored| serde_json::from_str(&stored.op_json).map_err(NodeError::from))
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn generated_grant_id(subject: &str, resource: &str, capability: &str) -> String {
|
||||||
|
format!(
|
||||||
|
"grant:{}",
|
||||||
|
geth_crypto::blake3_hex(format!("{subject}\0{resource}\0{capability}").as_bytes())
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn generated_auth_op_id(
|
||||||
|
kind: &str,
|
||||||
|
resource: &str,
|
||||||
|
stable_id: &str,
|
||||||
|
created_at: UnixMillis,
|
||||||
|
) -> AuthOpId {
|
||||||
|
AuthOpId::new(format!(
|
||||||
|
"auth-op:{}",
|
||||||
|
geth_crypto::blake3_hex(
|
||||||
|
format!("{}\0{kind}\0{resource}\0{stable_id}", created_at.0).as_bytes()
|
||||||
|
)
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
fn stable_node_id(agent_id: &str) -> String {
|
fn stable_node_id(agent_id: &str) -> String {
|
||||||
format!("node:{agent_id}")
|
format!("node:{agent_id}")
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -289,6 +289,35 @@ impl Store {
|
||||||
.map_err(StoreError::from)
|
.map_err(StoreError::from)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn insert_auth_op(&self, op: &StoredAuthOp) -> Result<(), StoreError> {
|
||||||
|
self.conn.execute(
|
||||||
|
r#"INSERT OR REPLACE INTO auth_ops(op_id, resource_id, op_json, created_at_ms)
|
||||||
|
VALUES (?1, ?2, ?3, ?4)"#,
|
||||||
|
params![op.op_id, op.resource_id, op.op_json, op.created_at_ms],
|
||||||
|
)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn list_auth_ops_for_resource(
|
||||||
|
&self,
|
||||||
|
resource_id: &str,
|
||||||
|
) -> Result<Vec<StoredAuthOp>, StoreError> {
|
||||||
|
let mut stmt = self.conn.prepare(
|
||||||
|
r#"SELECT op_id, resource_id, op_json, created_at_ms
|
||||||
|
FROM auth_ops WHERE resource_id = ?1 ORDER BY created_at_ms, op_id"#,
|
||||||
|
)?;
|
||||||
|
let rows = stmt.query_map(params![resource_id], |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_ssh_cert_request(
|
pub fn insert_ssh_cert_request(
|
||||||
&self,
|
&self,
|
||||||
request: &StoredSshCertRequest,
|
request: &StoredSshCertRequest,
|
||||||
|
|
@ -476,6 +505,14 @@ pub struct StoredPeerCard {
|
||||||
pub updated_at_ms: i64,
|
pub updated_at_ms: i64,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||||
|
pub struct StoredAuthOp {
|
||||||
|
pub op_id: String,
|
||||||
|
pub resource_id: String,
|
||||||
|
pub op_json: String,
|
||||||
|
pub created_at_ms: i64,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||||
pub struct StoredSshCertRequest {
|
pub struct StoredSshCertRequest {
|
||||||
pub request_id: String,
|
pub request_id: String,
|
||||||
|
|
@ -589,4 +626,31 @@ mod tests {
|
||||||
);
|
);
|
||||||
assert_eq!(store.list_peer_cards().expect("list"), vec![peer_card]);
|
assert_eq!(store.list_peer_cards().expect("list"), vec![peer_card]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn auth_ops_roundtrip_by_resource() {
|
||||||
|
let store = Store::open_memory().expect("open");
|
||||||
|
let first = StoredAuthOp {
|
||||||
|
op_id: "op:auth:1".to_owned(),
|
||||||
|
resource_id: "resource:notes".to_owned(),
|
||||||
|
op_json: r#"{"id":"op:auth:1"}"#.to_owned(),
|
||||||
|
created_at_ms: 1,
|
||||||
|
};
|
||||||
|
let second = StoredAuthOp {
|
||||||
|
op_id: "op:auth:2".to_owned(),
|
||||||
|
resource_id: "resource:other".to_owned(),
|
||||||
|
op_json: r#"{"id":"op:auth:2"}"#.to_owned(),
|
||||||
|
created_at_ms: 2,
|
||||||
|
};
|
||||||
|
|
||||||
|
store.insert_auth_op(&second).expect("insert second");
|
||||||
|
store.insert_auth_op(&first).expect("insert first");
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
store
|
||||||
|
.list_auth_ops_for_resource("resource:notes")
|
||||||
|
.expect("list auth ops"),
|
||||||
|
vec![first]
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -154,6 +154,75 @@ fn auth_explain_distinguishes_discovered_peer_candidates() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn auth_grant_revoke_and_explain_use_local_auth_log() {
|
||||||
|
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::AuthGrant {
|
||||||
|
subject: "node:laptop".to_owned(),
|
||||||
|
resource: "resource:cas:local".to_owned(),
|
||||||
|
capability: "cas.fetch".to_owned(),
|
||||||
|
grant_id: Some("grant:test-fetch".to_owned()),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.expect("grant capability");
|
||||||
|
match response {
|
||||||
|
geth_control::ControlResponse::AuthOpRecorded { op } => {
|
||||||
|
assert_eq!(op.resource.to_string(), "resource:cas:local");
|
||||||
|
}
|
||||||
|
other => panic!("unexpected response: {other:?}"),
|
||||||
|
}
|
||||||
|
|
||||||
|
let response = geth_node::handle_request(
|
||||||
|
&node,
|
||||||
|
geth_control::ControlRequest::AuthExplain {
|
||||||
|
subject: "node:laptop".to_owned(),
|
||||||
|
resource: "resource:cas:local".to_owned(),
|
||||||
|
capability: "cas.fetch".to_owned(),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.expect("explain grant");
|
||||||
|
match response {
|
||||||
|
geth_control::ControlResponse::AuthExplain(explanation) => {
|
||||||
|
assert!(explanation.allowed);
|
||||||
|
assert_eq!(explanation.evaluated_ops, 1);
|
||||||
|
assert!(explanation.reason.contains("grant:test-fetch"));
|
||||||
|
}
|
||||||
|
other => panic!("unexpected response: {other:?}"),
|
||||||
|
}
|
||||||
|
|
||||||
|
geth_node::handle_request(
|
||||||
|
&node,
|
||||||
|
geth_control::ControlRequest::AuthRevoke {
|
||||||
|
resource: "resource:cas:local".to_owned(),
|
||||||
|
grant_id: "grant:test-fetch".to_owned(),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.expect("revoke grant");
|
||||||
|
|
||||||
|
let response = geth_node::handle_request(
|
||||||
|
&node,
|
||||||
|
geth_control::ControlRequest::AuthExplain {
|
||||||
|
subject: "node:laptop".to_owned(),
|
||||||
|
resource: "resource:cas:local".to_owned(),
|
||||||
|
capability: "cas.fetch".to_owned(),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.expect("explain revoke");
|
||||||
|
match response {
|
||||||
|
geth_control::ControlResponse::AuthExplain(explanation) => {
|
||||||
|
assert!(!explanation.allowed);
|
||||||
|
assert_eq!(explanation.evaluated_ops, 2);
|
||||||
|
assert!(explanation.reason.contains("no active"));
|
||||||
|
}
|
||||||
|
other => panic!("unexpected response: {other:?}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[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");
|
||||||
|
|
|
||||||
|
|
@ -110,8 +110,10 @@ bindings. Revoked identity subtrees are excluded from that active view.
|
||||||
The authorization plane is `geth-auth`: resource-local signed operation logs,
|
The authorization plane is `geth-auth`: resource-local signed operation logs,
|
||||||
grants, revocations, groups, and `auth explain`. Auth operations reduce into a
|
grants, revocations, groups, and `auth explain`. Auth operations reduce into a
|
||||||
current permission view for resources, grants, groups, and bearer access. The
|
current permission view for resources, grants, groups, and bearer access. The
|
||||||
library can explain direct and group grants, while daemon-side enforcement and
|
library can explain direct and group grants. The daemon persists local auth
|
||||||
durable auth-log storage are still future work.
|
grant/revoke operations and `geth auth explain` evaluates that local operation
|
||||||
|
log. Signature validation, replication, and module enforcement are still future
|
||||||
|
work.
|
||||||
|
|
||||||
Both keychain and auth operations use `geth-codec` canonical envelopes for
|
Both keychain and auth operations use `geth-codec` canonical envelopes for
|
||||||
signature payloads. The envelope includes a version, an explicit signature
|
signature payloads. The envelope includes a version, an explicit signature
|
||||||
|
|
|
||||||
|
|
@ -159,11 +159,15 @@ resource-scoped capability decisions.
|
||||||
`kv.write_prefix:apps/foo/`.
|
`kv.write_prefix:apps/foo/`.
|
||||||
- Tests cover grant, revoke, group membership, and denied access.
|
- Tests cover grant, revoke, group membership, and denied access.
|
||||||
|
|
||||||
- `[ ]` `auth explain` real decision path.
|
- `[~]` `auth explain` real decision path.
|
||||||
Acceptance criteria:
|
Acceptance criteria:
|
||||||
- `geth auth explain <subject> <resource> <capability>` reports allowed/denied.
|
- `[x]` `geth auth grant` and `geth auth revoke` persist local auth ops.
|
||||||
- Output includes the operation chain or missing grant that caused the result.
|
- `[x]` `geth auth explain <subject> <resource> <capability>` reports
|
||||||
- JSON output is stable enough for tests and scripts.
|
allowed/denied from the local auth-op reducer when local ops exist.
|
||||||
|
- `[x]` Output includes the grant ID or missing grant that caused the result.
|
||||||
|
- `[x]` JSON output is stable enough for tests and scripts.
|
||||||
|
- `[ ]` Future completion requires signed-op validation before accepting
|
||||||
|
replicated auth ops.
|
||||||
|
|
||||||
- `[ ]` Resource secrets and bearer invites.
|
- `[ ]` Resource secrets and bearer invites.
|
||||||
Acceptance criteria:
|
Acceptance criteria:
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue