fix: complete Runtime verification cutover
This commit is contained in:
@@ -2126,15 +2126,28 @@ async fn require_runtime_auth(
|
|||||||
unix_now_seconds(),
|
unix_now_seconds(),
|
||||||
) {
|
) {
|
||||||
Ok(context) => {
|
Ok(context) => {
|
||||||
if state.workspace_auth.as_ref().is_some_and(|workspace_auth| {
|
let workspace_verification_exists = match state.workspace_auth.as_deref() {
|
||||||
workspace_auth
|
Some(workspace_auth) => match workspace_auth
|
||||||
.verifier
|
.verifications
|
||||||
.has_active_workspace_issuer(&context.workspace_id)
|
.get(&context.workspace_id, workspace_auth.signer.runtime_id())
|
||||||
}) {
|
{
|
||||||
|
Ok(record) => record.is_some(),
|
||||||
|
Err(error) => {
|
||||||
|
return RuntimeHttpRestError::new(
|
||||||
|
StatusCode::SERVICE_UNAVAILABLE,
|
||||||
|
"workspace_runtime_verification_unavailable",
|
||||||
|
error.to_string(),
|
||||||
|
)
|
||||||
|
.into_response();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
None => false,
|
||||||
|
};
|
||||||
|
if workspace_verification_exists {
|
||||||
return RuntimeHttpRestError::new(
|
return RuntimeHttpRestError::new(
|
||||||
StatusCode::FORBIDDEN,
|
StatusCode::FORBIDDEN,
|
||||||
"workspace_identity_required",
|
"workspace_identity_required",
|
||||||
"Legacy Server-issued capability is disabled for this Workspace",
|
"Legacy Server-issued capability is disabled after signed Workspace Runtime verification",
|
||||||
)
|
)
|
||||||
.into_response();
|
.into_response();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3433,11 +3433,10 @@ fn workspace_runtime_operation(method: &str, path_and_query: &str) -> &'static s
|
|||||||
{
|
{
|
||||||
return "workers:create";
|
return "workers:create";
|
||||||
}
|
}
|
||||||
if path.ends_with("/input")
|
if path.ends_with("/workspace-api") {
|
||||||
|| path.ends_with("/restore")
|
return "workers:create";
|
||||||
|| path.ends_with("/workspace-api")
|
}
|
||||||
|| path.contains("/attachments")
|
if path.ends_with("/input") || path.ends_with("/restore") || path.contains("/attachments") {
|
||||||
{
|
|
||||||
return "workers:input";
|
return "workers:input";
|
||||||
}
|
}
|
||||||
if path.ends_with("/stop") || path.ends_with("/cancel") {
|
if path.ends_with("/stop") || path.ends_with("/cancel") {
|
||||||
@@ -4556,13 +4555,20 @@ impl WorkspaceWorkerRuntime for RemoteWorkerRuntime {
|
|||||||
&self,
|
&self,
|
||||||
worker_id: &str,
|
worker_id: &str,
|
||||||
) -> Option<crate::observation::RuntimeObservationSource> {
|
) -> Option<crate::observation::RuntimeObservationSource> {
|
||||||
|
let path = format!("/v1/workers/{worker_id}/protocol/ws");
|
||||||
|
let bearer_token = match &self.workspace_authorization {
|
||||||
|
Some(authorization) => authorization
|
||||||
|
.issue("GET", &path, "workers:protocol", Some(worker_id), &[])
|
||||||
|
.ok(),
|
||||||
|
None => self
|
||||||
|
.runtime_capability_token(&path)
|
||||||
|
.or_else(|| self.bearer_token.clone()),
|
||||||
|
};
|
||||||
Some(crate::observation::RuntimeObservationSource::remote_ws(
|
Some(crate::observation::RuntimeObservationSource::remote_ws(
|
||||||
crate::observation::RuntimeObservationSourceConfig {
|
crate::observation::RuntimeObservationSourceConfig {
|
||||||
worker: RuntimeWorkerRef::new(&self.runtime_id, worker_id),
|
worker: RuntimeWorkerRef::new(&self.runtime_id, worker_id),
|
||||||
endpoint: self.ws_endpoint(worker_id),
|
endpoint: self.ws_endpoint(worker_id),
|
||||||
bearer_token: self
|
bearer_token,
|
||||||
.runtime_capability_token(&format!("/v1/workers/{worker_id}/protocol"))
|
|
||||||
.or_else(|| self.bearer_token.clone()),
|
|
||||||
},
|
},
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
@@ -6631,6 +6637,19 @@ mod tests {
|
|||||||
assert!(!format!("{failure:?}").contains("secret-token"));
|
assert!(!format!("{failure:?}").contains("secret-token"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn workspace_runtime_operation_matches_runtime_permission_classifier() {
|
||||||
|
let worker_id = EmbeddedWorkerId::from_legacy_u64(1).to_string();
|
||||||
|
assert_eq!(
|
||||||
|
workspace_runtime_operation("POST", &format!("/v1/workers/{worker_id}/workspace-api")),
|
||||||
|
"workers:create"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
workspace_runtime_operation("GET", &format!("/v1/workers/{worker_id}/protocol/ws")),
|
||||||
|
"workers:protocol"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn remote_runtime_registry_routes_commands_without_browser_secret_leaks() {
|
fn remote_runtime_registry_routes_commands_without_browser_secret_leaks() {
|
||||||
let worker_id = EmbeddedWorkerId::from_legacy_u64(1).to_string();
|
let worker_id = EmbeddedWorkerId::from_legacy_u64(1).to_string();
|
||||||
|
|||||||
@@ -2123,23 +2123,12 @@ impl WorkspaceApi {
|
|||||||
))
|
))
|
||||||
})?;
|
})?;
|
||||||
let runtime_binding_store = store.clone();
|
let runtime_binding_store = store.clone();
|
||||||
let configured_runtime_endpoints = config
|
|
||||||
.remote_runtime_sources
|
|
||||||
.iter()
|
|
||||||
.filter_map(|source| {
|
|
||||||
(source.workspace_id.as_deref() == Some(config.workspace_id.as_str()))
|
|
||||||
.then(|| (source.runtime_id.clone(), source.base_url.clone()))
|
|
||||||
})
|
|
||||||
.collect::<HashMap<_, _>>();
|
|
||||||
let expected_runtime_bindings = store
|
let expected_runtime_bindings = store
|
||||||
.list_workspace_runtime_bindings(&config.workspace_id, false)
|
.list_workspace_runtime_bindings(&config.workspace_id, false)
|
||||||
.await?
|
.await?
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.filter(|binding| binding.runtime_id != EMBEDDED_RUNTIME_ID)
|
.filter(|binding| binding.runtime_id != EMBEDDED_RUNTIME_ID)
|
||||||
.filter(|binding| binding.state == StoredRuntimeBindingState::Verified)
|
.filter(|binding| binding.state != StoredRuntimeBindingState::Revoked)
|
||||||
.filter(|binding| {
|
|
||||||
configured_runtime_endpoints.get(&binding.runtime_id) == Some(&binding.base_url)
|
|
||||||
})
|
|
||||||
.map(|binding| {
|
.map(|binding| {
|
||||||
(
|
(
|
||||||
(binding.workspace_id.clone(), binding.runtime_id.clone()),
|
(binding.workspace_id.clone(), binding.runtime_id.clone()),
|
||||||
@@ -2171,6 +2160,7 @@ impl WorkspaceApi {
|
|||||||
.unwrap_or(false)
|
.unwrap_or(false)
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
{
|
||||||
let active_expectations = api
|
let active_expectations = api
|
||||||
.runtime_binding_expectations
|
.runtime_binding_expectations
|
||||||
.read()
|
.read()
|
||||||
@@ -2183,7 +2173,21 @@ impl WorkspaceApi {
|
|||||||
.unregister_runtime(&source.runtime_id);
|
.unregister_runtime(&source.runtime_id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
drop(active_expectations);
|
}
|
||||||
|
for binding in api
|
||||||
|
.store
|
||||||
|
.list_workspace_runtime_bindings(&api.config.workspace_id, false)
|
||||||
|
.await?
|
||||||
|
.into_iter()
|
||||||
|
.filter(|binding| {
|
||||||
|
binding.authentication_mode
|
||||||
|
== crate::store::WorkspaceRuntimeAuthenticationMode::WorkspaceIdentity
|
||||||
|
})
|
||||||
|
.filter(|binding| binding.state != StoredRuntimeBindingState::Revoked)
|
||||||
|
{
|
||||||
|
let activate = binding.state == StoredRuntimeBindingState::Verified;
|
||||||
|
api.register_workspace_runtime_binding(binding, activate)?;
|
||||||
|
}
|
||||||
Ok(api)
|
Ok(api)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2342,6 +2346,37 @@ impl WorkspaceApi {
|
|||||||
self.config.workspace_id.as_str()
|
self.config.workspace_id.as_str()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn register_workspace_runtime_binding(
|
||||||
|
&self,
|
||||||
|
binding: WorkspaceRuntimeBinding,
|
||||||
|
activate: bool,
|
||||||
|
) -> Result<()> {
|
||||||
|
let backend_url = self
|
||||||
|
.config
|
||||||
|
.backend_base_url
|
||||||
|
.clone()
|
||||||
|
.unwrap_or_else(|| "http://127.0.0.1:8787".to_string());
|
||||||
|
let mut remote_config = remote_runtime_config_from_binding(&binding)
|
||||||
|
.map_err(|diagnostic| Error::Store(diagnostic.message))?;
|
||||||
|
remote_config.workspace_authorization = Some(WorkspaceRuntimeAuthorization::new(
|
||||||
|
self.store.clone(),
|
||||||
|
self.signing_identities.clone(),
|
||||||
|
backend_url.clone(),
|
||||||
|
activate.then_some(binding),
|
||||||
|
));
|
||||||
|
let remote_runtime = RemoteWorkerRuntime::new(
|
||||||
|
remote_config.clone(),
|
||||||
|
self.config.workspace_id.clone(),
|
||||||
|
backend_url,
|
||||||
|
)
|
||||||
|
.map(|runtime| runtime.with_resource_broker(self.resource_broker.clone()))
|
||||||
|
.map_err(|error| error.into_error())?;
|
||||||
|
self.runtime.register_or_replace(remote_runtime);
|
||||||
|
self.runtime_subscription_broker
|
||||||
|
.register_remote_runtime(remote_config);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
pub fn runtime_subscription_broker(&self) -> &RuntimeSubscriptionBroker {
|
pub fn runtime_subscription_broker(&self) -> &RuntimeSubscriptionBroker {
|
||||||
&self.runtime_subscription_broker
|
&self.runtime_subscription_broker
|
||||||
}
|
}
|
||||||
@@ -13629,16 +13664,20 @@ async fn create_remote_runtime(
|
|||||||
updated_at: now,
|
updated_at: now,
|
||||||
revoked_at: None,
|
revoked_at: None,
|
||||||
};
|
};
|
||||||
let (mutation, _) = api
|
let (mutation, binding) = api
|
||||||
.store
|
.store
|
||||||
.put_workspace_runtime_binding_key(record, request.expected_revision, &actor.account_id)
|
.put_workspace_runtime_binding_key(record, request.expected_revision, &actor.account_id)
|
||||||
.await?;
|
.await?;
|
||||||
api.runtime_binding_expectations
|
api.runtime_binding_expectations
|
||||||
.write()
|
.write()
|
||||||
.unwrap_or_else(std::sync::PoisonError::into_inner)
|
.unwrap_or_else(std::sync::PoisonError::into_inner)
|
||||||
.remove(&(api.config.workspace_id.clone(), runtime_id.clone()));
|
.insert(
|
||||||
|
(api.config.workspace_id.clone(), runtime_id.clone()),
|
||||||
|
binding.clone(),
|
||||||
|
);
|
||||||
api.runtime_subscription_broker
|
api.runtime_subscription_broker
|
||||||
.unregister_runtime(&runtime_id);
|
.unregister_runtime(&runtime_id);
|
||||||
|
api.register_workspace_runtime_binding(binding, false)?;
|
||||||
let resource = workspace_runtime_resources_response(&api, &api.config.workspace_id)
|
let resource = workspace_runtime_resources_response(&api, &api.config.workspace_id)
|
||||||
.await?
|
.await?
|
||||||
.items
|
.items
|
||||||
@@ -13778,6 +13817,7 @@ async fn perform_workspace_runtime_verification(
|
|||||||
.await
|
.await
|
||||||
.map_err(|error| error.to_string())?;
|
.map_err(|error| error.to_string())?;
|
||||||
|
|
||||||
|
let result = async {
|
||||||
let challenge_body = serde_json::to_vec(&challenge).map_err(|error| error.to_string())?;
|
let challenge_body = serde_json::to_vec(&challenge).map_err(|error| error.to_string())?;
|
||||||
let challenge_claims = WorkspaceCapabilityClaims {
|
let challenge_claims = WorkspaceCapabilityClaims {
|
||||||
issuer: backend_url.to_string(),
|
issuer: backend_url.to_string(),
|
||||||
@@ -13888,12 +13928,27 @@ async fn perform_workspace_runtime_verification(
|
|||||||
last_outcome: "verified".to_string(),
|
last_outcome: "verified".to_string(),
|
||||||
verified_at: Some(verified_at.clone()),
|
verified_at: Some(verified_at.clone()),
|
||||||
checked_at: verified_at,
|
checked_at: verified_at,
|
||||||
..pending
|
..pending.clone()
|
||||||
};
|
};
|
||||||
api.store
|
api.store
|
||||||
.complete_workspace_runtime_verification(&verified)
|
.complete_workspace_runtime_verification(&verified)
|
||||||
.await
|
.await
|
||||||
.map_err(|error| error.to_string())
|
.map_err(|error| error.to_string())
|
||||||
|
}
|
||||||
|
.await;
|
||||||
|
if result.is_err() {
|
||||||
|
let checked_at = Utc::now().to_rfc3339_opts(SecondsFormat::Millis, true);
|
||||||
|
let _ = api
|
||||||
|
.store
|
||||||
|
.record_workspace_runtime_verification_outcome_if_current(
|
||||||
|
&pending,
|
||||||
|
"failed",
|
||||||
|
"verification_failed",
|
||||||
|
&checked_at,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
result
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn test_runtime_connection(
|
async fn test_runtime_connection(
|
||||||
@@ -13918,6 +13973,7 @@ async fn test_runtime_connection(
|
|||||||
if binding.authentication_mode == StoredRuntimeAuthenticationMode::WorkspaceIdentity {
|
if binding.authentication_mode == StoredRuntimeAuthenticationMode::WorkspaceIdentity {
|
||||||
match perform_workspace_runtime_verification(&api, api.runtime.clone(), &binding).await {
|
match perform_workspace_runtime_verification(&api, api.runtime.clone(), &binding).await {
|
||||||
Ok(verified_binding) => {
|
Ok(verified_binding) => {
|
||||||
|
api.register_workspace_runtime_binding(verified_binding.clone(), true)?;
|
||||||
api.runtime_binding_expectations
|
api.runtime_binding_expectations
|
||||||
.write()
|
.write()
|
||||||
.unwrap_or_else(std::sync::PoisonError::into_inner)
|
.unwrap_or_else(std::sync::PoisonError::into_inner)
|
||||||
@@ -13933,22 +13989,6 @@ async fn test_runtime_connection(
|
|||||||
.map_err(|error| Error::Store(format!("{error:?}")))?;
|
.map_err(|error| Error::Store(format!("{error:?}")))?;
|
||||||
}
|
}
|
||||||
Err(message) => {
|
Err(message) => {
|
||||||
if let Ok(Some(mut evidence)) = api
|
|
||||||
.store
|
|
||||||
.get_workspace_runtime_verification(api.workspace_id(), &runtime_id)
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
if evidence.state != "verified" {
|
|
||||||
evidence.state = "failed".to_string();
|
|
||||||
evidence.verified_at = None;
|
|
||||||
}
|
|
||||||
evidence.last_outcome = "verification_failed".to_string();
|
|
||||||
evidence.checked_at = Utc::now().to_rfc3339();
|
|
||||||
let _ = api
|
|
||||||
.store
|
|
||||||
.record_workspace_runtime_verification_attempt(&evidence)
|
|
||||||
.await;
|
|
||||||
}
|
|
||||||
let mut result = runtime_connection_test_failure(
|
let mut result = runtime_connection_test_failure(
|
||||||
api.workspace_id(),
|
api.workspace_id(),
|
||||||
&runtime_id,
|
&runtime_id,
|
||||||
@@ -13981,15 +14021,19 @@ async fn test_runtime_connection(
|
|||||||
|
|
||||||
if ping.is_err()
|
if ping.is_err()
|
||||||
&& binding.authentication_mode == StoredRuntimeAuthenticationMode::WorkspaceIdentity
|
&& binding.authentication_mode == StoredRuntimeAuthenticationMode::WorkspaceIdentity
|
||||||
&& let Some(mut evidence) = api
|
&& let Some(evidence) = api
|
||||||
.store
|
.store
|
||||||
.get_workspace_runtime_verification(api.workspace_id(), &runtime_id)
|
.get_workspace_runtime_verification(api.workspace_id(), &runtime_id)
|
||||||
.await?
|
.await?
|
||||||
{
|
{
|
||||||
evidence.last_outcome = "connectivity_failed".to_string();
|
let checked_at = Utc::now().to_rfc3339();
|
||||||
evidence.checked_at = Utc::now().to_rfc3339();
|
|
||||||
api.store
|
api.store
|
||||||
.record_workspace_runtime_verification_attempt(&evidence)
|
.record_workspace_runtime_verification_outcome_if_current(
|
||||||
|
&evidence,
|
||||||
|
"failed",
|
||||||
|
"connectivity_failed",
|
||||||
|
&checked_at,
|
||||||
|
)
|
||||||
.await?;
|
.await?;
|
||||||
}
|
}
|
||||||
let current_binding = api
|
let current_binding = api
|
||||||
@@ -16505,7 +16549,6 @@ fn validate_public_runtime_id(runtime_id: &str) -> ApiResult<()> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
fn remote_runtime_config_from_binding(
|
fn remote_runtime_config_from_binding(
|
||||||
binding: &crate::store::WorkspaceRuntimeBinding,
|
binding: &crate::store::WorkspaceRuntimeBinding,
|
||||||
) -> std::result::Result<RemoteRuntimeConfig, RuntimeDiagnostic> {
|
) -> std::result::Result<RemoteRuntimeConfig, RuntimeDiagnostic> {
|
||||||
@@ -20349,6 +20392,98 @@ mod tests {
|
|||||||
assert!(!serialized.contains("materialized_path"));
|
assert!(!serialized.contains("materialized_path"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn verified_workspace_runtime_binding_is_restored_into_live_registry() {
|
||||||
|
let dir = tempfile::tempdir().unwrap();
|
||||||
|
let root = dir.path().to_path_buf();
|
||||||
|
let api = test_api(&root).await;
|
||||||
|
let actor = test_owner_actor();
|
||||||
|
let identity = api
|
||||||
|
.signing_identities
|
||||||
|
.provision_existing(&api.config.workspace_id, &actor.account_id)
|
||||||
|
.unwrap();
|
||||||
|
let runtime_identity = RuntimeIdentityMaterial::generate("restored-runtime").unwrap();
|
||||||
|
api.store
|
||||||
|
.upsert_workspace_runtime_binding_record(
|
||||||
|
WorkspaceRuntimeBinding {
|
||||||
|
workspace_id: api.config.workspace_id.clone(),
|
||||||
|
runtime_id: "restored-runtime".to_string(),
|
||||||
|
display_name: "Restored Runtime".to_string(),
|
||||||
|
base_url: "https://8.8.8.8".to_string(),
|
||||||
|
public_key: runtime_identity.public_key,
|
||||||
|
public_key_fingerprint: String::new(),
|
||||||
|
binding_revision: 1,
|
||||||
|
state: StoredRuntimeBindingState::Configured,
|
||||||
|
authentication_mode: StoredRuntimeAuthenticationMode::WorkspaceIdentity,
|
||||||
|
workspace_key_id: Some(identity.key_id.clone()),
|
||||||
|
workspace_key_generation: Some(identity.revision),
|
||||||
|
created_at: "1".to_string(),
|
||||||
|
updated_at: "1".to_string(),
|
||||||
|
revoked_at: None,
|
||||||
|
},
|
||||||
|
false,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
let configured = api
|
||||||
|
.store
|
||||||
|
.get_workspace_runtime_binding(&api.config.workspace_id, "restored-runtime")
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
.unwrap();
|
||||||
|
let evidence = crate::store::WorkspaceRuntimeVerificationEvidence {
|
||||||
|
workspace_id: configured.workspace_id.clone(),
|
||||||
|
runtime_id: configured.runtime_id.clone(),
|
||||||
|
binding_revision: configured.binding_revision,
|
||||||
|
workspace_key_id: identity.key_id,
|
||||||
|
workspace_identity_revision: identity.revision,
|
||||||
|
workspace_trust_generation: identity.revision,
|
||||||
|
runtime_public_key_fingerprint: configured.public_key_fingerprint.clone(),
|
||||||
|
runtime_identity_revision: 1,
|
||||||
|
challenge_id: "restart-challenge".to_string(),
|
||||||
|
state: "verified".to_string(),
|
||||||
|
last_outcome: "verified".to_string(),
|
||||||
|
verified_at: Some("2".to_string()),
|
||||||
|
checked_at: "2".to_string(),
|
||||||
|
};
|
||||||
|
api.store
|
||||||
|
.record_workspace_runtime_verification_attempt(&evidence)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
api.store
|
||||||
|
.complete_workspace_runtime_verification(&evidence)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
drop(api);
|
||||||
|
|
||||||
|
let restored_config = test_server_config(&root);
|
||||||
|
let restored_store =
|
||||||
|
SqliteWorkspaceStore::open(restored_config.database_path.clone()).unwrap();
|
||||||
|
let restored = WorkspaceApi::new(restored_config, Arc::new(restored_store))
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
assert!(
|
||||||
|
restored
|
||||||
|
.runtime
|
||||||
|
.list_runtimes(100)
|
||||||
|
.items
|
||||||
|
.iter()
|
||||||
|
.any(|runtime| runtime.runtime_id == "restored-runtime"),
|
||||||
|
"verified persisted Runtime binding must return to the live registry"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
restored
|
||||||
|
.runtime_binding_expectations
|
||||||
|
.read()
|
||||||
|
.unwrap()
|
||||||
|
.contains_key(&(
|
||||||
|
restored.config.workspace_id.clone(),
|
||||||
|
"restored-runtime".to_string(),
|
||||||
|
)),
|
||||||
|
"restored Runtime must retain its revision-fenced binding expectation"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn remote_runtime_registration_is_workspace_scoped_revisioned_and_configured() {
|
async fn remote_runtime_registration_is_workspace_scoped_revisioned_and_configured() {
|
||||||
let dir = tempfile::tempdir().unwrap();
|
let dir = tempfile::tempdir().unwrap();
|
||||||
@@ -20416,18 +20551,18 @@ mod tests {
|
|||||||
.list_runtimes(100)
|
.list_runtimes(100)
|
||||||
.items
|
.items
|
||||||
.iter()
|
.iter()
|
||||||
.all(|runtime| runtime.runtime_id != "configured-runtime"),
|
.any(|runtime| runtime.runtime_id == "configured-runtime"),
|
||||||
"configured binding must remove a stale active Runtime projection"
|
"configured binding must remain registered so verification can reach it"
|
||||||
);
|
);
|
||||||
assert!(
|
assert!(
|
||||||
!api.runtime_binding_expectations
|
api.runtime_binding_expectations
|
||||||
.read()
|
.read()
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.contains_key(&(
|
.contains_key(&(
|
||||||
api.config.workspace_id.clone(),
|
api.config.workspace_id.clone(),
|
||||||
"configured-runtime".to_string(),
|
"configured-runtime".to_string(),
|
||||||
)),
|
)),
|
||||||
"configured binding must not remain a control expectation"
|
"configured binding must remain fenced by its current binding revision"
|
||||||
);
|
);
|
||||||
let replacement_identity = RuntimeIdentityMaterial::generate("configured-runtime").unwrap();
|
let replacement_identity = RuntimeIdentityMaterial::generate("configured-runtime").unwrap();
|
||||||
let generic_put = scoped_put_runtime_trust_key(
|
let generic_put = scoped_put_runtime_trust_key(
|
||||||
|
|||||||
@@ -771,6 +771,13 @@ pub trait ControlPlaneStore: Send + Sync + WorkspaceDeletionStore {
|
|||||||
&self,
|
&self,
|
||||||
evidence: &WorkspaceRuntimeVerificationEvidence,
|
evidence: &WorkspaceRuntimeVerificationEvidence,
|
||||||
) -> Result<()>;
|
) -> Result<()>;
|
||||||
|
async fn record_workspace_runtime_verification_outcome_if_current(
|
||||||
|
&self,
|
||||||
|
expected: &WorkspaceRuntimeVerificationEvidence,
|
||||||
|
state: &str,
|
||||||
|
outcome: &str,
|
||||||
|
checked_at: &str,
|
||||||
|
) -> Result<bool>;
|
||||||
async fn complete_workspace_runtime_verification(
|
async fn complete_workspace_runtime_verification(
|
||||||
&self,
|
&self,
|
||||||
evidence: &WorkspaceRuntimeVerificationEvidence,
|
evidence: &WorkspaceRuntimeVerificationEvidence,
|
||||||
@@ -2336,6 +2343,65 @@ impl SqliteWorkspaceStore {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn record_workspace_runtime_verification_outcome_if_current(
|
||||||
|
&self,
|
||||||
|
expected: &WorkspaceRuntimeVerificationEvidence,
|
||||||
|
state: &str,
|
||||||
|
outcome: &str,
|
||||||
|
checked_at: &str,
|
||||||
|
) -> Result<bool> {
|
||||||
|
validate_workspace_runtime_verification(expected)?;
|
||||||
|
if !matches!(state, "pending" | "verified" | "failed") {
|
||||||
|
return Err(Error::Store(
|
||||||
|
"invalid Workspace Runtime verification state".to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
if !matches!(
|
||||||
|
outcome,
|
||||||
|
"challenge_issued" | "verified" | "verification_failed" | "connectivity_failed"
|
||||||
|
) {
|
||||||
|
return Err(Error::Store(
|
||||||
|
"invalid Workspace Runtime verification outcome".to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
if checked_at.is_empty() || checked_at.len() > 128 {
|
||||||
|
return Err(Error::Store(
|
||||||
|
"invalid Workspace Runtime verification checked_at".to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
self.with_conn(|conn| {
|
||||||
|
let changed = conn.execute(
|
||||||
|
r#"UPDATE workspace_runtime_verifications
|
||||||
|
SET state = CASE WHEN state = 'verified' THEN state ELSE ?10 END,
|
||||||
|
last_outcome = ?11,
|
||||||
|
checked_at = ?12
|
||||||
|
WHERE workspace_id = ?1 AND runtime_id = ?2
|
||||||
|
AND binding_revision = ?3
|
||||||
|
AND workspace_key_id = ?4
|
||||||
|
AND workspace_identity_revision = ?5
|
||||||
|
AND workspace_trust_generation = ?6
|
||||||
|
AND runtime_public_key_fingerprint = ?7
|
||||||
|
AND runtime_identity_revision = ?8
|
||||||
|
AND challenge_id = ?9"#,
|
||||||
|
params![
|
||||||
|
expected.workspace_id,
|
||||||
|
expected.runtime_id,
|
||||||
|
expected.binding_revision,
|
||||||
|
expected.workspace_key_id,
|
||||||
|
expected.workspace_identity_revision,
|
||||||
|
expected.workspace_trust_generation,
|
||||||
|
expected.runtime_public_key_fingerprint,
|
||||||
|
expected.runtime_identity_revision,
|
||||||
|
expected.challenge_id,
|
||||||
|
state,
|
||||||
|
outcome,
|
||||||
|
checked_at,
|
||||||
|
],
|
||||||
|
)?;
|
||||||
|
Ok(changed == 1)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
pub fn complete_workspace_runtime_verification(
|
pub fn complete_workspace_runtime_verification(
|
||||||
&self,
|
&self,
|
||||||
evidence: &WorkspaceRuntimeVerificationEvidence,
|
evidence: &WorkspaceRuntimeVerificationEvidence,
|
||||||
@@ -2348,6 +2414,36 @@ impl SqliteWorkspaceStore {
|
|||||||
}
|
}
|
||||||
self.with_conn_mut(|conn| {
|
self.with_conn_mut(|conn| {
|
||||||
let tx = conn.transaction_with_behavior(TransactionBehavior::Immediate)?;
|
let tx = conn.transaction_with_behavior(TransactionBehavior::Immediate)?;
|
||||||
|
let current_attempt = tx.query_row(
|
||||||
|
r#"SELECT EXISTS(
|
||||||
|
SELECT 1 FROM workspace_runtime_verifications
|
||||||
|
WHERE workspace_id = ?1 AND runtime_id = ?2
|
||||||
|
AND binding_revision = ?3
|
||||||
|
AND workspace_key_id = ?4
|
||||||
|
AND workspace_identity_revision = ?5
|
||||||
|
AND workspace_trust_generation = ?6
|
||||||
|
AND runtime_public_key_fingerprint = ?7
|
||||||
|
AND runtime_identity_revision = ?8
|
||||||
|
AND challenge_id = ?9
|
||||||
|
)"#,
|
||||||
|
params![
|
||||||
|
evidence.workspace_id,
|
||||||
|
evidence.runtime_id,
|
||||||
|
evidence.binding_revision,
|
||||||
|
evidence.workspace_key_id,
|
||||||
|
evidence.workspace_identity_revision,
|
||||||
|
evidence.workspace_trust_generation,
|
||||||
|
evidence.runtime_public_key_fingerprint,
|
||||||
|
evidence.runtime_identity_revision,
|
||||||
|
evidence.challenge_id,
|
||||||
|
],
|
||||||
|
|row| row.get::<_, bool>(0),
|
||||||
|
)?;
|
||||||
|
if !current_attempt {
|
||||||
|
return Err(Error::RuntimeBindingConflict(
|
||||||
|
"Runtime verification attempt was superseded".to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
let changed = tx.execute(
|
let changed = tx.execute(
|
||||||
r#"UPDATE workspace_runtime_bindings
|
r#"UPDATE workspace_runtime_bindings
|
||||||
SET state = 'verified', updated_at = ?7
|
SET state = 'verified', updated_at = ?7
|
||||||
@@ -3193,6 +3289,18 @@ impl ControlPlaneStore for SqliteWorkspaceStore {
|
|||||||
SqliteWorkspaceStore::record_workspace_runtime_verification_attempt(self, evidence)
|
SqliteWorkspaceStore::record_workspace_runtime_verification_attempt(self, evidence)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn record_workspace_runtime_verification_outcome_if_current(
|
||||||
|
&self,
|
||||||
|
expected: &WorkspaceRuntimeVerificationEvidence,
|
||||||
|
state: &str,
|
||||||
|
outcome: &str,
|
||||||
|
checked_at: &str,
|
||||||
|
) -> Result<bool> {
|
||||||
|
SqliteWorkspaceStore::record_workspace_runtime_verification_outcome_if_current(
|
||||||
|
self, expected, state, outcome, checked_at,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
async fn complete_workspace_runtime_verification(
|
async fn complete_workspace_runtime_verification(
|
||||||
&self,
|
&self,
|
||||||
evidence: &WorkspaceRuntimeVerificationEvidence,
|
evidence: &WorkspaceRuntimeVerificationEvidence,
|
||||||
@@ -9857,6 +9965,9 @@ mod tests {
|
|||||||
verified_at: Some("2".to_string()),
|
verified_at: Some("2".to_string()),
|
||||||
checked_at: "2".to_string(),
|
checked_at: "2".to_string(),
|
||||||
};
|
};
|
||||||
|
store
|
||||||
|
.record_workspace_runtime_verification_attempt(&evidence)
|
||||||
|
.unwrap();
|
||||||
let verified = store
|
let verified = store
|
||||||
.complete_workspace_runtime_verification(&evidence)
|
.complete_workspace_runtime_verification(&evidence)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
@@ -9886,6 +9997,43 @@ mod tests {
|
|||||||
store
|
store
|
||||||
.complete_workspace_runtime_verification(&evidence)
|
.complete_workspace_runtime_verification(&evidence)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
let newer_pending = WorkspaceRuntimeVerificationEvidence {
|
||||||
|
challenge_id: "challenge-b".to_string(),
|
||||||
|
state: "pending".to_string(),
|
||||||
|
last_outcome: "challenge_issued".to_string(),
|
||||||
|
verified_at: None,
|
||||||
|
checked_at: "4".to_string(),
|
||||||
|
..evidence.clone()
|
||||||
|
};
|
||||||
|
store
|
||||||
|
.record_workspace_runtime_verification_attempt(&newer_pending)
|
||||||
|
.unwrap();
|
||||||
|
let newer_verified = WorkspaceRuntimeVerificationEvidence {
|
||||||
|
state: "verified".to_string(),
|
||||||
|
last_outcome: "verified".to_string(),
|
||||||
|
verified_at: Some("5".to_string()),
|
||||||
|
checked_at: "5".to_string(),
|
||||||
|
..newer_pending
|
||||||
|
};
|
||||||
|
store
|
||||||
|
.complete_workspace_runtime_verification(&newer_verified)
|
||||||
|
.unwrap();
|
||||||
|
assert!(
|
||||||
|
!store
|
||||||
|
.record_workspace_runtime_verification_outcome_if_current(
|
||||||
|
&pending_retry,
|
||||||
|
"failed",
|
||||||
|
"verification_failed",
|
||||||
|
"6",
|
||||||
|
)
|
||||||
|
.unwrap()
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
store
|
||||||
|
.get_workspace_runtime_verification("workspace-a", "runtime-a")
|
||||||
|
.unwrap(),
|
||||||
|
Some(newer_verified.clone())
|
||||||
|
);
|
||||||
drop(store);
|
drop(store);
|
||||||
|
|
||||||
let reopened = SqliteWorkspaceStore::open(&path).unwrap();
|
let reopened = SqliteWorkspaceStore::open(&path).unwrap();
|
||||||
@@ -9893,7 +10041,7 @@ mod tests {
|
|||||||
reopened
|
reopened
|
||||||
.get_workspace_runtime_verification("workspace-a", "runtime-a")
|
.get_workspace_runtime_verification("workspace-a", "runtime-a")
|
||||||
.unwrap(),
|
.unwrap(),
|
||||||
Some(evidence)
|
Some(newer_verified)
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
reopened
|
reopened
|
||||||
|
|||||||
Reference in New Issue
Block a user