Add smooth node enrollment flow

This commit is contained in:
Eric Wendland 2026-05-21 18:01:38 +02:00
commit 27a79768e4
12 changed files with 1662 additions and 22 deletions

View file

@ -79,12 +79,28 @@ impl Store {
created_at_ms INTEGER NOT NULL,
PRIMARY KEY (op_id, signer, namespace)
);
CREATE TABLE IF NOT EXISTS node_enrollment_requests (
request_id TEXT PRIMARY KEY,
requester_node TEXT NOT NULL,
request_json TEXT NOT NULL,
status TEXT NOT NULL,
created_at_ms INTEGER NOT NULL
);
CREATE TABLE IF NOT EXISTS auth_ops (
op_id TEXT PRIMARY KEY,
resource_id TEXT NOT NULL,
op_json TEXT NOT NULL,
created_at_ms INTEGER NOT NULL
);
CREATE TABLE IF NOT EXISTS auth_signatures (
op_id TEXT NOT NULL,
signer TEXT NOT NULL,
signer_public_key TEXT NOT NULL DEFAULT '',
namespace TEXT NOT NULL,
signature BLOB NOT NULL,
created_at_ms INTEGER NOT NULL,
PRIMARY KEY (op_id, signer, namespace)
);
CREATE TABLE IF NOT EXISTS grants (
grant_id TEXT PRIMARY KEY,
resource_id TEXT NOT NULL,
@ -935,6 +951,43 @@ impl Store {
.map_err(StoreError::from)
}
pub fn insert_auth_signature(&self, signature: &StoredAuthSignature) -> Result<(), StoreError> {
self.conn.execute(
r#"INSERT OR REPLACE INTO auth_signatures(
op_id, signer, signer_public_key, namespace, signature, created_at_ms
)
VALUES (?1, ?2, ?3, ?4, ?5, ?6)"#,
params![
signature.op_id,
signature.signer,
signature.signer_public_key,
signature.namespace,
signature.signature,
signature.created_at_ms
],
)?;
Ok(())
}
pub fn list_auth_signatures(&self) -> Result<Vec<StoredAuthSignature>, StoreError> {
let mut stmt = self.conn.prepare(
r#"SELECT op_id, signer, signer_public_key, namespace, signature, created_at_ms
FROM auth_signatures ORDER BY created_at_ms, op_id, signer, namespace"#,
)?;
let rows = stmt.query_map([], |row| {
Ok(StoredAuthSignature {
op_id: row.get(0)?,
signer: row.get(1)?,
signer_public_key: row.get(2)?,
namespace: row.get(3)?,
signature: row.get(4)?,
created_at_ms: row.get(5)?,
})
})?;
rows.collect::<Result<Vec<_>, _>>()
.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)
@ -1000,6 +1053,68 @@ impl Store {
.map_err(StoreError::from)
}
pub fn insert_node_enrollment_request(
&self,
request: &StoredNodeEnrollmentRequest,
) -> Result<(), StoreError> {
self.conn.execute(
r#"INSERT OR REPLACE INTO node_enrollment_requests(
request_id, requester_node, request_json, status, created_at_ms
)
VALUES (?1, ?2, ?3, ?4, ?5)"#,
params![
request.request_id,
request.requester_node,
request.request_json,
request.status,
request.created_at_ms
],
)?;
Ok(())
}
pub fn get_node_enrollment_request(
&self,
request_id: &str,
) -> Result<Option<StoredNodeEnrollmentRequest>, StoreError> {
let mut stmt = self.conn.prepare(
r#"SELECT request_id, requester_node, request_json, status, created_at_ms
FROM node_enrollment_requests WHERE request_id = ?1"#,
)?;
let mut rows = stmt.query(params![request_id])?;
if let Some(row) = rows.next()? {
Ok(Some(StoredNodeEnrollmentRequest {
request_id: row.get(0)?,
requester_node: row.get(1)?,
request_json: row.get(2)?,
status: row.get(3)?,
created_at_ms: row.get(4)?,
}))
} else {
Ok(None)
}
}
pub fn list_node_enrollment_requests(
&self,
) -> Result<Vec<StoredNodeEnrollmentRequest>, StoreError> {
let mut stmt = self.conn.prepare(
r#"SELECT request_id, requester_node, request_json, status, created_at_ms
FROM node_enrollment_requests ORDER BY created_at_ms, request_id"#,
)?;
let rows = stmt.query_map([], |row| {
Ok(StoredNodeEnrollmentRequest {
request_id: row.get(0)?,
requester_node: row.get(1)?,
request_json: row.get(2)?,
status: row.get(3)?,
created_at_ms: row.get(4)?,
})
})?;
rows.collect::<Result<Vec<_>, _>>()
.map_err(StoreError::from)
}
pub fn insert_ssh_cert_request(
&self,
request: &StoredSshCertRequest,
@ -1381,6 +1496,16 @@ pub struct StoredAuthOp {
pub created_at_ms: i64,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct StoredAuthSignature {
pub op_id: String,
pub signer: String,
pub signer_public_key: String,
pub namespace: String,
pub signature: Vec<u8>,
pub created_at_ms: i64,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct StoredKeychainOp {
pub op_id: String,
@ -1398,6 +1523,15 @@ pub struct StoredKeychainSignature {
pub created_at_ms: i64,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct StoredNodeEnrollmentRequest {
pub request_id: String,
pub requester_node: String,
pub request_json: String,
pub status: String,
pub created_at_ms: i64,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct StoredSshCertRequest {
pub request_id: String,
@ -1546,6 +1680,33 @@ mod tests {
);
}
#[test]
fn node_enrollment_requests_roundtrip() {
let store = Store::open_memory().expect("open");
let request = StoredNodeEnrollmentRequest {
request_id: "node-enrollment:1".to_owned(),
requester_node: "node:laptop".to_owned(),
request_json: r#"{"id":"node-enrollment:1"}"#.to_owned(),
status: "pending".to_owned(),
created_at_ms: 7,
};
store
.insert_node_enrollment_request(&request)
.expect("insert enrollment request");
assert_eq!(
store
.get_node_enrollment_request("node-enrollment:1")
.expect("get enrollment request"),
Some(request.clone())
);
assert_eq!(
store
.list_node_enrollment_requests()
.expect("list enrollment requests"),
vec![request]
);
}
#[test]
fn auth_ops_roundtrip_by_resource() {
let store = Store::open_memory().expect("open");
@ -1574,6 +1735,26 @@ mod tests {
assert_eq!(store.list_auth_ops().expect("list all auth ops").len(), 2);
}
#[test]
fn auth_signatures_roundtrip() {
let store = Store::open_memory().expect("open");
let signature = StoredAuthSignature {
op_id: "op:auth:1".to_owned(),
signer: "ssh:blake3:admin".to_owned(),
signer_public_key: "ssh-ed25519 AAAAADMIN eric@geth".to_owned(),
namespace: "geth.auth-op.v1@geth.local".to_owned(),
signature: b"-----BEGIN SSH SIGNATURE-----".to_vec(),
created_at_ms: 3,
};
store
.insert_auth_signature(&signature)
.expect("insert auth signature");
assert_eq!(
store.list_auth_signatures().expect("list auth signatures"),
vec![signature]
);
}
#[test]
fn keychain_ops_roundtrip() {
let store = Store::open_memory().expect("open");