Add resource secret epoch metadata
This commit is contained in:
parent
ce260625d6
commit
a05112e6a2
12 changed files with 287 additions and 13 deletions
|
|
@ -161,6 +161,8 @@ pub enum AuthCommand {
|
|||
#[derive(Debug, Subcommand)]
|
||||
pub enum SecretCommand {
|
||||
Status,
|
||||
Create { resource: String },
|
||||
Rotate { resource: String },
|
||||
}
|
||||
|
||||
#[derive(Debug, Subcommand)]
|
||||
|
|
@ -390,9 +392,10 @@ fn request_for_command(command: Command) -> Result<ControlRequest> {
|
|||
Command::Auth {
|
||||
command: AuthCommand::Revoke { resource, grant_id },
|
||||
} => ControlRequest::AuthRevoke { resource, grant_id },
|
||||
Command::Secret { command } => ControlRequest::ModuleStub {
|
||||
module: "secret".to_owned(),
|
||||
command: format!("{command:?}"),
|
||||
Command::Secret { command } => match command {
|
||||
SecretCommand::Status => ControlRequest::SecretStatus,
|
||||
SecretCommand::Create { resource } => ControlRequest::SecretCreate { resource },
|
||||
SecretCommand::Rotate { resource } => ControlRequest::SecretRotate { resource },
|
||||
},
|
||||
Command::Cas { command } => match command {
|
||||
CasCommand::Add { path } => ControlRequest::CasAdd { path },
|
||||
|
|
@ -636,6 +639,20 @@ fn print_response(response: ControlResponse, json: bool) -> Result<()> {
|
|||
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) => {
|
||||
println!("allowed: {}", explain.allowed);
|
||||
println!("subject: {}", explain.subject);
|
||||
|
|
|
|||
|
|
@ -15,5 +15,6 @@ geth-document = { path = "../geth-document" }
|
|||
geth-keychain = { path = "../geth-keychain" }
|
||||
geth-kv = { path = "../geth-kv" }
|
||||
geth-resource = { path = "../geth-resource" }
|
||||
geth-secrets = { path = "../geth-secrets" }
|
||||
geth-ssh-identity = { path = "../geth-ssh-identity" }
|
||||
geth-types = { path = "../geth-types" }
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ use geth_document::DocumentResource;
|
|||
use geth_keychain::KeychainOp;
|
||||
use geth_kv::{KvEntry, KvResource};
|
||||
use geth_resource::ResourceDescriptor;
|
||||
use geth_secrets::ResourceMasterSecret;
|
||||
use geth_ssh_identity::{
|
||||
SshCertApproval, SshCertRequest, SshCertificateRecord, SshRevocationEntry,
|
||||
};
|
||||
|
|
@ -48,6 +49,13 @@ pub enum ControlRequest {
|
|||
admin_key_path: Option<PathBuf>,
|
||||
},
|
||||
KeychainStatus,
|
||||
SecretStatus,
|
||||
SecretCreate {
|
||||
resource: String,
|
||||
},
|
||||
SecretRotate {
|
||||
resource: String,
|
||||
},
|
||||
AuthExplain {
|
||||
subject: String,
|
||||
resource: String,
|
||||
|
|
@ -167,6 +175,12 @@ pub enum ControlResponse {
|
|||
KeychainInitialized {
|
||||
ops: Vec<KeychainOp>,
|
||||
},
|
||||
SecretStatus {
|
||||
secrets: Vec<ResourceMasterSecret>,
|
||||
},
|
||||
SecretCreated {
|
||||
secret: ResourceMasterSecret,
|
||||
},
|
||||
AuthExplain(AuthExplanation),
|
||||
AuthOpRecorded {
|
||||
op: AuthOp,
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ geth-iroh = { path = "../geth-iroh" }
|
|||
geth-keychain = { path = "../geth-keychain" }
|
||||
geth-kv = { path = "../geth-kv" }
|
||||
geth-resource = { path = "../geth-resource" }
|
||||
geth-secrets = { path = "../geth-secrets" }
|
||||
geth-ssh-identity = { path = "../geth-ssh-identity" }
|
||||
geth-store = { path = "../geth-store" }
|
||||
geth-types = { path = "../geth-types" }
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ use geth_iroh::{EndpointStatus, GethIrohConfig, GethIrohEndpoint, GethRelayMode}
|
|||
use geth_keychain::{KeychainOp, KeychainOpKind};
|
||||
use geth_kv::{KvEntry, KvResource};
|
||||
use geth_resource::ResourceDescriptor;
|
||||
use geth_secrets::ResourceMasterSecret;
|
||||
use geth_ssh_identity::{
|
||||
SshCertApproval, SshCertKind, SshCertRequest, SshCertRequestStatus, SshCertificateRecord,
|
||||
SshRevocationEntry, SshRevocationKind, build_ssh_cert_sign_command, cert_request_id,
|
||||
|
|
@ -21,7 +22,8 @@ use geth_ssh_identity::{
|
|||
};
|
||||
use geth_store::{
|
||||
Store, StoredAuthOp, StoredDbResource, StoredDocumentResource, StoredKeychainOp, StoredKvEntry,
|
||||
StoredKvStore, StoredResource, StoredSshCertRequest, StoredSshCertificate, StoredSshRevocation,
|
||||
StoredKvStore, StoredResource, StoredResourceSecret, StoredSshCertRequest,
|
||||
StoredSshCertificate, StoredSshRevocation,
|
||||
};
|
||||
use geth_types::{
|
||||
AuthOpId, Capability, KeyId, NodeId, PrincipalId, ResourceId, ResourceKind, ResourceName,
|
||||
|
|
@ -67,6 +69,8 @@ pub enum NodeError {
|
|||
InvalidDocumentName(String),
|
||||
#[error("document not found: {0}")]
|
||||
DocumentNotFound(String),
|
||||
#[error("resource not found: {0}")]
|
||||
ResourceNotFound(String),
|
||||
#[error("invalid ssh certificate kind: {0}")]
|
||||
InvalidSshCertKind(String),
|
||||
#[error("invalid ssh certificate request status: {0}")]
|
||||
|
|
@ -335,6 +339,27 @@ pub fn handle_request(
|
|||
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 {
|
||||
subject,
|
||||
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> {
|
||||
store.insert_auth_op(&StoredAuthOp {
|
||||
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(
|
||||
&self,
|
||||
hash: &str,
|
||||
|
|
@ -753,6 +812,15 @@ pub struct StoredDocumentResource {
|
|||
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)]
|
||||
pub struct CasObject {
|
||||
pub hash: String,
|
||||
|
|
@ -1072,4 +1140,39 @@ mod tests {
|
|||
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]
|
||||
fn ssh_cert_request_approval_and_revocation_export_use_local_state() {
|
||||
let home = tempfile::tempdir().expect("tempdir");
|
||||
|
|
|
|||
Loading…
Reference in a new issue