feat: add runtime trust capability auth
This commit is contained in:
@@ -13,6 +13,7 @@ use std::{
|
||||
sync::{Arc, RwLock},
|
||||
time::Duration,
|
||||
};
|
||||
use worker_runtime::auth::{CapabilityTokenSigner, capability_claims};
|
||||
use worker_runtime::catalog::{
|
||||
ConfigBundleRef, CreateWorkerRequest, ProfileSelector, ProfileSourceArchiveHttpRef,
|
||||
ProfileSourceArchiveSource, WorkerDetail as EmbeddedWorkerDetail,
|
||||
@@ -2015,11 +2016,18 @@ pub struct RemoteRuntimeConfig {
|
||||
pub display_name: String,
|
||||
pub base_url: String,
|
||||
pub bearer_token: Option<String>,
|
||||
pub auth: Option<RemoteRuntimeAuthConfig>,
|
||||
pub cached_capabilities: RuntimeCapabilitySummary,
|
||||
pub cached_status: String,
|
||||
pub timeout: Duration,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct RemoteRuntimeAuthConfig {
|
||||
pub server_id: String,
|
||||
pub server_private_key: String,
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for RemoteRuntimeConfig {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("RemoteRuntimeConfig")
|
||||
@@ -2030,6 +2038,7 @@ impl std::fmt::Debug for RemoteRuntimeConfig {
|
||||
"bearer_token",
|
||||
&self.bearer_token.as_ref().map(|_| "<redacted>"),
|
||||
)
|
||||
.field("auth", &self.auth.as_ref().map(|_| "<capability-signer>"))
|
||||
.field("cached_capabilities", &self.cached_capabilities)
|
||||
.field("cached_status", &self.cached_status)
|
||||
.field("timeout", &self.timeout)
|
||||
@@ -2049,6 +2058,7 @@ impl RemoteRuntimeConfig {
|
||||
display_name: display_name.into(),
|
||||
base_url: base_url.into(),
|
||||
bearer_token,
|
||||
auth: None,
|
||||
cached_capabilities: remote_runtime_capabilities(
|
||||
200, false, false, "unknown", "unknown",
|
||||
),
|
||||
@@ -2062,6 +2072,11 @@ impl RemoteRuntimeConfig {
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_auth(mut self, auth: RemoteRuntimeAuthConfig) -> Self {
|
||||
self.auth = Some(auth);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_cached_status(mut self, status: impl Into<String>) -> Self {
|
||||
self.cached_status = status.into();
|
||||
self
|
||||
@@ -2081,6 +2096,7 @@ pub struct RemoteWorkerRuntime {
|
||||
backend_base_url: String,
|
||||
workspace_id: String,
|
||||
bearer_token: Option<String>,
|
||||
auth: Option<RemoteRuntimeAuthConfig>,
|
||||
cached_capabilities: RuntimeCapabilitySummary,
|
||||
cached_status: String,
|
||||
host_id: String,
|
||||
@@ -2088,6 +2104,21 @@ pub struct RemoteWorkerRuntime {
|
||||
http: BlockingHttpClient,
|
||||
}
|
||||
|
||||
fn all_remote_runtime_permissions() -> Vec<String> {
|
||||
[
|
||||
"workers:list",
|
||||
"workers:create",
|
||||
"workers:read",
|
||||
"workers:delete",
|
||||
"workers:input",
|
||||
"workers:stop",
|
||||
"workers:protocol",
|
||||
]
|
||||
.into_iter()
|
||||
.map(str::to_string)
|
||||
.collect()
|
||||
}
|
||||
|
||||
impl RemoteWorkerRuntime {
|
||||
pub fn new(
|
||||
config: RemoteRuntimeConfig,
|
||||
@@ -2112,6 +2143,7 @@ impl RemoteWorkerRuntime {
|
||||
backend_base_url: backend_base_url.trim_end_matches('/').to_string(),
|
||||
workspace_id,
|
||||
bearer_token: config.bearer_token,
|
||||
auth: config.auth,
|
||||
cached_capabilities: config.cached_capabilities,
|
||||
cached_status: config.cached_status,
|
||||
resource_broker: BackendResourceBroker::default(),
|
||||
@@ -2150,7 +2182,7 @@ impl RemoteWorkerRuntime {
|
||||
where
|
||||
T: DeserializeOwned + Send + 'static,
|
||||
{
|
||||
self.send_json(self.http.get(self.endpoint(path)))
|
||||
self.send_json(path, self.http.get(self.endpoint(path)))
|
||||
}
|
||||
|
||||
fn post_json<B, T>(&self, path: &str, body: &B) -> Result<T, RuntimeDiagnostic>
|
||||
@@ -2158,25 +2190,56 @@ impl RemoteWorkerRuntime {
|
||||
B: Serialize + ?Sized,
|
||||
T: DeserializeOwned + Send + 'static,
|
||||
{
|
||||
self.send_json(self.http.post(self.endpoint(path)).json(body))
|
||||
self.send_json(path, self.http.post(self.endpoint(path)).json(body))
|
||||
}
|
||||
|
||||
fn delete_json<T>(&self, path: &str) -> Result<T, RuntimeDiagnostic>
|
||||
where
|
||||
T: DeserializeOwned + Send + 'static,
|
||||
{
|
||||
self.send_json(self.http.delete(self.endpoint(path)))
|
||||
self.send_json(path, self.http.delete(self.endpoint(path)))
|
||||
}
|
||||
|
||||
fn send_json<T>(&self, request: RequestBuilder) -> Result<T, RuntimeDiagnostic>
|
||||
fn runtime_capability_token(&self, path: &str) -> Option<String> {
|
||||
let auth = self.auth.as_ref()?;
|
||||
let signer = CapabilityTokenSigner::new(&auth.server_id, &auth.server_private_key);
|
||||
let claims = capability_claims(
|
||||
&auth.server_id,
|
||||
&self.runtime_id,
|
||||
&self.workspace_id,
|
||||
all_remote_runtime_permissions(),
|
||||
300,
|
||||
)
|
||||
.map_err(|error| {
|
||||
eprintln!(
|
||||
"failed to build Runtime capability claims for {} {}: {error}",
|
||||
self.runtime_id, path
|
||||
);
|
||||
error
|
||||
})
|
||||
.ok()?;
|
||||
signer
|
||||
.sign(&claims)
|
||||
.map_err(|error| {
|
||||
eprintln!(
|
||||
"failed to sign Runtime capability token for {} {}: {error}",
|
||||
self.runtime_id, path
|
||||
);
|
||||
error
|
||||
})
|
||||
.ok()
|
||||
}
|
||||
|
||||
fn send_json<T>(&self, path: &str, request: RequestBuilder) -> Result<T, RuntimeDiagnostic>
|
||||
where
|
||||
T: DeserializeOwned + Send + 'static,
|
||||
{
|
||||
let runtime_id = self.runtime_id.clone();
|
||||
let bearer_token = self.bearer_token.clone();
|
||||
let capability_token = self.runtime_capability_token(path);
|
||||
run_blocking_http(move || {
|
||||
let request = request.header(CONTENT_TYPE, "application/json");
|
||||
let request = if let Some(token) = bearer_token.as_deref() {
|
||||
let request = if let Some(token) = capability_token.as_deref().or(bearer_token.as_deref()) {
|
||||
request.header(AUTHORIZATION, format!("Bearer {token}"))
|
||||
} else {
|
||||
request
|
||||
@@ -2653,7 +2716,9 @@ impl WorkspaceWorkerRuntime for RemoteWorkerRuntime {
|
||||
runtime_id: self.runtime_id.clone(),
|
||||
worker_id: worker_id.to_string(),
|
||||
endpoint: self.ws_endpoint(worker_id),
|
||||
bearer_token: self.bearer_token.clone(),
|
||||
bearer_token: self
|
||||
.runtime_capability_token(&format!("/v1/workers/{worker_id}/protocol"))
|
||||
.or_else(|| self.bearer_token.clone()),
|
||||
},
|
||||
))
|
||||
}
|
||||
|
||||
@@ -1,12 +1,17 @@
|
||||
use std::net::SocketAddr;
|
||||
use std::path::PathBuf;
|
||||
use std::collections::VecDeque;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::process::ExitCode;
|
||||
use std::sync::Arc;
|
||||
|
||||
use chrono::Utc;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tokio::net::TcpListener;
|
||||
use yoi_workspace_server::store::RepositoryRecord;
|
||||
use worker_runtime::auth::{RuntimeIdentityMaterial, decode_public_key};
|
||||
use yoi_workspace_server::hosts::{RemoteRuntimeAuthConfig, RemoteRuntimeConfig};
|
||||
use yoi_workspace_server::store::{RepositoryRecord, SqliteWorkspaceStore, TrustedRuntimeRecord};
|
||||
use yoi_workspace_server::{
|
||||
BackendRuntimesConfigFile, ControlPlaneStore, ServerConfig, SqliteWorkspaceStore,
|
||||
BackendRuntimesConfigFile, ControlPlaneStore, ServerConfig,
|
||||
WORKSPACE_BACKEND_CONFIG_TEMPLATE, WorkspaceBackendConfigFile, WorkspaceIdentity,
|
||||
WorkspaceRecord, serve,
|
||||
};
|
||||
@@ -17,6 +22,8 @@ enum Command {
|
||||
Init(InitOptions),
|
||||
ConfigDefault,
|
||||
ConfigDiff(WorkspacePathOptions),
|
||||
Identity(Vec<String>),
|
||||
TrustRuntime(Vec<String>),
|
||||
Skills(SkillsCommand),
|
||||
Help,
|
||||
}
|
||||
@@ -72,6 +79,8 @@ async fn run() -> Result<(), Box<dyn std::error::Error>> {
|
||||
Command::Init(options) => run_init(options).await,
|
||||
Command::ConfigDefault => run_config_default(),
|
||||
Command::ConfigDiff(options) => run_config_diff(options),
|
||||
Command::Identity(args) => run_identity_command(args),
|
||||
Command::TrustRuntime(args) => run_trust_runtime_command(args),
|
||||
Command::Skills(command) => run_skills(command),
|
||||
Command::Help => Ok(()),
|
||||
}
|
||||
@@ -92,6 +101,8 @@ fn parse_command(args: &[String]) -> Result<Command, CliError> {
|
||||
Ok(Command::Init(parse_init_options(rest)?))
|
||||
}
|
||||
"config" => parse_config_command(rest),
|
||||
"identity" => Ok(Command::Identity(rest.to_vec())),
|
||||
"trust-runtime" => Ok(Command::TrustRuntime(rest.to_vec())),
|
||||
"skills" => parse_skills_command(rest),
|
||||
"serve" => {
|
||||
if rest.iter().any(|arg| arg == "--help" || arg == "-h") {
|
||||
@@ -105,7 +116,7 @@ fn parse_command(args: &[String]) -> Result<Command, CliError> {
|
||||
Ok(Command::Help)
|
||||
}
|
||||
other => Err(CliError(format!(
|
||||
"unknown command `{other}`; expected `init`, `config`, `skills`, or `serve`"
|
||||
"unknown command `{other}`; expected `init`, `config`, `identity`, `trust-runtime`, `skills`, or `serve`"
|
||||
))),
|
||||
}
|
||||
}
|
||||
@@ -169,6 +180,254 @@ fn run_config_diff(options: WorkspacePathOptions) -> Result<(), Box<dyn std::err
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
struct ServerIdentityFile {
|
||||
identity: RuntimeIdentityMaterial,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
struct PublicIdentityView {
|
||||
identity_id: String,
|
||||
public_key: String,
|
||||
}
|
||||
|
||||
fn server_identity_path() -> PathBuf {
|
||||
ServerConfig::default_server_data_root().join("identity.toml")
|
||||
}
|
||||
|
||||
fn read_server_identity_file(path: &Path) -> Result<Option<ServerIdentityFile>, Box<dyn std::error::Error>> {
|
||||
if !path.exists() {
|
||||
return Ok(None);
|
||||
}
|
||||
let contents = std::fs::read_to_string(path)?;
|
||||
Ok(Some(toml::from_str(&contents)?))
|
||||
}
|
||||
|
||||
fn write_server_identity_file(path: &Path, identity: &ServerIdentityFile) -> Result<(), Box<dyn std::error::Error>> {
|
||||
if let Some(parent) = path.parent() {
|
||||
std::fs::create_dir_all(parent)?;
|
||||
}
|
||||
let contents = toml::to_string_pretty(identity)?;
|
||||
write_secret_file(path, contents.as_bytes())?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn write_secret_file(path: &Path, contents: &[u8]) -> std::io::Result<()> {
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::os::unix::fs::OpenOptionsExt;
|
||||
let mut file = std::fs::OpenOptions::new()
|
||||
.create(true)
|
||||
.write(true)
|
||||
.truncate(true)
|
||||
.mode(0o600)
|
||||
.open(path)?;
|
||||
use std::io::Write as _;
|
||||
file.write_all(contents)?;
|
||||
}
|
||||
#[cfg(not(unix))]
|
||||
{
|
||||
std::fs::write(path, contents)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn public_identity_view(identity: &RuntimeIdentityMaterial) -> PublicIdentityView {
|
||||
PublicIdentityView {
|
||||
identity_id: identity.identity_id.clone(),
|
||||
public_key: identity.public_key.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
fn run_identity_command(args: Vec<String>) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let mut args = VecDeque::from(args);
|
||||
let subcommand = args.pop_front().ok_or_else(|| CliError("identity requires `init` or `show`".to_string()))?;
|
||||
match subcommand.as_str() {
|
||||
"init" => {
|
||||
let mut server_id = None;
|
||||
let mut replace = false;
|
||||
while let Some(arg) = args.pop_front() {
|
||||
let (flag, inline_value) = split_flag_value(arg)?;
|
||||
match flag.as_str() {
|
||||
"--server-id" => server_id = Some(take_value(&flag, inline_value, &mut args)?),
|
||||
"--replace" => {
|
||||
ensure_no_inline_value(&flag, inline_value.as_deref())?;
|
||||
replace = true;
|
||||
}
|
||||
_ => return Err(Box::new(CliError(format!("unknown identity init argument `{flag}`")))),
|
||||
}
|
||||
}
|
||||
let server_id = server_id.unwrap_or_else(|| "server-main".to_string());
|
||||
let path = server_identity_path();
|
||||
if read_server_identity_file(&path)?.is_some() && !replace {
|
||||
return Err(Box::new(CliError(format!(
|
||||
"server identity already exists at {}; pass --replace to rotate it",
|
||||
path.display()
|
||||
))));
|
||||
}
|
||||
let identity = RuntimeIdentityMaterial::generate(server_id)?;
|
||||
write_server_identity_file(&path, &ServerIdentityFile { identity: identity.clone() })?;
|
||||
println!("server_id={}", identity.identity_id);
|
||||
println!("public_key={}", identity.public_key);
|
||||
println!("identity_file={}", path.display());
|
||||
Ok(())
|
||||
}
|
||||
"show" => {
|
||||
let mut json = false;
|
||||
while let Some(arg) = args.pop_front() {
|
||||
let (flag, inline_value) = split_flag_value(arg)?;
|
||||
match flag.as_str() {
|
||||
"--json" => {
|
||||
ensure_no_inline_value(&flag, inline_value.as_deref())?;
|
||||
json = true;
|
||||
}
|
||||
_ => return Err(Box::new(CliError(format!("unknown identity show argument `{flag}`")))),
|
||||
}
|
||||
}
|
||||
let path = server_identity_path();
|
||||
let identity = read_server_identity_file(&path)?.ok_or_else(|| {
|
||||
CliError(format!("server identity is not initialized at {}", path.display()))
|
||||
})?;
|
||||
let view = public_identity_view(&identity.identity);
|
||||
if json {
|
||||
println!("{}", serde_json::to_string_pretty(&view)?);
|
||||
} else {
|
||||
println!("server_id={}", view.identity_id);
|
||||
println!("public_key={}", view.public_key);
|
||||
println!("identity_file={}", path.display());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
_ => Err(Box::new(CliError(format!("unknown identity subcommand `{subcommand}`")))),
|
||||
}
|
||||
}
|
||||
|
||||
fn run_trust_runtime_command(args: Vec<String>) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let mut args = VecDeque::from(args);
|
||||
let subcommand = args.pop_front().ok_or_else(|| CliError("trust-runtime requires `add`, `list`, or `revoke`".to_string()))?;
|
||||
let database_path = ServerConfig::default_server_database_path();
|
||||
if let Some(parent) = database_path.parent() {
|
||||
std::fs::create_dir_all(parent)?;
|
||||
}
|
||||
let store = SqliteWorkspaceStore::open(&database_path)?;
|
||||
match subcommand.as_str() {
|
||||
"add" => {
|
||||
let mut runtime_id = None;
|
||||
let mut base_url = None;
|
||||
let mut public_key = None;
|
||||
let mut display_name = None;
|
||||
while let Some(arg) = args.pop_front() {
|
||||
let (flag, inline_value) = split_flag_value(arg)?;
|
||||
match flag.as_str() {
|
||||
"--runtime-id" => runtime_id = Some(take_value(&flag, inline_value, &mut args)?),
|
||||
"--base-url" | "--endpoint" => base_url = Some(take_value(&flag, inline_value, &mut args)?),
|
||||
"--public-key" => public_key = Some(take_value(&flag, inline_value, &mut args)?),
|
||||
"--display-name" => display_name = Some(take_value(&flag, inline_value, &mut args)?),
|
||||
_ => return Err(Box::new(CliError(format!("unknown trust-runtime add argument `{flag}`")))),
|
||||
}
|
||||
}
|
||||
let runtime_id = runtime_id.ok_or_else(|| CliError("trust-runtime add requires --runtime-id".to_string()))?;
|
||||
let base_url = base_url.ok_or_else(|| CliError("trust-runtime add requires --base-url".to_string()))?;
|
||||
let public_key = public_key.ok_or_else(|| CliError("trust-runtime add requires --public-key".to_string()))?;
|
||||
decode_public_key(&public_key)?;
|
||||
let now = Utc::now().to_rfc3339();
|
||||
store.upsert_trusted_runtime(&TrustedRuntimeRecord {
|
||||
runtime_id: runtime_id.clone(),
|
||||
display_name: display_name.unwrap_or_else(|| runtime_id.clone()),
|
||||
base_url,
|
||||
public_key,
|
||||
created_at: now.clone(),
|
||||
updated_at: now,
|
||||
revoked_at: None,
|
||||
})?;
|
||||
println!("trusted_runtime_id={runtime_id}");
|
||||
println!("server_db={}", database_path.display());
|
||||
Ok(())
|
||||
}
|
||||
"list" => {
|
||||
let mut json = false;
|
||||
let mut include_revoked = false;
|
||||
while let Some(arg) = args.pop_front() {
|
||||
let (flag, inline_value) = split_flag_value(arg)?;
|
||||
match flag.as_str() {
|
||||
"--json" => {
|
||||
ensure_no_inline_value(&flag, inline_value.as_deref())?;
|
||||
json = true;
|
||||
}
|
||||
"--include-revoked" => {
|
||||
ensure_no_inline_value(&flag, inline_value.as_deref())?;
|
||||
include_revoked = true;
|
||||
}
|
||||
_ => return Err(Box::new(CliError(format!("unknown trust-runtime list argument `{flag}`")))),
|
||||
}
|
||||
}
|
||||
let records = store.list_trusted_runtimes(include_revoked)?;
|
||||
if json {
|
||||
println!("{}", serde_json::to_string_pretty(&records)?);
|
||||
} else {
|
||||
for runtime in records {
|
||||
println!(
|
||||
"runtime_id={} base_url={} public_key={} revoked_at={}",
|
||||
runtime.runtime_id,
|
||||
runtime.base_url,
|
||||
runtime.public_key,
|
||||
runtime.revoked_at.unwrap_or_default()
|
||||
);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
"revoke" => {
|
||||
let mut runtime_id = None;
|
||||
while let Some(arg) = args.pop_front() {
|
||||
let (flag, inline_value) = split_flag_value(arg)?;
|
||||
match flag.as_str() {
|
||||
"--runtime-id" => runtime_id = Some(take_value(&flag, inline_value, &mut args)?),
|
||||
_ => return Err(Box::new(CliError(format!("unknown trust-runtime revoke argument `{flag}`")))),
|
||||
}
|
||||
}
|
||||
let runtime_id = runtime_id.ok_or_else(|| CliError("trust-runtime revoke requires --runtime-id".to_string()))?;
|
||||
let now = Utc::now().to_rfc3339();
|
||||
if !store.revoke_trusted_runtime(&runtime_id, &now)? {
|
||||
return Err(Box::new(CliError(format!("trusted runtime `{runtime_id}` is not registered or is already revoked"))));
|
||||
}
|
||||
println!("revoked_runtime_id={runtime_id}");
|
||||
Ok(())
|
||||
}
|
||||
_ => Err(Box::new(CliError(format!("unknown trust-runtime subcommand `{subcommand}`")))),
|
||||
}
|
||||
}
|
||||
|
||||
fn split_flag_value(arg: String) -> Result<(String, Option<String>), CliError> {
|
||||
if let Some((flag, value)) = arg.split_once('=') {
|
||||
if flag.is_empty() {
|
||||
return Err(CliError("empty flag name".to_string()));
|
||||
}
|
||||
Ok((flag.to_string(), Some(value.to_string())))
|
||||
} else {
|
||||
Ok((arg, None))
|
||||
}
|
||||
}
|
||||
|
||||
fn take_value(
|
||||
flag: &str,
|
||||
inline_value: Option<String>,
|
||||
args: &mut VecDeque<String>,
|
||||
) -> Result<String, CliError> {
|
||||
if let Some(value) = inline_value {
|
||||
return Ok(value);
|
||||
}
|
||||
args.pop_front()
|
||||
.ok_or_else(|| CliError(format!("{flag} requires a value")))
|
||||
}
|
||||
|
||||
fn ensure_no_inline_value(flag: &str, inline_value: Option<&str>) -> Result<(), CliError> {
|
||||
if inline_value.is_some() {
|
||||
return Err(CliError(format!("{flag} does not accept a value")));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn run_skills(command: SkillsCommand) -> Result<(), Box<dyn std::error::Error>> {
|
||||
match command {
|
||||
SkillsCommand::List(options) => {
|
||||
@@ -224,6 +483,7 @@ async fn run_serve(options: ServeOptions) -> Result<(), Box<dyn std::error::Erro
|
||||
)?;
|
||||
resolved.database_path = database_path.clone();
|
||||
resolved.server.database_path = database_path.clone();
|
||||
append_trusted_runtime_sources(store.as_ref(), &mut resolved.server.remote_runtime_sources)?;
|
||||
if let Some(listen) = options.listen {
|
||||
resolved = resolved.with_listen(listen);
|
||||
}
|
||||
@@ -239,6 +499,36 @@ async fn run_serve(options: ServeOptions) -> Result<(), Box<dyn std::error::Erro
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn append_trusted_runtime_sources(
|
||||
store: &SqliteWorkspaceStore,
|
||||
remote_runtime_sources: &mut Vec<RemoteRuntimeConfig>,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let Some(server_identity) = read_server_identity_file(&server_identity_path())? else {
|
||||
if !store.list_trusted_runtimes(false)?.is_empty() {
|
||||
return Err(Box::new(CliError(
|
||||
"trusted runtimes are registered but server identity is not initialized; run `yoi-workspace-server identity init`".to_string(),
|
||||
)));
|
||||
}
|
||||
return Ok(());
|
||||
};
|
||||
for runtime in store.list_trusted_runtimes(false)? {
|
||||
let auth = RemoteRuntimeAuthConfig {
|
||||
server_id: server_identity.identity.identity_id.clone(),
|
||||
server_private_key: server_identity.identity.private_key.clone(),
|
||||
};
|
||||
let remote = RemoteRuntimeConfig::new(
|
||||
runtime.runtime_id.clone(),
|
||||
runtime.display_name,
|
||||
runtime.base_url,
|
||||
None,
|
||||
)
|
||||
.with_auth(auth);
|
||||
remote_runtime_sources.retain(|existing| existing.runtime_id != runtime.runtime_id);
|
||||
remote_runtime_sources.push(remote);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn select_serve_workspace(store: &SqliteWorkspaceStore) -> Result<WorkspaceRecord, CliError> {
|
||||
let workspaces = store
|
||||
.list_workspaces()
|
||||
|
||||
@@ -72,6 +72,11 @@ const MIGRATIONS: &[Migration] = &[
|
||||
name: "sqlite memory authority documents and staging resolutions",
|
||||
apply: create_memory_authority_tables,
|
||||
},
|
||||
Migration {
|
||||
version: 12,
|
||||
name: "trusted remote runtime registry",
|
||||
apply: create_trusted_runtime_registry_tables,
|
||||
},
|
||||
];
|
||||
|
||||
struct Migration {
|
||||
@@ -107,6 +112,17 @@ pub struct RepositoryRecord {
|
||||
pub updated_at: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
pub struct TrustedRuntimeRecord {
|
||||
pub runtime_id: String,
|
||||
pub display_name: String,
|
||||
pub base_url: String,
|
||||
pub public_key: String,
|
||||
pub created_at: String,
|
||||
pub updated_at: String,
|
||||
pub revoked_at: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
pub struct AccountRecord {
|
||||
pub account_id: String,
|
||||
@@ -498,6 +514,59 @@ impl SqliteWorkspaceStore {
|
||||
.map_err(|_| Error::Store("sqlite connection lock poisoned".to_string()))?;
|
||||
f(&conn)
|
||||
}
|
||||
|
||||
pub fn upsert_trusted_runtime(&self, record: &TrustedRuntimeRecord) -> Result<()> {
|
||||
self.with_conn(|conn| {
|
||||
conn.execute(
|
||||
r#"INSERT INTO trusted_runtime_records (
|
||||
runtime_id, display_name, base_url, public_key, created_at, updated_at, revoked_at
|
||||
) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)
|
||||
ON CONFLICT(runtime_id) DO UPDATE SET
|
||||
display_name = excluded.display_name,
|
||||
base_url = excluded.base_url,
|
||||
public_key = excluded.public_key,
|
||||
updated_at = excluded.updated_at,
|
||||
revoked_at = excluded.revoked_at"#,
|
||||
params![
|
||||
record.runtime_id,
|
||||
record.display_name,
|
||||
record.base_url,
|
||||
record.public_key,
|
||||
record.created_at,
|
||||
record.updated_at,
|
||||
record.revoked_at,
|
||||
],
|
||||
)?;
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
pub fn list_trusted_runtimes(&self, include_revoked: bool) -> Result<Vec<TrustedRuntimeRecord>> {
|
||||
self.with_conn(|conn| {
|
||||
let sql = if include_revoked {
|
||||
r#"SELECT runtime_id, display_name, base_url, public_key, created_at, updated_at, revoked_at
|
||||
FROM trusted_runtime_records ORDER BY runtime_id ASC"#
|
||||
} else {
|
||||
r#"SELECT runtime_id, display_name, base_url, public_key, created_at, updated_at, revoked_at
|
||||
FROM trusted_runtime_records WHERE revoked_at IS NULL ORDER BY runtime_id ASC"#
|
||||
};
|
||||
let mut stmt = conn.prepare(sql)?;
|
||||
let rows = stmt.query_map([], read_trusted_runtime_record)?;
|
||||
rows.collect::<std::result::Result<Vec<_>, _>>().map_err(Error::from)
|
||||
})
|
||||
}
|
||||
|
||||
pub fn revoke_trusted_runtime(&self, runtime_id: &str, revoked_at: &str) -> Result<bool> {
|
||||
self.with_conn(|conn| {
|
||||
let changed = conn.execute(
|
||||
r#"UPDATE trusted_runtime_records
|
||||
SET revoked_at = ?2, updated_at = ?2
|
||||
WHERE runtime_id = ?1 AND revoked_at IS NULL"#,
|
||||
params![runtime_id, revoked_at],
|
||||
)?;
|
||||
Ok(changed > 0)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
@@ -1599,6 +1668,18 @@ fn account_select_sql(where_clause: &str) -> String {
|
||||
)
|
||||
}
|
||||
|
||||
fn read_trusted_runtime_record(row: &rusqlite::Row<'_>) -> rusqlite::Result<TrustedRuntimeRecord> {
|
||||
Ok(TrustedRuntimeRecord {
|
||||
runtime_id: row.get(0)?,
|
||||
display_name: row.get(1)?,
|
||||
base_url: row.get(2)?,
|
||||
public_key: row.get(3)?,
|
||||
created_at: row.get(4)?,
|
||||
updated_at: row.get(5)?,
|
||||
revoked_at: row.get(6)?,
|
||||
})
|
||||
}
|
||||
|
||||
fn read_account_record(row: &rusqlite::Row<'_>) -> rusqlite::Result<AccountRecord> {
|
||||
Ok(AccountRecord {
|
||||
account_id: row.get(0)?,
|
||||
@@ -2005,6 +2086,23 @@ CREATE TABLE IF NOT EXISTS memory_staging_resolutions (
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn create_trusted_runtime_registry_tables(conn: &Connection) -> Result<()> {
|
||||
conn.execute_batch(
|
||||
r#"
|
||||
CREATE TABLE IF NOT EXISTS trusted_runtime_records (
|
||||
runtime_id TEXT PRIMARY KEY,
|
||||
display_name TEXT NOT NULL,
|
||||
base_url TEXT NOT NULL,
|
||||
public_key TEXT NOT NULL,
|
||||
created_at TEXT NOT NULL,
|
||||
updated_at TEXT NOT NULL,
|
||||
revoked_at TEXT
|
||||
);
|
||||
"#,
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn create_account_identity_tables(conn: &Connection) -> Result<()> {
|
||||
conn.execute_batch(
|
||||
r#"
|
||||
|
||||
Reference in New Issue
Block a user