feat: completed new role service

This commit is contained in:
=
2025-09-27 20:42:37 +05:30
parent c6cab7a528
commit a846831b26
7 changed files with 512 additions and 1 deletions

View File

@@ -2,7 +2,7 @@ import { MongoAbility } from "@casl/ability";
import { MongoQuery } from "@ucast/mongo2js";
import { Knex } from "knex";
import { ActionProjectType, TMemberships } from "@app/db/schemas";
import { AccessScope, AccessScopeData, ActionProjectType, TMemberships } from "@app/db/schemas";
import { ActorAuthMethod, ActorType } from "@app/services/auth/auth-type";
import { OrgPermissionSet } from "./org-permission";
@@ -49,6 +49,14 @@ export type TGetProjectPermissionArg = {
actionProjectType: ActionProjectType;
};
export type TGetOrgPermissionArg = {
actor: ActorType;
actorId: string;
orgId: string;
actorAuthMethod: ActorAuthMethod;
actorOrgId?: string;
};
export type TPermissionServiceFactory = {
getOrgPermission: (
type: ActorType,

View File

@@ -0,0 +1,41 @@
import { AccessScope } from "@app/db/schemas";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types";
import { BadRequestError } from "@app/lib/errors";
import { TRoleScopeFactory } from "../role-types";
type TNamespaceRoleScopeFactoryDep = {
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
};
// eslint-disable-next-line @typescript-eslint/no-unused-vars
export const newNamespaceRoleFactory = (_dto: TNamespaceRoleScopeFactoryDep): TRoleScopeFactory => {
const onCreateRoleGuard: TRoleScopeFactory["onCreateRoleGuard"] = async () => {};
const onUpdateRoleGuard: TRoleScopeFactory["onUpdateRoleGuard"] = async () => {};
const onDeleteRoleGuard: TRoleScopeFactory["onDeleteRoleGuard"] = async () => {};
const onListRoleGuard: TRoleScopeFactory["onListRoleGuard"] = async () => {};
const onGetRoleByIdGuard: TRoleScopeFactory["onGetRoleByIdGuard"] = async () => {};
const onGetRoleBySlugGuard: TRoleScopeFactory["onGetRoleBySlugGuard"] = async () => {};
const getScopeField: TRoleScopeFactory["getScopeField"] = (dto) => {
if (dto.scope === AccessScope.Namespace) {
return { key: "namespaceId" as const, value: dto.namespaceId };
}
throw new BadRequestError({ message: "Invalid scope provided for the factory" });
};
return {
onCreateRoleGuard,
onUpdateRoleGuard,
onDeleteRoleGuard,
onListRoleGuard,
onGetRoleByIdGuard,
onGetRoleBySlugGuard,
getScopeField
};
};

View File

@@ -0,0 +1,97 @@
import { ForbiddenError } from "@casl/ability";
import { AccessScope } from "@app/db/schemas";
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types";
import { BadRequestError } from "@app/lib/errors";
import { TRoleScopeFactory } from "../role-types";
type TOrgRoleScopeFactoryDep = {
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">;
};
export const newOrgRoleFactory = ({ permissionService }: TOrgRoleScopeFactoryDep): TRoleScopeFactory => {
const getScopeField: TRoleScopeFactory["getScopeField"] = (dto) => {
if (dto.scope === AccessScope.Organization) {
return { key: "orgId" as const, value: dto.orgId };
}
throw new BadRequestError({ message: "Invalid scope provided for the factory" });
};
const onCreateRoleGuard: TRoleScopeFactory["onCreateRoleGuard"] = async (dto) => {
const { permission } = await permissionService.getOrgPermission(
dto.permission.type,
dto.permission.id,
dto.permission.orgId,
dto.permission.authMethod,
dto.permission.orgId
);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Role);
};
const onUpdateRoleGuard: TRoleScopeFactory["onUpdateRoleGuard"] = async (dto) => {
const { permission } = await permissionService.getOrgPermission(
dto.permission.type,
dto.permission.id,
dto.permission.orgId,
dto.permission.authMethod,
dto.permission.orgId
);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Role);
};
const onDeleteRoleGuard: TRoleScopeFactory["onDeleteRoleGuard"] = async (dto) => {
const { permission } = await permissionService.getOrgPermission(
dto.permission.type,
dto.permission.id,
dto.permission.orgId,
dto.permission.authMethod,
dto.permission.orgId
);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Delete, OrgPermissionSubjects.Role);
};
const onListRoleGuard: TRoleScopeFactory["onListRoleGuard"] = async (dto) => {
const { permission } = await permissionService.getOrgPermission(
dto.permission.type,
dto.permission.id,
dto.permission.orgId,
dto.permission.authMethod,
dto.permission.orgId
);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Role);
};
const onGetRoleByIdGuard: TRoleScopeFactory["onGetRoleByIdGuard"] = async (dto) => {
const { permission } = await permissionService.getOrgPermission(
dto.permission.type,
dto.permission.id,
dto.permission.orgId,
dto.permission.authMethod,
dto.permission.orgId
);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Role);
};
const onGetRoleBySlugGuard: TRoleScopeFactory["onGetRoleBySlugGuard"] = async (dto) => {
const { permission } = await permissionService.getOrgPermission(
dto.permission.type,
dto.permission.id,
dto.permission.orgId,
dto.permission.authMethod,
dto.permission.orgId
);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Role);
};
return {
onCreateRoleGuard,
onUpdateRoleGuard,
onDeleteRoleGuard,
onListRoleGuard,
onGetRoleByIdGuard,
onGetRoleBySlugGuard,
getScopeField
};
};

View File

@@ -0,0 +1,109 @@
import { ForbiddenError } from "@casl/ability";
import { AccessScope, ActionProjectType } from "@app/db/schemas";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types";
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
import { BadRequestError } from "@app/lib/errors";
import { TRoleScopeFactory } from "../role-types";
type TProjectRoleScopeFactoryDep = {
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
};
export const newProjectRoleFactory = ({ permissionService }: TProjectRoleScopeFactoryDep): TRoleScopeFactory => {
const getScopeField: TRoleScopeFactory["getScopeField"] = (dto) => {
if (dto.scope === AccessScope.Project) {
return { key: "projectId" as const, value: dto.projectId };
}
throw new BadRequestError({ message: "Invalid scope provided for the factory" });
};
const onCreateRoleGuard: TRoleScopeFactory["onCreateRoleGuard"] = async (dto) => {
const scope = getScopeField(dto.scopeData);
const { permission } = await permissionService.getProjectPermission({
actor: dto.permission.type,
actorId: dto.permission.id,
actionProjectType: ActionProjectType.Any,
actorAuthMethod: dto.permission.authMethod,
projectId: scope.value,
actorOrgId: dto.permission.orgId
});
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Create, ProjectPermissionSub.Role);
};
const onUpdateRoleGuard: TRoleScopeFactory["onUpdateRoleGuard"] = async (dto) => {
const scope = getScopeField(dto.scopeData);
const { permission } = await permissionService.getProjectPermission({
actor: dto.permission.type,
actorId: dto.permission.id,
actionProjectType: ActionProjectType.Any,
actorAuthMethod: dto.permission.authMethod,
projectId: scope.value,
actorOrgId: dto.permission.orgId
});
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Role);
};
const onDeleteRoleGuard: TRoleScopeFactory["onDeleteRoleGuard"] = async (dto) => {
const scope = getScopeField(dto.scopeData);
const { permission } = await permissionService.getProjectPermission({
actor: dto.permission.type,
actorId: dto.permission.id,
actionProjectType: ActionProjectType.Any,
actorAuthMethod: dto.permission.authMethod,
projectId: scope.value,
actorOrgId: dto.permission.orgId
});
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Delete, ProjectPermissionSub.Role);
};
const onListRoleGuard: TRoleScopeFactory["onListRoleGuard"] = async (dto) => {
const scope = getScopeField(dto.scopeData);
const { permission } = await permissionService.getProjectPermission({
actor: dto.permission.type,
actorId: dto.permission.id,
actionProjectType: ActionProjectType.Any,
actorAuthMethod: dto.permission.authMethod,
projectId: scope.value,
actorOrgId: dto.permission.orgId
});
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Role);
};
const onGetRoleByIdGuard: TRoleScopeFactory["onGetRoleByIdGuard"] = async (dto) => {
const scope = getScopeField(dto.scopeData);
const { permission } = await permissionService.getProjectPermission({
actor: dto.permission.type,
actorId: dto.permission.id,
actionProjectType: ActionProjectType.Any,
actorAuthMethod: dto.permission.authMethod,
projectId: scope.value,
actorOrgId: dto.permission.orgId
});
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Role);
};
const onGetRoleBySlugGuard: TRoleScopeFactory["onGetRoleBySlugGuard"] = async (dto) => {
const scope = getScopeField(dto.scopeData);
const { permission } = await permissionService.getProjectPermission({
actor: dto.permission.type,
actorId: dto.permission.id,
actionProjectType: ActionProjectType.Any,
actorAuthMethod: dto.permission.authMethod,
projectId: scope.value,
actorOrgId: dto.permission.orgId
});
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Role);
};
return {
onCreateRoleGuard,
onUpdateRoleGuard,
onDeleteRoleGuard,
onListRoleGuard,
onGetRoleByIdGuard,
onGetRoleBySlugGuard,
getScopeField
};
};

View File

@@ -0,0 +1,10 @@
import { TDbClient } from "@app/db";
import { TableName } from "@app/db/schemas";
import { ormify } from "@app/lib/knex";
export type TRoleDALFactory = ReturnType<typeof roleDALFactory>;
export const roleDALFactory = (db: TDbClient) => {
const orm = ormify(db, TableName.Role);
return orm;
};

View File

@@ -0,0 +1,176 @@
import { AccessScope, TableName } from "@app/db/schemas";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types";
import { BadRequestError, NotFoundError } from "@app/lib/errors";
import { validateHandlebarTemplate } from "@app/lib/template/validate-handlebars";
import { newNamespaceRoleFactory } from "./namespace/namespace-role-factory";
import { newOrgRoleFactory } from "./org/org-role-factory";
import { newProjectRoleFactory } from "./project/project-role-factory";
import { TRoleDALFactory } from "./role-dal";
import {
TCreateRoleDTO,
TDeleteRoleDTO,
TGetRoleByIdDTO,
TGetRoleBySlugDTO,
TListRoleDTO,
TUpdateRoleDTO
} from "./role-types";
type TRoleServiceFactoryDep = {
roleDAL: TRoleDALFactory;
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission" | "getOrgPermission">;
};
export type TRoleServiceFactory = ReturnType<typeof roleServiceFactory>;
export const roleServiceFactory = ({ roleDAL, permissionService }: TRoleServiceFactoryDep) => {
const orgRoleFactory = newOrgRoleFactory({
permissionService
});
const projectRoleFactory = newProjectRoleFactory({
permissionService
});
const namespaceRoleFactory = newNamespaceRoleFactory({
permissionService
});
const scopeFactory = {
[AccessScope.Organization]: orgRoleFactory,
[AccessScope.Project]: projectRoleFactory,
[AccessScope.Namespace]: namespaceRoleFactory
};
const createRole = async (dto: TCreateRoleDTO) => {
const { data, scopeData } = dto;
const factory = scopeFactory[scopeData.scope];
await factory.onCreateRoleGuard(dto);
validateHandlebarTemplate("Role Creation", JSON.stringify(data.permissions || []), {
allowedExpressions: (val) => val.includes("identity.")
});
const scope = factory.getScopeField(scopeData);
const role = await roleDAL.create({
name: data.name,
description: data.description,
slug: data.slug,
permissions: data.permissions,
[scope.key]: scope.value
});
return { ...role, [scope.key]: scope.value };
};
const updateRole = async (dto: TUpdateRoleDTO) => {
const { data, scopeData } = dto;
const factory = scopeFactory[scopeData.scope];
const scope = factory.getScopeField(scopeData);
await factory.onUpdateRoleGuard(dto);
const existingRole = await roleDAL.findOne({
id: dto.selector.id,
[scope.key]: scope.value
});
if (!existingRole) throw new NotFoundError({ message: `Role with ${dto.selector.id} not found` });
if (data.slug) {
const existingSlug = await roleDAL.findOne({
slug: data.slug,
[scope.key]: scope.value
});
if (existingSlug && existingRole.id !== existingSlug.id)
throw new BadRequestError({ message: `Role with ${data.slug} not found` });
}
validateHandlebarTemplate("Role Update", JSON.stringify(data.permissions || []), {
allowedExpressions: (val) => val.includes("identity.")
});
const role = await roleDAL.updateById(existingRole.id, {
name: data?.name,
description: data?.description,
slug: data?.slug,
permissions: data?.permissions
});
return { ...role, [scope.key]: scope.value };
};
const deleteRole = async (dto: TDeleteRoleDTO) => {
const { scopeData } = dto;
const factory = scopeFactory[scopeData.scope];
const scope = factory.getScopeField(scopeData);
await factory.onDeleteRoleGuard(dto);
const existingRole = await roleDAL.findOne({
id: dto.selector.id,
[scope.key]: scope.value
});
if (!existingRole) throw new NotFoundError({ message: `Role with ${dto.selector.id} not found` });
const [role] = await roleDAL.delete({
id: existingRole.id,
[scope.key]: scope.value
});
return { ...role, [scope.key]: scope.value };
};
const listRoles = async (dto: TListRoleDTO) => {
const { scopeData } = dto;
const factory = scopeFactory[scopeData.scope];
await factory.onListRoleGuard(dto);
const scope = factory.getScopeField(scopeData);
const roles = await roleDAL.find(
{
[scope.key]: scope.value
},
{ limit: dto.data.limit, offset: dto.data.offset, sort: [[`${TableName.Role}.slug` as "slug", "asc"]] }
);
return { roles };
};
const getRoleById = async (dto: TGetRoleByIdDTO) => {
const { scopeData, selector } = dto;
const factory = scopeFactory[scopeData.scope];
await factory.onGetRoleByIdGuard(dto);
const scope = factory.getScopeField(scopeData);
const role = await roleDAL.findOne({
id: selector.id,
[scope.key]: scope.value
});
if (!role) throw new NotFoundError({ message: `Role with id ${dto.selector.id} not found` });
return { ...role, [scope.key]: scope.value };
};
const getRoleBySlug = async (dto: TGetRoleBySlugDTO) => {
const { scopeData, selector } = dto;
const factory = scopeFactory[scopeData.scope];
await factory.onGetRoleBySlugGuard(dto);
const scope = factory.getScopeField(scopeData);
const role = await roleDAL.findOne({
slug: selector.slug,
[scope.key]: scope.value
});
if (!role) throw new NotFoundError({ message: `Role with slug ${dto.selector.slug} not found` });
return { ...role, [scope.key]: scope.value };
};
return {
createRole,
updateRole,
deleteRole,
listRoles,
getRoleById,
getRoleBySlug
};
};

View File

@@ -0,0 +1,70 @@
import { AccessScopeData } from "@app/db/schemas";
import { OrgServiceActor } from "@app/lib/types";
export interface TRoleScopeFactory {
onCreateRoleGuard: (arg: TCreateRoleDTO) => Promise<void>;
onUpdateRoleGuard: (arg: TUpdateRoleDTO) => Promise<void>;
onDeleteRoleGuard: (arg: TDeleteRoleDTO) => Promise<void>;
onListRoleGuard: (arg: TListRoleDTO) => Promise<void>;
onGetRoleByIdGuard: (arg: TGetRoleByIdDTO) => Promise<void>;
onGetRoleBySlugGuard: (arg: TGetRoleBySlugDTO) => Promise<void>;
getScopeField: (scope: AccessScopeData) => { key: "orgId" | "namespaceId" | "projectId"; value: string };
}
export type TCreateRoleDTO = {
permission: OrgServiceActor;
scopeData: AccessScopeData;
data: {
name: string;
description?: string;
slug: string;
permissions: unknown;
};
};
export type TUpdateRoleDTO = {
permission: OrgServiceActor;
scopeData: AccessScopeData;
selector: {
id: string;
};
data: Partial<{
name: string;
description?: string;
slug: string;
permissions: unknown;
}>;
};
export type TListRoleDTO = {
permission: OrgServiceActor;
scopeData: AccessScopeData;
data: {
limit?: number;
offset?: number;
};
};
export type TDeleteRoleDTO = {
permission: OrgServiceActor;
scopeData: AccessScopeData;
selector: {
id: string;
};
};
export type TGetRoleByIdDTO = {
permission: OrgServiceActor;
scopeData: AccessScopeData;
selector: {
id: string;
};
};
export type TGetRoleBySlugDTO = {
permission: OrgServiceActor;
scopeData: AccessScopeData;
selector: {
slug: string;
};
};