fix: preserve Runtime binding trust boundaries
This commit is contained in:
@@ -14,6 +14,7 @@ use std::{
|
|||||||
error::Error as _,
|
error::Error as _,
|
||||||
future::Future,
|
future::Future,
|
||||||
io::Read as _,
|
io::Read as _,
|
||||||
|
net::{IpAddr, SocketAddr, ToSocketAddrs},
|
||||||
path::PathBuf,
|
path::PathBuf,
|
||||||
pin::Pin,
|
pin::Pin,
|
||||||
sync::{Arc, RwLock},
|
sync::{Arc, RwLock},
|
||||||
@@ -2850,6 +2851,7 @@ pub struct RemoteRuntimeConfig {
|
|||||||
pub base_url: String,
|
pub base_url: String,
|
||||||
pub bearer_token: Option<String>,
|
pub bearer_token: Option<String>,
|
||||||
pub auth: Option<RemoteRuntimeAuthConfig>,
|
pub auth: Option<RemoteRuntimeAuthConfig>,
|
||||||
|
pub strict_public_egress: bool,
|
||||||
pub cached_worker_creation_available: bool,
|
pub cached_worker_creation_available: bool,
|
||||||
pub cached_os: String,
|
pub cached_os: String,
|
||||||
pub cached_arch: String,
|
pub cached_arch: String,
|
||||||
@@ -2874,6 +2876,7 @@ impl std::fmt::Debug for RemoteRuntimeConfig {
|
|||||||
&self.bearer_token.as_ref().map(|_| "<redacted>"),
|
&self.bearer_token.as_ref().map(|_| "<redacted>"),
|
||||||
)
|
)
|
||||||
.field("auth", &self.auth.as_ref().map(|_| "<capability-signer>"))
|
.field("auth", &self.auth.as_ref().map(|_| "<capability-signer>"))
|
||||||
|
.field("strict_public_egress", &self.strict_public_egress)
|
||||||
.field(
|
.field(
|
||||||
"cached_worker_creation_available",
|
"cached_worker_creation_available",
|
||||||
&self.cached_worker_creation_available,
|
&self.cached_worker_creation_available,
|
||||||
@@ -2900,6 +2903,7 @@ impl RemoteRuntimeConfig {
|
|||||||
base_url: base_url.into(),
|
base_url: base_url.into(),
|
||||||
bearer_token,
|
bearer_token,
|
||||||
auth: None,
|
auth: None,
|
||||||
|
strict_public_egress: false,
|
||||||
cached_worker_creation_available: false,
|
cached_worker_creation_available: false,
|
||||||
cached_os: "unknown".to_string(),
|
cached_os: "unknown".to_string(),
|
||||||
cached_arch: "unknown".to_string(),
|
cached_arch: "unknown".to_string(),
|
||||||
@@ -2918,6 +2922,11 @@ impl RemoteRuntimeConfig {
|
|||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn with_strict_public_egress(mut self, strict: bool) -> Self {
|
||||||
|
self.strict_public_egress = strict;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
pub fn with_cached_status(mut self, status: impl Into<String>) -> Self {
|
pub fn with_cached_status(mut self, status: impl Into<String>) -> Self {
|
||||||
self.cached_status = status.into();
|
self.cached_status = status.into();
|
||||||
self
|
self
|
||||||
@@ -2975,6 +2984,78 @@ impl WorkdirHttpAuthorization for RemoteWorkdirAuthorization {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn resolve_strict_remote_runtime_endpoint(
|
||||||
|
endpoint: &str,
|
||||||
|
) -> Result<(String, Vec<SocketAddr>), String> {
|
||||||
|
let endpoint =
|
||||||
|
Url::parse(endpoint).map_err(|_| "endpoint must be an absolute https URL".to_string())?;
|
||||||
|
if endpoint.scheme() != "https"
|
||||||
|
|| endpoint.host_str().is_none()
|
||||||
|
|| !endpoint.username().is_empty()
|
||||||
|
|| endpoint.password().is_some()
|
||||||
|
|| endpoint.query().is_some()
|
||||||
|
|| endpoint.fragment().is_some()
|
||||||
|
{
|
||||||
|
return Err(
|
||||||
|
"endpoint must be an https origin without credentials, query, or fragment".to_string(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
let host = endpoint.host_str().expect("checked above").to_string();
|
||||||
|
if host.eq_ignore_ascii_case("localhost")
|
||||||
|
|| host.ends_with(".localhost")
|
||||||
|
|| host
|
||||||
|
.parse::<IpAddr>()
|
||||||
|
.is_ok_and(is_disallowed_remote_runtime_address)
|
||||||
|
{
|
||||||
|
return Err("endpoint host is not public".to_string());
|
||||||
|
}
|
||||||
|
let port = endpoint.port_or_known_default().unwrap_or(443);
|
||||||
|
let addresses = (host.as_str(), port)
|
||||||
|
.to_socket_addrs()
|
||||||
|
.map_err(|_| "endpoint DNS resolution failed".to_string())?
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
if addresses.is_empty()
|
||||||
|
|| addresses
|
||||||
|
.iter()
|
||||||
|
.any(|address| is_disallowed_remote_runtime_address(address.ip()))
|
||||||
|
{
|
||||||
|
return Err("endpoint DNS resolution included a non-public address".to_string());
|
||||||
|
}
|
||||||
|
Ok((host, addresses))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn is_disallowed_remote_runtime_address(address: IpAddr) -> bool {
|
||||||
|
match address {
|
||||||
|
IpAddr::V4(address) => {
|
||||||
|
let octets = address.octets();
|
||||||
|
address.is_private()
|
||||||
|
|| address.is_loopback()
|
||||||
|
|| address.is_link_local()
|
||||||
|
|| address.is_unspecified()
|
||||||
|
|| address.is_broadcast()
|
||||||
|
|| address.is_documentation()
|
||||||
|
|| address.is_multicast()
|
||||||
|
|| octets[0] == 0
|
||||||
|
|| (octets[0] == 100 && (64..=127).contains(&octets[1]))
|
||||||
|
|| (octets[0] == 192 && octets[1] == 0 && octets[2] == 0)
|
||||||
|
|| (octets[0] == 198 && (octets[1] == 18 || octets[1] == 19))
|
||||||
|
|| octets[0] >= 240
|
||||||
|
}
|
||||||
|
IpAddr::V6(address) => {
|
||||||
|
let segments = address.segments();
|
||||||
|
address.is_loopback()
|
||||||
|
|| address.is_unspecified()
|
||||||
|
|| address.is_multicast()
|
||||||
|
|| (segments[0] & 0xfe00) == 0xfc00
|
||||||
|
|| (segments[0] & 0xffc0) == 0xfe80
|
||||||
|
|| (segments[0] == 0x2001 && segments[1] == 0x0db8)
|
||||||
|
|| address.to_ipv4_mapped().is_some_and(|address| {
|
||||||
|
is_disallowed_remote_runtime_address(IpAddr::V4(address))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct RemoteWorkerRuntime {
|
pub struct RemoteWorkerRuntime {
|
||||||
runtime_id: String,
|
runtime_id: String,
|
||||||
@@ -3060,13 +3141,30 @@ impl RemoteWorkerRuntime {
|
|||||||
) -> Result<Self, RuntimeRegistryError> {
|
) -> Result<Self, RuntimeRegistryError> {
|
||||||
validate_backend_identifier("runtime_id", &config.runtime_id)?;
|
validate_backend_identifier("runtime_id", &config.runtime_id)?;
|
||||||
let base_url = config.base_url.trim_end_matches('/').to_string();
|
let base_url = config.base_url.trim_end_matches('/').to_string();
|
||||||
|
let pinned_endpoint = if config.strict_public_egress {
|
||||||
|
Some(
|
||||||
|
resolve_strict_remote_runtime_endpoint(&base_url).map_err(|message| {
|
||||||
|
RuntimeRegistryError::RuntimeOperationFailed {
|
||||||
|
runtime_id: config.runtime_id.clone(),
|
||||||
|
code: "remote_runtime_endpoint_not_allowed".to_string(),
|
||||||
|
message,
|
||||||
|
}
|
||||||
|
})?,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
let timeout = config.timeout;
|
let timeout = config.timeout;
|
||||||
|
let blocking_resolution = pinned_endpoint.clone();
|
||||||
let http = run_blocking_http(move || {
|
let http = run_blocking_http(move || {
|
||||||
BlockingHttpClient::builder()
|
let mut builder = BlockingHttpClient::builder()
|
||||||
.timeout(timeout)
|
.timeout(timeout)
|
||||||
.redirect(reqwest::redirect::Policy::none())
|
.redirect(reqwest::redirect::Policy::none())
|
||||||
.no_proxy()
|
.no_proxy();
|
||||||
.build()
|
if let Some((host, addresses)) = &blocking_resolution {
|
||||||
|
builder = builder.resolve_to_addrs(host, addresses);
|
||||||
|
}
|
||||||
|
builder.build()
|
||||||
})
|
})
|
||||||
.map_err(|err| RuntimeRegistryError::RuntimeOperationFailed {
|
.map_err(|err| RuntimeRegistryError::RuntimeOperationFailed {
|
||||||
runtime_id: config.runtime_id.clone(),
|
runtime_id: config.runtime_id.clone(),
|
||||||
@@ -3076,10 +3174,15 @@ impl RemoteWorkerRuntime {
|
|||||||
// Workdir command-output waits are bounded to 20 seconds by Runtime;
|
// Workdir command-output waits are bounded to 20 seconds by Runtime;
|
||||||
// leave transport margin while retaining a finite client timeout.
|
// leave transport margin while retaining a finite client timeout.
|
||||||
let workdir_timeout = timeout.max(Duration::from_secs(30));
|
let workdir_timeout = timeout.max(Duration::from_secs(30));
|
||||||
let async_http = AsyncHttpClient::builder()
|
let mut async_builder = AsyncHttpClient::builder()
|
||||||
.timeout(workdir_timeout)
|
.timeout(workdir_timeout)
|
||||||
.redirect(reqwest::redirect::Policy::none())
|
.redirect(reqwest::redirect::Policy::none())
|
||||||
.no_proxy()
|
.no_proxy();
|
||||||
|
if let Some((host, addresses)) = &pinned_endpoint {
|
||||||
|
async_builder = async_builder.resolve_to_addrs(host, addresses);
|
||||||
|
}
|
||||||
|
let async_http =
|
||||||
|
async_builder
|
||||||
.build()
|
.build()
|
||||||
.map_err(|err| RuntimeRegistryError::RuntimeOperationFailed {
|
.map_err(|err| RuntimeRegistryError::RuntimeOperationFailed {
|
||||||
runtime_id: config.runtime_id.clone(),
|
runtime_id: config.runtime_id.clone(),
|
||||||
@@ -4772,6 +4875,43 @@ mod tests {
|
|||||||
use std::sync::{Arc, Mutex};
|
use std::sync::{Arc, Mutex};
|
||||||
use std::thread;
|
use std::thread;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn strict_remote_runtime_egress_rejects_disallowed_endpoint_before_client_use() {
|
||||||
|
let config = RemoteRuntimeConfig::new(
|
||||||
|
"runtime-private",
|
||||||
|
"Private Runtime",
|
||||||
|
"https://169.254.169.254/latest/meta-data",
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
.with_strict_public_egress(true);
|
||||||
|
let error = match RemoteWorkerRuntime::new(
|
||||||
|
config,
|
||||||
|
"workspace-a".to_string(),
|
||||||
|
"http://127.0.0.1:1".to_string(),
|
||||||
|
) {
|
||||||
|
Err(error) => error,
|
||||||
|
Ok(_) => panic!("disallowed endpoint unexpectedly produced a Runtime client"),
|
||||||
|
};
|
||||||
|
assert!(matches!(
|
||||||
|
error,
|
||||||
|
RuntimeRegistryError::RuntimeOperationFailed { code, .. }
|
||||||
|
if code == "remote_runtime_endpoint_not_allowed"
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn strict_remote_runtime_egress_accepts_and_pins_public_https_address() {
|
||||||
|
let config =
|
||||||
|
RemoteRuntimeConfig::new("runtime-public", "Public Runtime", "https://8.8.8.8", None)
|
||||||
|
.with_strict_public_egress(true);
|
||||||
|
RemoteWorkerRuntime::new(
|
||||||
|
config,
|
||||||
|
"workspace-a".to_string(),
|
||||||
|
"http://127.0.0.1:1".to_string(),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn remote_worker_create_timeout_covers_runtime_phase_budgets() {
|
fn remote_worker_create_timeout_covers_runtime_phase_budgets() {
|
||||||
assert!(REMOTE_WORKER_CREATE_TIMEOUT > Duration::from_secs(60 + 10 + 5));
|
assert!(REMOTE_WORKER_CREATE_TIMEOUT > Duration::from_secs(60 + 10 + 5));
|
||||||
|
|||||||
@@ -646,6 +646,7 @@ fn append_workspace_runtime_sources(
|
|||||||
.into_iter()
|
.into_iter()
|
||||||
.filter(|binding| {
|
.filter(|binding| {
|
||||||
binding.runtime_id != yoi_workspace_server::hosts::EMBEDDED_RUNTIME_ID
|
binding.runtime_id != yoi_workspace_server::hosts::EMBEDDED_RUNTIME_ID
|
||||||
|
&& binding.state == WorkspaceRuntimeBindingState::Verified
|
||||||
})
|
})
|
||||||
.collect::<Vec<_>>()
|
.collect::<Vec<_>>()
|
||||||
})
|
})
|
||||||
@@ -674,7 +675,10 @@ fn append_workspace_runtime_sources(
|
|||||||
None,
|
None,
|
||||||
)
|
)
|
||||||
.with_workspace_id(runtime.workspace_id.clone())
|
.with_workspace_id(runtime.workspace_id.clone())
|
||||||
.with_auth(auth);
|
.with_auth(auth)
|
||||||
|
.with_strict_public_egress(
|
||||||
|
runtime.authentication_mode == WorkspaceRuntimeAuthenticationMode::WorkspaceIdentity,
|
||||||
|
);
|
||||||
remote_runtime_sources.retain(|existing| {
|
remote_runtime_sources.retain(|existing| {
|
||||||
existing.workspace_id.as_deref() != Some(runtime.workspace_id.as_str())
|
existing.workspace_id.as_deref() != Some(runtime.workspace_id.as_str())
|
||||||
|| existing.runtime_id != runtime.runtime_id
|
|| existing.runtime_id != runtime.runtime_id
|
||||||
|
|||||||
@@ -128,7 +128,8 @@ use crate::hosts::{
|
|||||||
WorkerLifecycleRequest, WorkerLifecycleResult, WorkerOperationState, WorkerRestoreResult,
|
WorkerLifecycleRequest, WorkerLifecycleResult, WorkerOperationState, WorkerRestoreResult,
|
||||||
WorkerSpawnAcceptanceRequirement, WorkerSpawnIntent, WorkerSpawnRequest, WorkerSpawnResult,
|
WorkerSpawnAcceptanceRequirement, WorkerSpawnIntent, WorkerSpawnRequest, WorkerSpawnResult,
|
||||||
WorkerSpawnWorkingDirectoryRequest, WorkerSummary, WorkerTicketAssignmentRequest,
|
WorkerSpawnWorkingDirectoryRequest, WorkerSummary, WorkerTicketAssignmentRequest,
|
||||||
WorkerWorkspaceSummary, worker_spawn_create_fingerprint, workspace_worker_summary,
|
WorkerWorkspaceSummary, is_disallowed_remote_runtime_address, worker_spawn_create_fingerprint,
|
||||||
|
workspace_worker_summary,
|
||||||
};
|
};
|
||||||
use crate::identity::WorkspaceIdentity;
|
use crate::identity::WorkspaceIdentity;
|
||||||
use crate::memory_backend::execute_memory_backend_operation_with_authority;
|
use crate::memory_backend::execute_memory_backend_operation_with_authority;
|
||||||
@@ -11815,6 +11816,14 @@ async fn scoped_put_runtime_trust_key(
|
|||||||
.store
|
.store
|
||||||
.get_workspace_runtime_binding(&path.workspace_id, &path.runtime_id)
|
.get_workspace_runtime_binding(&path.workspace_id, &path.runtime_id)
|
||||||
.await?;
|
.await?;
|
||||||
|
if existing.as_ref().is_some_and(|binding| {
|
||||||
|
binding.authentication_mode == StoredRuntimeAuthenticationMode::WorkspaceIdentity
|
||||||
|
}) {
|
||||||
|
return Err(settings_bad_request(
|
||||||
|
"workspace_identity_runtime_key_managed_by_binding",
|
||||||
|
"replace a Workspace identity Runtime public bundle through the Runtime registration operation with expected_revision",
|
||||||
|
));
|
||||||
|
}
|
||||||
let source = api
|
let source = api
|
||||||
.config
|
.config
|
||||||
.remote_runtime_sources
|
.remote_runtime_sources
|
||||||
@@ -16025,7 +16034,7 @@ async fn validate_runtime_connection_request(
|
|||||||
|| host.ends_with(".localhost")
|
|| host.ends_with(".localhost")
|
||||||
|| host
|
|| host
|
||||||
.parse::<IpAddr>()
|
.parse::<IpAddr>()
|
||||||
.is_ok_and(is_disallowed_runtime_address)
|
.is_ok_and(is_disallowed_remote_runtime_address)
|
||||||
{
|
{
|
||||||
return Err(settings_bad_request(
|
return Err(settings_bad_request(
|
||||||
"remote_runtime_endpoint_not_allowed",
|
"remote_runtime_endpoint_not_allowed",
|
||||||
@@ -16054,7 +16063,7 @@ async fn validate_runtime_connection_request(
|
|||||||
if addresses.is_empty()
|
if addresses.is_empty()
|
||||||
|| addresses
|
|| addresses
|
||||||
.iter()
|
.iter()
|
||||||
.any(|address| is_disallowed_runtime_address(address.ip()))
|
.any(|address| is_disallowed_remote_runtime_address(address.ip()))
|
||||||
{
|
{
|
||||||
return Err(settings_bad_request(
|
return Err(settings_bad_request(
|
||||||
"remote_runtime_endpoint_not_allowed",
|
"remote_runtime_endpoint_not_allowed",
|
||||||
@@ -16074,38 +16083,6 @@ async fn validate_runtime_connection_request(
|
|||||||
Ok(endpoint)
|
Ok(endpoint)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn is_disallowed_runtime_address(address: IpAddr) -> bool {
|
|
||||||
match address {
|
|
||||||
IpAddr::V4(address) => {
|
|
||||||
let octets = address.octets();
|
|
||||||
address.is_private()
|
|
||||||
|| address.is_loopback()
|
|
||||||
|| address.is_link_local()
|
|
||||||
|| address.is_unspecified()
|
|
||||||
|| address.is_broadcast()
|
|
||||||
|| address.is_documentation()
|
|
||||||
|| address.is_multicast()
|
|
||||||
|| octets[0] == 0
|
|
||||||
|| (octets[0] == 100 && (64..=127).contains(&octets[1]))
|
|
||||||
|| (octets[0] == 192 && octets[1] == 0 && octets[2] == 0)
|
|
||||||
|| (octets[0] == 198 && (octets[1] == 18 || octets[1] == 19))
|
|
||||||
|| octets[0] >= 240
|
|
||||||
}
|
|
||||||
IpAddr::V6(address) => {
|
|
||||||
let segments = address.segments();
|
|
||||||
address.is_loopback()
|
|
||||||
|| address.is_unspecified()
|
|
||||||
|| address.is_multicast()
|
|
||||||
|| (segments[0] & 0xfe00) == 0xfc00
|
|
||||||
|| (segments[0] & 0xffc0) == 0xfe80
|
|
||||||
|| (segments[0] == 0x2001 && segments[1] == 0x0db8)
|
|
||||||
|| address
|
|
||||||
.to_ipv4_mapped()
|
|
||||||
.is_some_and(|address| is_disallowed_runtime_address(IpAddr::V4(address)))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn validate_public_runtime_id(runtime_id: &str) -> ApiResult<()> {
|
fn validate_public_runtime_id(runtime_id: &str) -> ApiResult<()> {
|
||||||
if runtime_id.is_empty() {
|
if runtime_id.is_empty() {
|
||||||
return Err(settings_bad_request(
|
return Err(settings_bad_request(
|
||||||
@@ -16136,7 +16113,10 @@ fn remote_runtime_config_from_binding(
|
|||||||
binding.base_url.clone(),
|
binding.base_url.clone(),
|
||||||
None,
|
None,
|
||||||
)
|
)
|
||||||
.with_workspace_id(binding.workspace_id.clone());
|
.with_workspace_id(binding.workspace_id.clone())
|
||||||
|
.with_strict_public_egress(
|
||||||
|
binding.authentication_mode == StoredRuntimeAuthenticationMode::WorkspaceIdentity,
|
||||||
|
);
|
||||||
Ok(remote)
|
Ok(remote)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -18270,6 +18250,7 @@ mod tests {
|
|||||||
server_id: "server-test".to_owned(),
|
server_id: "server-test".to_owned(),
|
||||||
server_private_key: "unused".to_owned(),
|
server_private_key: "unused".to_owned(),
|
||||||
}),
|
}),
|
||||||
|
strict_public_egress: false,
|
||||||
cached_worker_creation_available: true,
|
cached_worker_creation_available: true,
|
||||||
cached_os: "test".to_owned(),
|
cached_os: "test".to_owned(),
|
||||||
cached_arch: "test".to_owned(),
|
cached_arch: "test".to_owned(),
|
||||||
@@ -20008,6 +19989,25 @@ mod tests {
|
|||||||
assert!(binding.workspace_key_id.is_some());
|
assert!(binding.workspace_key_id.is_some());
|
||||||
assert_eq!(binding.workspace_key_generation, Some(1));
|
assert_eq!(binding.workspace_key_generation, Some(1));
|
||||||
assert!(!created.runtime.worker_creation_available);
|
assert!(!created.runtime.worker_creation_available);
|
||||||
|
let replacement_identity = RuntimeIdentityMaterial::generate("configured-runtime").unwrap();
|
||||||
|
let generic_put = scoped_put_runtime_trust_key(
|
||||||
|
State(api.clone()),
|
||||||
|
AxumPath(ScopedRuntimePath {
|
||||||
|
workspace_id: api.config.workspace_id.clone(),
|
||||||
|
runtime_id: "configured-runtime".to_string(),
|
||||||
|
}),
|
||||||
|
Extension(actor.clone()),
|
||||||
|
Json(PutRuntimeTrustKeyRequest {
|
||||||
|
public_key: replacement_identity.public_key,
|
||||||
|
expected_revision: Some(1),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap_err();
|
||||||
|
assert_eq!(
|
||||||
|
generic_put.into_response().status(),
|
||||||
|
StatusCode::BAD_REQUEST
|
||||||
|
);
|
||||||
let configured_test = scoped_test_runtime_connection(
|
let configured_test = scoped_test_runtime_connection(
|
||||||
State(api.clone()),
|
State(api.clone()),
|
||||||
AxumPath(ScopedRuntimePath {
|
AxumPath(ScopedRuntimePath {
|
||||||
@@ -24783,6 +24783,7 @@ mod tests {
|
|||||||
server_id: "server-main".to_string(),
|
server_id: "server-main".to_string(),
|
||||||
server_private_key: identity.private_key.clone(),
|
server_private_key: identity.private_key.clone(),
|
||||||
}),
|
}),
|
||||||
|
strict_public_egress: false,
|
||||||
cached_worker_creation_available: true,
|
cached_worker_creation_available: true,
|
||||||
cached_os: "test".to_string(),
|
cached_os: "test".to_string(),
|
||||||
cached_arch: "test".to_string(),
|
cached_arch: "test".to_string(),
|
||||||
@@ -26309,6 +26310,7 @@ mod tests {
|
|||||||
base_url: endpoint,
|
base_url: endpoint,
|
||||||
bearer_token: Some("test-connection-token".to_string()),
|
bearer_token: Some("test-connection-token".to_string()),
|
||||||
auth: None,
|
auth: None,
|
||||||
|
strict_public_egress: false,
|
||||||
cached_worker_creation_available: true,
|
cached_worker_creation_available: true,
|
||||||
cached_os: "linux".to_string(),
|
cached_os: "linux".to_string(),
|
||||||
cached_arch: "x86_64".to_string(),
|
cached_arch: "x86_64".to_string(),
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
RuntimeTrustKeyStatus,
|
RuntimeTrustKeyStatus,
|
||||||
} from '$lib/generated/workspace-api';
|
} from '$lib/generated/workspace-api';
|
||||||
import {
|
import {
|
||||||
|
createRemoteRuntime,
|
||||||
previewRuntimePublicKeyFingerprint,
|
previewRuntimePublicKeyFingerprint,
|
||||||
putRuntimeTrustKey,
|
putRuntimeTrustKey,
|
||||||
revealRuntimeTrustKey,
|
revealRuntimeTrustKey,
|
||||||
@@ -152,7 +153,23 @@
|
|||||||
const operation = routeFence.capture(data.runtimeId);
|
const operation = routeFence.capture(data.runtimeId);
|
||||||
busyAction = 'save';
|
busyAction = 'save';
|
||||||
try {
|
try {
|
||||||
|
const binding = data.runtimeDetail.runtime.management.binding;
|
||||||
|
if (binding?.authentication_mode === 'workspace_identity') {
|
||||||
|
if (!data.runtimeDetail.endpoint) {
|
||||||
|
throw new RuntimeTrustRequestError('The authoritative Runtime endpoint is unavailable.');
|
||||||
|
}
|
||||||
|
await createRemoteRuntime(data.workspaceId, {
|
||||||
|
public_bundle: {
|
||||||
|
identity_id: operation.runtimeId,
|
||||||
|
public_key: key,
|
||||||
|
},
|
||||||
|
display_name: data.runtimeDetail.runtime.label,
|
||||||
|
endpoint: data.runtimeDetail.endpoint,
|
||||||
|
expected_revision: binding.revision,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
await putRuntimeTrustKey(data.workspaceId, operation.runtimeId, request);
|
await putRuntimeTrustKey(data.workspaceId, operation.runtimeId, request);
|
||||||
|
}
|
||||||
if (!isCurrentRoute(operation)) return;
|
if (!isCurrentRoute(operation)) return;
|
||||||
publicKey = '';
|
publicKey = '';
|
||||||
fingerprintConfirmation = '';
|
fingerprintConfirmation = '';
|
||||||
|
|||||||
Reference in New Issue
Block a user