Wire keychain status to local keychain ops
This commit is contained in:
parent
f54920bb65
commit
99f6dee26f
12 changed files with 191 additions and 20 deletions
|
|
@ -131,7 +131,10 @@ pub enum ResourceCommand {
|
|||
|
||||
#[derive(Debug, Subcommand)]
|
||||
pub enum KeychainCommand {
|
||||
Init,
|
||||
Init {
|
||||
#[arg(long)]
|
||||
admin_key: Option<PathBuf>,
|
||||
},
|
||||
Status,
|
||||
}
|
||||
|
||||
|
|
@ -341,10 +344,9 @@ fn request_for_command(command: Command) -> Result<ControlRequest> {
|
|||
command: ResourceCommand::Create { kind, name },
|
||||
} => ControlRequest::ResourceCreate { kind, name },
|
||||
Command::Keychain {
|
||||
command: KeychainCommand::Init,
|
||||
} => ControlRequest::ModuleStub {
|
||||
module: "keychain".to_owned(),
|
||||
command: "init".to_owned(),
|
||||
command: KeychainCommand::Init { admin_key },
|
||||
} => ControlRequest::KeychainInit {
|
||||
admin_key_path: admin_key,
|
||||
},
|
||||
Command::Keychain {
|
||||
command: KeychainCommand::Status,
|
||||
|
|
@ -595,6 +597,12 @@ fn print_response(response: ControlResponse, json: bool) -> Result<()> {
|
|||
println!("devices: {}", status.devices);
|
||||
println!("nodes: {}", status.nodes);
|
||||
}
|
||||
ControlResponse::KeychainInitialized { ops } => {
|
||||
println!("initialized keychain");
|
||||
for op in ops {
|
||||
println!("recorded keychain op: {}", op.id);
|
||||
}
|
||||
}
|
||||
ControlResponse::AuthExplain(explain) => {
|
||||
println!("allowed: {}", explain.allowed);
|
||||
println!("subject: {}", explain.subject);
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ serde.workspace = true
|
|||
serde_json.workspace = true
|
||||
thiserror.workspace = true
|
||||
geth-auth = { path = "../geth-auth" }
|
||||
geth-keychain = { path = "../geth-keychain" }
|
||||
geth-resource = { path = "../geth-resource" }
|
||||
geth-ssh-identity = { path = "../geth-ssh-identity" }
|
||||
geth-types = { path = "../geth-types" }
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
use geth_auth::{AuthExplanation, AuthOp};
|
||||
use geth_keychain::KeychainOp;
|
||||
use geth_resource::ResourceDescriptor;
|
||||
use geth_ssh_identity::{
|
||||
SshCertApproval, SshCertRequest, SshCertificateRecord, SshRevocationEntry,
|
||||
|
|
@ -31,6 +32,9 @@ pub enum ControlRequest {
|
|||
hash: BlobHash,
|
||||
},
|
||||
CasList,
|
||||
KeychainInit {
|
||||
admin_key_path: Option<PathBuf>,
|
||||
},
|
||||
KeychainStatus,
|
||||
AuthExplain {
|
||||
subject: String,
|
||||
|
|
@ -114,6 +118,9 @@ pub enum ControlResponse {
|
|||
blobs: Vec<CasBlob>,
|
||||
},
|
||||
KeychainStatus(KeychainStatusResponse),
|
||||
KeychainInitialized {
|
||||
ops: Vec<KeychainOp>,
|
||||
},
|
||||
AuthExplain(AuthExplanation),
|
||||
AuthOpRecorded {
|
||||
op: AuthOp,
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ geth-config = { path = "../geth-config" }
|
|||
geth-control = { path = "../geth-control" }
|
||||
geth-crypto = { path = "../geth-crypto" }
|
||||
geth-iroh = { path = "../geth-iroh" }
|
||||
geth-keychain = { path = "../geth-keychain" }
|
||||
geth-resource = { path = "../geth-resource" }
|
||||
geth-ssh-identity = { path = "../geth-ssh-identity" }
|
||||
geth-store = { path = "../geth-store" }
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ use geth_control::{
|
|||
};
|
||||
use geth_crypto::AgentKey;
|
||||
use geth_iroh::{EndpointStatus, GethIrohConfig, GethIrohEndpoint, GethRelayMode};
|
||||
use geth_keychain::{KeychainOp, KeychainOpKind};
|
||||
use geth_resource::ResourceDescriptor;
|
||||
use geth_ssh_identity::{
|
||||
SshCertApproval, SshCertKind, SshCertRequest, SshCertRequestStatus, SshCertificateRecord,
|
||||
|
|
@ -16,12 +17,12 @@ use geth_ssh_identity::{
|
|||
certificate_id, revocation_id, ssh_public_key_fingerprint,
|
||||
};
|
||||
use geth_store::{
|
||||
Store, StoredAuthOp, StoredResource, StoredSshCertRequest, StoredSshCertificate,
|
||||
StoredSshRevocation,
|
||||
Store, StoredAuthOp, StoredKeychainOp, StoredResource, StoredSshCertRequest,
|
||||
StoredSshCertificate, StoredSshRevocation,
|
||||
};
|
||||
use geth_types::{
|
||||
AuthOpId, Capability, NodeId, PrincipalId, ResourceId, ResourceKind, ResourceName, SshCertId,
|
||||
SshCertRequestId, UnixMillis,
|
||||
AuthOpId, Capability, KeyId, NodeId, PrincipalId, ResourceId, ResourceKind, ResourceName,
|
||||
SshCertId, SshCertRequestId, UnixMillis,
|
||||
};
|
||||
use std::path::Path;
|
||||
use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
|
||||
|
|
@ -237,13 +238,40 @@ pub fn handle_request(
|
|||
.collect(),
|
||||
})
|
||||
}
|
||||
ControlRequest::KeychainInit { admin_key_path } => {
|
||||
let mut ops = Vec::new();
|
||||
let created_at = UnixMillis(geth_store::now_ms());
|
||||
let init = KeychainOp {
|
||||
id: generated_keychain_op_id("keychain-init", "local", created_at),
|
||||
created_at,
|
||||
kind: KeychainOpKind::KeychainInit,
|
||||
};
|
||||
store_keychain_op(&store, &init)?;
|
||||
ops.push(init);
|
||||
|
||||
if let Some(admin_key_path) = admin_key_path {
|
||||
let public_key = std::fs::read_to_string(admin_key_path)?;
|
||||
let created_at = UnixMillis(geth_store::now_ms());
|
||||
let admin_key = KeyId::new(ssh_public_key_fingerprint(&public_key));
|
||||
let op = KeychainOp {
|
||||
id: generated_keychain_op_id("admin-key-add", admin_key.as_str(), created_at),
|
||||
created_at,
|
||||
kind: KeychainOpKind::AdminKeyAdd { key: admin_key },
|
||||
};
|
||||
store_keychain_op(&store, &op)?;
|
||||
ops.push(op);
|
||||
}
|
||||
|
||||
Ok(ControlResponse::KeychainInitialized { ops })
|
||||
}
|
||||
ControlRequest::KeychainStatus => {
|
||||
let view = geth_keychain::reduce_keychain_ops(&load_keychain_ops(&store)?);
|
||||
Ok(ControlResponse::KeychainStatus(KeychainStatusResponse {
|
||||
initialized: false,
|
||||
admin_keys: 0,
|
||||
users: 0,
|
||||
devices: 0,
|
||||
nodes: 1,
|
||||
initialized: view.initialized,
|
||||
admin_keys: view.admin_keys.len(),
|
||||
users: view.users.len(),
|
||||
devices: view.devices.len(),
|
||||
nodes: view.nodes.len(),
|
||||
}))
|
||||
}
|
||||
ControlRequest::AuthExplain {
|
||||
|
|
@ -518,6 +546,23 @@ fn load_auth_ops_for_resource(store: &Store, resource: &str) -> Result<Vec<AuthO
|
|||
.collect()
|
||||
}
|
||||
|
||||
fn store_keychain_op(store: &Store, op: &KeychainOp) -> Result<(), NodeError> {
|
||||
store.insert_keychain_op(&StoredKeychainOp {
|
||||
op_id: op.id.to_string(),
|
||||
op_json: serde_json::to_string(op)?,
|
||||
created_at_ms: op.created_at.0,
|
||||
})?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn load_keychain_ops(store: &Store) -> Result<Vec<KeychainOp>, NodeError> {
|
||||
store
|
||||
.list_keychain_ops()?
|
||||
.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:{}",
|
||||
|
|
@ -539,6 +584,13 @@ fn generated_auth_op_id(
|
|||
))
|
||||
}
|
||||
|
||||
fn generated_keychain_op_id(kind: &str, stable_id: &str, created_at: UnixMillis) -> AuthOpId {
|
||||
AuthOpId::new(format!(
|
||||
"keychain-op:{}",
|
||||
geth_crypto::blake3_hex(format!("{}\0{kind}\0{stable_id}", created_at.0).as_bytes())
|
||||
))
|
||||
}
|
||||
|
||||
fn stable_node_id(agent_id: &str) -> String {
|
||||
format!("node:{agent_id}")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -318,6 +318,31 @@ impl Store {
|
|||
.map_err(StoreError::from)
|
||||
}
|
||||
|
||||
pub fn insert_keychain_op(&self, op: &StoredKeychainOp) -> Result<(), StoreError> {
|
||||
self.conn.execute(
|
||||
r#"INSERT OR REPLACE INTO keychain_ops(op_id, op_json, created_at_ms)
|
||||
VALUES (?1, ?2, ?3)"#,
|
||||
params![op.op_id, op.op_json, op.created_at_ms],
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn list_keychain_ops(&self) -> Result<Vec<StoredKeychainOp>, StoreError> {
|
||||
let mut stmt = self.conn.prepare(
|
||||
r#"SELECT op_id, op_json, created_at_ms
|
||||
FROM keychain_ops ORDER BY created_at_ms, op_id"#,
|
||||
)?;
|
||||
let rows = stmt.query_map([], |row| {
|
||||
Ok(StoredKeychainOp {
|
||||
op_id: row.get(0)?,
|
||||
op_json: row.get(1)?,
|
||||
created_at_ms: row.get(2)?,
|
||||
})
|
||||
})?;
|
||||
rows.collect::<Result<Vec<_>, _>>()
|
||||
.map_err(StoreError::from)
|
||||
}
|
||||
|
||||
pub fn insert_ssh_cert_request(
|
||||
&self,
|
||||
request: &StoredSshCertRequest,
|
||||
|
|
@ -513,6 +538,13 @@ pub struct StoredAuthOp {
|
|||
pub created_at_ms: i64,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct StoredKeychainOp {
|
||||
pub op_id: String,
|
||||
pub op_json: String,
|
||||
pub created_at_ms: i64,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct StoredSshCertRequest {
|
||||
pub request_id: String,
|
||||
|
|
@ -653,4 +685,27 @@ mod tests {
|
|||
vec![first]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn keychain_ops_roundtrip() {
|
||||
let store = Store::open_memory().expect("open");
|
||||
let first = StoredKeychainOp {
|
||||
op_id: "op:keychain:1".to_owned(),
|
||||
op_json: r#"{"id":"op:keychain:1"}"#.to_owned(),
|
||||
created_at_ms: 1,
|
||||
};
|
||||
let second = StoredKeychainOp {
|
||||
op_id: "op:keychain:2".to_owned(),
|
||||
op_json: r#"{"id":"op:keychain:2"}"#.to_owned(),
|
||||
created_at_ms: 2,
|
||||
};
|
||||
|
||||
store.insert_keychain_op(&second).expect("insert second");
|
||||
store.insert_keychain_op(&first).expect("insert first");
|
||||
|
||||
assert_eq!(
|
||||
store.list_keychain_ops().expect("list keychain ops"),
|
||||
vec![first, second]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -223,6 +223,40 @@ fn auth_grant_revoke_and_explain_use_local_auth_log() {
|
|||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn keychain_init_and_status_use_local_keychain_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 admin_key_path = home.path().join("admin.pub");
|
||||
std::fs::write(&admin_key_path, "ssh-ed25519 AAAAADMIN eric@geth\n").expect("write admin key");
|
||||
|
||||
let response = geth_node::handle_request(
|
||||
&node,
|
||||
geth_control::ControlRequest::KeychainInit {
|
||||
admin_key_path: Some(admin_key_path),
|
||||
},
|
||||
)
|
||||
.expect("init keychain");
|
||||
match response {
|
||||
geth_control::ControlResponse::KeychainInitialized { ops } => {
|
||||
assert_eq!(ops.len(), 2);
|
||||
}
|
||||
other => panic!("unexpected response: {other:?}"),
|
||||
}
|
||||
|
||||
let response = geth_node::handle_request(&node, geth_control::ControlRequest::KeychainStatus)
|
||||
.expect("keychain status");
|
||||
match response {
|
||||
geth_control::ControlResponse::KeychainStatus(status) => {
|
||||
assert!(status.initialized);
|
||||
assert_eq!(status.admin_keys, 1);
|
||||
assert_eq!(status.users, 0);
|
||||
}
|
||||
other => panic!("unexpected response: {other:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ssh_cert_request_approval_and_revocation_export_use_local_state() {
|
||||
let home = tempfile::tempdir().expect("tempdir");
|
||||
|
|
|
|||
Loading…
Reference in a new issue