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

@ -1,8 +1,11 @@
use geth_types::{AgentId, AuthOpId, DeviceId, KeyId, NodeId, UnixMillis, UserId};
use geth_types::{
AgentId, AuthOpId, Capability, DeviceId, KeyId, NodeId, ResourceId, UnixMillis, UserId,
};
use serde::{Deserialize, Serialize};
use std::collections::{BTreeMap, BTreeSet};
pub const KEYCHAIN_SIGNATURE_NAMESPACE: &str = "geth.keychain.v1@geth.local";
pub const NODE_ENROLLMENT_REQUEST_NAMESPACE: &str = "geth.node-enrollment-request.v1@geth.local";
pub type SignedKeychainOp = geth_codec::SignedEnvelope<KeychainOp, KeyId>;
@ -38,6 +41,111 @@ pub struct KeychainOpSignature {
pub created_at: UnixMillis,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct NodeEnrollmentRequest {
pub id: AuthOpId,
pub requester_node: NodeId,
pub requester_agent: AgentId,
pub requester_agent_public_key: String,
pub requested_node_name: String,
pub requested_capabilities: Vec<NodeEnrollmentCapability>,
pub endpoint_id: Option<String>,
pub reason: Option<String>,
pub status: NodeEnrollmentStatus,
pub created_at: UnixMillis,
pub provenance: Option<NodeEnrollmentProvenance>,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct NodeEnrollmentCapability {
pub resource: ResourceId,
pub capability: Capability,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct NodeEnrollmentProvenance {
pub namespace: String,
pub signer_node: NodeId,
pub signer_agent: AgentId,
pub signer_public_key: String,
pub signature_hex: String,
pub signed_at: UnixMillis,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum NodeEnrollmentStatus {
Pending,
Approved,
Rejected,
}
impl NodeEnrollmentStatus {
#[must_use]
pub fn as_str(&self) -> &'static str {
match self {
Self::Pending => "pending",
Self::Approved => "approved",
Self::Rejected => "rejected",
}
}
}
impl std::fmt::Display for NodeEnrollmentStatus {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.as_str())
}
}
impl std::str::FromStr for NodeEnrollmentStatus {
type Err = KeychainError;
fn from_str(value: &str) -> Result<Self, Self::Err> {
match value {
"pending" => Ok(Self::Pending),
"approved" => Ok(Self::Approved),
"rejected" => Ok(Self::Rejected),
_ => Err(KeychainError::InvalidEnrollmentStatus(value.to_owned())),
}
}
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct NodeEnrollmentRequestSigningPayload {
pub id: AuthOpId,
pub requester_node: NodeId,
pub requester_agent: AgentId,
pub requester_agent_public_key: String,
pub requested_node_name: String,
pub requested_capabilities: Vec<NodeEnrollmentCapability>,
pub endpoint_id: Option<String>,
pub reason: Option<String>,
pub created_at: UnixMillis,
}
#[derive(Debug, thiserror::Error)]
pub enum KeychainError {
#[error("invalid node enrollment status: {0}")]
InvalidEnrollmentStatus(String),
}
#[must_use]
pub fn node_enrollment_request_signing_payload(
request: &NodeEnrollmentRequest,
) -> NodeEnrollmentRequestSigningPayload {
NodeEnrollmentRequestSigningPayload {
id: request.id.clone(),
requester_node: request.requester_node.clone(),
requester_agent: request.requester_agent.clone(),
requester_agent_public_key: request.requester_agent_public_key.clone(),
requested_node_name: request.requested_node_name.clone(),
requested_capabilities: request.requested_capabilities.clone(),
endpoint_id: request.endpoint_id.clone(),
reason: request.reason.clone(),
created_at: request.created_at,
}
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "kind", rename_all = "kebab-case")]
pub enum KeychainOpKind {