fix(api): harden user self-service and resource linking
This commit is contained in:
@@ -85,6 +85,82 @@ describe("user router authorization", () => {
|
||||
expect(update).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("requires authentication for dashboard layout saves", async () => {
|
||||
const update = vi.fn();
|
||||
const caller = createCaller(createContext({
|
||||
user: {
|
||||
update,
|
||||
},
|
||||
}, { session: false }));
|
||||
|
||||
await expect(caller.saveDashboardLayout({
|
||||
layout: { version: 2, gridCols: 12, widgets: [] },
|
||||
})).rejects.toMatchObject({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "Authentication required",
|
||||
});
|
||||
|
||||
expect(update).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("requires authentication for favorite project reads", async () => {
|
||||
const findUnique = vi.fn();
|
||||
const caller = createCaller(createContext({
|
||||
user: {
|
||||
findUnique,
|
||||
},
|
||||
}, { session: false }));
|
||||
|
||||
await expect(caller.getFavoriteProjectIds()).rejects.toMatchObject({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "Authentication required",
|
||||
});
|
||||
|
||||
expect(findUnique).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("requires authentication for column preference reads and writes", async () => {
|
||||
const findUnique = vi.fn();
|
||||
const update = vi.fn();
|
||||
const caller = createCaller(createContext({
|
||||
user: {
|
||||
findUnique,
|
||||
update,
|
||||
},
|
||||
}, { session: false }));
|
||||
|
||||
await expect(caller.getColumnPreferences()).rejects.toMatchObject({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "Authentication required",
|
||||
});
|
||||
await expect(caller.setColumnPreferences({
|
||||
view: "resources",
|
||||
visible: ["name"],
|
||||
})).rejects.toMatchObject({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "Authentication required",
|
||||
});
|
||||
|
||||
expect(findUnique).not.toHaveBeenCalled();
|
||||
expect(update).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("requires authentication for MFA status lookups", async () => {
|
||||
const findUniqueOrThrow = vi.fn();
|
||||
const caller = createCaller(createContext({
|
||||
user: {
|
||||
findUniqueOrThrow,
|
||||
},
|
||||
}, { session: false }));
|
||||
|
||||
await expect(caller.getMfaStatus()).rejects.toMatchObject({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "Authentication required",
|
||||
});
|
||||
|
||||
expect(findUniqueOrThrow).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("forbids regular users from listing assignable users", async () => {
|
||||
const findMany = vi.fn();
|
||||
const caller = createCaller(createContext({
|
||||
@@ -140,6 +216,49 @@ describe("user router authorization", () => {
|
||||
expect(findUnique).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("forbids non-admin users from setting explicit permission overrides", async () => {
|
||||
const findUnique = vi.fn();
|
||||
const update = vi.fn();
|
||||
const caller = createCaller(createContext({
|
||||
user: {
|
||||
findUnique,
|
||||
update,
|
||||
},
|
||||
}, { role: SystemRole.MANAGER }));
|
||||
|
||||
await expect(caller.setPermissions({
|
||||
userId: "user_2",
|
||||
overrides: {
|
||||
granted: ["manageProjects"],
|
||||
},
|
||||
})).rejects.toMatchObject({
|
||||
code: "FORBIDDEN",
|
||||
message: "Admin role required",
|
||||
});
|
||||
|
||||
expect(findUnique).not.toHaveBeenCalled();
|
||||
expect(update).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("forbids non-admin users from resetting permission overrides", async () => {
|
||||
const findUnique = vi.fn();
|
||||
const update = vi.fn();
|
||||
const caller = createCaller(createContext({
|
||||
user: {
|
||||
findUnique,
|
||||
update,
|
||||
},
|
||||
}, { role: SystemRole.MANAGER }));
|
||||
|
||||
await expect(caller.resetPermissions({ userId: "user_2" })).rejects.toMatchObject({
|
||||
code: "FORBIDDEN",
|
||||
message: "Admin role required",
|
||||
});
|
||||
|
||||
expect(findUnique).not.toHaveBeenCalled();
|
||||
expect(update).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("forbids non-admin users from disabling TOTP for other users", async () => {
|
||||
const findUnique = vi.fn();
|
||||
const caller = createCaller(createContext({
|
||||
@@ -186,15 +305,39 @@ describe("user router authorization", () => {
|
||||
expect(update).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("forbids non-admin users from auto-linking users by email", async () => {
|
||||
const userFindMany = vi.fn();
|
||||
const resourceFindFirst = vi.fn();
|
||||
const resourceUpdate = vi.fn();
|
||||
const caller = createCaller(createContext({
|
||||
user: {
|
||||
findMany: userFindMany,
|
||||
},
|
||||
resource: {
|
||||
findFirst: resourceFindFirst,
|
||||
update: resourceUpdate,
|
||||
},
|
||||
}, { role: SystemRole.MANAGER }));
|
||||
|
||||
await expect(caller.autoLinkAllByEmail()).rejects.toMatchObject({
|
||||
code: "FORBIDDEN",
|
||||
message: "Admin role required",
|
||||
});
|
||||
|
||||
expect(userFindMany).not.toHaveBeenCalled();
|
||||
expect(resourceFindFirst).not.toHaveBeenCalled();
|
||||
expect(resourceUpdate).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("keeps TOTP verification public for the login flow", async () => {
|
||||
const findUniqueOrThrow = vi.fn().mockResolvedValue({
|
||||
const findUnique = vi.fn().mockResolvedValue({
|
||||
id: "user_1",
|
||||
totpEnabled: false,
|
||||
totpSecret: null,
|
||||
});
|
||||
const caller = createCaller(createContext({
|
||||
user: {
|
||||
findUniqueOrThrow,
|
||||
findUnique,
|
||||
},
|
||||
}, { session: false }));
|
||||
|
||||
@@ -203,7 +346,7 @@ describe("user router authorization", () => {
|
||||
message: "TOTP is not enabled for this user.",
|
||||
});
|
||||
|
||||
expect(findUniqueOrThrow).toHaveBeenCalledWith({
|
||||
expect(findUnique).toHaveBeenCalledWith({
|
||||
where: { id: "user_1" },
|
||||
select: { id: true, totpSecret: true, totpEnabled: true },
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user