fix: enforce Workspace Runtime binding lifecycle
This commit is contained in:
@@ -10331,11 +10331,27 @@ fn working_directory_diagnostics(
|
|||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn require_active_workspace_runtime_binding(
|
||||||
|
api: &WorkspaceApi,
|
||||||
|
runtime_id: &str,
|
||||||
|
) -> ApiResult<()> {
|
||||||
|
let binding = api
|
||||||
|
.store
|
||||||
|
.get_workspace_runtime_binding(&api.config.workspace_id, runtime_id)
|
||||||
|
.await?
|
||||||
|
.ok_or_else(|| Error::UnknownRuntime(runtime_id.to_string()))?;
|
||||||
|
if binding.revoked_at.is_some() {
|
||||||
|
return Err(Error::UnknownRuntime(runtime_id.to_string()).into());
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
async fn scoped_list_runtime_working_directories(
|
async fn scoped_list_runtime_working_directories(
|
||||||
State(api): State<WorkspaceApi>,
|
State(api): State<WorkspaceApi>,
|
||||||
AxumPath(path): AxumPath<ScopedRuntimePath>,
|
AxumPath(path): AxumPath<ScopedRuntimePath>,
|
||||||
) -> ApiResult<Json<BrowserWorkingDirectoryListResponse>> {
|
) -> ApiResult<Json<BrowserWorkingDirectoryListResponse>> {
|
||||||
validate_workspace_scope(&api, &path.workspace_id)?;
|
validate_workspace_scope(&api, &path.workspace_id)?;
|
||||||
|
require_active_workspace_runtime_binding(&api, &path.runtime_id).await?;
|
||||||
let (items, diagnostics) = runtime_working_directory_summaries(&api, &path.runtime_id)?;
|
let (items, diagnostics) = runtime_working_directory_summaries(&api, &path.runtime_id)?;
|
||||||
Ok(Json(BrowserWorkingDirectoryListResponse {
|
Ok(Json(BrowserWorkingDirectoryListResponse {
|
||||||
workspace_id: api.config.workspace_id.clone(),
|
workspace_id: api.config.workspace_id.clone(),
|
||||||
@@ -10349,6 +10365,7 @@ async fn scoped_create_runtime_working_directory(
|
|||||||
AxumPath(path): AxumPath<ScopedRuntimePath>,
|
AxumPath(path): AxumPath<ScopedRuntimePath>,
|
||||||
Json(request): Json<BrowserWorkingDirectoryCreateRequest>,
|
Json(request): Json<BrowserWorkingDirectoryCreateRequest>,
|
||||||
) -> ApiResult<(StatusCode, Json<BrowserWorkingDirectoryCreateResponse>)> {
|
) -> ApiResult<(StatusCode, Json<BrowserWorkingDirectoryCreateResponse>)> {
|
||||||
|
require_active_workspace_runtime_binding(&api, &path.runtime_id).await?;
|
||||||
create_workspace_working_directory(
|
create_workspace_working_directory(
|
||||||
&api,
|
&api,
|
||||||
&path.workspace_id,
|
&path.workspace_id,
|
||||||
@@ -10363,6 +10380,7 @@ async fn scoped_runtime_working_directory_detail(
|
|||||||
AxumPath(path): AxumPath<ScopedRuntimeWorkingDirectoryPath>,
|
AxumPath(path): AxumPath<ScopedRuntimeWorkingDirectoryPath>,
|
||||||
) -> ApiResult<Json<BrowserWorkingDirectoryDetailResponse>> {
|
) -> ApiResult<Json<BrowserWorkingDirectoryDetailResponse>> {
|
||||||
validate_workspace_scope(&api, &path.workspace_id)?;
|
validate_workspace_scope(&api, &path.workspace_id)?;
|
||||||
|
require_active_workspace_runtime_binding(&api, &path.runtime_id).await?;
|
||||||
working_directory_detail_for_runtime(api, &path.runtime_id, &path.working_directory_id)
|
working_directory_detail_for_runtime(api, &path.runtime_id, &path.working_directory_id)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -10374,6 +10392,7 @@ async fn scoped_cleanup_runtime_working_directory(
|
|||||||
Json(request): Json<WorkingDirectoryRemovalRequest>,
|
Json(request): Json<WorkingDirectoryRemovalRequest>,
|
||||||
) -> ApiResult<Json<WorkingDirectoryRemovalResponse>> {
|
) -> ApiResult<Json<WorkingDirectoryRemovalResponse>> {
|
||||||
validate_workspace_scope(&api, &path.workspace_id)?;
|
validate_workspace_scope(&api, &path.workspace_id)?;
|
||||||
|
require_active_workspace_runtime_binding(&api, &path.runtime_id).await?;
|
||||||
let registered_runtime = registered_workdir_runtime_id(&api, &path.working_directory_id)?;
|
let registered_runtime = registered_workdir_runtime_id(&api, &path.working_directory_id)?;
|
||||||
if registered_runtime != path.runtime_id {
|
if registered_runtime != path.runtime_id {
|
||||||
return Err(ApiError::from(Error::WorkspacePermissionDenied(
|
return Err(ApiError::from(Error::WorkspacePermissionDenied(
|
||||||
@@ -13587,34 +13606,48 @@ async fn delete_remote_runtime(
|
|||||||
)
|
)
|
||||||
.into());
|
.into());
|
||||||
}
|
}
|
||||||
match api
|
let has_other_active_binding = api
|
||||||
.runtime
|
.store
|
||||||
.unregister_if_idle(&runtime_id, api.config.max_records.min(200))
|
.has_other_active_workspace_runtime_binding(&api.config.workspace_id, &runtime_id)
|
||||||
.map_err(|err| err.into_error())?
|
.await?;
|
||||||
{
|
if !has_other_active_binding {
|
||||||
RuntimeRegistryUnregisterResult::Removed | RuntimeRegistryUnregisterResult::NotFound => {}
|
match api
|
||||||
RuntimeRegistryUnregisterResult::BlockedByWorkers {
|
.runtime
|
||||||
worker_count,
|
.unregister_if_idle(&runtime_id, api.config.max_records.min(200))
|
||||||
diagnostics,
|
.map_err(|err| err.into_error())?
|
||||||
} => {
|
{
|
||||||
let mut diagnostics = diagnostics;
|
RuntimeRegistryUnregisterResult::Removed
|
||||||
diagnostics.push(settings_diagnostic(
|
| RuntimeRegistryUnregisterResult::NotFound => {}
|
||||||
"remote_runtime_delete_blocked",
|
RuntimeRegistryUnregisterResult::BlockedByWorkers {
|
||||||
DiagnosticSeverity::Error,
|
worker_count,
|
||||||
format!(
|
|
||||||
"Remote Runtime '{runtime_id}' has {worker_count} active worker(s); stop or move them before deleting it."
|
|
||||||
),
|
|
||||||
));
|
|
||||||
return Err(ApiError::with_diagnostics(
|
|
||||||
Error::RuntimeOperationFailed {
|
|
||||||
runtime_id,
|
|
||||||
code: "remote_runtime_delete_blocked".to_string(),
|
|
||||||
message: "Remote Runtime has active workers".to_string(),
|
|
||||||
},
|
|
||||||
diagnostics,
|
diagnostics,
|
||||||
));
|
} => {
|
||||||
|
let mut diagnostics = diagnostics;
|
||||||
|
diagnostics.push(settings_diagnostic(
|
||||||
|
"remote_runtime_delete_blocked",
|
||||||
|
DiagnosticSeverity::Error,
|
||||||
|
format!(
|
||||||
|
"Remote Runtime '{runtime_id}' has {worker_count} active worker(s); stop or move them before deleting its final Workspace registration."
|
||||||
|
),
|
||||||
|
));
|
||||||
|
return Err(ApiError::with_diagnostics(
|
||||||
|
Error::RuntimeOperationFailed {
|
||||||
|
runtime_id,
|
||||||
|
code: "remote_runtime_delete_blocked".to_string(),
|
||||||
|
message: "Remote Runtime has active workers".to_string(),
|
||||||
|
},
|
||||||
|
diagnostics,
|
||||||
|
));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if !api
|
||||||
|
.store
|
||||||
|
.delete_workspace_runtime_binding(&api.config.workspace_id, &runtime_id)
|
||||||
|
.await?
|
||||||
|
{
|
||||||
|
return Err(Error::UnknownRuntime(runtime_id).into());
|
||||||
|
}
|
||||||
Ok(StatusCode::NO_CONTENT)
|
Ok(StatusCode::NO_CONTENT)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -16130,16 +16163,20 @@ fn runtime_binding_summary(
|
|||||||
binding: &WorkspaceRuntimeBinding,
|
binding: &WorkspaceRuntimeBinding,
|
||||||
verification: Option<&crate::store::WorkspaceRuntimeVerificationEvidence>,
|
verification: Option<&crate::store::WorkspaceRuntimeVerificationEvidence>,
|
||||||
) -> WorkspaceRuntimeBindingSummary {
|
) -> WorkspaceRuntimeBindingSummary {
|
||||||
let valid_verification = verification.filter(|verification| {
|
let current_verification = verification.filter(|verification| {
|
||||||
verification.state == "verified"
|
verification.binding_revision == binding.binding_revision
|
||||||
&& verification.binding_revision == binding.binding_revision
|
|
||||||
&& verification.runtime_public_key_fingerprint == binding.public_key_fingerprint
|
&& verification.runtime_public_key_fingerprint == binding.public_key_fingerprint
|
||||||
&& verification.workspace_key_id
|
&& verification.workspace_key_id
|
||||||
== binding.workspace_key_id.as_deref().unwrap_or_default()
|
== binding.workspace_key_id.as_deref().unwrap_or_default()
|
||||||
});
|
});
|
||||||
|
let valid_verification = current_verification.filter(|verification| {
|
||||||
|
verification.state == "verified" && verification.last_outcome == "verified"
|
||||||
|
});
|
||||||
let connection_state = match binding.state {
|
let connection_state = match binding.state {
|
||||||
StoredRuntimeBindingState::Revoked => RuntimeConnectionDisplayState::Revoked,
|
StoredRuntimeBindingState::Revoked => RuntimeConnectionDisplayState::Revoked,
|
||||||
_ if verification.is_some_and(|verification| verification.last_outcome != "verified") => {
|
_ if current_verification
|
||||||
|
.is_some_and(|verification| verification.last_outcome != "verified") =>
|
||||||
|
{
|
||||||
RuntimeConnectionDisplayState::Unavailable
|
RuntimeConnectionDisplayState::Unavailable
|
||||||
}
|
}
|
||||||
StoredRuntimeBindingState::Verified
|
StoredRuntimeBindingState::Verified
|
||||||
@@ -16161,7 +16198,7 @@ fn runtime_binding_summary(
|
|||||||
revision: binding.binding_revision,
|
revision: binding.binding_revision,
|
||||||
workspace_key_id: binding.workspace_key_id.clone(),
|
workspace_key_id: binding.workspace_key_id.clone(),
|
||||||
workspace_key_generation: binding.workspace_key_generation,
|
workspace_key_generation: binding.workspace_key_generation,
|
||||||
verification: verification.and_then(runtime_verification_summary),
|
verification: current_verification.and_then(runtime_verification_summary),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -20957,6 +20994,59 @@ mod tests {
|
|||||||
config
|
config
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn scoped_runtime_workdir_access_requires_workspace_binding() {
|
||||||
|
let dir = tempfile::tempdir().unwrap();
|
||||||
|
let api = test_api(dir.path()).await;
|
||||||
|
|
||||||
|
let response = require_active_workspace_runtime_binding(&api, "unregistered-runtime")
|
||||||
|
.await
|
||||||
|
.unwrap_err()
|
||||||
|
.into_response();
|
||||||
|
|
||||||
|
assert_eq!(response.status(), StatusCode::NOT_FOUND);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn runtime_binding_summary_omits_stale_verification_revision() {
|
||||||
|
let binding = WorkspaceRuntimeBinding {
|
||||||
|
workspace_id: "workspace-a".to_string(),
|
||||||
|
runtime_id: "runtime-a".to_string(),
|
||||||
|
display_name: "Runtime A".to_string(),
|
||||||
|
base_url: "https://runtime.example.test".to_string(),
|
||||||
|
public_key: "runtime-public-key".to_string(),
|
||||||
|
public_key_fingerprint: "sha256:runtime".to_string(),
|
||||||
|
binding_revision: 2,
|
||||||
|
state: crate::store::WorkspaceRuntimeBindingState::Revoked,
|
||||||
|
authentication_mode:
|
||||||
|
crate::store::WorkspaceRuntimeAuthenticationMode::WorkspaceIdentity,
|
||||||
|
workspace_key_id: Some("WK-a".to_string()),
|
||||||
|
workspace_key_generation: Some(1),
|
||||||
|
created_at: "1".to_string(),
|
||||||
|
updated_at: "2".to_string(),
|
||||||
|
revoked_at: Some("2".to_string()),
|
||||||
|
};
|
||||||
|
let stale = crate::store::WorkspaceRuntimeVerificationEvidence {
|
||||||
|
workspace_id: "workspace-a".to_string(),
|
||||||
|
runtime_id: "runtime-a".to_string(),
|
||||||
|
binding_revision: 1,
|
||||||
|
workspace_key_id: "WK-a".to_string(),
|
||||||
|
workspace_identity_revision: 1,
|
||||||
|
workspace_trust_generation: 1,
|
||||||
|
runtime_public_key_fingerprint: "sha256:runtime".to_string(),
|
||||||
|
runtime_identity_revision: 1,
|
||||||
|
challenge_id: "challenge-a".to_string(),
|
||||||
|
state: "failed".to_string(),
|
||||||
|
last_outcome: "connectivity_failed".to_string(),
|
||||||
|
verified_at: None,
|
||||||
|
checked_at: "1".to_string(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let summary = runtime_binding_summary(&binding, Some(&stale));
|
||||||
|
|
||||||
|
assert_eq!(summary.verification, None);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn embedded_runtime_request_source_uses_backend_public_url_audience() {
|
fn embedded_runtime_request_source_uses_backend_public_url_audience() {
|
||||||
let temp = tempfile::tempdir().unwrap();
|
let temp = tempfile::tempdir().unwrap();
|
||||||
@@ -27954,9 +28044,8 @@ mod tests {
|
|||||||
let persisted = store
|
let persisted = store
|
||||||
.get_workspace_runtime_binding(TEST_WORKSPACE_ID, "team-runtime")
|
.get_workspace_runtime_binding(TEST_WORKSPACE_ID, "team-runtime")
|
||||||
.await
|
.await
|
||||||
.unwrap()
|
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert!(persisted.revoked_at.is_some());
|
assert!(persisted.is_none());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test(flavor = "multi_thread")]
|
#[tokio::test(flavor = "multi_thread")]
|
||||||
|
|||||||
@@ -799,6 +799,16 @@ pub trait ControlPlaneStore: Send + Sync + WorkspaceDeletionStore {
|
|||||||
workspace_id: &str,
|
workspace_id: &str,
|
||||||
include_revoked: bool,
|
include_revoked: bool,
|
||||||
) -> Result<Vec<WorkspaceRuntimeBinding>>;
|
) -> Result<Vec<WorkspaceRuntimeBinding>>;
|
||||||
|
async fn has_other_active_workspace_runtime_binding(
|
||||||
|
&self,
|
||||||
|
workspace_id: &str,
|
||||||
|
runtime_id: &str,
|
||||||
|
) -> Result<bool>;
|
||||||
|
async fn delete_workspace_runtime_binding(
|
||||||
|
&self,
|
||||||
|
workspace_id: &str,
|
||||||
|
runtime_id: &str,
|
||||||
|
) -> Result<bool>;
|
||||||
async fn upsert_workspace_runtime_binding_record(
|
async fn upsert_workspace_runtime_binding_record(
|
||||||
&self,
|
&self,
|
||||||
record: WorkspaceRuntimeBinding,
|
record: WorkspaceRuntimeBinding,
|
||||||
@@ -1860,6 +1870,88 @@ impl SqliteWorkspaceStore {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn has_other_active_workspace_runtime_binding(
|
||||||
|
&self,
|
||||||
|
workspace_id: &str,
|
||||||
|
runtime_id: &str,
|
||||||
|
) -> Result<bool> {
|
||||||
|
validate_identifier("workspace_id", workspace_id)?;
|
||||||
|
validate_identifier("runtime_id", runtime_id)?;
|
||||||
|
self.with_conn(|conn| {
|
||||||
|
conn.query_row(
|
||||||
|
"SELECT EXISTS(
|
||||||
|
SELECT 1 FROM workspace_runtime_bindings
|
||||||
|
WHERE runtime_id = ?1 AND workspace_id <> ?2 AND revoked_at IS NULL
|
||||||
|
)",
|
||||||
|
params![runtime_id, workspace_id],
|
||||||
|
|row| row.get(0),
|
||||||
|
)
|
||||||
|
.map_err(Into::into)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn delete_workspace_runtime_binding(
|
||||||
|
&self,
|
||||||
|
workspace_id: &str,
|
||||||
|
runtime_id: &str,
|
||||||
|
) -> Result<bool> {
|
||||||
|
validate_identifier("workspace_id", workspace_id)?;
|
||||||
|
validate_identifier("runtime_id", runtime_id)?;
|
||||||
|
self.with_conn_mut(|conn| {
|
||||||
|
let tx = conn.transaction_with_behavior(TransactionBehavior::Immediate)?;
|
||||||
|
let revoked_at = tx
|
||||||
|
.query_row(
|
||||||
|
"SELECT revoked_at FROM workspace_runtime_bindings WHERE workspace_id = ?1 AND runtime_id = ?2",
|
||||||
|
params![workspace_id, runtime_id],
|
||||||
|
|row| row.get::<_, Option<String>>(0),
|
||||||
|
)
|
||||||
|
.optional()?;
|
||||||
|
let Some(revoked_at) = revoked_at else {
|
||||||
|
tx.commit()?;
|
||||||
|
return Ok(false);
|
||||||
|
};
|
||||||
|
if revoked_at.is_none() {
|
||||||
|
return Err(crate::Error::RuntimeBindingConflict(format!(
|
||||||
|
"Runtime binding `{runtime_id}` must be revoked before deletion"
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
let worker_count: i64 = tx.query_row(
|
||||||
|
"SELECT COUNT(*) FROM worker_registry WHERE workspace_id = ?1 AND runtime_id = ?2",
|
||||||
|
params![workspace_id, runtime_id],
|
||||||
|
|row| row.get(0),
|
||||||
|
)?;
|
||||||
|
if worker_count > 0 {
|
||||||
|
return Err(crate::Error::RuntimeBindingConflict(format!(
|
||||||
|
"Runtime binding `{runtime_id}` still has {worker_count} registered Worker(s) in Workspace `{workspace_id}`"
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
let workdir_count: i64 = tx.query_row(
|
||||||
|
"SELECT COUNT(*) FROM workdir_registry WHERE workspace_id = ?1 AND runtime_id = ?2",
|
||||||
|
params![workspace_id, runtime_id],
|
||||||
|
|row| row.get(0),
|
||||||
|
)?;
|
||||||
|
if workdir_count > 0 {
|
||||||
|
return Err(crate::Error::RuntimeBindingConflict(format!(
|
||||||
|
"Runtime binding `{runtime_id}` still has {workdir_count} registered Workdir(s) in Workspace `{workspace_id}`"
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
tx.execute(
|
||||||
|
"DELETE FROM worker_mutation_source_proof_jtis WHERE workspace_id = ?1 AND runtime_id = ?2",
|
||||||
|
params![workspace_id, runtime_id],
|
||||||
|
)?;
|
||||||
|
tx.execute(
|
||||||
|
"DELETE FROM workspace_runtime_binding_audit WHERE workspace_id = ?1 AND runtime_id = ?2",
|
||||||
|
params![workspace_id, runtime_id],
|
||||||
|
)?;
|
||||||
|
let deleted = tx.execute(
|
||||||
|
"DELETE FROM workspace_runtime_bindings WHERE workspace_id = ?1 AND runtime_id = ?2",
|
||||||
|
params![workspace_id, runtime_id],
|
||||||
|
)?;
|
||||||
|
tx.commit()?;
|
||||||
|
Ok(deleted == 1)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
pub fn upsert_workspace_runtime_binding(
|
pub fn upsert_workspace_runtime_binding(
|
||||||
&self,
|
&self,
|
||||||
mut record: WorkspaceRuntimeBinding,
|
mut record: WorkspaceRuntimeBinding,
|
||||||
@@ -1928,6 +2020,10 @@ impl SqliteWorkspaceStore {
|
|||||||
],
|
],
|
||||||
)
|
)
|
||||||
.map_err(map_runtime_binding_write_error)?;
|
.map_err(map_runtime_binding_write_error)?;
|
||||||
|
tx.execute(
|
||||||
|
"DELETE FROM workspace_runtime_verifications WHERE workspace_id = ?1 AND runtime_id = ?2",
|
||||||
|
params![record.workspace_id, record.runtime_id],
|
||||||
|
)?;
|
||||||
tx.commit()?;
|
tx.commit()?;
|
||||||
return Ok(WorkspaceRuntimeBindingUpsert::Replaced);
|
return Ok(WorkspaceRuntimeBindingUpsert::Replaced);
|
||||||
}
|
}
|
||||||
@@ -1968,14 +2064,22 @@ impl SqliteWorkspaceStore {
|
|||||||
validate_identifier("workspace_id", workspace_id)?;
|
validate_identifier("workspace_id", workspace_id)?;
|
||||||
validate_identifier("runtime_id", runtime_id)?;
|
validate_identifier("runtime_id", runtime_id)?;
|
||||||
validate_non_empty("revoked_at", revoked_at)?;
|
validate_non_empty("revoked_at", revoked_at)?;
|
||||||
self.with_conn(|conn| {
|
self.with_conn_mut(|conn| {
|
||||||
let changed = conn.execute(
|
let tx = conn.transaction_with_behavior(TransactionBehavior::Immediate)?;
|
||||||
|
let changed = tx.execute(
|
||||||
r#"UPDATE workspace_runtime_bindings
|
r#"UPDATE workspace_runtime_bindings
|
||||||
SET state = 'revoked', revoked_at = ?3, updated_at = ?3,
|
SET state = 'revoked', revoked_at = ?3, updated_at = ?3,
|
||||||
binding_revision = binding_revision + 1
|
binding_revision = binding_revision + 1
|
||||||
WHERE workspace_id = ?1 AND runtime_id = ?2 AND revoked_at IS NULL"#,
|
WHERE workspace_id = ?1 AND runtime_id = ?2 AND revoked_at IS NULL"#,
|
||||||
params![workspace_id, runtime_id, revoked_at],
|
params![workspace_id, runtime_id, revoked_at],
|
||||||
)?;
|
)?;
|
||||||
|
if changed > 0 {
|
||||||
|
tx.execute(
|
||||||
|
"DELETE FROM workspace_runtime_verifications WHERE workspace_id = ?1 AND runtime_id = ?2",
|
||||||
|
params![workspace_id, runtime_id],
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
tx.commit()?;
|
||||||
Ok(changed > 0)
|
Ok(changed > 0)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -2081,6 +2185,10 @@ impl SqliteWorkspaceStore {
|
|||||||
record.updated_at,
|
record.updated_at,
|
||||||
],
|
],
|
||||||
)?;
|
)?;
|
||||||
|
tx.execute(
|
||||||
|
"DELETE FROM workspace_runtime_verifications WHERE workspace_id = ?1 AND runtime_id = ?2",
|
||||||
|
params![record.workspace_id, record.runtime_id],
|
||||||
|
)?;
|
||||||
insert_workspace_runtime_binding_audit(
|
insert_workspace_runtime_binding_audit(
|
||||||
&tx,
|
&tx,
|
||||||
&record.workspace_id,
|
&record.workspace_id,
|
||||||
@@ -2211,6 +2319,10 @@ impl SqliteWorkspaceStore {
|
|||||||
WHERE workspace_id = ?1 AND runtime_id = ?2"#,
|
WHERE workspace_id = ?1 AND runtime_id = ?2"#,
|
||||||
params![workspace_id, runtime_id, revoked_at, next_revision],
|
params![workspace_id, runtime_id, revoked_at, next_revision],
|
||||||
)?;
|
)?;
|
||||||
|
tx.execute(
|
||||||
|
"DELETE FROM workspace_runtime_verifications WHERE workspace_id = ?1 AND runtime_id = ?2",
|
||||||
|
params![workspace_id, runtime_id],
|
||||||
|
)?;
|
||||||
insert_workspace_runtime_binding_audit(
|
insert_workspace_runtime_binding_audit(
|
||||||
&tx,
|
&tx,
|
||||||
workspace_id,
|
workspace_id,
|
||||||
@@ -3331,6 +3443,26 @@ impl ControlPlaneStore for SqliteWorkspaceStore {
|
|||||||
SqliteWorkspaceStore::list_workspace_runtime_bindings(self, workspace_id, include_revoked)
|
SqliteWorkspaceStore::list_workspace_runtime_bindings(self, workspace_id, include_revoked)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn has_other_active_workspace_runtime_binding(
|
||||||
|
&self,
|
||||||
|
workspace_id: &str,
|
||||||
|
runtime_id: &str,
|
||||||
|
) -> Result<bool> {
|
||||||
|
SqliteWorkspaceStore::has_other_active_workspace_runtime_binding(
|
||||||
|
self,
|
||||||
|
workspace_id,
|
||||||
|
runtime_id,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn delete_workspace_runtime_binding(
|
||||||
|
&self,
|
||||||
|
workspace_id: &str,
|
||||||
|
runtime_id: &str,
|
||||||
|
) -> Result<bool> {
|
||||||
|
SqliteWorkspaceStore::delete_workspace_runtime_binding(self, workspace_id, runtime_id)
|
||||||
|
}
|
||||||
|
|
||||||
async fn upsert_workspace_runtime_binding_record(
|
async fn upsert_workspace_runtime_binding_record(
|
||||||
&self,
|
&self,
|
||||||
record: WorkspaceRuntimeBinding,
|
record: WorkspaceRuntimeBinding,
|
||||||
@@ -10044,6 +10176,28 @@ mod tests {
|
|||||||
.revoked_at
|
.revoked_at
|
||||||
.is_some()
|
.is_some()
|
||||||
);
|
);
|
||||||
|
assert!(
|
||||||
|
reopened
|
||||||
|
.has_other_active_workspace_runtime_binding("workspace-a", "shared")
|
||||||
|
.unwrap()
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
reopened
|
||||||
|
.delete_workspace_runtime_binding("workspace-a", "shared")
|
||||||
|
.unwrap()
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
reopened
|
||||||
|
.get_workspace_runtime_binding("workspace-a", "shared")
|
||||||
|
.unwrap()
|
||||||
|
.is_none()
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
reopened
|
||||||
|
.get_workspace_runtime_binding("workspace-b", "shared")
|
||||||
|
.unwrap()
|
||||||
|
.is_some()
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -10485,6 +10639,29 @@ mod tests {
|
|||||||
.unwrap();
|
.unwrap();
|
||||||
assert_eq!(replaced, WorkspaceRuntimeBindingMutation::Replaced);
|
assert_eq!(replaced, WorkspaceRuntimeBindingMutation::Replaced);
|
||||||
assert_eq!(replaced_binding.binding_revision, 2);
|
assert_eq!(replaced_binding.binding_revision, 2);
|
||||||
|
store
|
||||||
|
.record_workspace_runtime_verification_attempt(&WorkspaceRuntimeVerificationEvidence {
|
||||||
|
workspace_id: "workspace-a".to_string(),
|
||||||
|
runtime_id: "runtime-a".to_string(),
|
||||||
|
binding_revision: 2,
|
||||||
|
workspace_key_id: "WK-a".to_string(),
|
||||||
|
workspace_identity_revision: 1,
|
||||||
|
workspace_trust_generation: 1,
|
||||||
|
runtime_public_key_fingerprint: replaced_binding.public_key_fingerprint.clone(),
|
||||||
|
runtime_identity_revision: 1,
|
||||||
|
challenge_id: "challenge-a".to_string(),
|
||||||
|
state: "failed".to_string(),
|
||||||
|
last_outcome: "connectivity_failed".to_string(),
|
||||||
|
verified_at: None,
|
||||||
|
checked_at: "3".to_string(),
|
||||||
|
})
|
||||||
|
.unwrap();
|
||||||
|
assert!(
|
||||||
|
store
|
||||||
|
.get_workspace_runtime_verification("workspace-a", "runtime-a")
|
||||||
|
.unwrap()
|
||||||
|
.is_some()
|
||||||
|
);
|
||||||
let (revoked, revoked_binding) = store
|
let (revoked, revoked_binding) = store
|
||||||
.revoke_workspace_runtime_binding_key("workspace-a", "runtime-a", 2, "owner", "4")
|
.revoke_workspace_runtime_binding_key("workspace-a", "runtime-a", 2, "owner", "4")
|
||||||
.unwrap();
|
.unwrap();
|
||||||
@@ -10492,6 +10669,12 @@ mod tests {
|
|||||||
assert_eq!(revoked_binding.binding_revision, 3);
|
assert_eq!(revoked_binding.binding_revision, 3);
|
||||||
assert_eq!(revoked_binding.revoked_at.as_deref(), Some("4"));
|
assert_eq!(revoked_binding.revoked_at.as_deref(), Some("4"));
|
||||||
assert_eq!(revoked_binding.state, WorkspaceRuntimeBindingState::Revoked);
|
assert_eq!(revoked_binding.state, WorkspaceRuntimeBindingState::Revoked);
|
||||||
|
assert_eq!(
|
||||||
|
store
|
||||||
|
.get_workspace_runtime_verification("workspace-a", "runtime-a")
|
||||||
|
.unwrap(),
|
||||||
|
None
|
||||||
|
);
|
||||||
let (reactivated, reactivated_binding) = store
|
let (reactivated, reactivated_binding) = store
|
||||||
.put_workspace_runtime_binding_key(
|
.put_workspace_runtime_binding_key(
|
||||||
binding(second.public_key.clone(), "5"),
|
binding(second.public_key.clone(), "5"),
|
||||||
|
|||||||
Reference in New Issue
Block a user