import { createMongoAbility, MongoAbility, RawRuleOf } from "@casl/ability"; import { PackRule, unpackRules } from "@casl/ability/extra"; import { MongoQuery } from "@ucast/mongo2js"; import { OrgMembershipRole, ProjectMembershipRole, ServiceTokenScopes, TIdentityProjectMemberships, TProjectMemberships } from "@app/db/schemas"; import { conditionsMatcher } from "@app/lib/casl"; import { BadRequestError, UnauthorizedError } from "@app/lib/errors"; import { ActorType } from "@app/services/auth/auth-type"; import { TOrgRoleDALFactory } from "@app/services/org/org-role-dal"; import { TProjectRoleDALFactory } from "@app/services/project-role/project-role-dal"; import { TServiceTokenDALFactory } from "@app/services/service-token/service-token-dal"; import { orgAdminPermissions, orgMemberPermissions, orgNoAccessPermissions, OrgPermissionSet } from "./org-permission"; import { TPermissionDALFactory } from "./permission-dal"; import { TBuildProjectPermissionDTO } from "./permission-types"; import { buildServiceTokenProjectPermission, projectAdminPermissions, projectMemberPermissions, projectNoAccessPermissions, ProjectPermissionSet, projectViewerPermission } from "./project-permission"; type TPermissionServiceFactoryDep = { orgRoleDAL: Pick; projectRoleDAL: Pick; serviceTokenDAL: Pick; permissionDAL: TPermissionDALFactory; }; export type TPermissionServiceFactory = ReturnType; export const permissionServiceFactory = ({ permissionDAL, orgRoleDAL, projectRoleDAL, serviceTokenDAL }: TPermissionServiceFactoryDep) => { const buildOrgPermission = (role: string, permission?: unknown) => { switch (role) { case OrgMembershipRole.Admin: return orgAdminPermissions; case OrgMembershipRole.Member: return orgMemberPermissions; case OrgMembershipRole.NoAccess: return orgNoAccessPermissions; case OrgMembershipRole.Custom: return createMongoAbility( unpackRules>>( permission as PackRule>>[] ), { conditionsMatcher } ); default: throw new BadRequestError({ name: "OrgRoleInvalid", message: "Org role not found" }); } }; const buildProjectPermission = (projectUserRoles: TBuildProjectPermissionDTO) => { const rules = projectUserRoles .map(({ role, permissions }) => { switch (role) { case ProjectMembershipRole.Admin: return projectAdminPermissions; case ProjectMembershipRole.Member: return projectMemberPermissions; case ProjectMembershipRole.Viewer: return projectViewerPermission; case ProjectMembershipRole.NoAccess: return projectNoAccessPermissions; case ProjectMembershipRole.Custom: { return unpackRules>>( permissions as PackRule>>[] ); } default: throw new BadRequestError({ name: "ProjectRoleInvalid", message: "Project role not found" }); } }) .reduce((curr, prev) => prev.concat(curr), []); return createMongoAbility(rules, { conditionsMatcher }); }; /* * Get user permission in an organization * */ const getUserOrgPermission = async (userId: string, orgId: string, userOrgId?: string) => { const membership = await permissionDAL.getOrgPermission(userId, orgId); if (!membership) throw new UnauthorizedError({ name: "User not in org" }); if (membership.role === OrgMembershipRole.Custom && !membership.permissions) { throw new BadRequestError({ name: "Custom permission not found" }); } if (membership.orgAuthEnforced && membership.orgId !== userOrgId) { throw new BadRequestError({ name: "Cannot access org-scoped resource" }); } return { permission: buildOrgPermission(membership.role, membership.permissions), membership }; }; const getIdentityOrgPermission = async (identityId: string, orgId: string) => { const membership = await permissionDAL.getOrgIdentityPermission(identityId, orgId); if (!membership) throw new UnauthorizedError({ name: "Identity not in org" }); if (membership.role === OrgMembershipRole.Custom && !membership.permissions) { throw new BadRequestError({ name: "Custom permission not found" }); } return { permission: buildOrgPermission(membership.role, membership.permissions), membership }; }; const getOrgPermission = async (type: ActorType, id: string, orgId: string, actorOrgId?: string) => { switch (type) { case ActorType.USER: return getUserOrgPermission(id, orgId, actorOrgId); case ActorType.IDENTITY: return getIdentityOrgPermission(id, orgId); default: throw new UnauthorizedError({ message: "Permission not defined", name: "Get org permission" }); } }; // instead of actor type this will fetch by role slug. meaning it can be the pre defined slugs like // admin member or user defined ones like biller etc const getOrgPermissionByRole = async (role: string, orgId: string) => { const isCustomRole = !Object.values(OrgMembershipRole).includes(role as OrgMembershipRole); if (isCustomRole) { const orgRole = await orgRoleDAL.findOne({ slug: role, orgId }); if (!orgRole) throw new BadRequestError({ message: "Role not found" }); return { permission: buildOrgPermission(OrgMembershipRole.Custom, orgRole.permissions), role: orgRole }; } return { permission: buildOrgPermission(role, []) }; }; // user permission for a project in an organization const getUserProjectPermission = async ( userId: string, projectId: string, userOrgId?: string ): Promise> => { const userProjectPermission = await permissionDAL.getProjectPermission(userId, projectId); if (!userProjectPermission) throw new UnauthorizedError({ name: "User not in project" }); if ( userProjectPermission.roles.some(({ role, permissions }) => role === ProjectMembershipRole.Custom && !permissions) ) { throw new BadRequestError({ name: "Custom permission not found" }); } if (userProjectPermission.orgAuthEnforced && userProjectPermission.orgId !== userOrgId) { throw new BadRequestError({ name: "Cannot access org-scoped resource" }); } return { permission: buildProjectPermission(userProjectPermission.roles), membership: userProjectPermission, hasRole: (role: string) => userProjectPermission.roles.findIndex( ({ role: slug, customRoleSlug }) => role === slug || slug === customRoleSlug ) !== -1 }; }; const getIdentityProjectPermission = async ( identityId: string, projectId: string ): Promise> => { const identityProjectPermission = await permissionDAL.getProjectIdentityPermission(identityId, projectId); if (!identityProjectPermission) throw new UnauthorizedError({ name: "Identity not in project" }); if ( identityProjectPermission.roles.some( ({ role, permissions }) => role === ProjectMembershipRole.Custom && !permissions ) ) { throw new BadRequestError({ name: "Custom permission not found" }); } return { permission: buildProjectPermission(identityProjectPermission.roles), membership: identityProjectPermission, hasRole: (role: string) => identityProjectPermission.roles.findIndex( ({ role: slug, customRoleSlug }) => role === slug || slug === customRoleSlug ) !== -1 }; }; const getServiceTokenProjectPermission = async (serviceTokenId: string, projectId: string) => { const serviceToken = await serviceTokenDAL.findById(serviceTokenId); if (!serviceToken) throw new BadRequestError({ message: "Service token not found" }); if (serviceToken.projectId !== projectId) throw new UnauthorizedError({ message: "Failed to find service authorization for given project" }); const scopes = ServiceTokenScopes.parse(serviceToken.scopes || []); return { permission: buildServiceTokenProjectPermission(scopes, serviceToken.permissions), membership: undefined }; }; type TProjectPermissionRT = T extends ActorType.SERVICE ? { permission: MongoAbility; membership: undefined; hasRole: (arg: string) => boolean; } // service token doesn't have both membership and roles : { permission: MongoAbility; membership: (T extends ActorType.USER ? TProjectMemberships : TIdentityProjectMemberships) & { orgAuthEnforced: boolean | null | undefined; orgId: string; roles: Array<{ role: string }>; }; hasRole: (role: string) => boolean; }; const getProjectPermission = async ( type: T, id: string, projectId: string, actorOrgId?: string ): Promise> => { switch (type) { case ActorType.USER: return getUserProjectPermission(id, projectId, actorOrgId) as Promise>; case ActorType.SERVICE: return getServiceTokenProjectPermission(id, projectId) as Promise>; case ActorType.IDENTITY: return getIdentityProjectPermission(id, projectId) as Promise>; default: throw new UnauthorizedError({ message: "Permission not defined", name: "Get project permission" }); } }; const getProjectPermissionByRole = async (role: string, projectId: string) => { const isCustomRole = !Object.values(ProjectMembershipRole).includes(role as ProjectMembershipRole); if (isCustomRole) { const projectRole = await projectRoleDAL.findOne({ slug: role, projectId }); if (!projectRole) throw new BadRequestError({ message: "Role not found" }); return { permission: buildProjectPermission([ { role: ProjectMembershipRole.Custom, permissions: projectRole.permissions } ]), role: projectRole }; } return { permission: buildProjectPermission([{ role, permissions: [] }]) }; }; return { getUserOrgPermission, getOrgPermission, getUserProjectPermission, getProjectPermission, getOrgPermissionByRole, getProjectPermissionByRole, buildOrgPermission, buildProjectPermission }; };