Skip to content

Commit 2c59e1d

Browse files
authored
Merge pull request #25 from PackmindHub/claude/verify-api-security-xcKgw
Harden session/ingestion token separation and gate admin routes
2 parents ddf48eb + 0c29649 commit 2c59e1d

16 files changed

Lines changed: 130 additions & 15 deletions

File tree

backend/src/application/auth/login.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import type { IUserRepository } from "@/domain/ports/user-repository";
22
import type { IAuditRepository } from "@/domain/ports/audit-repository";
33
import type { User } from "@/domain/user";
44
import { verifyPassword } from "@/infrastructure/crypto/password";
5-
import { signToken } from "@/infrastructure/crypto/jwt";
5+
import { signSessionToken } from "@/infrastructure/crypto/jwt";
66

77
interface LoginDeps {
88
users: IUserRepository;
@@ -23,7 +23,7 @@ export async function login(
2323
const valid = await verifyPassword(passwordHash, input.password);
2424
if (!valid) return { error: "invalid_credentials" };
2525

26-
const sessionJwt = await signToken(
26+
const sessionJwt = await signSessionToken(
2727
{ sub: user.id, email: user.email, role: user.role },
2828
"1d",
2929
);

backend/src/http/audit.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { z } from "zod";
33
import type { AppVariables } from "@/types";
44
import type { AppDeps } from "@/bootstrap/compose";
55
import { sessionAuth } from "@/middleware/session-auth";
6+
import { requireAdmin } from "@/middleware/require-admin";
67
import { listAuditEvents } from "@/application/audit/list-audit-events";
78
import { exportAuditEventsCsv } from "@/application/audit/export-audit-events";
89
import { ALL_AUDIT_ACTIONS, type AuditAction, type AuditFilters } from "@/domain/audit";
@@ -54,6 +55,7 @@ function readQueryRecord(c: { req: { queries: () => Record<string, string[] | un
5455
export function createAuditRoute(deps: Pick<AppDeps, "audit">) {
5556
const route = new Hono<{ Variables: AppVariables }>();
5657
route.use("*", sessionAuth);
58+
route.use("*", requireAdmin);
5759

5860
route.get("/", async (c) => {
5961
const queries = readQueryRecord(c);

backend/src/http/integrations.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import type { AppVariables } from "@/types";
55
import type { AppDeps } from "@/bootstrap/compose";
66
import { DEFAULT_LOKI_QUERY } from "@/domain/event";
77
import { sessionAuth } from "@/middleware/session-auth";
8+
import { requireAdmin } from "@/middleware/require-admin";
89
import { listIntegrations } from "@/application/integrations/list-integrations";
910
import { createIntegration } from "@/application/integrations/create-integration";
1011
import { updateIntegration } from "@/application/integrations/update-integration";
@@ -81,6 +82,7 @@ export function createIntegrationsRoute(
8182

8283
const route = new Hono<{ Variables: AppVariables }>();
8384
route.use("*", sessionAuth);
85+
route.use("*", requireAdmin);
8486

8587
route.get("/", async (c) => {
8688
return c.json(await listIntegrations(deps));

backend/src/http/marketplace-sources.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { z } from "zod";
33
import type { AppVariables } from "@/types";
44
import type { AppDeps } from "@/bootstrap/compose";
55
import { sessionAuth } from "@/middleware/session-auth";
6+
import { requireAdmin } from "@/middleware/require-admin";
67
import { listMarketplaceSources } from "@/application/marketplace-sources/list-marketplace-sources";
78
import { createMarketplaceSource } from "@/application/marketplace-sources/create-marketplace-source";
89
import { updateMarketplaceSource } from "@/application/marketplace-sources/update-marketplace-source";
@@ -66,6 +67,7 @@ export function createMarketplaceSourcesRoute(
6667

6768
const route = new Hono<{ Variables: AppVariables }>();
6869
route.use("*", sessionAuth);
70+
route.use("*", requireAdmin);
6971

7072
route.get("/", async (c) => {
7173
return c.json(await listMarketplaceSources(deps));

backend/src/http/marketplaces.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { z } from "zod";
33
import type { AppVariables } from "@/types";
44
import type { AppDeps } from "@/bootstrap/compose";
55
import { sessionAuth } from "@/middleware/session-auth";
6+
import { requireAdmin } from "@/middleware/require-admin";
67
import { deleteMarketplace } from "@/application/marketplaces/delete-marketplace";
78
import {
89
DELETE_MARKETPLACES_MAX_BATCH,
@@ -53,7 +54,7 @@ export function createMarketplacesRoute(
5354
return c.json({ marketplaces: await listMarketplaces(deps, { includeIgnored }) });
5455
});
5556

56-
route.post("/bulk-delete", async (c) => {
57+
route.post("/bulk-delete", requireAdmin, async (c) => {
5758
const body = bulkDeleteSchema.parse(await c.req.json());
5859
const result = await deleteMarketplaces(deps, {
5960
names: body.names,
@@ -72,7 +73,7 @@ export function createMarketplacesRoute(
7273
return c.json(result);
7374
});
7475

75-
route.patch("/bulk-status", async (c) => {
76+
route.patch("/bulk-status", requireAdmin, async (c) => {
7677
const body = bulkStatusSchema.parse(await c.req.json());
7778
const result = await updateMarketplacesStatus(deps, {
7879
names: body.names,
@@ -93,7 +94,7 @@ export function createMarketplacesRoute(
9394
return c.json(detail);
9495
});
9596

96-
route.patch("/:name", async (c) => {
97+
route.patch("/:name", requireAdmin, async (c) => {
9798
const name = decodeURIComponent(c.req.param("name"));
9899
const body = updateSchema.parse(await c.req.json());
99100
const result = await updateMarketplace(
@@ -104,7 +105,7 @@ export function createMarketplacesRoute(
104105
return c.json(result);
105106
});
106107

107-
route.delete("/:name", async (c) => {
108+
route.delete("/:name", requireAdmin, async (c) => {
108109
const name = decodeURIComponent(c.req.param("name"));
109110
const mode = deleteModeSchema.parse(c.req.query("mode"));
110111
const withSources = c.req.query("withSources") === "true";

backend/src/http/plugins.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { listPlugins } from "@/application/plugins/list-plugins";
55
import { updatePlugin } from "@/application/plugins/update-plugin";
66
import type { AppDeps } from "@/bootstrap/compose";
77
import { sessionAuth } from "@/middleware/session-auth";
8+
import { requireAdmin } from "@/middleware/require-admin";
89
import type { AppVariables } from "@/types";
910

1011
const updateSchema = z.object({
@@ -35,7 +36,7 @@ export function createPluginsRoute(
3536
return c.json(data);
3637
});
3738

38-
route.patch("/:pluginName", async (c) => {
39+
route.patch("/:pluginName", requireAdmin, async (c) => {
3940
const pluginName = decodeURIComponent(c.req.param("pluginName"));
4041
const body = updateSchema.parse(await c.req.json());
4142
const result = await updatePlugin(

backend/src/http/skills/delete.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { z } from "zod";
33
import type { AppDeps } from "@/bootstrap/compose";
44
import { deleteSkills, DELETE_SKILLS_MAX_BATCH } from "@/application/skills/delete-skills";
55
import { sessionAuth } from "@/middleware/session-auth";
6+
import { requireAdmin } from "@/middleware/require-admin";
67
import type { AppVariables } from "@/types";
78

89
const deleteSchema = z.object({
@@ -20,6 +21,7 @@ const deleteSchema = z.object({
2021
export function createDeleteRoute(deps: Pick<AppDeps, "skills" | "events" | "audit">) {
2122
const route = new Hono<{ Variables: AppVariables }>();
2223
route.use("*", sessionAuth);
24+
route.use("*", requireAdmin);
2325

2426
route.post("/delete", async (c) => {
2527
const { skills } = deleteSchema.parse(await c.req.json());

backend/src/http/skills/update-status-bulk.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
} from "@/application/skills/update-skills-status";
77
import type { AppDeps } from "@/bootstrap/compose";
88
import { sessionAuth } from "@/middleware/session-auth";
9+
import { requireAdmin } from "@/middleware/require-admin";
910
import type { AppVariables } from "@/types";
1011

1112
const bulkSchema = z.object({
@@ -24,6 +25,7 @@ const bulkSchema = z.object({
2425
export function createUpdateStatusBulkRoute(deps: Pick<AppDeps, "skills" | "audit">) {
2526
const route = new Hono<{ Variables: AppVariables }>();
2627
route.use("*", sessionAuth);
28+
route.use("*", requireAdmin);
2729

2830
route.patch("/status/bulk", async (c) => {
2931
const body = bulkSchema.parse(await c.req.json());

backend/src/http/skills/update-status.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { z } from "zod";
33
import { updateSkillStatus } from "@/application/skills/update-skill-status";
44
import type { AppDeps } from "@/bootstrap/compose";
55
import { sessionAuth } from "@/middleware/session-auth";
6+
import { requireAdmin } from "@/middleware/require-admin";
67
import type { AppVariables } from "@/types";
78

89
const updateSchema = z.object({
@@ -14,6 +15,7 @@ const updateSchema = z.object({
1415
export function createUpdateStatusRoute(deps: Pick<AppDeps, "skills" | "audit">) {
1516
const route = new Hono<{ Variables: AppVariables }>();
1617
route.use("*", sessionAuth);
18+
route.use("*", requireAdmin);
1719

1820
route.patch("/status", async (c) => {
1921
const body = updateSchema.parse(await c.req.json());

backend/src/http/telemetry.test.ts

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { Hono } from "hono";
33
import { createTelemetryRoute } from "./telemetry";
44
import type { Token } from "@/domain/token";
55
import type { ITokenRepository } from "@/domain/ports/token-repository";
6-
import { createIngestionToken } from "@/infrastructure/crypto/jwt";
6+
import { createIngestionToken, signSessionToken } from "@/infrastructure/crypto/jwt";
77

88
function makeDeps() {
99
const tokens: ITokenRepository = {
@@ -58,6 +58,24 @@ describe("POST /api/v0/telemetry/v1/logs", () => {
5858
expect(res.status).toBe(401);
5959
});
6060

61+
it("returns 401 when a session JWT is presented as the Bearer token", async () => {
62+
const deps = makeDeps();
63+
const app = makeApp(deps);
64+
const sessionJwt = await signSessionToken(
65+
{ sub: "user-1", email: "alice@example.com", role: "admin" },
66+
"1h",
67+
);
68+
const res = await app.request("/api/v0/telemetry/v1/logs", {
69+
method: "POST",
70+
headers: {
71+
"Content-Type": "application/json",
72+
Authorization: `Bearer ${sessionJwt}`,
73+
},
74+
body: JSON.stringify({ resourceLogs: [] }),
75+
});
76+
expect(res.status).toBe(401);
77+
});
78+
6179
it("returns 400 with partialSuccess on invalid JSON body", async () => {
6280
const deps = makeDeps();
6381
const app = makeApp(deps);

0 commit comments

Comments
 (0)