Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
79 changes: 57 additions & 22 deletions packages/control-plane/src/router.test-support.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { buildServiceAuthHeaders, type ServiceName } from "@open-inspect/shared/
import type { BackgroundTasks } from "./platform-ports";
import { createTestBackgroundTasks } from "./background-tasks.test-support";
import { Hono } from "hono";
import { BUILT_IN_ROLE_REGISTRY } from "@open-inspect/shared/rbac";
import { BUILT_IN_ROLE_REGISTRY, type PermissionId } from "@open-inspect/shared/rbac";
import type { SqlDatabase, SqlStatement } from "./db/sql-database";
import { cloudflareHost, createControlPlaneApp, type RouteCatalogEntry } from "./routing/hono-app";
import { listRouteContracts, type RouteContract } from "./routing/route-contracts";
Expand Down Expand Up @@ -73,35 +73,70 @@ export function contractFor(method: string, path: string): RouteContract | undef
);
}

/** How admission's authorization lookups are answered, and where every other statement goes. */
export interface AuthorizationDatabaseOptions {
userId?: string;
/** Grants of a custom role; omitted, the user is a workspace owner. */
permissions?: readonly PermissionId[];
/** Answers statements that are not admission's; omitted, they answer null and no rows. */
statement?: (sql: string) => SqlStatement;
batch?: SqlDatabase["batch"];
}

/** A statement that answers null and no rows, for data access mocked at the store. */
export function emptyStatement(): SqlStatement {
const statement: SqlStatement = {
bind: () => statement,
first: async <T>() => null as T | null,
all: async <T>() => ({ results: [] as T[], meta: { changes: 0 } }),
run: async <T>() => ({ results: [] as T[], meta: { changes: 0 } }),
};
return statement;
}

/**
* A database whose effective-authorization lookup answers with an active
* workspace owner, for request-level unit tests of admitted handlers whose
* data access is mocked at the store.
* A database whose effective-authorization lookup answers for one active
* user, for request-level unit tests of admitted handlers. The two
* statements admission issues (the user's role, then a custom role's
* grants) are recognized here so no suite has to know their shape.
*/
export function ownerAuthorizationDatabase(userId = "user-1"): SqlDatabase {
export function authorizationDatabase(options: AuthorizationDatabaseOptions = {}): SqlDatabase {
const { userId = "user-1", permissions, statement = emptyStatement, batch } = options;
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
const role = permissions
? { role_id: "role-1", role_key: null, role_name: "Custom" }
: { role_id: BUILT_IN_ROLE_REGISTRY.owner.id, role_key: "owner", role_name: "Owner" };
return {
prepare(sql: string) {
const statement: SqlStatement = {
bind: () => statement,
first: async <T>() =>
(sql.includes("FROM users u")
? {
user_id: userId,
suspended_at: null,
role_id: BUILT_IN_ROLE_REGISTRY.owner.id,
role_key: "owner",
role_name: "Owner",
}
: null) as T | null,
all: async <T>() => ({ results: [] as T[], meta: { changes: 0 } }),
run: async <T>() => ({ results: [] as T[], meta: { changes: 0 } }),
};
return statement;
if (sql.includes("FROM users u")) {
const lookup: SqlStatement = {
...emptyStatement(),
bind: () => lookup,
first: async <T>() => ({ user_id: userId, suspended_at: null, ...role }) as T | null,
};
return lookup;
}
if (sql.includes("FROM role_permissions")) {
const grants: SqlStatement = {
...emptyStatement(),
bind: () => grants,
all: async <T>() => ({
results: (permissions ?? []).map((permission_id) => ({ permission_id })) as T[],
meta: { changes: 0 },
}),
};
return grants;
}
return statement(sql);
},
batch: async () => [],
batch: batch ?? (async () => []),
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
};
}

/** An owner's database: every permission, data access mocked at the store. */
export function ownerAuthorizationDatabase(userId = "user-1"): SqlDatabase {
return authorizationDatabase({ userId });
}

/** Compile a catalog path into the legacy raw-path matcher, for handler-level fixtures. */
export function routePathPattern(path: string): RegExp {
return new RegExp(`^${path.replace(/:(\w+)/g, "(?<$1>[^/]+)")}$`);
Expand Down
25 changes: 13 additions & 12 deletions packages/control-plane/src/routes/autofix.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
import { Hono } from "hono";
import { PrAutofixFeedbackStore } from "../db/pr-autofix-feedback-store";
import { admit } from "../routing/admit";
import type { ControlPlaneHonoEnv } from "../routing/hono-env";
import {
defineRoutes,
error,
json,
NO_AUTHORIZATION,
SCM_AGNOSTIC_WEB_SERVICE_ROUTE,
type Route,
type RequestContext,
} from "./shared";

const handleActivity: Route["handler"] = async (request, _env, _match, ctx) => {
async function handleActivity(request: Request, ctx: RequestContext): Promise<Response> {
const url = new URL(request.url);
const rawLimit = url.searchParams.get("limit") ?? "50";
const limit = Number(rawLimit);
Expand All @@ -29,13 +31,12 @@ const handleActivity: Route["handler"] = async (request, _env, _match, ctx) => {
}
throw caught;
}
};
}

export const autofixRoutes: Route[] = defineRoutes(SCM_AGNOSTIC_WEB_SERVICE_ROUTE, [
{
method: "GET",
path: "/autofix/activity",
authorization: NO_AUTHORIZATION,
handler: handleActivity,
},
]);
export const autofixRoutes = new Hono<ControlPlaneHonoEnv>();

autofixRoutes.get(
"/autofix/activity",
admit({ ...SCM_AGNOSTIC_WEB_SERVICE_ROUTE, authorization: NO_AUTHORIZATION }),
(c) => handleActivity(c.var.admitted.request, c.var.admitted.ctx)
);
Loading
Loading