fix(trips): reject on-behalf trip creation without trips.manage
POST /trips honored body.user_id for any trips.record holder — impersonation on create (reads were already scoped). Now an explicit Czech 403 unless the caller has the manager capability; manager on-behalf flow unchanged. +4 tests. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -19,6 +19,9 @@ import tripsRoutes from "../routes/admin/trips";
|
||||
// WHOLE filtered set (not one 25-row page), honors the month filter in
|
||||
// both wire formats ("month=5&year=2098" and "month=2098-05"), and scopes
|
||||
// non-managers to their own trips exactly like the list does.
|
||||
// 3. POST /trips rejects a non-manager filing a trip under someone else's
|
||||
// user_id with an explicit Czech 403 (impersonation gap), while
|
||||
// self-creates and manager on-behalf creates keep working.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/** Note-prefix for test-created rows so cleanup never touches real data. */
|
||||
@@ -66,6 +69,18 @@ async function authGet(path: string, token: string) {
|
||||
});
|
||||
}
|
||||
|
||||
async function authPost(path: string, token: string, body: unknown) {
|
||||
return app.inject({
|
||||
method: "POST",
|
||||
url: path,
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
payload: body as object,
|
||||
});
|
||||
}
|
||||
|
||||
async function authPut(path: string, token: string, body: unknown) {
|
||||
return app.inject({
|
||||
method: "PUT",
|
||||
@@ -539,3 +554,88 @@ describe("GET /api/admin/trips/stats", () => {
|
||||
expect(filtered.json().data.count).toBe(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe("POST /api/admin/trips — on-behalf authorization", () => {
|
||||
function postBody(params: { userId?: number; note: string }) {
|
||||
return {
|
||||
vehicle_id: vehicleAId,
|
||||
...(params.userId !== undefined ? { user_id: params.userId } : {}),
|
||||
trip_date: "2098-11-10",
|
||||
start_km: 100,
|
||||
end_km: 150,
|
||||
route_from: "Praha",
|
||||
route_to: "Brno",
|
||||
is_business: true,
|
||||
notes: `${N}${params.note}`,
|
||||
};
|
||||
}
|
||||
|
||||
it("rejects a non-manager filing a trip under another user's id with 403", async () => {
|
||||
const res = await authPost(
|
||||
"/api/admin/trips",
|
||||
scopeToken,
|
||||
postBody({ userId: adminUserId, note: "authz_spoof" }),
|
||||
);
|
||||
|
||||
expect(res.statusCode).toBe(403);
|
||||
expect(res.json()).toEqual({
|
||||
success: false,
|
||||
error: "Nemáte oprávnění zadat jízdu za jiného uživatele",
|
||||
});
|
||||
|
||||
// …and the trip must NOT have been created.
|
||||
const created = await prisma.trips.findFirst({
|
||||
where: { notes: `${N}authz_spoof` },
|
||||
});
|
||||
expect(created).toBeNull();
|
||||
});
|
||||
|
||||
it("lets a non-manager create a trip without user_id (defaults to self)", async () => {
|
||||
const res = await authPost(
|
||||
"/api/admin/trips",
|
||||
scopeToken,
|
||||
postBody({ note: "authz_self_implicit" }),
|
||||
);
|
||||
|
||||
expect(res.statusCode).toBe(201);
|
||||
expect(res.json().success).toBe(true);
|
||||
|
||||
const created = await prisma.trips.findFirst({
|
||||
where: { notes: `${N}authz_self_implicit` },
|
||||
});
|
||||
expect(created).not.toBeNull();
|
||||
expect(created!.user_id).toBe(scopeUserId);
|
||||
});
|
||||
|
||||
it("lets a non-manager create a trip with their OWN user_id", async () => {
|
||||
const res = await authPost(
|
||||
"/api/admin/trips",
|
||||
scopeToken,
|
||||
postBody({ userId: scopeUserId, note: "authz_self_explicit" }),
|
||||
);
|
||||
|
||||
expect(res.statusCode).toBe(201);
|
||||
|
||||
const created = await prisma.trips.findFirst({
|
||||
where: { notes: `${N}authz_self_explicit` },
|
||||
});
|
||||
expect(created).not.toBeNull();
|
||||
expect(created!.user_id).toBe(scopeUserId);
|
||||
});
|
||||
|
||||
it("lets a manager/admin create a trip on behalf of another user", async () => {
|
||||
const res = await authPost(
|
||||
"/api/admin/trips",
|
||||
adminToken,
|
||||
postBody({ userId: scopeUserId, note: "authz_on_behalf" }),
|
||||
);
|
||||
|
||||
expect(res.statusCode).toBe(201);
|
||||
|
||||
const created = await prisma.trips.findFirst({
|
||||
where: { notes: `${N}authz_on_behalf` },
|
||||
});
|
||||
expect(created).not.toBeNull();
|
||||
expect(created!.user_id).toBe(scopeUserId);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user