fix: permissions not restricted for certain management pages / actions (#1219)

* fix: restrict parts of manage navigation to admins

* fix: restrict stats cards on manage home page

* fix: restrict access to amount of certain stats for manage home

* fix: restrict visibility of board create button

* fix: restrict access to integration pages

* fix: restrict access to tools pages for admins

* fix: restrict access to user and group pages

* test: adjust tests to match permission changes for routes

* fix: remove certain pages from spotlight without admin

* fix: app management not restricted
This commit is contained in:
Meier Lukas
2024-10-05 17:03:32 +02:00
committed by GitHub
parent 770768eb21
commit 1421ccc917
28 changed files with 756 additions and 322 deletions

View File

@@ -11,6 +11,11 @@ import { appRouter } from "../app";
// Mock the auth module to return an empty session
vi.mock("@homarr/auth", () => ({ auth: () => ({}) as Session }));
const defaultSession: Session = {
user: { id: createId(), permissions: [], colorScheme: "light" },
expires: new Date().toISOString(),
};
describe("all should return all apps", () => {
test("should return all apps", async () => {
const db = createDb();
@@ -89,7 +94,7 @@ describe("create should create a new app with all arguments", () => {
const db = createDb();
const caller = appRouter.createCaller({
db,
session: null,
session: defaultSession,
});
const input = {
name: "Mantine",
@@ -112,7 +117,7 @@ describe("create should create a new app with all arguments", () => {
const db = createDb();
const caller = appRouter.createCaller({
db,
session: null,
session: defaultSession,
});
const input = {
name: "Mantine",
@@ -137,7 +142,7 @@ describe("update should update an app", () => {
const db = createDb();
const caller = appRouter.createCaller({
db,
session: null,
session: defaultSession,
});
const appId = createId();
@@ -172,7 +177,7 @@ describe("update should update an app", () => {
const db = createDb();
const caller = appRouter.createCaller({
db,
session: null,
session: defaultSession,
});
const actAsync = async () =>
@@ -192,7 +197,7 @@ describe("delete should delete an app", () => {
const db = createDb();
const caller = appRouter.createCaller({
db,
session: null,
session: defaultSession,
});
const appId = createId();

View File

@@ -1,21 +1,26 @@
import { describe, expect, test, vi } from "vitest";
import type { Session } from "@homarr/auth";
import * as env from "@homarr/auth/env.mjs";
import { createId, eq } from "@homarr/db";
import { groupMembers, groupPermissions, groups, users } from "@homarr/db/schema/sqlite";
import { createDb } from "@homarr/db/test";
import type { GroupPermissionKey } from "@homarr/definitions";
import { groupRouter } from "../group";
const defaultOwnerId = createId();
const defaultSession = {
user: {
id: defaultOwnerId,
permissions: [],
colorScheme: "light",
},
expires: new Date().toISOString(),
} satisfies Session;
const createSession = (permissions: GroupPermissionKey[]) =>
({
user: {
id: defaultOwnerId,
permissions,
colorScheme: "light",
},
expires: new Date().toISOString(),
}) satisfies Session;
const defaultSession = createSession([]);
const adminSession = createSession(["admin"]);
// Mock the auth module to return an empty session
vi.mock("@homarr/auth", async () => {
@@ -32,7 +37,7 @@ describe("paginated should return a list of groups with pagination", () => {
async (page, expectedCount) => {
// Arrange
const db = createDb();
const caller = groupRouter.createCaller({ db, session: defaultSession });
const caller = groupRouter.createCaller({ db, session: adminSession });
await db.insert(groups).values(
[1, 2, 3, 4, 5].map((number) => ({
@@ -55,7 +60,7 @@ describe("paginated should return a list of groups with pagination", () => {
test("with 5 groups in database and pagesize set to 3 it should return total count 5", async () => {
// Arrange
const db = createDb();
const caller = groupRouter.createCaller({ db, session: defaultSession });
const caller = groupRouter.createCaller({ db, session: adminSession });
await db.insert(groups).values(
[1, 2, 3, 4, 5].map((number) => ({
@@ -76,7 +81,7 @@ describe("paginated should return a list of groups with pagination", () => {
test("groups should contain id, name, email and image of members", async () => {
// Arrange
const db = createDb();
const caller = groupRouter.createCaller({ db, session: defaultSession });
const caller = groupRouter.createCaller({ db, session: adminSession });
const user = createDummyUser();
await db.insert(users).values(user);
@@ -112,7 +117,7 @@ describe("paginated should return a list of groups with pagination", () => {
async (query, expectedCount, firstKey) => {
// Arrange
const db = createDb();
const caller = groupRouter.createCaller({ db, session: defaultSession });
const caller = groupRouter.createCaller({ db, session: adminSession });
await db.insert(groups).values(
["first", "second", "third", "forth", "fifth"].map((key, index) => ({
@@ -131,13 +136,25 @@ describe("paginated should return a list of groups with pagination", () => {
expect(result.items.at(0)?.name).toBe(firstKey);
},
);
test("without admin permissions it should throw unauthorized error", async () => {
// Arrange
const db = createDb();
const caller = groupRouter.createCaller({ db, session: defaultSession });
// Act
const actAsync = async () => await caller.getPaginated({});
// Assert
await expect(actAsync()).rejects.toThrow("Permission denied");
});
});
describe("byId should return group by id including members and permissions", () => {
test('should return group with id "1" with members and permissions', async () => {
// Arrange
const db = createDb();
const caller = groupRouter.createCaller({ db, session: defaultSession });
const caller = groupRouter.createCaller({ db, session: adminSession });
const user = createDummyUser();
const groupId = "1";
@@ -180,7 +197,7 @@ describe("byId should return group by id including members and permissions", ()
test("with group id 1 and group 2 in database it should throw NOT_FOUND error", async () => {
// Arrange
const db = createDb();
const caller = groupRouter.createCaller({ db, session: defaultSession });
const caller = groupRouter.createCaller({ db, session: adminSession });
await db.insert(groups).values({
id: "2",
@@ -193,13 +210,25 @@ describe("byId should return group by id including members and permissions", ()
// Assert
await expect(actAsync()).rejects.toThrow("Group not found");
});
test("without admin permissions it should throw unauthorized error", async () => {
// Arrange
const db = createDb();
const caller = groupRouter.createCaller({ db, session: defaultSession });
// Act
const actAsync = async () => await caller.getById({ id: "1" });
// Assert
await expect(actAsync()).rejects.toThrow("Permission denied");
});
});
describe("create should create group in database", () => {
test("with valid input (64 character name) and non existing name it should be successful", async () => {
// Arrange
const db = createDb();
const caller = groupRouter.createCaller({ db, session: defaultSession });
const caller = groupRouter.createCaller({ db, session: adminSession });
const name = "a".repeat(64);
await db.insert(users).values(defaultSession.user);
@@ -223,7 +252,7 @@ describe("create should create group in database", () => {
test("with more than 64 characters name it should fail while validation", async () => {
// Arrange
const db = createDb();
const caller = groupRouter.createCaller({ db, session: defaultSession });
const caller = groupRouter.createCaller({ db, session: adminSession });
const longName = "a".repeat(65);
// Act
@@ -244,7 +273,7 @@ describe("create should create group in database", () => {
])("with similar name %s it should fail to create %s", async (similarName, nameToCreate) => {
// Arrange
const db = createDb();
const caller = groupRouter.createCaller({ db, session: defaultSession });
const caller = groupRouter.createCaller({ db, session: adminSession });
await db.insert(groups).values({
id: createId(),
@@ -257,6 +286,18 @@ describe("create should create group in database", () => {
// Assert
await expect(actAsync()).rejects.toThrow("similar name");
});
test("without admin permissions it should throw unauthorized error", async () => {
// Arrange
const db = createDb();
const caller = groupRouter.createCaller({ db, session: defaultSession });
// Act
const actAsync = async () => await caller.createGroup({ name: "test" });
// Assert
await expect(actAsync()).rejects.toThrow("Permission denied");
});
});
describe("update should update name with value that is no duplicate", () => {
@@ -266,7 +307,7 @@ describe("update should update name with value that is no duplicate", () => {
])("update should update name from %s to %s normalized", async (initialValue, updateValue, expectedValue) => {
// Arrange
const db = createDb();
const caller = groupRouter.createCaller({ db, session: defaultSession });
const caller = groupRouter.createCaller({ db, session: adminSession });
const groupId = createId();
await db.insert(groups).values([
@@ -299,7 +340,7 @@ describe("update should update name with value that is no duplicate", () => {
])("with similar name %s it should fail to update %s", async (updateValue, initialDuplicate) => {
// Arrange
const db = createDb();
const caller = groupRouter.createCaller({ db, session: defaultSession });
const caller = groupRouter.createCaller({ db, session: adminSession });
const groupId = createId();
await db.insert(groups).values([
@@ -327,7 +368,7 @@ describe("update should update name with value that is no duplicate", () => {
test("with non existing id it should throw not found error", async () => {
// Arrange
const db = createDb();
const caller = groupRouter.createCaller({ db, session: defaultSession });
const caller = groupRouter.createCaller({ db, session: adminSession });
await db.insert(groups).values({
id: createId(),
@@ -344,13 +385,29 @@ describe("update should update name with value that is no duplicate", () => {
// Assert
await expect(act()).rejects.toThrow("Group not found");
});
test("without admin permissions it should throw unauthorized error", async () => {
// Arrange
const db = createDb();
const caller = groupRouter.createCaller({ db, session: defaultSession });
// Act
const actAsync = async () =>
await caller.updateGroup({
id: createId(),
name: "test",
});
// Assert
await expect(actAsync()).rejects.toThrow("Permission denied");
});
});
describe("savePermissions should save permissions for group", () => {
test("with existing group and permissions it should save permissions", async () => {
// Arrange
const db = createDb();
const caller = groupRouter.createCaller({ db, session: defaultSession });
const caller = groupRouter.createCaller({ db, session: adminSession });
const groupId = createId();
await db.insert(groups).values({
@@ -380,7 +437,7 @@ describe("savePermissions should save permissions for group", () => {
test("with non existing group it should throw not found error", async () => {
// Arrange
const db = createDb();
const caller = groupRouter.createCaller({ db, session: defaultSession });
const caller = groupRouter.createCaller({ db, session: adminSession });
await db.insert(groups).values({
id: createId(),
@@ -397,13 +454,29 @@ describe("savePermissions should save permissions for group", () => {
// Assert
await expect(actAsync()).rejects.toThrow("Group not found");
});
test("without admin permissions it should throw unauthorized error", async () => {
// Arrange
const db = createDb();
const caller = groupRouter.createCaller({ db, session: defaultSession });
// Act
const actAsync = async () =>
await caller.savePermissions({
groupId: createId(),
permissions: ["integration-create", "board-full-all"],
});
// Assert
await expect(actAsync()).rejects.toThrow("Permission denied");
});
});
describe("transferOwnership should transfer ownership of group", () => {
test("with existing group and user it should transfer ownership", async () => {
// Arrange
const db = createDb();
const caller = groupRouter.createCaller({ db, session: defaultSession });
const caller = groupRouter.createCaller({ db, session: adminSession });
const groupId = createId();
const newUserId = createId();
@@ -440,7 +513,7 @@ describe("transferOwnership should transfer ownership of group", () => {
test("with non existing group it should throw not found error", async () => {
// Arrange
const db = createDb();
const caller = groupRouter.createCaller({ db, session: defaultSession });
const caller = groupRouter.createCaller({ db, session: adminSession });
await db.insert(groups).values({
id: createId(),
@@ -457,13 +530,29 @@ describe("transferOwnership should transfer ownership of group", () => {
// Assert
await expect(actAsync()).rejects.toThrow("Group not found");
});
test("without admin permissions it should throw unauthorized error", async () => {
// Arrange
const db = createDb();
const caller = groupRouter.createCaller({ db, session: defaultSession });
// Act
const actAsync = async () =>
await caller.transferOwnership({
groupId: createId(),
userId: createId(),
});
// Assert
await expect(actAsync()).rejects.toThrow("Permission denied");
});
});
describe("deleteGroup should delete group", () => {
test("with existing group it should delete group", async () => {
// Arrange
const db = createDb();
const caller = groupRouter.createCaller({ db, session: defaultSession });
const caller = groupRouter.createCaller({ db, session: adminSession });
const groupId = createId();
await db.insert(groups).values([
@@ -492,7 +581,7 @@ describe("deleteGroup should delete group", () => {
test("with non existing group it should throw not found error", async () => {
// Arrange
const db = createDb();
const caller = groupRouter.createCaller({ db, session: defaultSession });
const caller = groupRouter.createCaller({ db, session: adminSession });
await db.insert(groups).values({
id: createId(),
@@ -508,13 +597,30 @@ describe("deleteGroup should delete group", () => {
// Assert
await expect(actAsync()).rejects.toThrow("Group not found");
});
test("without admin permissions it should throw unauthorized error", async () => {
// Arrange
const db = createDb();
const caller = groupRouter.createCaller({ db, session: defaultSession });
// Act
const actAsync = async () =>
await caller.deleteGroup({
id: createId(),
});
// Assert
await expect(actAsync()).rejects.toThrow("Permission denied");
});
});
describe("addMember should add member to group", () => {
test("with existing group and user it should add member", async () => {
// Arrange
const db = createDb();
const caller = groupRouter.createCaller({ db, session: defaultSession });
const spy = vi.spyOn(env, "env", "get");
spy.mockReturnValue({ AUTH_PROVIDERS: ["credentials"] } as never);
const caller = groupRouter.createCaller({ db, session: adminSession });
const groupId = createId();
const userId = createId();
@@ -552,7 +658,7 @@ describe("addMember should add member to group", () => {
test("with non existing group it should throw not found error", async () => {
// Arrange
const db = createDb();
const caller = groupRouter.createCaller({ db, session: defaultSession });
const caller = groupRouter.createCaller({ db, session: adminSession });
await db.insert(users).values({
id: createId(),
@@ -569,13 +675,67 @@ describe("addMember should add member to group", () => {
// Assert
await expect(actAsync()).rejects.toThrow("Group not found");
});
test("without admin permissions it should throw unauthorized error", async () => {
// Arrange
const db = createDb();
const caller = groupRouter.createCaller({ db, session: defaultSession });
// Act
const actAsync = async () =>
await caller.addMember({
groupId: createId(),
userId: createId(),
});
// Assert
await expect(actAsync()).rejects.toThrow("Permission denied");
});
test("without credentials provider it should throw FORBIDDEN error", async () => {
// Arrange
const db = createDb();
const spy = vi.spyOn(env, "env", "get");
spy.mockReturnValue({ AUTH_PROVIDERS: ["ldap"] } as never);
const caller = groupRouter.createCaller({ db, session: adminSession });
const groupId = createId();
const userId = createId();
await db.insert(users).values([
{
id: userId,
name: "User",
},
{
id: defaultOwnerId,
name: "Creator",
},
]);
await db.insert(groups).values({
id: groupId,
name: "Group",
ownerId: defaultOwnerId,
});
// Act
const actAsync = async () =>
await caller.addMember({
groupId,
userId,
});
// Assert
await expect(actAsync()).rejects.toThrow("Credentials provider is disabled");
});
});
describe("removeMember should remove member from group", () => {
test("with existing group and user it should remove member", async () => {
// Arrange
const db = createDb();
const caller = groupRouter.createCaller({ db, session: defaultSession });
const spy = vi.spyOn(env, "env", "get");
spy.mockReturnValue({ AUTH_PROVIDERS: ["credentials"] } as never);
const caller = groupRouter.createCaller({ db, session: adminSession });
const groupId = createId();
const userId = createId();
@@ -616,7 +776,7 @@ describe("removeMember should remove member from group", () => {
test("with non existing group it should throw not found error", async () => {
// Arrange
const db = createDb();
const caller = groupRouter.createCaller({ db, session: defaultSession });
const caller = groupRouter.createCaller({ db, session: adminSession });
await db.insert(users).values({
id: createId(),
@@ -633,6 +793,62 @@ describe("removeMember should remove member from group", () => {
// Assert
await expect(actAsync()).rejects.toThrow("Group not found");
});
test("without admin permissions it should throw unauthorized error", async () => {
// Arrange
const db = createDb();
const caller = groupRouter.createCaller({ db, session: defaultSession });
// Act
const actAsync = async () =>
await caller.removeMember({
groupId: createId(),
userId: createId(),
});
// Assert
await expect(actAsync()).rejects.toThrow("Permission denied");
});
test("without credentials provider it should throw FORBIDDEN error", async () => {
// Arrange
const db = createDb();
const spy = vi.spyOn(env, "env", "get");
spy.mockReturnValue({ AUTH_PROVIDERS: ["ldap"] } as never);
const caller = groupRouter.createCaller({ db, session: adminSession });
const groupId = createId();
const userId = createId();
await db.insert(users).values([
{
id: userId,
name: "User",
},
{
id: defaultOwnerId,
name: "Creator",
},
]);
await db.insert(groups).values({
id: groupId,
name: "Group",
ownerId: defaultOwnerId,
});
await db.insert(groupMembers).values({
groupId,
userId,
});
// Act
const actAsync = async () =>
await caller.removeMember({
groupId,
userId,
});
// Assert
await expect(actAsync()).rejects.toThrow("Credentials provider is disabled");
});
});
const createDummyUser = () => ({

View File

@@ -4,9 +4,22 @@ import type { Session } from "@homarr/auth";
import { createId, eq, schema } from "@homarr/db";
import { users } from "@homarr/db/schema/sqlite";
import { createDb } from "@homarr/db/test";
import type { GroupPermissionKey } from "@homarr/definitions";
import { userRouter } from "../user";
const defaultOwnerId = createId();
const createSession = (permissions: GroupPermissionKey[]) =>
({
user: {
id: defaultOwnerId,
permissions,
colorScheme: "light",
},
expires: new Date().toISOString(),
}) satisfies Session;
const defaultSession = createSession([]);
// Mock the auth module to return an empty session
vi.mock("@homarr/auth", async () => {
const mod = await import("@homarr/auth/security");
@@ -212,14 +225,13 @@ describe("editProfile shoud update user", () => {
const db = createDb();
const caller = userRouter.createCaller({
db,
session: null,
session: defaultSession,
});
const id = createId();
const emailVerified = new Date(2024, 0, 5);
await db.insert(schema.users).values({
id,
id: defaultOwnerId,
name: "TEST 1",
email: "abc@gmail.com",
emailVerified,
@@ -227,17 +239,17 @@ describe("editProfile shoud update user", () => {
// act
await caller.editProfile({
id: id,
id: defaultOwnerId,
name: "ABC",
email: "",
});
// assert
const user = await db.select().from(schema.users).where(eq(schema.users.id, id));
const user = await db.select().from(schema.users).where(eq(schema.users.id, defaultOwnerId));
expect(user).toHaveLength(1);
expect(user[0]).toStrictEqual({
id,
id: defaultOwnerId,
name: "ABC",
email: "abc@gmail.com",
emailVerified,
@@ -255,13 +267,11 @@ describe("editProfile shoud update user", () => {
const db = createDb();
const caller = userRouter.createCaller({
db,
session: null,
session: defaultSession,
});
const id = createId();
await db.insert(schema.users).values({
id,
id: defaultOwnerId,
name: "TEST 1",
email: "abc@gmail.com",
emailVerified: new Date(2024, 0, 5),
@@ -269,17 +279,17 @@ describe("editProfile shoud update user", () => {
// act
await caller.editProfile({
id,
id: defaultOwnerId,
name: "ABC",
email: "myNewEmail@gmail.com",
});
// assert
const user = await db.select().from(schema.users).where(eq(schema.users.id, id));
const user = await db.select().from(schema.users).where(eq(schema.users.id, defaultOwnerId));
expect(user).toHaveLength(1);
expect(user[0]).toStrictEqual({
id,
id: defaultOwnerId,
name: "ABC",
email: "myNewEmail@gmail.com",
emailVerified: null,
@@ -298,11 +308,9 @@ describe("delete should delete user", () => {
const db = createDb();
const caller = userRouter.createCaller({
db,
session: null,
session: defaultSession,
});
const userToDelete = createId();
const initialUsers = [
{
id: createId(),
@@ -317,7 +325,7 @@ describe("delete should delete user", () => {
colorScheme: "auto" as const,
},
{
id: userToDelete,
id: defaultOwnerId,
name: "User 2",
email: null,
emailVerified: null,
@@ -343,7 +351,7 @@ describe("delete should delete user", () => {
await db.insert(schema.users).values(initialUsers);
await caller.delete(userToDelete);
await caller.delete(defaultOwnerId);
const usersInDb = await db.select().from(schema.users);
expect(usersInDb).toStrictEqual([initialUsers[0], initialUsers[2]]);