mirror of
https://github.com/Dokploy/dokploy.git
synced 2026-07-21 22:05:23 +02:00
Merge pull request #4865 from Dokploy/fix/authz-websocket-handlers
fix(security): missing authorization on docker/terminal WebSocket handlers (member -> root)
This commit is contained in:
104
apps/dokploy/__test__/wss/authorize.test.ts
Normal file
104
apps/dokploy/__test__/wss/authorize.test.ts
Normal file
@@ -0,0 +1,104 @@
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
// Mock the permission + server helpers the wss authorizer composes.
|
||||
const mockHasPermission = vi.hoisted(() => vi.fn());
|
||||
const mockFindMember = vi.hoisted(() => vi.fn());
|
||||
const mockCheckServiceAccess = vi.hoisted(() => vi.fn());
|
||||
vi.mock("@dokploy/server/services/permission", () => ({
|
||||
hasPermission: mockHasPermission,
|
||||
findMemberByUserId: mockFindMember,
|
||||
checkServiceAccess: mockCheckServiceAccess,
|
||||
}));
|
||||
|
||||
const mockGetAccessibleServerIds = vi.hoisted(() => vi.fn());
|
||||
vi.mock("@dokploy/server", () => ({
|
||||
getAccessibleServerIds: mockGetAccessibleServerIds,
|
||||
}));
|
||||
|
||||
import {
|
||||
canAccessDockerOverWss,
|
||||
canAccessTerminalOverWss,
|
||||
} from "@/server/wss/authorize";
|
||||
|
||||
const USER = { id: "user-1" };
|
||||
const SESSION = { activeOrganizationId: "org-1" };
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
describe("canAccessDockerOverWss", () => {
|
||||
it("denies when there is no user or session", async () => {
|
||||
expect(await canAccessDockerOverWss(null, SESSION)).toBe(false);
|
||||
expect(await canAccessDockerOverWss(USER, null)).toBe(false);
|
||||
});
|
||||
|
||||
it("denies a member without docker permission", async () => {
|
||||
mockHasPermission.mockResolvedValue(false);
|
||||
expect(await canAccessDockerOverWss(USER, SESSION)).toBe(false);
|
||||
});
|
||||
|
||||
it("allows when the caller has docker permission (no server)", async () => {
|
||||
mockHasPermission.mockResolvedValue(true);
|
||||
expect(await canAccessDockerOverWss(USER, SESSION)).toBe(true);
|
||||
});
|
||||
|
||||
it("denies a remote server the caller cannot access, even with docker permission", async () => {
|
||||
mockHasPermission.mockResolvedValue(true);
|
||||
mockGetAccessibleServerIds.mockResolvedValue(new Set(["other-server"]));
|
||||
expect(await canAccessDockerOverWss(USER, SESSION, "srv-1")).toBe(false);
|
||||
});
|
||||
|
||||
it("allows a remote server the caller can access", async () => {
|
||||
mockHasPermission.mockResolvedValue(true);
|
||||
mockGetAccessibleServerIds.mockResolvedValue(new Set(["srv-1"]));
|
||||
expect(await canAccessDockerOverWss(USER, SESSION, "srv-1")).toBe(true);
|
||||
});
|
||||
|
||||
it("denies when the container belongs to a service the caller cannot access", async () => {
|
||||
mockCheckServiceAccess.mockRejectedValue(new Error("no access"));
|
||||
expect(await canAccessDockerOverWss(USER, SESSION, null, "svc-1")).toBe(
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
it("allows service access even without docker permission or server access", async () => {
|
||||
// A member granted the service but without canAccessToDocker, whose
|
||||
// service runs on a server they were not individually granted, must still
|
||||
// read its logs — matches application.readLogs (service access only).
|
||||
mockHasPermission.mockResolvedValue(false);
|
||||
mockGetAccessibleServerIds.mockResolvedValue(new Set());
|
||||
mockCheckServiceAccess.mockResolvedValue(undefined);
|
||||
expect(
|
||||
await canAccessDockerOverWss(USER, SESSION, "srv-remote", "svc-1"),
|
||||
).toBe(true);
|
||||
// Service path is authoritative — it must not fall through to docker/server.
|
||||
expect(mockHasPermission).not.toHaveBeenCalled();
|
||||
expect(mockGetAccessibleServerIds).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("canAccessTerminalOverWss", () => {
|
||||
it("denies the local host terminal to a plain member", async () => {
|
||||
mockFindMember.mockResolvedValue({ role: "member" });
|
||||
expect(await canAccessTerminalOverWss(USER, SESSION, "local")).toBe(false);
|
||||
});
|
||||
|
||||
it("allows the local host terminal to an owner", async () => {
|
||||
mockFindMember.mockResolvedValue({ role: "owner" });
|
||||
expect(await canAccessTerminalOverWss(USER, SESSION, "local")).toBe(true);
|
||||
});
|
||||
|
||||
it("allows the local host terminal to an admin", async () => {
|
||||
mockFindMember.mockResolvedValue({ role: "admin" });
|
||||
expect(await canAccessTerminalOverWss(USER, SESSION, "local")).toBe(true);
|
||||
});
|
||||
|
||||
it("gates a remote server terminal on server access", async () => {
|
||||
mockGetAccessibleServerIds.mockResolvedValue(new Set(["srv-1"]));
|
||||
expect(await canAccessTerminalOverWss(USER, SESSION, "srv-1")).toBe(true);
|
||||
expect(await canAccessTerminalOverWss(USER, SESSION, "srv-2")).toBe(false);
|
||||
// role lookup must not be needed for the remote path
|
||||
expect(mockFindMember).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -271,6 +271,7 @@ export const ShowGeneralApplication = ({ applicationId }: Props) => {
|
||||
<DockerTerminalModal
|
||||
appName={data?.appName || ""}
|
||||
serverId={data?.serverId || ""}
|
||||
serviceId={applicationId}
|
||||
>
|
||||
<Button
|
||||
variant="outline"
|
||||
|
||||
@@ -50,9 +50,10 @@ export const badgeStateColor = (state: string) => {
|
||||
interface Props {
|
||||
appName: string;
|
||||
serverId?: string;
|
||||
serviceId?: string;
|
||||
}
|
||||
|
||||
export const ShowDockerLogs = ({ appName, serverId }: Props) => {
|
||||
export const ShowDockerLogs = ({ appName, serverId, serviceId }: Props) => {
|
||||
const [containerId, setContainerId] = useState<string | undefined>();
|
||||
const [option, setOption] = useState<"swarm" | "native">("native");
|
||||
|
||||
@@ -182,6 +183,7 @@ export const ShowDockerLogs = ({ appName, serverId }: Props) => {
|
||||
serverId={serverId || ""}
|
||||
containerId={containerId || "select-a-container"}
|
||||
runType={option}
|
||||
serviceId={serviceId}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
@@ -55,12 +55,14 @@ interface Props {
|
||||
appName: string;
|
||||
serverId?: string;
|
||||
appType: "stack" | "docker-compose";
|
||||
serviceId?: string;
|
||||
}
|
||||
|
||||
export const ShowComposeContainers = ({
|
||||
appName,
|
||||
appType,
|
||||
serverId,
|
||||
serviceId,
|
||||
}: Props) => {
|
||||
const { data, isPending, refetch } =
|
||||
api.docker.getContainersByAppNameMatch.useQuery(
|
||||
@@ -122,6 +124,7 @@ export const ShowComposeContainers = ({
|
||||
key={container.containerId}
|
||||
container={container}
|
||||
serverId={serverId}
|
||||
serviceId={serviceId}
|
||||
onActionComplete={() => refetch()}
|
||||
/>
|
||||
))}
|
||||
@@ -142,12 +145,14 @@ interface ContainerRowProps {
|
||||
status: string;
|
||||
};
|
||||
serverId?: string;
|
||||
serviceId?: string;
|
||||
onActionComplete: () => void;
|
||||
}
|
||||
|
||||
const ContainerRow = ({
|
||||
container,
|
||||
serverId,
|
||||
serviceId,
|
||||
onActionComplete,
|
||||
}: ContainerRowProps) => {
|
||||
const [logsOpen, setLogsOpen] = useState(false);
|
||||
@@ -236,6 +241,7 @@ const ContainerRow = ({
|
||||
<DockerTerminalModal
|
||||
containerId={container.containerId}
|
||||
serverId={serverId || ""}
|
||||
serviceId={serviceId}
|
||||
>
|
||||
Terminal
|
||||
</DockerTerminalModal>
|
||||
@@ -280,6 +286,7 @@ const ContainerRow = ({
|
||||
containerId={container.containerId}
|
||||
serverId={serverId}
|
||||
runType="native"
|
||||
serviceId={serviceId}
|
||||
/>
|
||||
</div>
|
||||
</DialogContent>
|
||||
|
||||
@@ -206,6 +206,7 @@ export const ComposeActions = ({ composeId }: Props) => {
|
||||
appName={data?.appName || ""}
|
||||
serverId={data?.serverId || ""}
|
||||
appType={data?.composeType || "docker-compose"}
|
||||
serviceId={data?.composeId}
|
||||
>
|
||||
<Button
|
||||
variant="outline"
|
||||
|
||||
@@ -35,9 +35,14 @@ export const DockerLogs = dynamic(
|
||||
interface Props {
|
||||
appName: string;
|
||||
serverId?: string;
|
||||
serviceId?: string;
|
||||
}
|
||||
|
||||
export const ShowDockerLogsStack = ({ appName, serverId }: Props) => {
|
||||
export const ShowDockerLogsStack = ({
|
||||
appName,
|
||||
serverId,
|
||||
serviceId,
|
||||
}: Props) => {
|
||||
const [option, setOption] = useState<"swarm" | "native">("native");
|
||||
const [containerId, setContainerId] = useState<string | undefined>();
|
||||
|
||||
@@ -167,6 +172,7 @@ export const ShowDockerLogsStack = ({ appName, serverId }: Props) => {
|
||||
serverId={serverId || ""}
|
||||
containerId={containerId || "select-a-container"}
|
||||
runType={option}
|
||||
serviceId={serviceId}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
@@ -35,12 +35,14 @@ interface Props {
|
||||
appName: string;
|
||||
serverId?: string;
|
||||
appType: "stack" | "docker-compose";
|
||||
serviceId?: string;
|
||||
}
|
||||
|
||||
export const ShowDockerLogsCompose = ({
|
||||
appName,
|
||||
appType,
|
||||
serverId,
|
||||
serviceId,
|
||||
}: Props) => {
|
||||
const { data, isPending } = api.docker.getContainersByAppNameMatch.useQuery(
|
||||
{
|
||||
@@ -104,6 +106,7 @@ export const ShowDockerLogsCompose = ({
|
||||
serverId={serverId || ""}
|
||||
containerId={containerId || "select-a-container"}
|
||||
runType="native"
|
||||
serviceId={serviceId}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
@@ -23,6 +23,7 @@ interface Props {
|
||||
containerId: string;
|
||||
serverId?: string | null;
|
||||
runType: "swarm" | "native";
|
||||
serviceId?: string;
|
||||
}
|
||||
|
||||
export const priorities = [
|
||||
@@ -52,6 +53,7 @@ export const DockerLogsId: React.FC<Props> = ({
|
||||
containerId,
|
||||
serverId,
|
||||
runType,
|
||||
serviceId,
|
||||
}) => {
|
||||
const { data } = api.docker.getConfig.useQuery(
|
||||
{
|
||||
@@ -157,6 +159,10 @@ export const DockerLogsId: React.FC<Props> = ({
|
||||
params.append("serverId", serverId);
|
||||
}
|
||||
|
||||
if (serviceId) {
|
||||
params.append("serviceId", serviceId);
|
||||
}
|
||||
|
||||
const wsUrl = `${protocol}//${
|
||||
window.location.host
|
||||
}/docker-container-logs?${params.toString()}`;
|
||||
@@ -222,7 +228,7 @@ export const DockerLogsId: React.FC<Props> = ({
|
||||
ws.close();
|
||||
}
|
||||
};
|
||||
}, [containerId, serverId, lines, search, since]);
|
||||
}, [containerId, serverId, serviceId, lines, search, since]);
|
||||
|
||||
const handleDownload = () => {
|
||||
const logContent = filteredLogs
|
||||
|
||||
@@ -23,12 +23,14 @@ interface Props {
|
||||
containerId: string;
|
||||
serverId?: string;
|
||||
children?: React.ReactNode;
|
||||
serviceId?: string;
|
||||
}
|
||||
|
||||
export const DockerTerminalModal = ({
|
||||
children,
|
||||
containerId,
|
||||
serverId,
|
||||
serviceId,
|
||||
}: Props) => {
|
||||
const [mainDialogOpen, setMainDialogOpen] = useState(false);
|
||||
const [confirmDialogOpen, setConfirmDialogOpen] = useState(false);
|
||||
@@ -74,6 +76,7 @@ export const DockerTerminalModal = ({
|
||||
id="terminal"
|
||||
containerId={containerId}
|
||||
serverId={serverId || ""}
|
||||
serviceId={serviceId}
|
||||
/>
|
||||
<Dialog open={confirmDialogOpen} onOpenChange={setConfirmDialogOpen}>
|
||||
<DialogContent onEscapeKeyDown={(event) => event.preventDefault()}>
|
||||
|
||||
@@ -10,12 +10,14 @@ interface Props {
|
||||
id: string;
|
||||
containerId?: string;
|
||||
serverId?: string;
|
||||
serviceId?: string;
|
||||
}
|
||||
|
||||
export const DockerTerminal: React.FC<Props> = ({
|
||||
id,
|
||||
containerId,
|
||||
serverId,
|
||||
serviceId,
|
||||
}) => {
|
||||
const termRef = useRef(null);
|
||||
const [activeWay, setActiveWay] = React.useState<string | undefined>("bash");
|
||||
@@ -38,7 +40,7 @@ export const DockerTerminal: React.FC<Props> = ({
|
||||
const addonFit = new FitAddon();
|
||||
const protocol = window.location.protocol === "https:" ? "wss:" : "ws:";
|
||||
|
||||
const wsUrl = `${protocol}//${window.location.host}/docker-container-terminal?containerId=${containerId}&activeWay=${activeWay}${serverId ? `&serverId=${serverId}` : ""}`;
|
||||
const wsUrl = `${protocol}//${window.location.host}/docker-container-terminal?containerId=${containerId}&activeWay=${activeWay}${serverId ? `&serverId=${serverId}` : ""}${serviceId ? `&serviceId=${serviceId}` : ""}`;
|
||||
|
||||
const ws = new WebSocket(wsUrl);
|
||||
|
||||
|
||||
@@ -229,6 +229,7 @@ export const ShowGeneralLibsql = ({ libsqlId }: Props) => {
|
||||
)}
|
||||
<DockerTerminalModal
|
||||
appName={data?.appName || ""}
|
||||
serviceId={data?.libsqlId}
|
||||
serverId={data?.serverId || ""}
|
||||
>
|
||||
<Button
|
||||
|
||||
@@ -236,6 +236,7 @@ export const ShowGeneralMariadb = ({ mariadbId }: Props) => {
|
||||
))}
|
||||
<DockerTerminalModal
|
||||
appName={data?.appName || ""}
|
||||
serviceId={data?.mariadbId}
|
||||
serverId={data?.serverId || ""}
|
||||
>
|
||||
<Button
|
||||
|
||||
@@ -230,6 +230,7 @@ export const ShowGeneralMongo = ({ mongoId }: Props) => {
|
||||
</TooltipProvider>
|
||||
<DockerTerminalModal
|
||||
appName={data?.appName || ""}
|
||||
serviceId={data?.mongoId}
|
||||
serverId={data?.serverId || ""}
|
||||
>
|
||||
<Button
|
||||
|
||||
@@ -228,6 +228,7 @@ export const ShowGeneralMysql = ({ mysqlId }: Props) => {
|
||||
</TooltipProvider>
|
||||
<DockerTerminalModal
|
||||
appName={data?.appName || ""}
|
||||
serviceId={data?.mysqlId}
|
||||
serverId={data?.serverId || ""}
|
||||
>
|
||||
<Button
|
||||
|
||||
@@ -234,6 +234,7 @@ export const ShowGeneralPostgres = ({ postgresId }: Props) => {
|
||||
</TooltipProvider>
|
||||
<DockerTerminalModal
|
||||
appName={data?.appName || ""}
|
||||
serviceId={data?.postgresId}
|
||||
serverId={data?.serverId || ""}
|
||||
>
|
||||
<Button
|
||||
|
||||
@@ -229,6 +229,7 @@ export const ShowGeneralRedis = ({ redisId }: Props) => {
|
||||
</TooltipProvider>
|
||||
<DockerTerminalModal
|
||||
appName={data?.appName || ""}
|
||||
serviceId={data?.redisId}
|
||||
serverId={data?.serverId || ""}
|
||||
>
|
||||
<Button
|
||||
|
||||
@@ -40,6 +40,7 @@ interface Props {
|
||||
children?: React.ReactNode;
|
||||
serverId?: string;
|
||||
appType?: "stack" | "docker-compose";
|
||||
serviceId?: string;
|
||||
}
|
||||
|
||||
export const DockerTerminalModal = ({
|
||||
@@ -47,6 +48,7 @@ export const DockerTerminalModal = ({
|
||||
appName,
|
||||
serverId,
|
||||
appType,
|
||||
serviceId,
|
||||
}: Props) => {
|
||||
const { data, isPending } = api.docker.getContainersByAppNameMatch.useQuery(
|
||||
{
|
||||
@@ -131,6 +133,7 @@ export const DockerTerminalModal = ({
|
||||
serverId={serverId || ""}
|
||||
id="terminal"
|
||||
containerId={containerId || "select-a-container"}
|
||||
serviceId={serviceId}
|
||||
/>
|
||||
<Dialog open={confirmDialogOpen} onOpenChange={setConfirmDialogOpen}>
|
||||
<DialogContent onEscapeKeyDown={(event) => event.preventDefault()}>
|
||||
|
||||
@@ -347,6 +347,7 @@ const Service = (
|
||||
<ShowDockerLogs
|
||||
appName={data?.appName || ""}
|
||||
serverId={data?.serverId || ""}
|
||||
serviceId={data?.applicationId}
|
||||
/>
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
@@ -312,6 +312,7 @@ const Service = (
|
||||
serverId={data?.serverId || undefined}
|
||||
appName={data?.appName || ""}
|
||||
appType={data?.composeType || "docker-compose"}
|
||||
serviceId={data?.composeId}
|
||||
/>
|
||||
</div>
|
||||
</TabsContent>
|
||||
@@ -380,11 +381,13 @@ const Service = (
|
||||
serverId={data?.serverId || ""}
|
||||
appName={data?.appName || ""}
|
||||
appType={data?.composeType || "docker-compose"}
|
||||
serviceId={data?.composeId}
|
||||
/>
|
||||
) : (
|
||||
<ShowDockerLogsStack
|
||||
serverId={data?.serverId || ""}
|
||||
appName={data?.appName || ""}
|
||||
serviceId={data?.composeId}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -269,6 +269,7 @@ const Libsql = (
|
||||
<ShowDockerLogs
|
||||
serverId={data?.serverId || ""}
|
||||
appName={data?.appName || ""}
|
||||
serviceId={data?.libsqlId}
|
||||
/>
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
@@ -299,6 +299,7 @@ const Mariadb = (
|
||||
<ShowDockerLogs
|
||||
serverId={data?.serverId || ""}
|
||||
appName={data?.appName || ""}
|
||||
serviceId={data?.mariadbId}
|
||||
/>
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
@@ -299,6 +299,7 @@ const Mongo = (
|
||||
<ShowDockerLogs
|
||||
serverId={data?.serverId || ""}
|
||||
appName={data?.appName || ""}
|
||||
serviceId={data?.mongoId}
|
||||
/>
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
@@ -276,6 +276,7 @@ const MySql = (
|
||||
<ShowDockerLogs
|
||||
serverId={data?.serverId || ""}
|
||||
appName={data?.appName || ""}
|
||||
serviceId={data?.mysqlId}
|
||||
/>
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
@@ -284,6 +284,7 @@ const Postgresql = (
|
||||
<ShowDockerLogs
|
||||
serverId={data?.serverId || ""}
|
||||
appName={data?.appName || ""}
|
||||
serviceId={data?.postgresId}
|
||||
/>
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
@@ -297,6 +297,7 @@ const Redis = (
|
||||
<ShowDockerLogs
|
||||
serverId={data?.serverId || ""}
|
||||
appName={data?.appName || ""}
|
||||
serviceId={data?.redisId}
|
||||
/>
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
89
apps/dokploy/server/wss/authorize.ts
Normal file
89
apps/dokploy/server/wss/authorize.ts
Normal file
@@ -0,0 +1,89 @@
|
||||
import { getAccessibleServerIds } from "@dokploy/server";
|
||||
import {
|
||||
checkServiceAccess,
|
||||
findMemberByUserId,
|
||||
hasPermission,
|
||||
} from "@dokploy/server/services/permission";
|
||||
|
||||
type WssUser = { id: string } | null | undefined;
|
||||
type WssSession = { activeOrganizationId?: string | null } | null | undefined;
|
||||
|
||||
const buildCtx = (user: { id: string }, activeOrganizationId: string) => ({
|
||||
user: { id: user.id },
|
||||
session: { activeOrganizationId },
|
||||
});
|
||||
|
||||
// Authorizes docker/container operations opened over a WebSocket (container
|
||||
// terminal, container logs, container stats). Requires the docker permission
|
||||
// (owner/admin, or a member explicitly granted canAccessToDocker) and, for a
|
||||
// remote server, that the server is accessible to the caller. Previously these
|
||||
// handlers only checked session + organization, so any member could reach a
|
||||
// root shell / logs of any container.
|
||||
export const canAccessDockerOverWss = async (
|
||||
user: WssUser,
|
||||
session: WssSession,
|
||||
serverId?: string | null,
|
||||
serviceId?: string | null,
|
||||
): Promise<boolean> => {
|
||||
if (!user || !session?.activeOrganizationId) return false;
|
||||
|
||||
const ctx = buildCtx(user, session.activeOrganizationId);
|
||||
|
||||
// When the container belongs to a specific Dokploy service (opened from a
|
||||
// service page, so serviceId is present), access to that service is the
|
||||
// authoritative gate — matching the service tRPC endpoints (e.g.
|
||||
// application.readLogs, which check service access only). A member granted
|
||||
// the service can read its logs / open its terminal even without the broad
|
||||
// "docker" permission or explicit access to the server it runs on.
|
||||
if (serviceId) {
|
||||
try {
|
||||
await checkServiceAccess(ctx, serviceId, "read");
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Generic Docker overview (no service context): mirror the docker tRPC router
|
||||
// — require the docker permission and access to the target server.
|
||||
if (!(await hasPermission(ctx, { docker: ["read"] }))) return false;
|
||||
|
||||
if (serverId && serverId !== "local") {
|
||||
const accessible = await getAccessibleServerIds({
|
||||
userId: user.id,
|
||||
activeOrganizationId: session.activeOrganizationId,
|
||||
});
|
||||
if (!accessible.has(serverId)) return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
// Authorizes the host/server SSH terminal opened over a WebSocket. The local
|
||||
// host terminal is a root shell on the control-plane host, so it is restricted
|
||||
// to owner/admin. A remote server terminal is gated on server access.
|
||||
export const canAccessTerminalOverWss = async (
|
||||
user: WssUser,
|
||||
session: WssSession,
|
||||
serverId?: string | null,
|
||||
): Promise<boolean> => {
|
||||
if (!user || !session?.activeOrganizationId) return false;
|
||||
|
||||
if (serverId && serverId !== "local") {
|
||||
const accessible = await getAccessibleServerIds({
|
||||
userId: user.id,
|
||||
activeOrganizationId: session.activeOrganizationId,
|
||||
});
|
||||
return accessible.has(serverId);
|
||||
}
|
||||
|
||||
try {
|
||||
const member = await findMemberByUserId(
|
||||
user.id,
|
||||
session.activeOrganizationId,
|
||||
);
|
||||
return member?.role === "owner" || member?.role === "admin";
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
@@ -3,6 +3,7 @@ import { findServerById, IS_CLOUD, validateRequest } from "@dokploy/server";
|
||||
import { spawn } from "node-pty";
|
||||
import { Client } from "ssh2";
|
||||
import { WebSocketServer } from "ws";
|
||||
import { canAccessDockerOverWss } from "./authorize";
|
||||
import {
|
||||
getShell,
|
||||
isValidContainerId,
|
||||
@@ -41,6 +42,7 @@ export const setupDockerContainerLogsWebSocketServer = (
|
||||
const since = url.searchParams.get("since") ?? "all";
|
||||
const serverId = url.searchParams.get("serverId");
|
||||
const runType = url.searchParams.get("runType");
|
||||
const serviceId = url.searchParams.get("serviceId");
|
||||
const { user, session } = await validateRequest(req);
|
||||
|
||||
if (!containerId) {
|
||||
@@ -74,6 +76,11 @@ export const setupDockerContainerLogsWebSocketServer = (
|
||||
return;
|
||||
}
|
||||
|
||||
if (!(await canAccessDockerOverWss(user, session, serverId, serviceId))) {
|
||||
ws.close(4003, "Not authorized");
|
||||
return;
|
||||
}
|
||||
|
||||
// Set up keep-alive ping mechanism to prevent timeout
|
||||
// Send ping every 45 seconds to keep connection alive
|
||||
const pingInterval = setInterval(() => {
|
||||
|
||||
@@ -3,6 +3,7 @@ import { findServerById, IS_CLOUD, validateRequest } from "@dokploy/server";
|
||||
import { spawn } from "node-pty";
|
||||
import { Client } from "ssh2";
|
||||
import { WebSocketServer } from "ws";
|
||||
import { canAccessDockerOverWss } from "./authorize";
|
||||
import { isValidContainerId, isValidShell } from "./utils";
|
||||
|
||||
export const setupDockerContainerTerminalWebSocketServer = (
|
||||
@@ -32,6 +33,7 @@ export const setupDockerContainerTerminalWebSocketServer = (
|
||||
const containerId = url.searchParams.get("containerId");
|
||||
const activeWay = url.searchParams.get("activeWay");
|
||||
const serverId = url.searchParams.get("serverId");
|
||||
const serviceId = url.searchParams.get("serviceId");
|
||||
const { user, session } = await validateRequest(req);
|
||||
|
||||
if (!containerId) {
|
||||
@@ -58,6 +60,11 @@ export const setupDockerContainerTerminalWebSocketServer = (
|
||||
ws.close();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!(await canAccessDockerOverWss(user, session, serverId, serviceId))) {
|
||||
ws.close(4003, "Not authorized");
|
||||
return;
|
||||
}
|
||||
try {
|
||||
if (serverId) {
|
||||
const server = await findServerById(serverId);
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
validateRequest,
|
||||
} from "@dokploy/server";
|
||||
import { WebSocketServer } from "ws";
|
||||
import { canAccessDockerOverWss } from "./authorize";
|
||||
|
||||
export const setupDockerStatsMonitoringSocketServer = (
|
||||
server: http.Server<typeof http.IncomingMessage, typeof http.ServerResponse>,
|
||||
@@ -44,6 +45,7 @@ export const setupDockerStatsMonitoringSocketServer = (
|
||||
| "application"
|
||||
| "stack"
|
||||
| "docker-compose";
|
||||
const serviceId = url.searchParams.get("serviceId");
|
||||
const { user, session } = await validateRequest(req);
|
||||
|
||||
if (!appName) {
|
||||
@@ -55,6 +57,11 @@ export const setupDockerStatsMonitoringSocketServer = (
|
||||
ws.close();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!(await canAccessDockerOverWss(user, session, null, serviceId))) {
|
||||
ws.close(4003, "Not authorized");
|
||||
return;
|
||||
}
|
||||
const intervalId = setInterval(async () => {
|
||||
try {
|
||||
// Special case: when monitoring "dokploy", get host system stats instead of container stats
|
||||
|
||||
@@ -9,6 +9,7 @@ import { publicIpv4, publicIpv6 } from "public-ip";
|
||||
import { Client, type ConnectConfig } from "ssh2";
|
||||
import { WebSocketServer } from "ws";
|
||||
import { getDockerHost } from "../utils/docker";
|
||||
import { canAccessTerminalOverWss } from "./authorize";
|
||||
import { setupLocalServerSSHKey } from "./utils";
|
||||
|
||||
const COMMAND_TO_ALLOW_LOCAL_ACCESS = `
|
||||
@@ -93,6 +94,11 @@ export const setupTerminalWebSocketServer = (
|
||||
return;
|
||||
}
|
||||
|
||||
if (!(await canAccessTerminalOverWss(user, session, serverId))) {
|
||||
ws.close(4003, "Not authorized");
|
||||
return;
|
||||
}
|
||||
|
||||
let connectionDetails: ConnectConfig = {};
|
||||
|
||||
const isLocalServer = serverId === "local";
|
||||
|
||||
Reference in New Issue
Block a user