feat: added root org identity link functionality

This commit is contained in:
=
2025-10-20 18:58:22 +05:30
parent 545ea4e27c
commit b7644c9294
26 changed files with 753 additions and 82 deletions
@@ -91,6 +91,7 @@ export const membershipIdentityDALFactory = (db: TDbClient) => {
.select(
db.ref("name").withSchema(TableName.Identity).as("identityName"),
db.ref("id").withSchema(TableName.Identity).as("identityId"),
db.ref("orgId").withSchema(TableName.Identity).as("identityOrgId"),
db.ref("hasDeleteProtection").withSchema(TableName.Identity).as("identityHasDeleteProtection"),
db.ref("slug").withSchema(TableName.Role).as("roleSlug"),
@@ -132,6 +133,7 @@ export const membershipIdentityDALFactory = (db: TDbClient) => {
parentMapper: (el) => {
const {
identityId: actorIdentityId,
identityOrgId,
identityHasDeleteProtection,
identityName,
uaId,
@@ -153,6 +155,7 @@ export const membershipIdentityDALFactory = (db: TDbClient) => {
name: identityName,
id: actorIdentityId,
hasDeleteProtection: identityHasDeleteProtection,
identityOrgId,
authMethods: buildAuthMethods({
uaId,
awsId,
@@ -353,5 +356,34 @@ export const membershipIdentityDALFactory = (db: TDbClient) => {
}
};
return { ...orm, findIdentities, getIdentityById };
// this right nwo only support sub organization
const listAvailableIdentities = async (orgId: string, rootOrgId: string) => {
try {
const usersConnectedToOrg = db
.replicaNode()(TableName.Membership)
.whereNotNull(`${TableName.Membership}.actorIdentityId`)
.where(`${TableName.Membership}.scope`, AccessScope.Organization)
.where(`${TableName.Membership}.scopeOrgId`, orgId)
.select("actorIdentityId");
const docs = await db
.replicaNode()(TableName.Membership)
.join(TableName.Identity, `${TableName.Identity}.id`, `${TableName.Membership}.actorIdentityId`)
.where(`${TableName.Membership}.scope`, AccessScope.Organization)
.whereNotNull(`${TableName.Membership}.actorIdentityId`)
.where(`${TableName.Membership}.scopeOrgId`, rootOrgId)
.whereNotIn(`${TableName.Membership}.actorIdentityId`, usersConnectedToOrg)
.select(
db.ref("id").withSchema(TableName.Identity),
db.ref("name").withSchema(TableName.Identity),
db.ref("hasDeleteProtection").withSchema(TableName.Identity)
);
return docs;
} catch (error) {
throw new DatabaseError({ error, name: "ListAvailableUsers" });
}
};
return { ...orm, findIdentities, getIdentityById, listAvailableIdentities };
};
@@ -20,6 +20,7 @@ import {
import { newNamespaceMembershipIdentityFactory } from "./namespace/namespace-membership-identity-factory";
import { newOrgMembershipIdentityFactory } from "./org/org-membership-identity-factory";
import { newProjectMembershipIdentityFactory } from "./project/project-membership-identity-factory";
import { TIdentityDALFactory } from "../identity/identity-dal";
type TMembershipIdentityServiceFactoryDep = {
membershipIdentityDAL: TMembershipIdentityDALFactory;
@@ -31,6 +32,7 @@ type TMembershipIdentityServiceFactoryDep = {
>;
orgDAL: Pick<TOrgDALFactory, "findById">;
additionalPrivilegeDAL: Pick<TAdditionalPrivilegeDALFactory, "delete">;
identityDAL: Pick<TIdentityDALFactory, "findById">;
};
export type TMembershipIdentityServiceFactory = ReturnType<typeof membershipIdentityServiceFactory>;
@@ -41,12 +43,14 @@ export const membershipIdentityServiceFactory = ({
membershipRoleDAL,
permissionService,
orgDAL,
additionalPrivilegeDAL
additionalPrivilegeDAL,
identityDAL
}: TMembershipIdentityServiceFactoryDep) => {
const scopeFactory = {
[AccessScope.Organization]: newOrgMembershipIdentityFactory({
orgDAL,
permissionService
permissionService,
identityDAL
}),
[AccessScope.Project]: newProjectMembershipIdentityFactory({
membershipIdentityDAL,
@@ -305,7 +309,7 @@ export const membershipIdentityServiceFactory = ({
[SearchResourceOperators.$contains]: dto.data.identityName
}
: undefined,
role: dto.data.roles.length
role: dto.data?.roles?.length
? {
[SearchResourceOperators.$in]: dto.data.roles
}
@@ -329,11 +333,29 @@ export const membershipIdentityServiceFactory = ({
return membership;
};
const listAvailableIdentities = async (dto: TListMembershipIdentityDTO) => {
const { scopeData } = dto;
const factory = scopeFactory[scopeData.scope];
await factory.onListMembershipIdentityGuard(dto);
const organizationDetails = await orgDAL.findById(dto.scopeData.orgId);
if (!organizationDetails.rootOrgId) return { identities: [] };
const identities = await membershipIdentityDAL.listAvailableIdentities(
organizationDetails.id,
organizationDetails.rootOrgId
);
return { identities };
};
return {
createMembership,
updateMembership,
deleteMembership,
listMemberships,
getMembershipByIdentityId
getMembershipByIdentityId,
listAvailableIdentities
};
};
@@ -54,14 +54,11 @@ export type TUpdateMembershipIdentityDTO = {
export type TListMembershipIdentityDTO = {
permission: OrgServiceActor;
scopeData: AccessScopeData;
selector: {
identityId: string;
};
data: {
limit?: number;
offset?: number;
identityName?: string;
roles: string[];
roles?: string[];
};
};
@@ -12,15 +12,18 @@ import { TOrgDALFactory } from "@app/services/org/org-dal";
import { isCustomOrgRole } from "@app/services/org/org-role-fns";
import { TMembershipIdentityScopeFactory } from "../membership-identity-types";
import { TIdentityDALFactory } from "@app/services/identity/identity-dal";
type TOrgMembershipIdentityScopeFactoryDep = {
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission" | "getOrgPermissionByRoles">;
orgDAL: Pick<TOrgDALFactory, "findById">;
identityDAL: Pick<TIdentityDALFactory, "findById">;
};
export const newOrgMembershipIdentityFactory = ({
permissionService,
orgDAL
orgDAL,
identityDAL
}: TOrgMembershipIdentityScopeFactoryDep): TMembershipIdentityScopeFactory => {
const getScopeField: TMembershipIdentityScopeFactory["getScopeField"] = (dto) => {
if (dto.scope === AccessScope.Organization) {
@@ -38,12 +41,53 @@ export const newOrgMembershipIdentityFactory = ({
const isCustomRole: TMembershipIdentityScopeFactory["isCustomRole"] = (role: string) => isCustomOrgRole(role);
const onCreateMembershipIdentityGuard: TMembershipIdentityScopeFactory["onCreateMembershipIdentityGuard"] =
async () => {
throw new BadRequestError({
message: "Organization membership cannot be created for organization scoped identity"
});
};
const onCreateMembershipIdentityGuard: TMembershipIdentityScopeFactory["onCreateMembershipIdentityGuard"] = async (
dto
) => {
const { permission } = await permissionService.getOrgPermission({
actor: dto.permission.type,
actorId: dto.permission.id,
orgId: dto.permission.orgId,
actorAuthMethod: dto.permission.authMethod,
actorOrgId: dto.permission.orgId,
scope: OrganizationActionScope.ChildOrganization
});
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Create, OrgPermissionSubjects.Identity);
const identityDetails = await identityDAL.findById(dto.data.identityId);
if (identityDetails.orgId !== dto.permission.rootOrgId) {
throw new BadRequestError({ message: "Only identites from parent organization can be invited" });
}
const permissionRoles = await permissionService.getOrgPermissionByRoles(
dto.data.roles.map((el) => el.role),
dto.permission.orgId
);
const { shouldUseNewPrivilegeSystem } = await orgDAL.findById(dto.permission.orgId);
for (const permissionRole of permissionRoles) {
if (permissionRole?.role?.name !== OrgMembershipRole.NoAccess) {
const permissionBoundary = validatePrivilegeChangeOperation(
shouldUseNewPrivilegeSystem,
OrgPermissionIdentityActions.GrantPrivileges,
OrgPermissionSubjects.Identity,
permission,
permissionRole.permission
);
if (!permissionBoundary.isValid)
throw new PermissionBoundaryError({
message: constructPermissionErrorMessage(
"Failed to update identity org membership",
shouldUseNewPrivilegeSystem,
OrgPermissionIdentityActions.GrantPrivileges,
OrgPermissionSubjects.Identity
),
details: { missingPermissions: permissionBoundary.missingPermissions }
});
}
}
};
const onUpdateMembershipIdentityGuard: TMembershipIdentityScopeFactory["onUpdateMembershipIdentityGuard"] = async (
dto
@@ -87,12 +131,29 @@ export const newOrgMembershipIdentityFactory = ({
}
};
const onDeleteMembershipIdentityGuard: TMembershipIdentityScopeFactory["onDeleteMembershipIdentityGuard"] =
async () => {
throw new BadRequestError({
message: "Organization membership cannot be deleted for organization scoped identity"
});
};
const onDeleteMembershipIdentityGuard: TMembershipIdentityScopeFactory["onDeleteMembershipIdentityGuard"] = async (
dto
) => {
const { permission } = await permissionService.getOrgPermission({
actor: dto.permission.type,
actorId: dto.permission.id,
orgId: dto.permission.orgId,
actorAuthMethod: dto.permission.authMethod,
actorOrgId: dto.permission.orgId,
scope: OrganizationActionScope.ChildOrganization
});
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Create, OrgPermissionSubjects.Identity);
const identityDetails = await identityDAL.findById(dto.selector.identityId);
if (identityDetails.orgId !== dto.permission.rootOrgId) {
throw new BadRequestError({ message: "Only identites from parent organization can do this operation" });
}
if (identityDetails.orgId === dto.permission.orgId) {
throw new BadRequestError({ message: "Identity cannot exist as orphan" });
}
};
const onListMembershipIdentityGuard: TMembershipIdentityScopeFactory["onListMembershipIdentityGuard"] = async (
dto