fix: complete Repository access management contracts
This commit is contained in:
@@ -1890,7 +1890,8 @@ fn build_inner_router(api: WorkspaceApi) -> Router {
|
|||||||
)
|
)
|
||||||
.route(
|
.route(
|
||||||
"/api/w/{workspace_id}/settings/repository-access/credentials/{credential_id}",
|
"/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(
|
.route(
|
||||||
"/api/w/{workspace_id}/settings/repository-access/credentials/{credential_id}/rotate",
|
"/api/w/{workspace_id}/settings/repository-access/credentials/{credential_id}/rotate",
|
||||||
@@ -1903,7 +1904,8 @@ fn build_inner_router(api: WorkspaceApi) -> Router {
|
|||||||
)
|
)
|
||||||
.route(
|
.route(
|
||||||
"/api/w/{workspace_id}/settings/repository-access/host-trusts/{host_trust_id}",
|
"/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(
|
.route(
|
||||||
"/api/w/{workspace_id}/config/source-tree",
|
"/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<WorkspaceApi>,
|
||||||
|
AxumPath(path): AxumPath<ScopedRepositoryCredentialPath>,
|
||||||
|
Extension(actor): Extension<RequestActor>,
|
||||||
|
) -> ApiResult<Json<RepositorySshCredential>> {
|
||||||
|
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(
|
async fn scoped_create_repository_ssh_credential(
|
||||||
State(api): State<WorkspaceApi>,
|
State(api): State<WorkspaceApi>,
|
||||||
AxumPath(path): AxumPath<ScopedWorkspacePath>,
|
AxumPath(path): AxumPath<ScopedWorkspacePath>,
|
||||||
@@ -3448,6 +3466,22 @@ async fn scoped_list_repository_ssh_host_trusts(
|
|||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn scoped_get_repository_ssh_host_trust(
|
||||||
|
State(api): State<WorkspaceApi>,
|
||||||
|
AxumPath(path): AxumPath<ScopedRepositoryHostTrustPath>,
|
||||||
|
Extension(actor): Extension<RequestActor>,
|
||||||
|
) -> ApiResult<Json<RepositorySshHostTrust>> {
|
||||||
|
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(
|
async fn scoped_put_repository_ssh_host_trust(
|
||||||
State(api): State<WorkspaceApi>,
|
State(api): State<WorkspaceApi>,
|
||||||
AxumPath(path): AxumPath<ScopedWorkspacePath>,
|
AxumPath(path): AxumPath<ScopedWorkspacePath>,
|
||||||
|
|||||||
@@ -63,12 +63,12 @@
|
|||||||
credentials = [...credentials, created].sort((a, b) => a.credential_id.localeCompare(b.credential_id));
|
credentials = [...credentials, created].sort((a, b) => a.credential_id.localeCompare(b.credential_id));
|
||||||
credentialId = '';
|
credentialId = '';
|
||||||
credentialName = '';
|
credentialName = '';
|
||||||
privateKey = '';
|
|
||||||
passphrase = '';
|
|
||||||
message = `Credential ${created.credential_id} created. Pasted secret fields were cleared.`;
|
message = `Credential ${created.credential_id} created. Pasted secret fields were cleared.`;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
message = error instanceof Error ? error.message : 'Credential creation failed';
|
message = error instanceof Error ? error.message : 'Credential creation failed';
|
||||||
} finally {
|
} finally {
|
||||||
|
privateKey = '';
|
||||||
|
passphrase = '';
|
||||||
pending = false;
|
pending = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -88,13 +88,13 @@
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
credentials = credentials.map((entry) => entry.credential_id === rotated.credential_id ? rotated : entry);
|
credentials = credentials.map((entry) => entry.credential_id === rotated.credential_id ? rotated : entry);
|
||||||
rotatePrivateKey = '';
|
|
||||||
rotatePassphrase = '';
|
|
||||||
rotateCredentialId = null;
|
rotateCredentialId = null;
|
||||||
message = `Credential ${rotated.credential_id} rotated to revision ${rotated.current_revision}. Pasted secret fields were cleared.`;
|
message = `Credential ${rotated.credential_id} rotated to revision ${rotated.current_revision}. Pasted secret fields were cleared.`;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
message = error instanceof Error ? error.message : 'Credential rotation failed';
|
message = error instanceof Error ? error.message : 'Credential rotation failed';
|
||||||
} finally {
|
} finally {
|
||||||
|
rotatePrivateKey = '';
|
||||||
|
rotatePassphrase = '';
|
||||||
pending = false;
|
pending = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,46 @@
|
|||||||
|
type TestRegistrar = (name: string, body: () => void | Promise<void>) => 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}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user