feat: completed ap, org roles and project roles and project membership

This commit is contained in:
=
2025-10-09 23:58:10 +05:30
parent e232282bea
commit 4d68d48d89
13 changed files with 714 additions and 244 deletions
@@ -1,10 +1,21 @@
import { TDbClient } from "@app/db";
import { TableName } from "@app/db/schemas";
import { ormify } from "@app/lib/knex";
import { ormify, selectAllTableCols } from "@app/lib/knex";
export type TAdditionalPrivilegeDALFactory = ReturnType<typeof additionalPrivilegeDALFactory>;
export const additionalPrivilegeDALFactory = (db: TDbClient) => {
const orm = ormify(db, TableName.AdditionalPrivilege);
return orm;
const getMembershipById = async (privilegeId: string) => {
const doc = await db
.replicaNode()(TableName.AdditionalPrivilege)
.join(TableName.Membership, `${TableName.AdditionalPrivilege}.membershipId`, `${TableName.Membership}.id`)
.select(selectAllTableCols(TableName.Membership))
.where(`${TableName.AdditionalPrivilege}.id`, privilegeId)
.first();
return doc;
};
return { ...orm, getMembershipById };
};
@@ -16,6 +16,7 @@ import {
TCreateAdditionalPrivilegesDTO,
TDeleteAdditionalPrivilegesDTO,
TGetAdditionalPrivilegesByIdDTO,
TGetAdditionalPrivilegesByNameDTO,
TListAdditionalPrivilegesDTO,
TUpdateAdditionalPrivilegesDTO
} from "./additional-privilege-types";
@@ -71,7 +72,9 @@ export const additionalPrivilegeServiceFactory = ({
permissions: JSON.stringify(packRules(data.permissions as RawRule[]))
});
return { additionalPrivilege: { ...additionalPrivilege, permissions: data.permissions } };
return {
additionalPrivilege: { ...additionalPrivilege, permissions: unpackPermissions(additionalPrivilege.permissions) }
};
}
if (!data.temporaryAccessStartTime || !data.temporaryRange) {
@@ -92,7 +95,9 @@ export const additionalPrivilegeServiceFactory = ({
temporaryRange: data.temporaryRange
});
return { additionalPrivilege: { ...additionalPrivilege, permissions: data.permissions } };
return {
additionalPrivilege: { ...additionalPrivilege, permissions: unpackPermissions(additionalPrivilege.permissions) }
};
};
const updateAdditionalPrivilege = async (dto: TUpdateAdditionalPrivilegesDTO) => {
@@ -169,7 +174,7 @@ export const additionalPrivilegeServiceFactory = ({
const getAdditionalPrivilegeById = async (dto: TGetAdditionalPrivilegesByIdDTO) => {
const { scopeData, selector } = dto;
const factory = scopeFactory[scopeData.scope];
const { membershipId } = await factory.onListAdditionalPrivilegesGuard(dto);
const { membershipId } = await factory.onGetAdditionalPrivilegesByIdGuard(dto);
const additionalPrivilege = await additionalPrivilegeDAL.findOne({
id: selector.id,
membershipId
@@ -182,6 +187,22 @@ export const additionalPrivilegeServiceFactory = ({
};
};
const getAdditionalPrivilegeByName = async (dto: TGetAdditionalPrivilegesByNameDTO) => {
const { scopeData, selector } = dto;
const factory = scopeFactory[scopeData.scope];
const { membershipId } = await factory.onGetAdditionalPrivilegesByIdGuard(dto);
const additionalPrivilege = await additionalPrivilegeDAL.findOne({
name: selector.name,
membershipId
});
if (!additionalPrivilege)
throw new NotFoundError({ message: `Additional privilege with name ${selector.name} doesn't exist` });
return {
additionalPrivilege: { ...additionalPrivilege, permissions: unpackPermissions(additionalPrivilege.permissions) }
};
};
const listAdditionalPrivileges = async (dto: TListAdditionalPrivilegesDTO) => {
const { scopeData } = dto;
const factory = scopeFactory[scopeData.scope];
@@ -203,6 +224,7 @@ export const additionalPrivilegeServiceFactory = ({
updateAdditionalPrivilege,
deleteAdditionalPrivilege,
getAdditionalPrivilegeById,
getAdditionalPrivilegeByName,
listAdditionalPrivileges
};
};
@@ -6,7 +6,9 @@ export interface TAdditionalPrivilegesScopeFactory {
onUpdateAdditionalPrivilegesGuard: (arg: TUpdateAdditionalPrivilegesDTO) => Promise<{ membershipId: string }>;
onDeleteAdditionalPrivilegesGuard: (arg: TDeleteAdditionalPrivilegesDTO) => Promise<{ membershipId: string }>;
onListAdditionalPrivilegesGuard: (arg: TListAdditionalPrivilegesDTO) => Promise<{ membershipId: string }>;
onGetAdditionalPrivilegesByIdGuard: (arg: TGetAdditionalPrivilegesByIdDTO) => Promise<{ membershipId: string }>;
onGetAdditionalPrivilegesByIdGuard: (
arg: TGetAdditionalPrivilegesByIdDTO | TGetAdditionalPrivilegesByNameDTO
) => Promise<{ membershipId: string }>;
getScopeField: (scope: AccessScopeData) => { key: "orgId" | "namespaceId" | "projectId"; value: string };
}
@@ -71,3 +73,13 @@ export type TGetAdditionalPrivilegesByIdDTO = {
actorType: MembershipActors;
};
};
export type TGetAdditionalPrivilegesByNameDTO = {
permission: OrgServiceActor;
scopeData: AccessScopeData;
selector: {
name: string;
actorId: string;
actorType: MembershipActors;
};
};
@@ -1,88 +1,118 @@
import { AccessScope } from "@app/db/schemas";
import { NotFoundError } from "@app/lib/errors";
import { TMembershipDALFactory } from "../membership/membership-dal";
import { TProjectDALFactory } from "../project/project-dal";
import { AccessScope, AccessScopeData } from "@app/db/schemas";
import { TAdditionalPrivilegeDALFactory } from "../additional-privilege/additional-privilege-dal";
type TConvertorServiceFactoryDep = {
projectDAL: Pick<TProjectDALFactory, "findOne">;
membershipDAL: Pick<TMembershipDALFactory, "findOne">;
additionalPrivilegeDAL: Pick<TAdditionalPrivilegeDALFactory, "getMembershipById" | "findOne">;
};
export type TConvertorServiceFactory = ReturnType<typeof convertorServiceFactory>;
export const convertorServiceFactory = ({ projectDAL, membershipDAL }: TConvertorServiceFactoryDep) => {
const $getScopeDatabaseFields = (scopeData: AccessScopeData) => {
if (scopeData.scope === AccessScope.Organization) {
return { scopeOrgId: scopeData.orgId };
}
if (scopeData.scope === AccessScope.Namespace) {
return { scopeNamespaceId: scopeData.namespaceId, scopeOrgId: scopeData.orgId };
}
return { scopeProjectId: scopeData.projectId, scopeOrgId: scopeData.orgId };
};
export const convertorServiceFactory = ({
projectDAL,
membershipDAL,
additionalPrivilegeDAL
}: TConvertorServiceFactoryDep) => {
const projectSlugToId = async (dto: { slug: string; orgId: string }) => {
const project = await projectDAL.findOne({
orgId: dto.orgId,
slug: dto.slug
});
if (!project) throw new NotFoundError({ message: `Project with slug ${slug} not found` });
if (!project) throw new NotFoundError({ message: `Project with slug ${dto.slug} not found` });
return project;
};
const userMembershipIdToUserId = async (scopeData: AccessScopeData, membershipId: string) => {
const dbFields = $getScopeDatabaseFields(scopeData);
const userMembershipIdToUserId = async (membershipId: string, scope: AccessScope, orgId: string) => {
const membership = await membershipDAL.findOne({
scope: scopeData.scope,
scope,
id: membershipId,
...dbFields,
scopeOrgId: scopeData.orgId
scopeOrgId: orgId
});
if (!membership || !membership.actorUserId) {
throw new NotFoundError({ message: `Membership with id ${membershipId} not found` });
}
return membership.actorUserId;
return { userId: membership.actorUserId, membership };
};
const groupMembershipIdToGroupId = async (scopeData: AccessScopeData, membershipId: string) => {
const dbFields = $getScopeDatabaseFields(scopeData);
const groupMembershipIdToGroupId = async (membershipId: string, scope: AccessScope, orgId: string) => {
const membership = await membershipDAL.findOne({
scope: scopeData.scope,
scope,
id: membershipId,
...dbFields,
scopeOrgId: scopeData.orgId
scopeOrgId: orgId
});
if (!membership || !membership.actorGroupId) {
throw new NotFoundError({ message: `Membership with id ${membershipId} not found` });
}
return membership.actorGroupId;
return { groupId: membership.actorGroupId, membership };
};
const identityMembershipIdToGroupId = async (scopeData: AccessScopeData, membershipId: string) => {
const dbFields = $getScopeDatabaseFields(scopeData);
const identityMembershipIdToIdentityId = async (membershipId: string, scope: AccessScope, orgId: string) => {
const membership = await membershipDAL.findOne({
scope: scopeData.scope,
scope,
id: membershipId,
...dbFields,
scopeOrgId: scopeData.orgId
scopeOrgId: orgId
});
if (!membership || !membership.actorIdentityId) {
throw new NotFoundError({ message: `Membership with id ${membershipId} not found` });
}
return membership.actorIdentityId;
return { identityId: membership.actorIdentityId, membership };
};
const identityIdToMembershipId = async (identityId: string, scope: AccessScope, scopeId: string) => {
let fieldName = "scopeOrgId";
if (scope === AccessScope.Project) {
fieldName = "scopeProjectId";
} else if (scope === AccessScope.Namespace) {
fieldName = "scopeNamespaceId";
}
const membership = await membershipDAL.findOne({
scope,
actorIdentityId: identityId,
[fieldName]: scopeId
});
if (!membership) {
throw new NotFoundError({ message: `Identity with id ${identityId} not found` });
}
return { membershipId: membership.id, membership };
};
const additionalPrivilegeIdToMembershipId = async (privilegeId: string, scope: AccessScope, orgId: string) => {
const membership = await additionalPrivilegeDAL.getMembershipById(privilegeId);
if (!membership || membership.scope !== scope || membership.scopeOrgId !== orgId) {
throw new NotFoundError({ message: `Privilege with id ${privilegeId} not found` });
}
return { membership, membershipId: membership.id };
};
const additionalPrivilegeNameToId = async (privilegeName: string, membershipId: string) => {
const privilege = await additionalPrivilegeDAL.findOne({
name: privilegeName,
membershipId
});
if (!privilege) {
throw new NotFoundError({ message: `Privilege with slug ${privilegeName} not found` });
}
return { privilegeId: privilege.id, privilege };
};
return {
projectSlugToId,
userMembershipIdToUserId,
groupMembershipIdToGroupId,
identityMembershipIdToGroupId
identityMembershipIdToIdentityId,
additionalPrivilegeIdToMembershipId,
additionalPrivilegeNameToId,
identityIdToMembershipId
};
};
@@ -274,7 +274,7 @@ export const membershipUserServiceFactory = ({
const existingMembership = await membershipUserDAL.findOne({
scope: scopeData.scope,
...scopeDatabaseFields,
actorUserId: dto.selector.actorId
actorUserId: dto.selector.userId
});
if (!existingMembership)
throw new BadRequestError({
@@ -338,31 +338,31 @@ export const membershipUserServiceFactory = ({
},
tx
);
await membershipRoleDAL.insertMany(roleDocs, tx);
return doc;
const insertedRoles = await membershipRoleDAL.insertMany(roleDocs, tx);
return { ...doc, roles: insertedRoles };
});
return { memberships: membershipDoc };
return { membership: membershipDoc };
};
const deleteMembership = async (dto: TDeleteMembershipUserDTO) => {
const { scopeData } = dto;
const factory = scopeFactory[scopeData.scope];
const { actorIdOfDeletor } = await factory.onDeleteMembershipUserGuard(dto);
await factory.onDeleteMembershipUserGuard(dto);
const scopeDatabaseFields = factory.getScopeDatabaseFields(dto.scopeData);
const existingMembership = await membershipUserDAL.findOne({
scope: scopeData.scope,
...scopeDatabaseFields,
actorUserId: dto.selector.actorId
actorUserId: dto.selector.userId
});
if (!existingMembership)
throw new BadRequestError({
message: "User doesn't have membership"
});
if (existingMembership.actorUserId === actorIdOfDeletor)
if (existingMembership.actorUserId === dto.permission.id)
throw new BadRequestError({
message: "You can delete you own membership"
});
@@ -403,7 +403,7 @@ export const membershipUserServiceFactory = ({
await factory.onGetMembershipUserByUserIdGuard(dto);
const membership = await membershipUserDAL.getUserById({
scopeData,
userId: selector.actorId
userId: selector.userId
});
if (!membership) throw new NotFoundError({ message: `User membership not found` });
@@ -6,7 +6,7 @@ export interface TMembershipUserScopeFactory {
onCreateMembershipComplete: (arg: { id: string; email: string }[]) => Promise<void>;
onUpdateMembershipUserGuard: (arg: TUpdateMembershipUserDTO) => Promise<void>;
onDeleteMembershipUserGuard: (arg: TDeleteMembershipUserDTO) => Promise<{ actorIdOfDeletor: string }>;
onDeleteMembershipUserGuard: (arg: TDeleteMembershipUserDTO) => Promise<void>;
onListMembershipUserGuard: (arg: TListMembershipUserDTO) => Promise<void>;
onGetMembershipUserByUserIdGuard: (arg: TGetMembershipUserByUserIdDTO) => Promise<void>;
getScopeField: (scope: AccessScopeData) => { key: "orgId" | "namespaceId" | "projectId"; value: string };
@@ -37,7 +37,7 @@ export type TUpdateMembershipUserDTO = {
permission: OrgServiceActor;
scopeData: AccessScopeData;
selector: {
actorId: string;
userId: string;
};
data: {
isActive?: boolean;
@@ -67,7 +67,7 @@ export type TDeleteMembershipUserDTO = {
permission: OrgServiceActor;
scopeData: AccessScopeData;
selector: {
actorId: string;
userId: string;
};
};
@@ -75,6 +75,6 @@ export type TGetMembershipUserByUserIdDTO = {
permission: OrgServiceActor;
scopeData: AccessScopeData;
selector: {
actorId: string;
userId: string;
};
};
@@ -63,4 +63,4 @@ export const newNamespaceMembershipUserFactory = (
getScopeDatabaseFields,
isCustomRole
};
};
};
@@ -67,7 +67,6 @@ export const newOrgMembershipUserFactory = ({
dto.permission.orgId
);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Delete, OrgPermissionSubjects.Member);
return { actorIdOfDeletor: dto.permission.id };
};
const onListMembershipUserGuard: TMembershipUserScopeFactory["onListMembershipUserGuard"] = async (dto) => {
@@ -134,7 +134,6 @@ export const newProjectMembershipUserFactory = ({
actorOrgId: dto.permission.orgId
});
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionMemberActions.Delete, ProjectPermissionSub.Member);
return { actorIdOfDeletor: dto.permission.id };
};
const onListMembershipUserGuard: TMembershipUserScopeFactory["onListMembershipUserGuard"] = async (dto) => {