mirror of
https://github.com/Dokploy/dokploy.git
synced 2026-07-21 13:55:33 +02:00
fix(security): service access is authoritative for service-scoped wss ops
When a container is opened from a service page (serviceId present), gate on service access alone — matching application.readLogs — instead of requiring the docker permission first. This unblocks a member who has the service but not the canAccessToDocker permission (or the server it runs on) from reading its logs / opening its terminal. Generic Docker-overview ops (no serviceId) still require docker permission + server access.
This commit is contained in:
@@ -56,19 +56,25 @@ describe("canAccessDockerOverWss", () => {
|
||||
});
|
||||
|
||||
it("denies when the container belongs to a service the caller cannot access", async () => {
|
||||
mockHasPermission.mockResolvedValue(true);
|
||||
mockCheckServiceAccess.mockRejectedValue(new Error("no access"));
|
||||
expect(await canAccessDockerOverWss(USER, SESSION, null, "svc-1")).toBe(
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
it("allows when the caller has access to the container's service", async () => {
|
||||
mockHasPermission.mockResolvedValue(true);
|
||||
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, null, "svc-1")).toBe(
|
||||
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();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -28,21 +28,26 @@ export const canAccessDockerOverWss = async (
|
||||
if (!user || !session?.activeOrganizationId) return false;
|
||||
|
||||
const ctx = buildCtx(user, session.activeOrganizationId);
|
||||
if (!(await hasPermission(ctx, { docker: ["read"] }))) return false;
|
||||
|
||||
// When the container belongs to a known Dokploy service (opened from a
|
||||
// service page), enforce service-level access. Checked before the server
|
||||
// gate so the service grant is the primary authorization signal. Container
|
||||
// terminals opened from the generic Docker overview have no serviceId and
|
||||
// fall back to the docker-permission + server checks.
|
||||
// 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,
|
||||
|
||||
Reference in New Issue
Block a user