diff --git a/internal/cloud/access-control.test.ts b/internal/cloud/access-control.test.ts index 2bca154a..cac3538d 100644 --- a/internal/cloud/access-control.test.ts +++ b/internal/cloud/access-control.test.ts @@ -73,6 +73,23 @@ test("Postgres workspace update query preserves parameter numbering", async () = assert.equal(update.params[17], "user"); }); +test("Postgres access schema removes legacy device_pod_grants authority", async () => { + const calls = []; + const accessController = createAccessController({ + runtimeStore: { + query: async (sql, params = []) => { + calls.push({ sql, params }); + return { rows: [] }; + } + } + }); + + await accessController.store.countUsers(); + + assert.ok(calls.some((call) => call.sql === "DROP TABLE IF EXISTS device_pod_grants")); + assert.equal(calls.some((call) => /CREATE TABLE IF NOT EXISTS device_pod_grants\b/u.test(call.sql)), false); +}); + test("cloud api /auth/oidc/login returns 302 to Keycloak when issuer and client are configured", async () => { const server = createCloudApiServer({ env: { HWLAB_ACCESS_CONTROL_REQUIRED: "1", HWLAB_KEYCLOAK_ISSUER: OIDC_ISSUER, HWLAB_KEYCLOAK_CLIENT_ID: OIDC_CLIENT_ID, HWLAB_PUBLIC_ENDPOINT }, @@ -1088,11 +1105,6 @@ test("cloud api access control grants visible device pods and requires device-po assert.equal(emptyList.status, 200); assert.deepEqual(emptyList.body.devicePods, []); - const legacyGrant = await postJson(port, "/v1/admin/device-pod-grants", { - devicePodId: "device-pod-71-freq", - userId: userCreate.body.user.id - }, adminCookie); - assert.equal(legacyGrant.status, 404); await grantDevicePodAccess(port, adminCookie, userCreate.body.user.id, "device-pod-71-freq", ["viewer", "operator", "job_submitter"]); await grantDevicePodAccess(port, adminCookie, bobCreate.body.user.id, "device-pod-71-freq", ["viewer", "operator", "job_submitter"]); @@ -1190,11 +1202,6 @@ test("cloud api access control grants visible device pods and requires device-po assert.equal(unsupported.status, 400); assert.equal(unsupported.body.error.code, "unsupported_device_job_intent"); - const legacyRevoke = await fetch(`http://127.0.0.1:${port}/v1/admin/device-pod-grants/device-pod-71-freq/${bobCreate.body.user.id}`, { - method: "DELETE", - headers: { cookie: adminCookie } - }); - assert.equal(legacyRevoke.status, 404); await revokeDevicePodAccess(port, adminCookie, bobCreate.body.user.id, "device-pod-71-freq", ["viewer", "operator", "job_submitter"]); const afterRevoke = await getJson(port, "/v1/device-pods", bobLogin.cookie); diff --git a/internal/cloud/access-control.ts b/internal/cloud/access-control.ts index 922da81a..1f7cb9c2 100644 --- a/internal/cloud/access-control.ts +++ b/internal/cloud/access-control.ts @@ -194,7 +194,8 @@ const ACCESS_SCHEMA_STATEMENTS = Object.freeze([ PRIMARY KEY (user_id, relation, object) )`, `CREATE INDEX IF NOT EXISTS idx_access_tuples_user ON access_tuples(user_id, object)`, - `CREATE INDEX IF NOT EXISTS idx_access_tuples_object ON access_tuples(object, relation)` + `CREATE INDEX IF NOT EXISTS idx_access_tuples_object ON access_tuples(object, relation)`, + `DROP TABLE IF EXISTS device_pod_grants` ]); const MUTATING_INTENTS = new Set([ diff --git a/internal/db/migrations/0001_cloud_core_skeleton.sql b/internal/db/migrations/0001_cloud_core_skeleton.sql index ba7ca171..6aeab92f 100644 --- a/internal/db/migrations/0001_cloud_core_skeleton.sql +++ b/internal/db/migrations/0001_cloud_core_skeleton.sql @@ -184,13 +184,7 @@ CREATE TABLE IF NOT EXISTS device_pods ( updated_at TEXT NOT NULL ); -CREATE TABLE IF NOT EXISTS device_pod_grants ( - device_pod_id TEXT NOT NULL REFERENCES device_pods(id) ON DELETE CASCADE, - user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE, - created_by_admin_id TEXT NOT NULL REFERENCES users(id), - created_at TEXT NOT NULL, - PRIMARY KEY (device_pod_id, user_id) -); +DROP TABLE IF EXISTS device_pod_grants; CREATE TABLE IF NOT EXISTS device_leases ( device_pod_id TEXT PRIMARY KEY REFERENCES device_pods(id) ON DELETE CASCADE, diff --git a/internal/db/schema.test.ts b/internal/db/schema.test.ts index 37ddb7e4..b272c1a3 100644 --- a/internal/db/schema.test.ts +++ b/internal/db/schema.test.ts @@ -39,6 +39,8 @@ test("initial migration skeleton declares every frozen table", async () => { assert.match(sql, /\btarget\b/); assert.match(sql, /\bresult\b/); assert.match(sql, /\btimestamp\b/); + assert.doesNotMatch(sql, /CREATE TABLE IF NOT EXISTS device_pod_grants\b/u); + assert.match(sql, /DROP TABLE IF EXISTS device_pod_grants\b/u); }); test("initial migration exposes columns required by the durable runtime adapter", async () => { diff --git a/internal/dev-entrypoint/cloud-web-routes.test.mjs b/internal/dev-entrypoint/cloud-web-routes.test.mjs index f07a7f6e..88b51ea9 100644 --- a/internal/dev-entrypoint/cloud-web-routes.test.mjs +++ b/internal/dev-entrypoint/cloud-web-routes.test.mjs @@ -106,12 +106,6 @@ test("cloud web proxies authenticated Admin Access write routes without legacy g publicRoute: false, routeKey: "DELETE /v1/admin/access/users/usr_alice/tools/hwpod/can-use" }); - assert.deepEqual(cloudWebProxyRoutePolicy("POST", "/v1/admin/device-pod-grants"), { - proxy: false, - authRequired: false, - publicRoute: false, - routeKey: "POST /v1/admin/device-pod-grants" - }); }); test("cloud web proxies only supported authenticated device-pod write routes", () => { diff --git a/internal/dev-entrypoint/cloud-web-runtime.test.mjs b/internal/dev-entrypoint/cloud-web-runtime.test.mjs index 504e9b74..5b2de712 100644 --- a/internal/dev-entrypoint/cloud-web-runtime.test.mjs +++ b/internal/dev-entrypoint/cloud-web-runtime.test.mjs @@ -104,18 +104,6 @@ test("cloud web proxies Admin Access write routes and leaves legacy grants local }); assert.equal(grantResponse.status, 200); - const legacyResponse = await fetch(`${serverUrl(cloudWeb)}/v1/admin/device-pod-grants`, { - method: "POST", - headers: { "content-type": "application/json" }, - body: JSON.stringify({ userId: "usr_alice", devicePodId: "D601-71-FREQ" }) - }); - assert.equal(legacyResponse.status, 404); - assert.deepEqual(await legacyResponse.json(), { - error: "not_found", - serviceId: "hwlab-cloud-web", - path: "/v1/admin/device-pod-grants" - }); - assert.equal(upstreamRequests.length, 2); assert.deepEqual(upstreamRequests[0], { method: "POST",