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
+4 -2
View File
@@ -49,7 +49,8 @@ export const registerSubOrgRouter = async (server: FastifyZodProvider) => {
type: req.permission.type, type: req.permission.type,
authMethod: req.permission.authMethod, authMethod: req.permission.authMethod,
orgId: req.permission.orgId, orgId: req.permission.orgId,
parentOrgId: req.permission.parentOrgId parentOrgId: req.permission.parentOrgId,
rootOrgId: req.permission.rootOrgId
} }
}); });
@@ -107,7 +108,8 @@ export const registerSubOrgRouter = async (server: FastifyZodProvider) => {
type: req.permission.type, type: req.permission.type,
authMethod: req.permission.authMethod, authMethod: req.permission.authMethod,
orgId: req.permission.orgId, orgId: req.permission.orgId,
parentOrgId: req.permission.orgId parentOrgId: req.permission.orgId,
rootOrgId: req.permission.rootOrgId
}, },
data: { data: {
limit: req.query.limit, limit: req.query.limit,
@@ -285,19 +285,20 @@ export const licenseServiceFactory = ({
}; };
const updateSubscriptionOrgMemberCount = async (orgId: string, tx?: Knex) => { const updateSubscriptionOrgMemberCount = async (orgId: string, tx?: Knex) => {
if (instanceType === InstanceType.Cloud) { const org = await orgDAL.findOrgById(orgId);
const org = await orgDAL.findOrgById(orgId); if (!org) throw new NotFoundError({ message: `Organization with ID '${orgId}' not found` });
if (!org) throw new NotFoundError({ message: `Organization with ID '${orgId}' not found` });
const quantity = await licenseDAL.countOfOrgMembers(orgId, tx); const rootOrgId = org.rootOrgId || org.id;
const quantityIdentities = await licenseDAL.countOrgUsersAndIdentities(orgId, tx); if (instanceType === InstanceType.Cloud) {
const quantity = await licenseDAL.countOfOrgMembers(rootOrgId, tx);
const quantityIdentities = await licenseDAL.countOrgUsersAndIdentities(rootOrgId, tx);
if (org?.customerId) { if (org?.customerId) {
await licenseServerCloudApi.request.patch(`/api/license-server/v1/customers/${org.customerId}/cloud-plan`, { await licenseServerCloudApi.request.patch(`/api/license-server/v1/customers/${org.customerId}/cloud-plan`, {
quantity, quantity,
quantityIdentities quantityIdentities
}); });
} }
await keyStore.deleteItem(FEATURE_CACHE_KEY(orgId)); await keyStore.deleteItem(FEATURE_CACHE_KEY(rootOrgId));
} else if (instanceType === InstanceType.EnterpriseOnPrem) { } else if (instanceType === InstanceType.EnterpriseOnPrem) {
const usedSeats = await licenseDAL.countOfOrgMembers(null, tx); const usedSeats = await licenseDAL.countOfOrgMembers(null, tx);
const usedIdentitySeats = await licenseDAL.countOrgUsersAndIdentities(null, tx); const usedIdentitySeats = await licenseDAL.countOrgUsersAndIdentities(null, tx);
@@ -308,7 +309,7 @@ export const licenseServiceFactory = ({
usedIdentitySeats usedIdentitySeats
}); });
} }
await refreshPlan(orgId); await refreshPlan(rootOrgId);
}; };
// below all are api calls // below all are api calls
+3 -1
View File
@@ -585,6 +585,7 @@ export const registerRoutes = async (
}); });
const membershipIdentityService = membershipIdentityServiceFactory({ const membershipIdentityService = membershipIdentityServiceFactory({
identityDAL,
membershipIdentityDAL, membershipIdentityDAL,
membershipRoleDAL, membershipRoleDAL,
orgDAL, orgDAL,
@@ -1577,7 +1578,8 @@ export const registerRoutes = async (
permissionService, permissionService,
projectDAL, projectDAL,
accessTokenQueue, accessTokenQueue,
smtpService smtpService,
orgDAL
}); });
const identityService = identityServiceFactory({ const identityService = identityServiceFactory({
@@ -0,0 +1,137 @@
import { z } from "zod";
import { AccessScope, TemporaryPermissionMode } from "@app/db/schemas";
import { ApiDocsTags, PROJECT_IDENTITIES } from "@app/lib/api-docs";
import { ms } from "@app/lib/ms";
import { writeLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type";
const sanitizedOrgIdentityMembershipSchema = z.object({
id: z.string().uuid(),
orgId: z.string(),
identityId: z.string().uuid(),
createdAt: z.date(),
updatedAt: z.date()
});
export const registerOrgIdentityMembershipRouter = async (server: FastifyZodProvider) => {
server.route({
method: "POST",
url: "/identity-memberships/:identityId",
config: {
rateLimit: writeLimit
},
onRequest: verifyAuth([AuthMode.JWT]),
schema: {
hide: true,
// this is hidden so not updating tags
tags: [ApiDocsTags.ProjectIdentities],
description: "Create org identity membership",
security: [
{
bearerAuth: []
}
],
params: z.object({
identityId: z.string().trim()
}),
body: z.object({
roles: z
.array(
z.union([
z.object({
role: z.string().describe(PROJECT_IDENTITIES.CREATE_IDENTITY_MEMBERSHIP.roles.role),
isTemporary: z
.literal(false)
.default(false)
.describe(PROJECT_IDENTITIES.CREATE_IDENTITY_MEMBERSHIP.roles.role)
}),
z.object({
role: z.string().describe(PROJECT_IDENTITIES.CREATE_IDENTITY_MEMBERSHIP.roles.role),
isTemporary: z.literal(true).describe(PROJECT_IDENTITIES.CREATE_IDENTITY_MEMBERSHIP.roles.role),
temporaryMode: z
.nativeEnum(TemporaryPermissionMode)
.describe(PROJECT_IDENTITIES.CREATE_IDENTITY_MEMBERSHIP.roles.role),
temporaryRange: z
.string()
.refine((val) => ms(val) > 0, "Temporary range must be a positive number")
.describe(PROJECT_IDENTITIES.CREATE_IDENTITY_MEMBERSHIP.roles.role),
temporaryAccessStartTime: z
.string()
.datetime()
.describe(PROJECT_IDENTITIES.CREATE_IDENTITY_MEMBERSHIP.roles.role)
})
])
)
.describe(PROJECT_IDENTITIES.CREATE_IDENTITY_MEMBERSHIP.roles.description)
.max(1)
}),
response: {
200: z.object({
identityMembership: sanitizedOrgIdentityMembershipSchema
})
}
},
handler: async (req) => {
const { membership } = await server.services.membershipIdentity.createMembership({
permission: req.permission,
scopeData: {
scope: AccessScope.Organization,
orgId: req.permission.orgId
},
data: {
identityId: req.params.identityId,
roles: req.body.roles
}
});
return {
identityMembership: { ...membership, identityId: req.params.identityId, orgId: req.permission.orgId }
};
}
});
server.route({
method: "DELETE",
url: "/identity-memberships/:identityId",
config: {
rateLimit: writeLimit
},
onRequest: verifyAuth([AuthMode.JWT]),
schema: {
hide: true,
tags: [ApiDocsTags.ProjectIdentities],
description: "Delete org identity memberships",
security: [
{
bearerAuth: []
}
],
params: z.object({
identityId: z.string().trim().describe(PROJECT_IDENTITIES.DELETE_IDENTITY_MEMBERSHIP.identityId)
}),
response: {
200: z.object({
identityMembership: sanitizedOrgIdentityMembershipSchema
})
}
},
handler: async (req) => {
const { membership } = await server.services.membershipIdentity.deleteMembership({
permission: req.permission,
scopeData: {
scope: AccessScope.Organization,
orgId: req.permission.orgId
},
selector: {
identityId: req.params.identityId
}
});
return {
identityMembership: { ...membership, identityId: req.params.identityId, orgId: req.permission.orgId }
};
}
});
};
+2
View File
@@ -65,6 +65,7 @@ import { registerUserEngagementRouter } from "./user-engagement-router";
import { registerUserRouter } from "./user-router"; import { registerUserRouter } from "./user-router";
import { registerWebhookRouter } from "./webhook-router"; import { registerWebhookRouter } from "./webhook-router";
import { registerWorkflowIntegrationRouter } from "./workflow-integration-router"; import { registerWorkflowIntegrationRouter } from "./workflow-integration-router";
import { registerOrgIdentityMembershipRouter } from "./identity-org-membership-router";
export const registerV1Routes = async (server: FastifyZodProvider) => { export const registerV1Routes = async (server: FastifyZodProvider) => {
await server.register(registerSsoRouter, { prefix: "/sso" }); await server.register(registerSsoRouter, { prefix: "/sso" });
@@ -89,6 +90,7 @@ export const registerV1Routes = async (server: FastifyZodProvider) => {
); );
await server.register(registerPasswordRouter, { prefix: "/password" }); await server.register(registerPasswordRouter, { prefix: "/password" });
await server.register(registerOrgRouter, { prefix: "/organization" }); await server.register(registerOrgRouter, { prefix: "/organization" });
await server.register(registerOrgIdentityMembershipRouter, { prefix: "/organization" });
await server.register(registerAdminRouter, { prefix: "/admin" }); await server.register(registerAdminRouter, { prefix: "/admin" });
await server.register(registerOrgAdminRouter, { prefix: "/organization-admin" }); await server.register(registerOrgAdminRouter, { prefix: "/organization-admin" });
await server.register(registerUserRouter, { prefix: "/user" }); await server.register(registerUserRouter, { prefix: "/user" });
@@ -2,6 +2,7 @@ import RE2 from "re2";
import { z } from "zod"; import { z } from "zod";
import { import {
AccessScope,
AuditLogsSchema, AuditLogsSchema,
GroupsSchema, GroupsSchema,
IncidentContactsSchema, IncidentContactsSchema,
@@ -475,4 +476,68 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => {
return { groups }; return { groups };
} }
}); });
server.route({
method: "GET",
url: "/users/available",
schema: {
response: {
200: z.object({
users: z
.object({
id: z.string().uuid(),
username: z.string(),
email: z.string().nullable().optional(),
firstName: z.string().nullable().optional(),
lastName: z.string().nullable().optional()
})
.array()
})
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
const { users } = await server.services.membershipUser.listAvailableUsers({
permission: req.permission,
scopeData: {
orgId: req.permission.orgId,
scope: AccessScope.Organization
},
data: {}
});
return { users };
}
});
server.route({
method: "GET",
url: "/identities/available",
schema: {
response: {
200: z.object({
identities: z
.object({
id: z.string().uuid(),
name: z.string(),
hasDeleteProtection: z.boolean()
})
.array()
})
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
const { identities } = await server.services.membershipIdentity.listAvailableIdentities({
permission: req.permission,
scopeData: {
orgId: req.permission.orgId,
scope: AccessScope.Organization
},
data: {}
});
return { identities };
}
});
}; };
@@ -236,7 +236,7 @@ export const tokenServiceFactory = ({ tokenDAL, userDAL, membershipUserDAL, orgD
} }
orgId = subOrganization.id; orgId = subOrganization.id;
rootOrgId = token.organizationId; rootOrgId = token.organizationId;
parentOrgId = subOrganization.parentOrgId; parentOrgId = subOrganization.parentOrgId as string;
} else { } else {
const orgMembership = await membershipUserDAL.findOne({ const orgMembership = await membershipUserDAL.findOne({
actorUserId: user.id, actorUserId: user.id,
@@ -216,11 +216,12 @@ export const identityServiceFactory = ({
if (isCustomRole) customRole = rolePermissionDetails?.role; if (isCustomRole) customRole = rolePermissionDetails?.role;
} }
const identityDetails = await identityDAL.findById(id);
const identity = await identityDAL.transaction(async (tx) => { const identity = await identityDAL.transaction(async (tx) => {
const newIdentity = const newIdentity =
name || hasDeleteProtection identityDetails.orgId === actorOrgId && (name || hasDeleteProtection)
? await identityDAL.updateById(id, { name, hasDeleteProtection }, tx) ? await identityDAL.updateById(id, { name, hasDeleteProtection }, tx)
: await identityDAL.findById(id, tx); : identityDetails;
if (role) { if (role) {
await membershipRoleDAL.delete({ membershipId: identityOrgMembership.id }, tx); await membershipRoleDAL.delete({ membershipId: identityOrgMembership.id }, tx);
@@ -282,7 +283,6 @@ export const identityServiceFactory = ({
}); });
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Read, OrgPermissionSubjects.Identity); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Read, OrgPermissionSubjects.Identity);
// TODO(namespace): check this in identity service
const activeLockouts = await keyStore.getKeysByPattern(`lockout:identity:${id}:*`); const activeLockouts = await keyStore.getKeysByPattern(`lockout:identity:${id}:*`);
const activeLockoutAuthMethods = new Set<string>(); const activeLockoutAuthMethods = new Set<string>();
@@ -91,6 +91,7 @@ export const membershipIdentityDALFactory = (db: TDbClient) => {
.select( .select(
db.ref("name").withSchema(TableName.Identity).as("identityName"), db.ref("name").withSchema(TableName.Identity).as("identityName"),
db.ref("id").withSchema(TableName.Identity).as("identityId"), 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("hasDeleteProtection").withSchema(TableName.Identity).as("identityHasDeleteProtection"),
db.ref("slug").withSchema(TableName.Role).as("roleSlug"), db.ref("slug").withSchema(TableName.Role).as("roleSlug"),
@@ -132,6 +133,7 @@ export const membershipIdentityDALFactory = (db: TDbClient) => {
parentMapper: (el) => { parentMapper: (el) => {
const { const {
identityId: actorIdentityId, identityId: actorIdentityId,
identityOrgId,
identityHasDeleteProtection, identityHasDeleteProtection,
identityName, identityName,
uaId, uaId,
@@ -153,6 +155,7 @@ export const membershipIdentityDALFactory = (db: TDbClient) => {
name: identityName, name: identityName,
id: actorIdentityId, id: actorIdentityId,
hasDeleteProtection: identityHasDeleteProtection, hasDeleteProtection: identityHasDeleteProtection,
identityOrgId,
authMethods: buildAuthMethods({ authMethods: buildAuthMethods({
uaId, uaId,
awsId, 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 { newNamespaceMembershipIdentityFactory } from "./namespace/namespace-membership-identity-factory";
import { newOrgMembershipIdentityFactory } from "./org/org-membership-identity-factory"; import { newOrgMembershipIdentityFactory } from "./org/org-membership-identity-factory";
import { newProjectMembershipIdentityFactory } from "./project/project-membership-identity-factory"; import { newProjectMembershipIdentityFactory } from "./project/project-membership-identity-factory";
import { TIdentityDALFactory } from "../identity/identity-dal";
type TMembershipIdentityServiceFactoryDep = { type TMembershipIdentityServiceFactoryDep = {
membershipIdentityDAL: TMembershipIdentityDALFactory; membershipIdentityDAL: TMembershipIdentityDALFactory;
@@ -31,6 +32,7 @@ type TMembershipIdentityServiceFactoryDep = {
>; >;
orgDAL: Pick<TOrgDALFactory, "findById">; orgDAL: Pick<TOrgDALFactory, "findById">;
additionalPrivilegeDAL: Pick<TAdditionalPrivilegeDALFactory, "delete">; additionalPrivilegeDAL: Pick<TAdditionalPrivilegeDALFactory, "delete">;
identityDAL: Pick<TIdentityDALFactory, "findById">;
}; };
export type TMembershipIdentityServiceFactory = ReturnType<typeof membershipIdentityServiceFactory>; export type TMembershipIdentityServiceFactory = ReturnType<typeof membershipIdentityServiceFactory>;
@@ -41,12 +43,14 @@ export const membershipIdentityServiceFactory = ({
membershipRoleDAL, membershipRoleDAL,
permissionService, permissionService,
orgDAL, orgDAL,
additionalPrivilegeDAL additionalPrivilegeDAL,
identityDAL
}: TMembershipIdentityServiceFactoryDep) => { }: TMembershipIdentityServiceFactoryDep) => {
const scopeFactory = { const scopeFactory = {
[AccessScope.Organization]: newOrgMembershipIdentityFactory({ [AccessScope.Organization]: newOrgMembershipIdentityFactory({
orgDAL, orgDAL,
permissionService permissionService,
identityDAL
}), }),
[AccessScope.Project]: newProjectMembershipIdentityFactory({ [AccessScope.Project]: newProjectMembershipIdentityFactory({
membershipIdentityDAL, membershipIdentityDAL,
@@ -305,7 +309,7 @@ export const membershipIdentityServiceFactory = ({
[SearchResourceOperators.$contains]: dto.data.identityName [SearchResourceOperators.$contains]: dto.data.identityName
} }
: undefined, : undefined,
role: dto.data.roles.length role: dto.data?.roles?.length
? { ? {
[SearchResourceOperators.$in]: dto.data.roles [SearchResourceOperators.$in]: dto.data.roles
} }
@@ -329,11 +333,29 @@ export const membershipIdentityServiceFactory = ({
return membership; 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 { return {
createMembership, createMembership,
updateMembership, updateMembership,
deleteMembership, deleteMembership,
listMemberships, listMemberships,
getMembershipByIdentityId getMembershipByIdentityId,
listAvailableIdentities
}; };
}; };
@@ -54,14 +54,11 @@ export type TUpdateMembershipIdentityDTO = {
export type TListMembershipIdentityDTO = { export type TListMembershipIdentityDTO = {
permission: OrgServiceActor; permission: OrgServiceActor;
scopeData: AccessScopeData; scopeData: AccessScopeData;
selector: {
identityId: string;
};
data: { data: {
limit?: number; limit?: number;
offset?: number; offset?: number;
identityName?: string; 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 { isCustomOrgRole } from "@app/services/org/org-role-fns";
import { TMembershipIdentityScopeFactory } from "../membership-identity-types"; import { TMembershipIdentityScopeFactory } from "../membership-identity-types";
import { TIdentityDALFactory } from "@app/services/identity/identity-dal";
type TOrgMembershipIdentityScopeFactoryDep = { type TOrgMembershipIdentityScopeFactoryDep = {
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission" | "getOrgPermissionByRoles">; permissionService: Pick<TPermissionServiceFactory, "getOrgPermission" | "getOrgPermissionByRoles">;
orgDAL: Pick<TOrgDALFactory, "findById">; orgDAL: Pick<TOrgDALFactory, "findById">;
identityDAL: Pick<TIdentityDALFactory, "findById">;
}; };
export const newOrgMembershipIdentityFactory = ({ export const newOrgMembershipIdentityFactory = ({
permissionService, permissionService,
orgDAL orgDAL,
identityDAL
}: TOrgMembershipIdentityScopeFactoryDep): TMembershipIdentityScopeFactory => { }: TOrgMembershipIdentityScopeFactoryDep): TMembershipIdentityScopeFactory => {
const getScopeField: TMembershipIdentityScopeFactory["getScopeField"] = (dto) => { const getScopeField: TMembershipIdentityScopeFactory["getScopeField"] = (dto) => {
if (dto.scope === AccessScope.Organization) { if (dto.scope === AccessScope.Organization) {
@@ -38,12 +41,53 @@ export const newOrgMembershipIdentityFactory = ({
const isCustomRole: TMembershipIdentityScopeFactory["isCustomRole"] = (role: string) => isCustomOrgRole(role); const isCustomRole: TMembershipIdentityScopeFactory["isCustomRole"] = (role: string) => isCustomOrgRole(role);
const onCreateMembershipIdentityGuard: TMembershipIdentityScopeFactory["onCreateMembershipIdentityGuard"] = const onCreateMembershipIdentityGuard: TMembershipIdentityScopeFactory["onCreateMembershipIdentityGuard"] = async (
async () => { dto
throw new BadRequestError({ ) => {
message: "Organization membership cannot be created for organization scoped identity" 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 ( const onUpdateMembershipIdentityGuard: TMembershipIdentityScopeFactory["onUpdateMembershipIdentityGuard"] = async (
dto dto
@@ -87,12 +131,29 @@ export const newOrgMembershipIdentityFactory = ({
} }
}; };
const onDeleteMembershipIdentityGuard: TMembershipIdentityScopeFactory["onDeleteMembershipIdentityGuard"] = const onDeleteMembershipIdentityGuard: TMembershipIdentityScopeFactory["onDeleteMembershipIdentityGuard"] = async (
async () => { dto
throw new BadRequestError({ ) => {
message: "Organization membership cannot be deleted for organization scoped identity" 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 ( const onListMembershipIdentityGuard: TMembershipIdentityScopeFactory["onListMembershipIdentityGuard"] = async (
dto dto
@@ -291,13 +291,37 @@ export const membershipUserDALFactory = (db: TDbClient) => {
} }
}; };
// const listAvailableUsers = async (scopeData: AccessScopeData) => { // this right nwo only support sub organization
// try { const listAvailableUsers = async (orgId: string, rootOrgId: string) => {
// const query = await db.replicaNode()(TableName.Membership).where(`${TableName.Membership}.scopeOrgId`); try {
// } catch (error) { const usersConnectedToOrg = db
// throw new DatabaseError({ error, name: "ListAvailableUsers" }); .replicaNode()(TableName.Membership)
// } .whereNotNull(`${TableName.Membership}.actorUserId`)
// }; .where(`${TableName.Membership}.scope`, AccessScope.Organization)
.where(`${TableName.Membership}.scopeOrgId`, orgId)
.select("actorUserId");
return { ...orm, findUsers, getUserById }; const docs = await db
.replicaNode()(TableName.Membership)
.join(TableName.Users, `${TableName.Users}.id`, `${TableName.Membership}.actorUserId`)
.where(`${TableName.Membership}.scope`, AccessScope.Organization)
.where(`${TableName.Users}.isGhost`, false)
.whereNotNull(`${TableName.Membership}.actorUserId`)
.where(`${TableName.Membership}.scopeOrgId`, rootOrgId)
.whereNot(`${TableName.Membership}.actorUserId`, usersConnectedToOrg)
.select(
db.ref("id").withSchema(TableName.Users),
db.ref("email").withSchema(TableName.Users),
db.ref("username").withSchema(TableName.Users),
db.ref("firstName").withSchema(TableName.Users),
db.ref("lastName").withSchema(TableName.Users)
);
return docs;
} catch (error) {
throw new DatabaseError({ error, name: "ListAvailableUsers" });
}
};
return { ...orm, findUsers, getUserById, listAvailableUsers };
}; };
@@ -83,7 +83,8 @@ export const membershipUserServiceFactory = ({
orgDAL, orgDAL,
tokenService, tokenService,
userDAL, userDAL,
userGroupMembershipDAL userGroupMembershipDAL,
membershipUserDAL
}), }),
[AccessScope.Namespace]: newNamespaceMembershipUserFactory({}), [AccessScope.Namespace]: newNamespaceMembershipUserFactory({}),
[AccessScope.Project]: newProjectMembershipUserFactory({ [AccessScope.Project]: newProjectMembershipUserFactory({
@@ -471,11 +472,26 @@ export const membershipUserServiceFactory = ({
return membership; return membership;
}; };
// Should only be used for sub organization as of now
const listAvailableUsers = async (dto: TListMembershipUserDTO) => {
const { scopeData } = dto;
const factory = scopeFactory[scopeData.scope];
await factory.onListMembershipUserGuard(dto);
const organizationDetails = await orgDAL.findById(dto.scopeData.orgId);
if (!organizationDetails.rootOrgId) return { users: [] };
const users = await membershipUserDAL.listAvailableUsers(organizationDetails.id, organizationDetails.rootOrgId);
return { users };
};
return { return {
createMembership, createMembership,
updateMembership, updateMembership,
deleteMembership, deleteMembership,
listMemberships, listMemberships,
getMembershipByUserId getMembershipByUserId,
listAvailableUsers
}; };
}; };
@@ -93,3 +93,8 @@ export type TGetMembershipUserByUserIdDTO = {
userId: string; userId: string;
}; };
}; };
export type TListAvailableUsersDTO = {
permission: OrgServiceActor;
scopeData: AccessScopeData;
};
@@ -16,6 +16,7 @@ import { SmtpTemplates, TSmtpService } from "@app/services/smtp/smtp-service";
import { TUserDALFactory } from "@app/services/user/user-dal"; import { TUserDALFactory } from "@app/services/user/user-dal";
import { TMembershipUserScopeFactory } from "../membership-user-types"; import { TMembershipUserScopeFactory } from "../membership-user-types";
import { TMembershipUserDALFactory } from "../membership-user-dal";
type TOrgMembershipUserScopeFactoryDep = { type TOrgMembershipUserScopeFactoryDep = {
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">; permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">;
@@ -25,6 +26,7 @@ type TOrgMembershipUserScopeFactoryDep = {
orgDAL: Pick<TOrgDALFactory, "findById">; orgDAL: Pick<TOrgDALFactory, "findById">;
userGroupMembershipDAL: Pick<TUserGroupMembershipDALFactory, "delete">; userGroupMembershipDAL: Pick<TUserGroupMembershipDALFactory, "delete">;
licenseService: Pick<TLicenseServiceFactory, "getPlan">; licenseService: Pick<TLicenseServiceFactory, "getPlan">;
membershipUserDAL: Pick<TMembershipUserDALFactory, "find">;
}; };
export const newOrgMembershipUserFactory = ({ export const newOrgMembershipUserFactory = ({
@@ -33,7 +35,8 @@ export const newOrgMembershipUserFactory = ({
userDAL, userDAL,
orgDAL, orgDAL,
smtpService, smtpService,
licenseService licenseService,
membershipUserDAL
}: TOrgMembershipUserScopeFactoryDep): TMembershipUserScopeFactory => { }: TOrgMembershipUserScopeFactoryDep): TMembershipUserScopeFactory => {
const getScopeField: TMembershipUserScopeFactory["getScopeField"] = (dto) => { const getScopeField: TMembershipUserScopeFactory["getScopeField"] = (dto) => {
if (dto.scope === AccessScope.Organization) { if (dto.scope === AccessScope.Organization) {
@@ -51,7 +54,10 @@ export const newOrgMembershipUserFactory = ({
const isCustomRole: TMembershipUserScopeFactory["isCustomRole"] = (role: string) => isCustomOrgRole(role); const isCustomRole: TMembershipUserScopeFactory["isCustomRole"] = (role: string) => isCustomOrgRole(role);
const onCreateMembershipUserGuard: TMembershipUserScopeFactory["onCreateMembershipUserGuard"] = async (dto) => { const onCreateMembershipUserGuard: TMembershipUserScopeFactory["onCreateMembershipUserGuard"] = async (
dto,
newMembers
) => {
const { permission } = await permissionService.getOrgPermission({ const { permission } = await permissionService.getOrgPermission({
actor: dto.permission.type, actor: dto.permission.type,
actorId: dto.permission.id, actorId: dto.permission.id,
@@ -78,6 +84,19 @@ export const newOrgMembershipUserFactory = ({
message: "Failed to invite user due to org-level auth enforced for organization" message: "Failed to invite user due to org-level auth enforced for organization"
}); });
} }
if (org.rootOrgId) {
const rootOrgMembership = await membershipUserDAL.find({
scope: AccessScope.Organization,
$in: {
actorUserId: newMembers.map((el) => el.id)
},
scopeOrgId: org.rootOrgId
});
if (rootOrgMembership.length !== newMembers.length)
throw new BadRequestError({
message: "User doesn't have membership in root organization"
});
}
}; };
const onCreateMembershipComplete: TMembershipUserScopeFactory["onCreateMembershipComplete"] = async ( const onCreateMembershipComplete: TMembershipUserScopeFactory["onCreateMembershipComplete"] = async (
@@ -21,6 +21,7 @@ export const useOrganization = () => {
id: currentOrg?.subOrganization?.id || currentOrg?.id, id: currentOrg?.subOrganization?.id || currentOrg?.id,
parentOrgId: currentOrg.id parentOrgId: currentOrg.id
}, },
isSubOrganization: Boolean(currentOrg.subOrganization) isSubOrganization: Boolean(currentOrg.subOrganization),
isRootOrganization: !currentOrg.subOrganization
}; };
}; };
@@ -0,0 +1,2 @@
export { useCreateOrgIdentityMembership, useDeleteOrgIdentityMembership } from "./mutation";
export type { TCreateOrgIdentityMembershipDTO, TDeleteOrgIdentityMembershipDTO, TOrgIdentityMembership } from "./types";
@@ -0,0 +1,42 @@
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { apiRequest } from "@app/config/request";
import {
TCreateOrgIdentityMembershipDTO,
TDeleteOrgIdentityMembershipDTO,
TOrgIdentityMembership
} from "./types";
export const useCreateOrgIdentityMembership = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async ({ identityId, roles }: TCreateOrgIdentityMembershipDTO) => {
const { data } = await apiRequest.post<{ identityMembership: TOrgIdentityMembership }>(
`/api/v1/organization/identity-memberships/${identityId}`,
{ roles }
);
return data.identityMembership;
},
onSuccess: () => {
// Invalidate relevant queries if needed
queryClient.invalidateQueries({ queryKey: ["organization"] });
}
});
};
export const useDeleteOrgIdentityMembership = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async ({ identityId }: TDeleteOrgIdentityMembershipDTO) => {
const { data } = await apiRequest.delete<{ identityMembership: TOrgIdentityMembership }>(
`/api/v1/organization/identity-memberships/${identityId}`
);
return data.identityMembership;
},
onSuccess: () => {
// Invalidate relevant queries if needed
queryClient.invalidateQueries({ queryKey: ["organization"] });
}
});
};
@@ -0,0 +1,30 @@
import { TemporaryPermissionMode } from "@app/db/schemas";
export type TOrgIdentityMembership = {
id: string;
orgId: string;
identityId: string;
createdAt: string;
updatedAt: string;
};
export type TCreateOrgIdentityMembershipDTO = {
identityId: string;
roles: Array<
| {
role: string;
isTemporary?: false;
}
| {
role: string;
isTemporary: true;
temporaryMode: TemporaryPermissionMode;
temporaryRange: string;
temporaryAccessStartTime: string;
}
>;
};
export type TDeleteOrgIdentityMembershipDTO = {
identityId: string;
};
@@ -1,5 +1,6 @@
export { export {
useAddOrgPmtMethod, useAddOrgPmtMethod,
useGetAvailableOrgIdentities,
useAddOrgTaxId, useAddOrgTaxId,
useCreateCustomerPortalSession, useCreateCustomerPortalSession,
useCreateOrg, useCreateOrg,
@@ -42,7 +42,9 @@ export const organizationKeys = {
[...organizationKeys.getOrgIdentityMemberships(orgId), params] as const, [...organizationKeys.getOrgIdentityMemberships(orgId), params] as const,
getOrgGroups: (orgId: string) => [{ orgId }, "organization-groups"] as const, getOrgGroups: (orgId: string) => [{ orgId }, "organization-groups"] as const,
getOrgIntegrationAuths: (orgId: string) => [{ orgId }, "integration-auths"] as const, getOrgIntegrationAuths: (orgId: string) => [{ orgId }, "integration-auths"] as const,
getOrgById: (orgId: string) => ["organization", { orgId }] getOrgById: (orgId: string) => ["organization", { orgId }],
getAvailableIdentities: () => ["available-identities"],
getAvailableUsers: () => ["available-users"]
}; };
export const fetchOrganizations = async () => { export const fetchOrganizations = async () => {
@@ -574,3 +576,29 @@ export const useGetOrgIntegrationAuths = <TData = IntegrationAuth[],>(
select select
}); });
}; };
export const useGetAvailableOrgIdentities = (enabled = true) =>
useQuery({
queryKey: organizationKeys.getAvailableIdentities(),
queryFn: async () => {
const { data } = await apiRequest.get<{ identities: { name: string; id: string }[] }>(
`/api/v1/organization/identities/available`
);
return data.identities;
},
enabled
});
export const useGetAvailableOrgUsers = (enabled = true) =>
useQuery({
queryKey: organizationKeys.getAvailableUsers(),
queryFn: async () => {
const { data } = await apiRequest.get<{
users: { username: string; id: string; firstName: string; lastName: string }[];
}>(`/api/v1/organization/users/available`);
return data.users;
},
enabled
});
@@ -0,0 +1,129 @@
import { Controller, useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { useNavigate } from "@tanstack/react-router";
import { z } from "zod";
import { createNotification } from "@app/components/notifications";
import { Button, FilterableSelect, FormControl } from "@app/components/v2";
import { useOrganization } from "@app/context";
import { useGetAvailableOrgIdentities, useGetOrgRoles } from "@app/hooks/api";
import { useCreateOrgIdentityMembership } from "@app/hooks/api/orgIdentityMembership";
const schema = z
.object({
identity: z.object({ name: z.string(), id: z.string() }),
role: z.object({ name: z.string(), slug: z.string() })
})
.required();
export type FormData = z.infer<typeof schema>;
type Props = {
onClose: () => void;
};
export const IdentityLinkForm = ({ onClose }: Props) => {
const navigate = useNavigate();
const { currentOrg } = useOrganization();
const orgId = currentOrg?.id || "";
const { data: roles } = useGetOrgRoles(orgId);
const { mutateAsync: createMutateAsync } = useCreateOrgIdentityMembership();
const { data: rootOrgIdentities, isPending: isRootOrgLoading } = useGetAvailableOrgIdentities();
const {
control,
handleSubmit,
formState: { isSubmitting }
} = useForm<FormData>({
resolver: zodResolver(schema),
defaultValues: {}
});
const onFormSubmit = async ({ identity, role }: FormData) => {
try {
await createMutateAsync({
identityId: identity.id,
roles: [{ role: role.slug, isTemporary: false }]
});
createNotification({
text: "Successfully linked identity",
type: "success"
});
navigate({
to: "/organization/identities/$identityId",
params: {
identityId: identity.id
}
});
} catch (err) {
console.error(err);
const error = err as any;
const text = error?.response?.data?.message ?? "Failed to link identity";
createNotification({
text,
type: "error"
});
}
};
return (
<form onSubmit={handleSubmit(onFormSubmit)}>
<Controller
control={control}
name="identity"
render={({ field: { onChange, value }, fieldState: { error } }) => (
<FormControl label="Identity" errorText={error?.message} isError={Boolean(error)}>
<FilterableSelect
value={value}
onChange={onChange}
placeholder="Select identity..."
options={rootOrgIdentities}
getOptionValue={(option) => option.id}
getOptionLabel={(option) => option.name}
isLoading={isRootOrgLoading}
/>
</FormControl>
)}
/>
<Controller
control={control}
name="role"
render={({ field: { onChange, value }, fieldState: { error } }) => (
<FormControl
label="Role"
errorText={error?.message}
isError={Boolean(error)}
className="mt-4"
>
<FilterableSelect
value={value}
onChange={onChange}
options={roles}
placeholder="Select role..."
getOptionValue={(option) => option.slug}
getOptionLabel={(option) => option.name}
menuPortalTarget={document.body}
/>
</FormControl>
)}
/>
<div className="flex items-center">
<Button
className="mr-4"
size="sm"
type="submit"
isLoading={isSubmitting}
isDisabled={isSubmitting}
>
Link
</Button>
<Button colorSchema="secondary" variant="plain" onClick={() => onClose()}>
Cancel
</Button>
</div>
</form>
);
};
@@ -1,10 +1,15 @@
import { faArrowUpRightFromSquare, faBookOpen, faPlus } from "@fortawesome/free-solid-svg-icons"; import {
faArrowUpRightFromSquare,
faBookOpen,
faLink,
faPlus
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { UpgradePlanModal } from "@app/components/license/UpgradePlanModal"; import { UpgradePlanModal } from "@app/components/license/UpgradePlanModal";
import { createNotification } from "@app/components/notifications"; import { createNotification } from "@app/components/notifications";
import { OrgPermissionCan } from "@app/components/permissions"; import { OrgPermissionCan } from "@app/components/permissions";
import { Button, DeleteActionModal } from "@app/components/v2"; import { Button, DeleteActionModal, Modal, ModalContent } from "@app/components/v2";
import { import {
OrgPermissionIdentityActions, OrgPermissionIdentityActions,
OrgPermissionSubjects, OrgPermissionSubjects,
@@ -23,11 +28,12 @@ import { IdentityModal } from "./IdentityModal";
import { IdentityTable } from "./IdentityTable"; import { IdentityTable } from "./IdentityTable";
import { IdentityTokenAuthTokenModal } from "./IdentityTokenAuthTokenModal"; import { IdentityTokenAuthTokenModal } from "./IdentityTokenAuthTokenModal";
import { MachineAuthTemplateUsagesModal } from "./MachineAuthTemplateUsagesModal"; import { MachineAuthTemplateUsagesModal } from "./MachineAuthTemplateUsagesModal";
import { IdentityLinkForm } from "./IdentityLinkForm";
export const IdentitySection = withPermission( export const IdentitySection = withPermission(
() => { () => {
const { subscription } = useSubscription(); const { subscription } = useSubscription();
const { currentOrg } = useOrganization(); const { currentOrg, isSubOrganization } = useOrganization();
const orgId = currentOrg?.id || ""; const orgId = currentOrg?.id || "";
const { mutateAsync: deleteMutateAsync } = useDeleteIdentity(); const { mutateAsync: deleteMutateAsync } = useDeleteIdentity();
@@ -43,7 +49,8 @@ export const IdentitySection = withPermission(
"createTemplate", "createTemplate",
"editTemplate", "editTemplate",
"deleteTemplate", "deleteTemplate",
"viewUsages" "viewUsages",
"linkIdentity"
] as const); ] as const);
const isMoreIdentitiesAllowed = subscription?.identityLimit const isMoreIdentitiesAllowed = subscription?.identityLimit
@@ -105,8 +112,8 @@ export const IdentitySection = withPermission(
return ( return (
<div> <div>
<div className="rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4"> <div className="rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
<div className="mb-4 flex items-center justify-between"> <div className="mb-4 flex w-full items-center gap-4">
<div className="flex items-center gap-1"> <div className="flex flex-1 items-center gap-1">
<p className="text-xl font-medium text-mineshaft-100">Identities</p> <p className="text-xl font-medium text-mineshaft-100">Identities</p>
<a <a
href="https://infisical.com/docs/documentation/platform/identities/overview" href="https://infisical.com/docs/documentation/platform/identities/overview"
@@ -123,6 +130,26 @@ export const IdentitySection = withPermission(
</div> </div>
</a> </a>
</div> </div>
{isSubOrganization && (
<OrgPermissionCan
I={OrgPermissionIdentityActions.Create}
a={OrgPermissionSubjects.Identity}
>
{(isAllowed) => (
<Button
variant="plain"
colorSchema="secondary"
leftIcon={<FontAwesomeIcon icon={faLink} />}
onClick={() => {
handlePopUpOpen("linkIdentity");
}}
isDisabled={!isAllowed}
>
Link Identity
</Button>
)}
</OrgPermissionCan>
)}
<OrgPermissionCan <OrgPermissionCan
I={OrgPermissionIdentityActions.Create} I={OrgPermissionIdentityActions.Create}
a={OrgPermissionSubjects.Identity} a={OrgPermissionSubjects.Identity}
@@ -210,16 +237,17 @@ export const IdentitySection = withPermission(
?.name || "" ?.name || ""
} }
/> />
{/* <IdentityAuthMethodModal <Modal
popUp={popUp} isOpen={popUp.linkIdentity.isOpen}
handlePopUpOpen={handlePopUpOpen} onOpenChange={(isOpen) => handlePopUpToggle("linkIdentity", isOpen)}
handlePopUpToggle={handlePopUpToggle} >
/> */} <ModalContent
{/* <IdentityUniversalAuthClientSecretModal title="Assign Existing Identity"
popUp={popUp} subTitle="Assign an existing identity from your organization or namespace to this project. The identity will continue to be managed at its original scope."
handlePopUpOpen={handlePopUpOpen} >
handlePopUpToggle={handlePopUpToggle} <IdentityLinkForm onClose={() => handlePopUpClose("linkIdentity")} />
/> */} </ModalContent>
</Modal>
<IdentityTokenAuthTokenModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} /> <IdentityTokenAuthTokenModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
<DeleteActionModal <DeleteActionModal
isOpen={popUp.deleteIdentity.isOpen} isOpen={popUp.deleteIdentity.isOpen}
@@ -1,4 +1,4 @@
import { createFileRoute } from "@tanstack/react-router"; import { createFileRoute, redirect } from "@tanstack/react-router";
import { BillingPage } from "./BillingPage"; import { BillingPage } from "./BillingPage";
@@ -6,7 +6,14 @@ export const Route = createFileRoute(
"/_authenticate/_inject-org-details/_org-layout/organization/billing" "/_authenticate/_inject-org-details/_org-layout/organization/billing"
)({ )({
component: BillingPage, component: BillingPage,
beforeLoad: () => { beforeLoad: ({ search }) => {
if (search.subOrganization) {
throw redirect({
to: "/organization/projects",
search
});
}
return { return {
breadcrumbs: [ breadcrumbs: [
{ {
@@ -14,25 +14,39 @@ import { OrgSecurityTab } from "../OrgSecurityTab";
import { OrgSsoTab } from "../OrgSsoTab"; import { OrgSsoTab } from "../OrgSsoTab";
import { OrgWorkflowIntegrationTab } from "../OrgWorkflowIntegrationTab"; import { OrgWorkflowIntegrationTab } from "../OrgWorkflowIntegrationTab";
import { ProjectTemplatesTab } from "../ProjectTemplatesTab"; import { ProjectTemplatesTab } from "../ProjectTemplatesTab";
import { useOrganization } from "@app/context";
export const OrgTabGroup = () => { export const OrgTabGroup = () => {
const search = useSearch({ const search = useSearch({
from: ROUTE_PATHS.Organization.SettingsPage.id from: ROUTE_PATHS.Organization.SettingsPage.id
}); });
const { isSubOrganization } = useOrganization();
const tabs = [ const tabs = [
{ name: "General", key: "tab-org-general", component: OrgGeneralTab }, { name: "General", key: "tab-org-general", component: OrgGeneralTab },
{ {
name: "SSO", name: "SSO",
key: "sso-settings", key: "sso-settings",
component: OrgSsoTab component: OrgSsoTab,
isHidden: isSubOrganization
}, },
{ {
name: "Provisioning", name: "Provisioning",
key: "provisioning-settings", key: "provisioning-settings",
component: OrgProvisioningTab component: OrgProvisioningTab,
isHidden: isSubOrganization
},
{
name: "Security",
key: "tab-org-security",
component: OrgSecurityTab,
isHidden: isSubOrganization
},
{
name: "Encryption",
key: "tab-org-encryption",
component: OrgEncryptionTab
}, },
{ name: "Security", key: "tab-org-security", component: OrgSecurityTab },
{ name: "Encryption", key: "tab-org-encryption", component: OrgEncryptionTab },
{ {
name: "Workflow Integrations", name: "Workflow Integrations",
key: "workflow-integrations", key: "workflow-integrations",
@@ -57,17 +71,21 @@ export const OrgTabGroup = () => {
return ( return (
<Tabs orientation="vertical" value={selectedTab} onValueChange={setSelectedTab}> <Tabs orientation="vertical" value={selectedTab} onValueChange={setSelectedTab}>
<TabList> <TabList>
{tabs.map((tab) => ( {tabs
<Tab variant="org" value={tab.key} key={tab.key}> .filter((el) => !el.isHidden)
{tab.name} .map((tab) => (
</Tab> <Tab variant="org" value={tab.key} key={tab.key}>
))} {tab.name}
</Tab>
))}
</TabList> </TabList>
{tabs.map(({ key, component: Component }) => ( {tabs
<TabPanel value={key} key={`tab-panel-${key}`}> .filter((el) => !el.isHidden)
<Component /> .map(({ key, component: Component }) => (
</TabPanel> <TabPanel value={key} key={`tab-panel-${key}`}>
))} <Component />
</TabPanel>
))}
</Tabs> </Tabs>
); );
}; };