fix: enforce hierarchical external Workdir permissions
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
// @generated by yoi-companion-openapi-typescript@1. Do not edit.
|
||||
// Input: openapi/server-api.json
|
||||
// Input contract: 0.1.0 (sha256:3a298734fd19f3a224e4f1503385fb8a823b612d328224f45f677ebaca28b96c)
|
||||
// Input contract: 0.1.0 (sha256:7b06d005d3436758c96fc4cfd462df76460f89620fd1b58fdf05009d96bb9052)
|
||||
// Options: companion-operation-schema-closure,openapi-3.1,strict-integer-ranges,sorted-output
|
||||
// Output: web/workspace/src/lib/generated/companion-api.ts
|
||||
// Regenerate/check: cargo run -q -p server-api --example generate_companion_api_types -- --check
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// @generated by yoi-repository-openapi-typescript@1. Do not edit.
|
||||
// Input: openapi/server-api.json
|
||||
// Input contract: 0.1.0 (sha256:3a298734fd19f3a224e4f1503385fb8a823b612d328224f45f677ebaca28b96c)
|
||||
// Input contract: 0.1.0 (sha256:7b06d005d3436758c96fc4cfd462df76460f89620fd1b58fdf05009d96bb9052)
|
||||
// Options: repository-operation-schema-closure,openapi-3.1,format:uint64=safe-number,strict-safe-integers,sorted-output
|
||||
// Output: web/workspace/src/lib/generated/repository-api.ts
|
||||
// Regenerate/check: cargo run -q -p server-api --example generate_repository_openapi_types -- --check
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// @generated by yoi-runtime-openapi-typescript@1. Do not edit.
|
||||
// Input: openapi/server-api.json
|
||||
// Input contract: 0.1.0 (sha256:3a298734fd19f3a224e4f1503385fb8a823b612d328224f45f677ebaca28b96c)
|
||||
// Input contract: 0.1.0 (sha256:7b06d005d3436758c96fc4cfd462df76460f89620fd1b58fdf05009d96bb9052)
|
||||
// Options: runtime-operation-schema-closure,openapi-3.1,strict-integer-ranges,sorted-output
|
||||
// Output: web/workspace/src/lib/generated/runtime-api.ts
|
||||
// Regenerate/check: cargo run -q -p server-api --example generate_runtime_api_types -- --check
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// @generated by yoi-ticket-browser-openapi-typescript@1. Do not edit.
|
||||
// Input: openapi/server-api.json
|
||||
// Input contract: 0.1.0 (sha256:3a298734fd19f3a224e4f1503385fb8a823b612d328224f45f677ebaca28b96c)
|
||||
// Input contract: 0.1.0 (sha256:7b06d005d3436758c96fc4cfd462df76460f89620fd1b58fdf05009d96bb9052)
|
||||
// Options: ticket-objective-merge-request-browser-operation-schema-closure,openapi-3.1,strict-integer-ranges,sorted-output
|
||||
// Output: web/workspace/src/lib/generated/ticket-api.ts
|
||||
// Regenerate/check: cargo run -q -p server-api --example generate_ticket_api_types -- --check
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// @generated by yoi-worker-openapi-typescript@1. Do not edit.
|
||||
// Input: openapi/server-api.json
|
||||
// Input contract: 0.1.0 (sha256:3a298734fd19f3a224e4f1503385fb8a823b612d328224f45f677ebaca28b96c)
|
||||
// Input contract: 0.1.0 (sha256:7b06d005d3436758c96fc4cfd462df76460f89620fd1b58fdf05009d96bb9052)
|
||||
// Options: worker-operation-schema-closure,openapi-3.1,strict-integer-ranges,sorted-output
|
||||
// Output: web/workspace/src/lib/generated/worker-launch-api.ts
|
||||
// Regenerate/check: cargo run -q -p server-api --example generate_worker_launch_api_types -- --check
|
||||
|
||||
@@ -182,7 +182,9 @@ function parseWorkingDirectorySource(value: unknown): WorkingDirectorySource {
|
||||
);
|
||||
const kind = stringField(source, "kind");
|
||||
if (kind === "repository") {
|
||||
if (source.grant_id !== undefined || source.grant_permissions !== undefined) {
|
||||
if (
|
||||
source.grant_id !== undefined || source.grant_permissions !== undefined
|
||||
) {
|
||||
throw new Error(
|
||||
"Repository Workdir source must not contain External grant authority",
|
||||
);
|
||||
@@ -201,7 +203,7 @@ function parseWorkingDirectorySource(value: unknown): WorkingDirectorySource {
|
||||
return {
|
||||
kind,
|
||||
grant_id: stringField(source, "grant_id"),
|
||||
grant_permissions: parseExternalWorkdirPermissions(
|
||||
grant_permissions: parseExternalWorkdirGrantPermissions(
|
||||
source.grant_permissions,
|
||||
"External Workdir grant permissions",
|
||||
),
|
||||
@@ -226,6 +228,21 @@ export function parseExternalWorkdirPermissions(
|
||||
};
|
||||
}
|
||||
|
||||
export function parseExternalWorkdirGrantPermissions(
|
||||
value: unknown,
|
||||
label = "External Workdir grant permissions",
|
||||
): ExternalWorkdirPermissions {
|
||||
const permissions = parseExternalWorkdirPermissions(value, label);
|
||||
const hierarchical = permissions.read &&
|
||||
(!permissions.command || permissions.write);
|
||||
if (!hierarchical) {
|
||||
throw new Error(
|
||||
`${label} must be READ=(true,false,false), WRITE=(true,true,false), or COMMAND=(true,true,true)`,
|
||||
);
|
||||
}
|
||||
return permissions;
|
||||
}
|
||||
|
||||
function parseCleanupTarget(value: unknown): WorkingDirectoryCleanupTarget {
|
||||
const record = exactRecord(
|
||||
value,
|
||||
|
||||
@@ -106,7 +106,7 @@ Deno.test("Worker summary parser preserves attachment-effective External permiss
|
||||
source: {
|
||||
kind: "external_grant",
|
||||
grant_id: "grant-1",
|
||||
grant_permissions: { read: true, write: false, command: true },
|
||||
grant_permissions: { read: true, write: true, command: true },
|
||||
},
|
||||
materializer_kind: "client_hosted_external",
|
||||
status: "active",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { ExternalWorkdirPermissions } from "$lib/generated/workdir-api";
|
||||
|
||||
/** Render independent Workdir authority categories in a stable, non-conflating order. */
|
||||
/** Render an effective attachment/session capability intersection without widening it. */
|
||||
export function formatWorkdirPermissions(
|
||||
permissions: ExternalWorkdirPermissions,
|
||||
): string {
|
||||
@@ -10,3 +10,21 @@ export function formatWorkdirPermissions(
|
||||
if (permissions.command) categories.push("COMMAND");
|
||||
return categories.length > 0 ? categories.join(" · ") : "NONE";
|
||||
}
|
||||
|
||||
/** Render one validated External grant level with its inherited authority. */
|
||||
export function formatExternalGrantPermissionLevel(
|
||||
permissions: ExternalWorkdirPermissions,
|
||||
): string {
|
||||
if (permissions.read && permissions.write && permissions.command) {
|
||||
return "COMMAND (includes WRITE + READ)";
|
||||
}
|
||||
if (permissions.read && permissions.write && !permissions.command) {
|
||||
return "WRITE (includes READ)";
|
||||
}
|
||||
if (permissions.read && !permissions.write && !permissions.command) {
|
||||
return "READ";
|
||||
}
|
||||
throw new Error(
|
||||
"External Workdir grant has a non-hierarchical permission set",
|
||||
);
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
parseWorkerLaunchOptionsResponse,
|
||||
} from '$lib/workspace/api/workers';
|
||||
import { formatCurrentWorkdirRevision } from '$lib/workspace/settings/workdir-revision';
|
||||
import { formatWorkdirPermissions } from '$lib/workspace/settings/workdir-permissions';
|
||||
import { formatExternalGrantPermissionLevel } from '$lib/workspace/settings/workdir-permissions';
|
||||
import {
|
||||
buildCreateWorkspaceWorkerRequest,
|
||||
defaultWorkerLaunchForm,
|
||||
@@ -40,7 +40,7 @@
|
||||
: null;
|
||||
const label = directory.display_name ?? repositoryKey ?? 'External Workdir';
|
||||
if (directory.source.kind === 'external_grant') {
|
||||
return `${label} · External · ${formatWorkdirPermissions(directory.source.grant_permissions)}`;
|
||||
return `${label} · External · ${formatExternalGrantPermissionLevel(directory.source.grant_permissions)}`;
|
||||
}
|
||||
return `${label} · ${formatCurrentWorkdirRevision(directory, provider)}`;
|
||||
}
|
||||
|
||||
@@ -56,7 +56,7 @@ Deno.test("Workdir REST validation accepts External grant sources without Runtim
|
||||
source: {
|
||||
kind: "external_grant",
|
||||
grant_id: "grant-1",
|
||||
grant_permissions: { read: true, write: false, command: true },
|
||||
grant_permissions: { read: true, write: true, command: true },
|
||||
},
|
||||
materializer_kind: "client_hosted_external",
|
||||
status: "active",
|
||||
@@ -72,7 +72,7 @@ Deno.test("Workdir REST validation accepts External grant sources without Runtim
|
||||
}
|
||||
if (
|
||||
!detail.item.source.grant_permissions.read ||
|
||||
detail.item.source.grant_permissions.write ||
|
||||
!detail.item.source.grant_permissions.write ||
|
||||
!detail.item.source.grant_permissions.command
|
||||
) {
|
||||
throw new Error("External Workdir grant permissions were not preserved");
|
||||
@@ -83,6 +83,11 @@ Deno.test("Workdir REST validation rejects missing or misplaced External grant p
|
||||
for (
|
||||
const source of [
|
||||
{ kind: "external_grant", grant_id: "grant-1" },
|
||||
{
|
||||
kind: "external_grant",
|
||||
grant_id: "grant-1",
|
||||
grant_permissions: { read: false, write: false, command: true },
|
||||
},
|
||||
{
|
||||
kind: "external_grant",
|
||||
grant_id: "grant-1",
|
||||
@@ -113,7 +118,11 @@ Deno.test("Workdir REST validation rejects missing or misplaced External grant p
|
||||
rejected = true;
|
||||
}
|
||||
if (!rejected) {
|
||||
throw new Error(`invalid External permission projection was accepted: ${JSON.stringify(source)}`);
|
||||
throw new Error(
|
||||
`invalid External permission projection was accepted: ${
|
||||
JSON.stringify(source)
|
||||
}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -2,9 +2,12 @@ declare const Deno: {
|
||||
test(name: string, fn: () => void | Promise<void>): void;
|
||||
};
|
||||
|
||||
import { formatWorkdirPermissions } from "../src/lib/workspace/settings/workdir-permissions.ts";
|
||||
import {
|
||||
formatExternalGrantPermissionLevel,
|
||||
formatWorkdirPermissions,
|
||||
} from "../src/lib/workspace/settings/workdir-permissions.ts";
|
||||
|
||||
Deno.test("Workdir permissions render independent categories in stable order", () => {
|
||||
Deno.test("effective Workdir permissions render partial intersections without widening", () => {
|
||||
const cases = [
|
||||
[{ read: true, write: false, command: false }, "READ"],
|
||||
[{ read: true, write: true, command: false }, "READ · WRITE"],
|
||||
@@ -20,3 +23,32 @@ Deno.test("Workdir permissions render independent categories in stable order", (
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Deno.test("External grant levels show inherited authority and reject partial grants", () => {
|
||||
const cases = [
|
||||
[{ read: true, write: false, command: false }, "READ"],
|
||||
[{ read: true, write: true, command: false }, "WRITE (includes READ)"],
|
||||
[
|
||||
{ read: true, write: true, command: true },
|
||||
"COMMAND (includes WRITE + READ)",
|
||||
],
|
||||
] as const;
|
||||
for (const [permissions, expected] of cases) {
|
||||
const actual = formatExternalGrantPermissionLevel(permissions);
|
||||
if (actual !== expected) {
|
||||
throw new Error(`expected ${expected}, got ${actual}`);
|
||||
}
|
||||
}
|
||||
|
||||
let rejected = false;
|
||||
try {
|
||||
formatExternalGrantPermissionLevel({
|
||||
read: false,
|
||||
write: false,
|
||||
command: true,
|
||||
});
|
||||
} catch {
|
||||
rejected = true;
|
||||
}
|
||||
if (!rejected) throw new Error("command-only External grant was accepted");
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user