From 911162372cd31c9852107f97388b3bc92c3c51a2 Mon Sep 17 00:00:00 2001 From: Hare Date: Wed, 26 Aug 2026 11:38:23 +0900 Subject: [PATCH] fix: complete Repository access management contracts --- crates/workspace-server/src/server.rs | 38 ++++++++++++++- .../settings/repository-access/+page.svelte | 8 ++-- .../test/repository-access/ui.test.ts | 46 +++++++++++++++++++ 3 files changed, 86 insertions(+), 6 deletions(-) create mode 100644 web/workspace/test/repository-access/ui.test.ts diff --git a/crates/workspace-server/src/server.rs b/crates/workspace-server/src/server.rs index 2cf6fc6b..35919ab4 100644 --- a/crates/workspace-server/src/server.rs +++ b/crates/workspace-server/src/server.rs @@ -1890,7 +1890,8 @@ fn build_inner_router(api: WorkspaceApi) -> Router { ) .route( "/api/w/{workspace_id}/settings/repository-access/credentials/{credential_id}", - delete(scoped_delete_repository_ssh_credential), + get(scoped_get_repository_ssh_credential) + .delete(scoped_delete_repository_ssh_credential), ) .route( "/api/w/{workspace_id}/settings/repository-access/credentials/{credential_id}/rotate", @@ -1903,7 +1904,8 @@ fn build_inner_router(api: WorkspaceApi) -> Router { ) .route( "/api/w/{workspace_id}/settings/repository-access/host-trusts/{host_trust_id}", - delete(scoped_delete_repository_ssh_host_trust), + get(scoped_get_repository_ssh_host_trust) + .delete(scoped_delete_repository_ssh_host_trust), ) .route( "/api/w/{workspace_id}/config/source-tree", @@ -3389,6 +3391,22 @@ async fn scoped_list_repository_ssh_credentials( )) } +async fn scoped_get_repository_ssh_credential( + State(api): State, + AxumPath(path): AxumPath, + Extension(actor): Extension, +) -> ApiResult> { + require_manage_repository_secrets(&api, &path.workspace_id, &actor).await?; + let projection = active_repository_access_projection(&api, &path.workspace_id)?; + let credential = api + .repository_secrets + .list_credentials(&path.workspace_id, &projection)? + .into_iter() + .find(|credential| credential.credential_id == path.credential_id) + .ok_or_else(|| Error::InvalidRecordId(path.credential_id.clone()))?; + Ok(Json(credential)) +} + async fn scoped_create_repository_ssh_credential( State(api): State, AxumPath(path): AxumPath, @@ -3448,6 +3466,22 @@ async fn scoped_list_repository_ssh_host_trusts( )) } +async fn scoped_get_repository_ssh_host_trust( + State(api): State, + AxumPath(path): AxumPath, + Extension(actor): Extension, +) -> ApiResult> { + require_manage_repository_secrets(&api, &path.workspace_id, &actor).await?; + let projection = active_repository_access_projection(&api, &path.workspace_id)?; + let host_trust = api + .repository_secrets + .list_host_trusts(&path.workspace_id, &projection)? + .into_iter() + .find(|host_trust| host_trust.host_trust_id == path.host_trust_id) + .ok_or_else(|| Error::InvalidRecordId(path.host_trust_id.clone()))?; + Ok(Json(host_trust)) +} + async fn scoped_put_repository_ssh_host_trust( State(api): State, AxumPath(path): AxumPath, diff --git a/web/workspace/src/routes/w/[workspaceId]/settings/repository-access/+page.svelte b/web/workspace/src/routes/w/[workspaceId]/settings/repository-access/+page.svelte index e1b4225d..3852a6fc 100644 --- a/web/workspace/src/routes/w/[workspaceId]/settings/repository-access/+page.svelte +++ b/web/workspace/src/routes/w/[workspaceId]/settings/repository-access/+page.svelte @@ -63,12 +63,12 @@ credentials = [...credentials, created].sort((a, b) => a.credential_id.localeCompare(b.credential_id)); credentialId = ''; credentialName = ''; - privateKey = ''; - passphrase = ''; message = `Credential ${created.credential_id} created. Pasted secret fields were cleared.`; } catch (error) { message = error instanceof Error ? error.message : 'Credential creation failed'; } finally { + privateKey = ''; + passphrase = ''; pending = false; } } @@ -88,13 +88,13 @@ } ); credentials = credentials.map((entry) => entry.credential_id === rotated.credential_id ? rotated : entry); - rotatePrivateKey = ''; - rotatePassphrase = ''; rotateCredentialId = null; message = `Credential ${rotated.credential_id} rotated to revision ${rotated.current_revision}. Pasted secret fields were cleared.`; } catch (error) { message = error instanceof Error ? error.message : 'Credential rotation failed'; } finally { + rotatePrivateKey = ''; + rotatePassphrase = ''; pending = false; } } diff --git a/web/workspace/test/repository-access/ui.test.ts b/web/workspace/test/repository-access/ui.test.ts new file mode 100644 index 00000000..57e0186f --- /dev/null +++ b/web/workspace/test/repository-access/ui.test.ts @@ -0,0 +1,46 @@ +type TestRegistrar = (name: string, body: () => void | Promise) => void; + +const test = + (globalThis as unknown as { Deno: { test: TestRegistrar } }).Deno.test; + +function assert(condition: boolean, message: string): asserts condition { + if (!condition) throw new Error(message); +} + +const source = await Deno.readTextFile( + new URL( + "../../src/routes/w/[workspaceId]/settings/repository-access/+page.svelte", + import.meta.url, + ), +); + +test("Repository credential submissions clear write-only fields in finally blocks", () => { + const createStart = source.indexOf("async function createCredential()"); + const rotateStart = source.indexOf("async function rotateCredential("); + const deleteStart = source.indexOf("async function deleteCredential("); + assert( + createStart >= 0 && rotateStart > createStart && deleteStart > rotateStart, + "credential handlers should appear in source order", + ); + + const createBody = source.slice(createStart, rotateStart); + const rotateBody = source.slice(rotateStart, deleteStart); + for (const token of ["finally", "privateKey = ''", "passphrase = ''"]) { + assert( + createBody.includes(token), + `create handler should contain ${token}`, + ); + } + for ( + const token of [ + "finally", + "rotatePrivateKey = ''", + "rotatePassphrase = ''", + ] + ) { + assert( + rotateBody.includes(token), + `rotate handler should contain ${token}`, + ); + } +});