mirror of
https://github.com/ajnart/homarr.git
synced 2026-02-27 00:40:58 +01:00
fix: restrict access to docker containers page to admins (#912)
This commit is contained in:
@@ -1,12 +1,19 @@
|
||||
import { notFound } from "next/navigation";
|
||||
import { Stack, Title } from "@mantine/core";
|
||||
|
||||
import { api } from "@homarr/api/server";
|
||||
import { auth } from "@homarr/auth/next";
|
||||
import { getScopedI18n } from "@homarr/translation/server";
|
||||
|
||||
import { DynamicBreadcrumb } from "~/components/navigation/dynamic-breadcrumb";
|
||||
import { DockerTable } from "./docker-table";
|
||||
|
||||
export default async function DockerPage() {
|
||||
const session = await auth();
|
||||
if (!session?.user || !session.user.permissions.includes("admin")) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
const { containers, timestamp } = await api.docker.getContainers();
|
||||
const tDocker = await getScopedI18n("docker");
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ import type { DockerContainerState } from "@homarr/definitions";
|
||||
import { createCacheChannel } from "@homarr/redis";
|
||||
import { z } from "@homarr/validation";
|
||||
|
||||
import { createTRPCRouter, permissionRequiredProcedure, publicProcedure } from "../../trpc";
|
||||
import { createTRPCRouter, permissionRequiredProcedure } from "../../trpc";
|
||||
import { DockerSingleton } from "./docker-singleton";
|
||||
|
||||
const dockerCache = createCacheChannel<{
|
||||
@@ -16,7 +16,7 @@ const dockerCache = createCacheChannel<{
|
||||
}>("docker-containers", 5 * 60 * 1000);
|
||||
|
||||
export const dockerRouter = createTRPCRouter({
|
||||
getContainers: publicProcedure.query(async () => {
|
||||
getContainers: permissionRequiredProcedure.requiresPermission("admin").query(async () => {
|
||||
const { timestamp, data } = await dockerCache.consumeAsync(async () => {
|
||||
const dockerInstances = DockerSingleton.getInstance();
|
||||
const containers = await Promise.all(
|
||||
|
||||
90
packages/api/src/router/test/docker/docker-router.spec.ts
Normal file
90
packages/api/src/router/test/docker/docker-router.spec.ts
Normal file
@@ -0,0 +1,90 @@
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { describe, expect, test, vi } from "vitest";
|
||||
|
||||
import type { Session } from "@homarr/auth";
|
||||
import { objectKeys } from "@homarr/common";
|
||||
import type { Database } from "@homarr/db";
|
||||
import { getPermissionsWithChildren } from "@homarr/definitions";
|
||||
import type { GroupPermissionKey } from "@homarr/definitions";
|
||||
|
||||
import type { RouterInputs } from "../../..";
|
||||
import { dockerRouter } from "../../docker/docker-router";
|
||||
|
||||
// Mock the auth module to return an empty session
|
||||
vi.mock("@homarr/auth", () => ({ auth: () => ({}) as Session }));
|
||||
vi.mock("@homarr/redis", () => ({
|
||||
createCacheChannel: () => ({
|
||||
// eslint-disable-next-line @typescript-eslint/require-await
|
||||
consumeAsync: async () => ({
|
||||
timestamp: new Date().toISOString(),
|
||||
data: { containers: [] },
|
||||
}),
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||
invalidateAsync: async () => {},
|
||||
}),
|
||||
}));
|
||||
|
||||
const createSessionWithPermissions = (...permissions: GroupPermissionKey[]) =>
|
||||
({
|
||||
user: {
|
||||
id: "1",
|
||||
permissions,
|
||||
},
|
||||
expires: new Date().toISOString(),
|
||||
}) satisfies Session;
|
||||
|
||||
const procedureKeys = objectKeys(dockerRouter._def.procedures);
|
||||
|
||||
const validInputs: {
|
||||
[key in (typeof procedureKeys)[number]]: RouterInputs["docker"][key];
|
||||
} = {
|
||||
getContainers: undefined,
|
||||
startAll: { ids: ["1"] },
|
||||
stopAll: { ids: ["1"] },
|
||||
restartAll: { ids: ["1"] },
|
||||
removeAll: { ids: ["1"] },
|
||||
};
|
||||
|
||||
describe("All procedures should only be accessible for users with admin permission", () => {
|
||||
test.each(procedureKeys)("Procedure %s should be accessible for users with admin permission", async (procedure) => {
|
||||
// Arrange
|
||||
const caller = dockerRouter.createCaller({
|
||||
db: null as unknown as Database,
|
||||
session: createSessionWithPermissions("admin"),
|
||||
});
|
||||
|
||||
// Act
|
||||
const act = () => caller[procedure](validInputs[procedure] as never);
|
||||
|
||||
await expect(act()).resolves.not.toThrow();
|
||||
});
|
||||
|
||||
test.each(procedureKeys)("Procedure %s should not be accessible with other permissions", async (procedure) => {
|
||||
// Arrange
|
||||
const groupPermissionsWithoutAdmin = getPermissionsWithChildren(["admin"]).filter(
|
||||
(permission) => permission !== "admin",
|
||||
);
|
||||
const caller = dockerRouter.createCaller({
|
||||
db: null as unknown as Database,
|
||||
session: createSessionWithPermissions(...groupPermissionsWithoutAdmin),
|
||||
});
|
||||
|
||||
// Act
|
||||
const act = () => caller[procedure](validInputs[procedure] as never);
|
||||
|
||||
await expect(act()).rejects.toThrow(new TRPCError({ code: "FORBIDDEN", message: "Permission denied" }));
|
||||
});
|
||||
|
||||
test.each(procedureKeys)("Procedure %s should not be accessible without session", async (procedure) => {
|
||||
// Arrange
|
||||
const caller = dockerRouter.createCaller({
|
||||
db: null as unknown as Database,
|
||||
session: null,
|
||||
});
|
||||
|
||||
// Act
|
||||
const act = () => caller[procedure](validInputs[procedure] as never);
|
||||
|
||||
await expect(act()).rejects.toThrow(new TRPCError({ code: "UNAUTHORIZED" }));
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user