mirror of
https://github.com/Dokploy/dokploy.git
synced 2026-07-03 21:15:23 +02:00
feat(permissions): implement role-based access control and refactor user permissions
- Introduced a new Permissions component to manage role-based access across various components. - Updated user role checks to utilize the new permissions structure, replacing direct role comparisons with permission checks. - Refactored multiple components to enhance permission handling, ensuring only authorized users can access specific features. - Removed deprecated add-permissions component and streamlined user permission management. - Enhanced role management in the backend to support the new permissions schema, improving overall security and maintainability.
This commit is contained in:
@@ -5,111 +5,6 @@ import { organization, member } from "./account";
|
||||
import { createInsertSchema } from "drizzle-zod";
|
||||
import { z } from "zod";
|
||||
|
||||
export const PERMISSIONS = {
|
||||
PROJECT: {
|
||||
VIEW: {
|
||||
name: "project:view",
|
||||
description: "View projects",
|
||||
},
|
||||
CREATE: {
|
||||
name: "project:create",
|
||||
description: "Create projects",
|
||||
},
|
||||
DELETE: {
|
||||
name: "project:delete",
|
||||
description: "Delete projects",
|
||||
},
|
||||
},
|
||||
SERVICE: {
|
||||
VIEW: {
|
||||
name: "service:view",
|
||||
description: "View services",
|
||||
},
|
||||
CREATE: {
|
||||
name: "service:create",
|
||||
description: "Create services",
|
||||
},
|
||||
DELETE: {
|
||||
name: "service:delete",
|
||||
description: "Delete services",
|
||||
},
|
||||
},
|
||||
TRAEFIK: {
|
||||
ACCESS: {
|
||||
name: "traefik_files:access",
|
||||
description: "Access traefik files",
|
||||
},
|
||||
},
|
||||
DOCKER: {
|
||||
VIEW: {
|
||||
name: "docker:view",
|
||||
description: "View docker",
|
||||
},
|
||||
},
|
||||
API: {
|
||||
ACCESS: {
|
||||
name: "api:access",
|
||||
description: "Access API",
|
||||
},
|
||||
},
|
||||
SCHEDULES: {
|
||||
ACCESS: {
|
||||
name: "schedules:access",
|
||||
description: "Access schedules",
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
||||
export const ownerPermissions = [
|
||||
PERMISSIONS.PROJECT.VIEW,
|
||||
PERMISSIONS.PROJECT.CREATE,
|
||||
PERMISSIONS.PROJECT.DELETE,
|
||||
PERMISSIONS.SERVICE.VIEW,
|
||||
PERMISSIONS.SERVICE.CREATE,
|
||||
PERMISSIONS.SERVICE.DELETE,
|
||||
PERMISSIONS.TRAEFIK.ACCESS,
|
||||
PERMISSIONS.SCHEDULES.ACCESS,
|
||||
] as const;
|
||||
|
||||
export const adminPermissions = [
|
||||
PERMISSIONS.PROJECT.VIEW,
|
||||
PERMISSIONS.PROJECT.CREATE,
|
||||
PERMISSIONS.PROJECT.DELETE,
|
||||
PERMISSIONS.SERVICE.VIEW,
|
||||
PERMISSIONS.SERVICE.CREATE,
|
||||
PERMISSIONS.SERVICE.DELETE,
|
||||
PERMISSIONS.TRAEFIK.ACCESS,
|
||||
PERMISSIONS.DOCKER.VIEW,
|
||||
PERMISSIONS.API.ACCESS,
|
||||
PERMISSIONS.SCHEDULES.ACCESS,
|
||||
] as const;
|
||||
|
||||
export const memberPermissions = [
|
||||
PERMISSIONS.PROJECT.CREATE,
|
||||
PERMISSIONS.SERVICE.CREATE,
|
||||
PERMISSIONS.TRAEFIK.ACCESS,
|
||||
] as const;
|
||||
|
||||
export const defaultPermissions = [
|
||||
{
|
||||
name: "owner",
|
||||
description: "Owner of the organization with full access to all features",
|
||||
permissions: ownerPermissions,
|
||||
},
|
||||
{
|
||||
name: "admin",
|
||||
description:
|
||||
"Administrator with access to manage projects, services and configurations",
|
||||
permissions: adminPermissions,
|
||||
},
|
||||
{
|
||||
name: "member",
|
||||
description:
|
||||
"Regular member with access to create projects and manage services",
|
||||
permissions: memberPermissions,
|
||||
},
|
||||
] as const;
|
||||
|
||||
export const role = pgTable(
|
||||
"member_role",
|
||||
{
|
||||
|
||||
@@ -140,8 +140,18 @@ const { handler, api } = betterAuth({
|
||||
});
|
||||
}
|
||||
} else {
|
||||
const ownerRole = await db.query.role.findFirst({
|
||||
where: and(eq(schema.role.name, "owner")),
|
||||
});
|
||||
|
||||
if (!ownerRole) {
|
||||
throw new APIError("BAD_REQUEST", {
|
||||
message: "Owner role not found",
|
||||
});
|
||||
}
|
||||
|
||||
const isAdminPresent = await db.query.member.findFirst({
|
||||
where: eq(schema.member.role, "owner"),
|
||||
where: and(eq(schema.member.roleId, ownerRole.roleId)),
|
||||
});
|
||||
if (isAdminPresent) {
|
||||
throw new APIError("BAD_REQUEST", {
|
||||
@@ -152,8 +162,11 @@ const { handler, api } = betterAuth({
|
||||
}
|
||||
},
|
||||
after: async (user) => {
|
||||
const ownerRole = await db.query.role.findFirst({
|
||||
where: and(eq(schema.role.name, "owner")),
|
||||
});
|
||||
const isAdminPresent = await db.query.member.findFirst({
|
||||
where: eq(schema.member.role, "owner"),
|
||||
where: and(eq(schema.member.roleId, ownerRole?.roleId || "")),
|
||||
});
|
||||
|
||||
if (!IS_CLOUD) {
|
||||
@@ -186,7 +199,6 @@ const { handler, api } = betterAuth({
|
||||
await tx.insert(schema.member).values({
|
||||
userId: user.id,
|
||||
organizationId: organization?.id || "",
|
||||
role: "owner",
|
||||
createdAt: new Date(),
|
||||
roleId: ownerRole?.roleId || "",
|
||||
});
|
||||
@@ -202,14 +214,18 @@ const { handler, api } = betterAuth({
|
||||
where: eq(schema.member.userId, session.userId),
|
||||
orderBy: desc(schema.member.createdAt),
|
||||
with: {
|
||||
role: true,
|
||||
organization: true,
|
||||
},
|
||||
});
|
||||
|
||||
console.log(member);
|
||||
|
||||
return {
|
||||
data: {
|
||||
...session,
|
||||
activeOrganizationId: member?.organization.id,
|
||||
roleId: member?.roleId,
|
||||
},
|
||||
};
|
||||
},
|
||||
@@ -223,7 +239,7 @@ const { handler, api } = betterAuth({
|
||||
user: {
|
||||
modelName: "users",
|
||||
additionalFields: {
|
||||
role: {
|
||||
roleId: {
|
||||
type: "string",
|
||||
// required: true,
|
||||
input: false,
|
||||
@@ -331,6 +347,7 @@ export const validateRequest = async (request: IncomingMessage) => {
|
||||
),
|
||||
with: {
|
||||
organization: true,
|
||||
role: true,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -363,7 +380,7 @@ export const validateRequest = async (request: IncomingMessage) => {
|
||||
createdAt,
|
||||
updatedAt,
|
||||
twoFactorEnabled,
|
||||
role: member?.role || "member",
|
||||
role: member?.role,
|
||||
ownerId: member?.organization.ownerId || apiKeyRecord.user.id,
|
||||
},
|
||||
};
|
||||
@@ -392,6 +409,15 @@ export const validateRequest = async (request: IncomingMessage) => {
|
||||
};
|
||||
}
|
||||
|
||||
const mockSession = {
|
||||
session: {
|
||||
...session.session,
|
||||
},
|
||||
user: {
|
||||
...session.user,
|
||||
ownerId: session.user.ownerId,
|
||||
},
|
||||
};
|
||||
if (session?.user) {
|
||||
const member = await db.query.member.findFirst({
|
||||
where: and(
|
||||
@@ -402,17 +428,21 @@ export const validateRequest = async (request: IncomingMessage) => {
|
||||
),
|
||||
),
|
||||
with: {
|
||||
role: true,
|
||||
organization: true,
|
||||
},
|
||||
});
|
||||
|
||||
session.user.role = member?.role || "member";
|
||||
if (member?.role) {
|
||||
mockSession.user.role = member.role;
|
||||
}
|
||||
|
||||
if (member) {
|
||||
session.user.ownerId = member.organization.ownerId;
|
||||
mockSession.user.ownerId = member.organization.ownerId;
|
||||
} else {
|
||||
session.user.ownerId = session.user.id;
|
||||
mockSession.user.ownerId = session.user.id;
|
||||
}
|
||||
}
|
||||
|
||||
return session;
|
||||
return mockSession;
|
||||
};
|
||||
|
||||
131
packages/server/src/lib/permissions.ts
Normal file
131
packages/server/src/lib/permissions.ts
Normal file
@@ -0,0 +1,131 @@
|
||||
export const PERMISSIONS = {
|
||||
PROJECT: {
|
||||
VIEW: {
|
||||
name: "project:view",
|
||||
description: "View projects",
|
||||
},
|
||||
CREATE: {
|
||||
name: "project:create",
|
||||
description: "Create projects",
|
||||
},
|
||||
DELETE: {
|
||||
name: "project:delete",
|
||||
description: "Delete projects",
|
||||
},
|
||||
},
|
||||
SERVICE: {
|
||||
VIEW: {
|
||||
name: "service:view",
|
||||
description: "View services",
|
||||
},
|
||||
CREATE: {
|
||||
name: "service:create",
|
||||
description: "Create services",
|
||||
},
|
||||
DELETE: {
|
||||
name: "service:delete",
|
||||
description: "Delete services",
|
||||
},
|
||||
},
|
||||
TRAEFIK: {
|
||||
ACCESS: {
|
||||
name: "traefik_files:access",
|
||||
description: "Access traefik files",
|
||||
},
|
||||
},
|
||||
DOCKER: {
|
||||
VIEW: {
|
||||
name: "docker:view",
|
||||
description: "View docker",
|
||||
},
|
||||
},
|
||||
API: {
|
||||
ACCESS: {
|
||||
name: "api:access",
|
||||
description: "Access API",
|
||||
},
|
||||
},
|
||||
SCHEDULES: {
|
||||
ACCESS: {
|
||||
name: "schedules:access",
|
||||
description: "Access schedules",
|
||||
},
|
||||
},
|
||||
GIT_PROVIDERS: {
|
||||
ACCESS: {
|
||||
name: "git_providers:access",
|
||||
description: "Access git providers",
|
||||
},
|
||||
},
|
||||
SSH_KEYS: {
|
||||
ACCESS: {
|
||||
name: "ssh_keys:access",
|
||||
description: "Access ssh keys",
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
||||
export const ownerPermissions = [
|
||||
PERMISSIONS.PROJECT.VIEW,
|
||||
PERMISSIONS.PROJECT.CREATE,
|
||||
PERMISSIONS.PROJECT.DELETE,
|
||||
PERMISSIONS.SERVICE.VIEW,
|
||||
PERMISSIONS.SERVICE.CREATE,
|
||||
PERMISSIONS.SERVICE.DELETE,
|
||||
PERMISSIONS.TRAEFIK.ACCESS,
|
||||
PERMISSIONS.SCHEDULES.ACCESS,
|
||||
PERMISSIONS.GIT_PROVIDERS.ACCESS,
|
||||
PERMISSIONS.SSH_KEYS.ACCESS,
|
||||
] as const;
|
||||
|
||||
export const adminPermissions = [
|
||||
PERMISSIONS.PROJECT.VIEW,
|
||||
PERMISSIONS.PROJECT.CREATE,
|
||||
PERMISSIONS.PROJECT.DELETE,
|
||||
PERMISSIONS.SERVICE.VIEW,
|
||||
PERMISSIONS.SERVICE.CREATE,
|
||||
PERMISSIONS.SERVICE.DELETE,
|
||||
PERMISSIONS.TRAEFIK.ACCESS,
|
||||
PERMISSIONS.DOCKER.VIEW,
|
||||
PERMISSIONS.API.ACCESS,
|
||||
PERMISSIONS.SCHEDULES.ACCESS,
|
||||
PERMISSIONS.GIT_PROVIDERS.ACCESS,
|
||||
PERMISSIONS.SSH_KEYS.ACCESS,
|
||||
] as const;
|
||||
|
||||
export const memberPermissions = [
|
||||
PERMISSIONS.PROJECT.CREATE,
|
||||
PERMISSIONS.SERVICE.CREATE,
|
||||
PERMISSIONS.TRAEFIK.ACCESS,
|
||||
] as const;
|
||||
|
||||
export const defaultPermissions = [
|
||||
{
|
||||
name: "owner",
|
||||
description: "Owner of the organization with full access to all features",
|
||||
permissions: ownerPermissions,
|
||||
},
|
||||
{
|
||||
name: "admin",
|
||||
description:
|
||||
"Administrator with access to manage projects, services and configurations",
|
||||
permissions: adminPermissions,
|
||||
},
|
||||
{
|
||||
name: "member",
|
||||
description:
|
||||
"Regular member with access to create projects and manage services",
|
||||
permissions: memberPermissions,
|
||||
},
|
||||
] as const;
|
||||
|
||||
// Utility type to extract all permission names
|
||||
type ExtractPermissionNames<T> = T extends { name: infer U }
|
||||
? U
|
||||
: T extends object
|
||||
? {
|
||||
[K in keyof T]: ExtractPermissionNames<T[K]>;
|
||||
}[keyof T]
|
||||
: never;
|
||||
|
||||
export type PermissionName = ExtractPermissionNames<typeof PERMISSIONS>;
|
||||
@@ -3,6 +3,7 @@ import {
|
||||
invitation,
|
||||
member,
|
||||
organization,
|
||||
role,
|
||||
users,
|
||||
} from "@dokploy/server/db/schema";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
@@ -13,9 +14,6 @@ import { findWebServer } from "./web-server";
|
||||
export const findUserById = async (userId: string) => {
|
||||
const user = await db.query.users.findFirst({
|
||||
where: eq(users.id, userId),
|
||||
// with: {
|
||||
// account: true,
|
||||
// },
|
||||
});
|
||||
if (!user) {
|
||||
throw new TRPCError({
|
||||
@@ -37,8 +35,12 @@ export const findOrganizationById = async (organizationId: string) => {
|
||||
};
|
||||
|
||||
export const isAdminPresent = async () => {
|
||||
const ownerRole = await db.query.role.findFirst({
|
||||
where: eq(role.name, "owner"),
|
||||
});
|
||||
|
||||
const admin = await db.query.member.findFirst({
|
||||
where: eq(member.role, "owner"),
|
||||
where: eq(member.roleId, ownerRole?.roleId || ""),
|
||||
});
|
||||
|
||||
if (!admin) {
|
||||
@@ -48,8 +50,12 @@ export const isAdminPresent = async () => {
|
||||
};
|
||||
|
||||
export const findOwner = async () => {
|
||||
const ownerRole = await db.query.role.findFirst({
|
||||
where: eq(role.name, "owner"),
|
||||
});
|
||||
|
||||
const owner = await db.query.member.findFirst({
|
||||
where: eq(member.role, "owner"),
|
||||
where: eq(member.roleId, ownerRole?.roleId || ""),
|
||||
with: {
|
||||
user: true,
|
||||
},
|
||||
|
||||
@@ -1,15 +1,17 @@
|
||||
import { eq } from "drizzle-orm";
|
||||
import { db } from "../db";
|
||||
import {
|
||||
adminPermissions,
|
||||
type createRoleSchema,
|
||||
member,
|
||||
memberPermissions,
|
||||
ownerPermissions,
|
||||
role,
|
||||
type updateRoleSchema,
|
||||
} from "../db/schema";
|
||||
import type { z } from "zod";
|
||||
import {
|
||||
adminPermissions,
|
||||
memberPermissions,
|
||||
ownerPermissions,
|
||||
} from "../lib/permissions";
|
||||
|
||||
export const createRole = async (
|
||||
input: z.infer<typeof createRoleSchema>,
|
||||
|
||||
@@ -3,6 +3,7 @@ import { apikey, member, users } from "@dokploy/server/db/schema";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { and, eq } from "drizzle-orm";
|
||||
import { auth } from "../lib/auth";
|
||||
import { PERMISSIONS } from "../lib/permissions";
|
||||
|
||||
export type User = typeof users.$inferSelect;
|
||||
|
||||
@@ -44,13 +45,16 @@ export const canPerformCreationService = async (
|
||||
projectId: string,
|
||||
organizationId: string,
|
||||
) => {
|
||||
const { accessedProjects, canCreateServices } = await findMemberById(
|
||||
const { accessedProjects, role } = await findMemberById(
|
||||
userId,
|
||||
organizationId,
|
||||
);
|
||||
const haveAccessToProject = accessedProjects.includes(projectId);
|
||||
|
||||
if (canCreateServices && haveAccessToProject) {
|
||||
if (
|
||||
role?.permissions?.includes(PERMISSIONS.SERVICE.CREATE.name) &&
|
||||
haveAccessToProject
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -77,13 +81,16 @@ export const canPeformDeleteService = async (
|
||||
serviceId: string,
|
||||
organizationId: string,
|
||||
) => {
|
||||
const { accessedServices, canDeleteServices } = await findMemberById(
|
||||
const { accessedServices, role } = await findMemberById(
|
||||
userId,
|
||||
organizationId,
|
||||
);
|
||||
const haveAccessToService = accessedServices.includes(serviceId);
|
||||
|
||||
if (canDeleteServices && haveAccessToService) {
|
||||
if (
|
||||
role?.permissions?.includes(PERMISSIONS.SERVICE.DELETE.name) &&
|
||||
haveAccessToService
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -94,9 +101,9 @@ export const canPerformCreationProject = async (
|
||||
userId: string,
|
||||
organizationId: string,
|
||||
) => {
|
||||
const { canCreateProjects } = await findMemberById(userId, organizationId);
|
||||
const { role } = await findMemberById(userId, organizationId);
|
||||
|
||||
if (canCreateProjects) {
|
||||
if (role?.permissions?.includes(PERMISSIONS.PROJECT.CREATE.name)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -107,9 +114,9 @@ export const canPerformDeleteProject = async (
|
||||
userId: string,
|
||||
organizationId: string,
|
||||
) => {
|
||||
const { canDeleteProjects } = await findMemberById(userId, organizationId);
|
||||
const { role } = await findMemberById(userId, organizationId);
|
||||
|
||||
if (canDeleteProjects) {
|
||||
if (role?.permissions?.includes(PERMISSIONS.PROJECT.DELETE.name)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -135,11 +142,8 @@ export const canAccessToTraefikFiles = async (
|
||||
userId: string,
|
||||
organizationId: string,
|
||||
) => {
|
||||
const { canAccessToTraefikFiles } = await findMemberById(
|
||||
userId,
|
||||
organizationId,
|
||||
);
|
||||
return canAccessToTraefikFiles;
|
||||
const { role } = await findMemberById(userId, organizationId);
|
||||
return role?.permissions?.includes(PERMISSIONS.TRAEFIK.ACCESS.name);
|
||||
};
|
||||
|
||||
export const checkServiceAccess = async (
|
||||
@@ -183,7 +187,7 @@ export const checkServiceAccess = async (
|
||||
};
|
||||
|
||||
export const checkProjectAccess = async (
|
||||
authId: string,
|
||||
userId: string,
|
||||
action: "create" | "delete" | "access",
|
||||
organizationId: string,
|
||||
projectId?: string,
|
||||
@@ -192,16 +196,16 @@ export const checkProjectAccess = async (
|
||||
switch (action) {
|
||||
case "access":
|
||||
hasPermission = await canPerformAccessProject(
|
||||
authId,
|
||||
userId,
|
||||
projectId as string,
|
||||
organizationId,
|
||||
);
|
||||
break;
|
||||
case "create":
|
||||
hasPermission = await canPerformCreationProject(authId, organizationId);
|
||||
hasPermission = await canPerformCreationProject(userId, organizationId);
|
||||
break;
|
||||
case "delete":
|
||||
hasPermission = await canPerformDeleteProject(authId, organizationId);
|
||||
hasPermission = await canPerformDeleteProject(userId, organizationId);
|
||||
break;
|
||||
default:
|
||||
hasPermission = false;
|
||||
@@ -225,6 +229,7 @@ export const findMemberById = async (
|
||||
),
|
||||
with: {
|
||||
user: true,
|
||||
role: true,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user