mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-09-22 13:39:35 +00:00
feat: added root org identity link functionality
This commit is contained in:
@@ -49,7 +49,8 @@ export const registerSubOrgRouter = async (server: FastifyZodProvider) => {
|
||||
type: req.permission.type,
|
||||
authMethod: req.permission.authMethod,
|
||||
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,
|
||||
authMethod: req.permission.authMethod,
|
||||
orgId: req.permission.orgId,
|
||||
parentOrgId: req.permission.orgId
|
||||
parentOrgId: req.permission.orgId,
|
||||
rootOrgId: req.permission.rootOrgId
|
||||
},
|
||||
data: {
|
||||
limit: req.query.limit,
|
||||
|
||||
@@ -285,19 +285,20 @@ export const licenseServiceFactory = ({
|
||||
};
|
||||
|
||||
const updateSubscriptionOrgMemberCount = async (orgId: string, tx?: Knex) => {
|
||||
if (instanceType === InstanceType.Cloud) {
|
||||
const org = await orgDAL.findOrgById(orgId);
|
||||
if (!org) throw new NotFoundError({ message: `Organization with ID '${orgId}' not found` });
|
||||
const org = await orgDAL.findOrgById(orgId);
|
||||
if (!org) throw new NotFoundError({ message: `Organization with ID '${orgId}' not found` });
|
||||
|
||||
const quantity = await licenseDAL.countOfOrgMembers(orgId, tx);
|
||||
const quantityIdentities = await licenseDAL.countOrgUsersAndIdentities(orgId, tx);
|
||||
const rootOrgId = org.rootOrgId || org.id;
|
||||
if (instanceType === InstanceType.Cloud) {
|
||||
const quantity = await licenseDAL.countOfOrgMembers(rootOrgId, tx);
|
||||
const quantityIdentities = await licenseDAL.countOrgUsersAndIdentities(rootOrgId, tx);
|
||||
if (org?.customerId) {
|
||||
await licenseServerCloudApi.request.patch(`/api/license-server/v1/customers/${org.customerId}/cloud-plan`, {
|
||||
quantity,
|
||||
quantityIdentities
|
||||
});
|
||||
}
|
||||
await keyStore.deleteItem(FEATURE_CACHE_KEY(orgId));
|
||||
await keyStore.deleteItem(FEATURE_CACHE_KEY(rootOrgId));
|
||||
} else if (instanceType === InstanceType.EnterpriseOnPrem) {
|
||||
const usedSeats = await licenseDAL.countOfOrgMembers(null, tx);
|
||||
const usedIdentitySeats = await licenseDAL.countOrgUsersAndIdentities(null, tx);
|
||||
@@ -308,7 +309,7 @@ export const licenseServiceFactory = ({
|
||||
usedIdentitySeats
|
||||
});
|
||||
}
|
||||
await refreshPlan(orgId);
|
||||
await refreshPlan(rootOrgId);
|
||||
};
|
||||
|
||||
// below all are api calls
|
||||
|
||||
@@ -585,6 +585,7 @@ export const registerRoutes = async (
|
||||
});
|
||||
|
||||
const membershipIdentityService = membershipIdentityServiceFactory({
|
||||
identityDAL,
|
||||
membershipIdentityDAL,
|
||||
membershipRoleDAL,
|
||||
orgDAL,
|
||||
@@ -1577,7 +1578,8 @@ export const registerRoutes = async (
|
||||
permissionService,
|
||||
projectDAL,
|
||||
accessTokenQueue,
|
||||
smtpService
|
||||
smtpService,
|
||||
orgDAL
|
||||
});
|
||||
|
||||
const identityService = identityServiceFactory({
|
||||
|
||||
137
backend/src/server/routes/v1/identity-org-membership-router.ts
Normal file
137
backend/src/server/routes/v1/identity-org-membership-router.ts
Normal file
@@ -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 }
|
||||
};
|
||||
}
|
||||
});
|
||||
};
|
||||
@@ -65,6 +65,7 @@ import { registerUserEngagementRouter } from "./user-engagement-router";
|
||||
import { registerUserRouter } from "./user-router";
|
||||
import { registerWebhookRouter } from "./webhook-router";
|
||||
import { registerWorkflowIntegrationRouter } from "./workflow-integration-router";
|
||||
import { registerOrgIdentityMembershipRouter } from "./identity-org-membership-router";
|
||||
|
||||
export const registerV1Routes = async (server: FastifyZodProvider) => {
|
||||
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(registerOrgRouter, { prefix: "/organization" });
|
||||
await server.register(registerOrgIdentityMembershipRouter, { prefix: "/organization" });
|
||||
await server.register(registerAdminRouter, { prefix: "/admin" });
|
||||
await server.register(registerOrgAdminRouter, { prefix: "/organization-admin" });
|
||||
await server.register(registerUserRouter, { prefix: "/user" });
|
||||
|
||||
@@ -2,6 +2,7 @@ import RE2 from "re2";
|
||||
import { z } from "zod";
|
||||
|
||||
import {
|
||||
AccessScope,
|
||||
AuditLogsSchema,
|
||||
GroupsSchema,
|
||||
IncidentContactsSchema,
|
||||
@@ -475,4 +476,68 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => {
|
||||
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;
|
||||
rootOrgId = token.organizationId;
|
||||
parentOrgId = subOrganization.parentOrgId;
|
||||
parentOrgId = subOrganization.parentOrgId as string;
|
||||
} else {
|
||||
const orgMembership = await membershipUserDAL.findOne({
|
||||
actorUserId: user.id,
|
||||
|
||||
@@ -216,11 +216,12 @@ export const identityServiceFactory = ({
|
||||
if (isCustomRole) customRole = rolePermissionDetails?.role;
|
||||
}
|
||||
|
||||
const identityDetails = await identityDAL.findById(id);
|
||||
const identity = await identityDAL.transaction(async (tx) => {
|
||||
const newIdentity =
|
||||
name || hasDeleteProtection
|
||||
identityDetails.orgId === actorOrgId && (name || hasDeleteProtection)
|
||||
? await identityDAL.updateById(id, { name, hasDeleteProtection }, tx)
|
||||
: await identityDAL.findById(id, tx);
|
||||
: identityDetails;
|
||||
|
||||
if (role) {
|
||||
await membershipRoleDAL.delete({ membershipId: identityOrgMembership.id }, tx);
|
||||
@@ -282,7 +283,6 @@ export const identityServiceFactory = ({
|
||||
});
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Read, OrgPermissionSubjects.Identity);
|
||||
|
||||
// TODO(namespace): check this in identity service
|
||||
const activeLockouts = await keyStore.getKeysByPattern(`lockout:identity:${id}:*`);
|
||||
|
||||
const activeLockoutAuthMethods = new Set<string>();
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -291,13 +291,37 @@ export const membershipUserDALFactory = (db: TDbClient) => {
|
||||
}
|
||||
};
|
||||
|
||||
// const listAvailableUsers = async (scopeData: AccessScopeData) => {
|
||||
// try {
|
||||
// const query = await db.replicaNode()(TableName.Membership).where(`${TableName.Membership}.scopeOrgId`);
|
||||
// } catch (error) {
|
||||
// throw new DatabaseError({ error, name: "ListAvailableUsers" });
|
||||
// }
|
||||
// };
|
||||
// this right nwo only support sub organization
|
||||
const listAvailableUsers = async (orgId: string, rootOrgId: string) => {
|
||||
try {
|
||||
const usersConnectedToOrg = db
|
||||
.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,
|
||||
tokenService,
|
||||
userDAL,
|
||||
userGroupMembershipDAL
|
||||
userGroupMembershipDAL,
|
||||
membershipUserDAL
|
||||
}),
|
||||
[AccessScope.Namespace]: newNamespaceMembershipUserFactory({}),
|
||||
[AccessScope.Project]: newProjectMembershipUserFactory({
|
||||
@@ -471,11 +472,26 @@ export const membershipUserServiceFactory = ({
|
||||
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 {
|
||||
createMembership,
|
||||
updateMembership,
|
||||
deleteMembership,
|
||||
listMemberships,
|
||||
getMembershipByUserId
|
||||
getMembershipByUserId,
|
||||
listAvailableUsers
|
||||
};
|
||||
};
|
||||
|
||||
@@ -93,3 +93,8 @@ export type TGetMembershipUserByUserIdDTO = {
|
||||
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 { TMembershipUserScopeFactory } from "../membership-user-types";
|
||||
import { TMembershipUserDALFactory } from "../membership-user-dal";
|
||||
|
||||
type TOrgMembershipUserScopeFactoryDep = {
|
||||
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">;
|
||||
@@ -25,6 +26,7 @@ type TOrgMembershipUserScopeFactoryDep = {
|
||||
orgDAL: Pick<TOrgDALFactory, "findById">;
|
||||
userGroupMembershipDAL: Pick<TUserGroupMembershipDALFactory, "delete">;
|
||||
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
|
||||
membershipUserDAL: Pick<TMembershipUserDALFactory, "find">;
|
||||
};
|
||||
|
||||
export const newOrgMembershipUserFactory = ({
|
||||
@@ -33,7 +35,8 @@ export const newOrgMembershipUserFactory = ({
|
||||
userDAL,
|
||||
orgDAL,
|
||||
smtpService,
|
||||
licenseService
|
||||
licenseService,
|
||||
membershipUserDAL
|
||||
}: TOrgMembershipUserScopeFactoryDep): TMembershipUserScopeFactory => {
|
||||
const getScopeField: TMembershipUserScopeFactory["getScopeField"] = (dto) => {
|
||||
if (dto.scope === AccessScope.Organization) {
|
||||
@@ -51,7 +54,10 @@ export const newOrgMembershipUserFactory = ({
|
||||
|
||||
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({
|
||||
actor: dto.permission.type,
|
||||
actorId: dto.permission.id,
|
||||
@@ -78,6 +84,19 @@ export const newOrgMembershipUserFactory = ({
|
||||
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 (
|
||||
|
||||
@@ -21,6 +21,7 @@ export const useOrganization = () => {
|
||||
id: currentOrg?.subOrganization?.id || currentOrg?.id,
|
||||
parentOrgId: currentOrg.id
|
||||
},
|
||||
isSubOrganization: Boolean(currentOrg.subOrganization)
|
||||
isSubOrganization: Boolean(currentOrg.subOrganization),
|
||||
isRootOrganization: !currentOrg.subOrganization
|
||||
};
|
||||
};
|
||||
|
||||
2
frontend/src/hooks/api/orgIdentityMembership/index.tsx
Normal file
2
frontend/src/hooks/api/orgIdentityMembership/index.tsx
Normal file
@@ -0,0 +1,2 @@
|
||||
export { useCreateOrgIdentityMembership, useDeleteOrgIdentityMembership } from "./mutation";
|
||||
export type { TCreateOrgIdentityMembershipDTO, TDeleteOrgIdentityMembershipDTO, TOrgIdentityMembership } from "./types";
|
||||
42
frontend/src/hooks/api/orgIdentityMembership/mutation.tsx
Normal file
42
frontend/src/hooks/api/orgIdentityMembership/mutation.tsx
Normal file
@@ -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"] });
|
||||
}
|
||||
});
|
||||
};
|
||||
30
frontend/src/hooks/api/orgIdentityMembership/types.ts
Normal file
30
frontend/src/hooks/api/orgIdentityMembership/types.ts
Normal file
@@ -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 {
|
||||
useAddOrgPmtMethod,
|
||||
useGetAvailableOrgIdentities,
|
||||
useAddOrgTaxId,
|
||||
useCreateCustomerPortalSession,
|
||||
useCreateOrg,
|
||||
|
||||
@@ -42,7 +42,9 @@ export const organizationKeys = {
|
||||
[...organizationKeys.getOrgIdentityMemberships(orgId), params] as const,
|
||||
getOrgGroups: (orgId: string) => [{ orgId }, "organization-groups"] 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 () => {
|
||||
@@ -574,3 +576,29 @@ export const useGetOrgIntegrationAuths = <TData = IntegrationAuth[],>(
|
||||
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 { UpgradePlanModal } from "@app/components/license/UpgradePlanModal";
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { OrgPermissionCan } from "@app/components/permissions";
|
||||
import { Button, DeleteActionModal } from "@app/components/v2";
|
||||
import { Button, DeleteActionModal, Modal, ModalContent } from "@app/components/v2";
|
||||
import {
|
||||
OrgPermissionIdentityActions,
|
||||
OrgPermissionSubjects,
|
||||
@@ -23,11 +28,12 @@ import { IdentityModal } from "./IdentityModal";
|
||||
import { IdentityTable } from "./IdentityTable";
|
||||
import { IdentityTokenAuthTokenModal } from "./IdentityTokenAuthTokenModal";
|
||||
import { MachineAuthTemplateUsagesModal } from "./MachineAuthTemplateUsagesModal";
|
||||
import { IdentityLinkForm } from "./IdentityLinkForm";
|
||||
|
||||
export const IdentitySection = withPermission(
|
||||
() => {
|
||||
const { subscription } = useSubscription();
|
||||
const { currentOrg } = useOrganization();
|
||||
const { currentOrg, isSubOrganization } = useOrganization();
|
||||
const orgId = currentOrg?.id || "";
|
||||
|
||||
const { mutateAsync: deleteMutateAsync } = useDeleteIdentity();
|
||||
@@ -43,7 +49,8 @@ export const IdentitySection = withPermission(
|
||||
"createTemplate",
|
||||
"editTemplate",
|
||||
"deleteTemplate",
|
||||
"viewUsages"
|
||||
"viewUsages",
|
||||
"linkIdentity"
|
||||
] as const);
|
||||
|
||||
const isMoreIdentitiesAllowed = subscription?.identityLimit
|
||||
@@ -105,8 +112,8 @@ export const IdentitySection = withPermission(
|
||||
return (
|
||||
<div>
|
||||
<div className="rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
||||
<div className="mb-4 flex items-center justify-between">
|
||||
<div className="flex items-center gap-1">
|
||||
<div className="mb-4 flex w-full items-center gap-4">
|
||||
<div className="flex flex-1 items-center gap-1">
|
||||
<p className="text-xl font-medium text-mineshaft-100">Identities</p>
|
||||
<a
|
||||
href="https://infisical.com/docs/documentation/platform/identities/overview"
|
||||
@@ -123,6 +130,26 @@ export const IdentitySection = withPermission(
|
||||
</div>
|
||||
</a>
|
||||
</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
|
||||
I={OrgPermissionIdentityActions.Create}
|
||||
a={OrgPermissionSubjects.Identity}
|
||||
@@ -210,16 +237,17 @@ export const IdentitySection = withPermission(
|
||||
?.name || ""
|
||||
}
|
||||
/>
|
||||
{/* <IdentityAuthMethodModal
|
||||
popUp={popUp}
|
||||
handlePopUpOpen={handlePopUpOpen}
|
||||
handlePopUpToggle={handlePopUpToggle}
|
||||
/> */}
|
||||
{/* <IdentityUniversalAuthClientSecretModal
|
||||
popUp={popUp}
|
||||
handlePopUpOpen={handlePopUpOpen}
|
||||
handlePopUpToggle={handlePopUpToggle}
|
||||
/> */}
|
||||
<Modal
|
||||
isOpen={popUp.linkIdentity.isOpen}
|
||||
onOpenChange={(isOpen) => handlePopUpToggle("linkIdentity", isOpen)}
|
||||
>
|
||||
<ModalContent
|
||||
title="Assign Existing Identity"
|
||||
subTitle="Assign an existing identity from your organization or namespace to this project. The identity will continue to be managed at its original scope."
|
||||
>
|
||||
<IdentityLinkForm onClose={() => handlePopUpClose("linkIdentity")} />
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
<IdentityTokenAuthTokenModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
|
||||
<DeleteActionModal
|
||||
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";
|
||||
|
||||
@@ -6,7 +6,14 @@ export const Route = createFileRoute(
|
||||
"/_authenticate/_inject-org-details/_org-layout/organization/billing"
|
||||
)({
|
||||
component: BillingPage,
|
||||
beforeLoad: () => {
|
||||
beforeLoad: ({ search }) => {
|
||||
if (search.subOrganization) {
|
||||
throw redirect({
|
||||
to: "/organization/projects",
|
||||
search
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
breadcrumbs: [
|
||||
{
|
||||
|
||||
@@ -14,25 +14,39 @@ import { OrgSecurityTab } from "../OrgSecurityTab";
|
||||
import { OrgSsoTab } from "../OrgSsoTab";
|
||||
import { OrgWorkflowIntegrationTab } from "../OrgWorkflowIntegrationTab";
|
||||
import { ProjectTemplatesTab } from "../ProjectTemplatesTab";
|
||||
import { useOrganization } from "@app/context";
|
||||
|
||||
export const OrgTabGroup = () => {
|
||||
const search = useSearch({
|
||||
from: ROUTE_PATHS.Organization.SettingsPage.id
|
||||
});
|
||||
const { isSubOrganization } = useOrganization();
|
||||
|
||||
const tabs = [
|
||||
{ name: "General", key: "tab-org-general", component: OrgGeneralTab },
|
||||
{
|
||||
name: "SSO",
|
||||
key: "sso-settings",
|
||||
component: OrgSsoTab
|
||||
component: OrgSsoTab,
|
||||
isHidden: isSubOrganization
|
||||
},
|
||||
{
|
||||
name: "Provisioning",
|
||||
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",
|
||||
key: "workflow-integrations",
|
||||
@@ -57,17 +71,21 @@ export const OrgTabGroup = () => {
|
||||
return (
|
||||
<Tabs orientation="vertical" value={selectedTab} onValueChange={setSelectedTab}>
|
||||
<TabList>
|
||||
{tabs.map((tab) => (
|
||||
<Tab variant="org" value={tab.key} key={tab.key}>
|
||||
{tab.name}
|
||||
</Tab>
|
||||
))}
|
||||
{tabs
|
||||
.filter((el) => !el.isHidden)
|
||||
.map((tab) => (
|
||||
<Tab variant="org" value={tab.key} key={tab.key}>
|
||||
{tab.name}
|
||||
</Tab>
|
||||
))}
|
||||
</TabList>
|
||||
{tabs.map(({ key, component: Component }) => (
|
||||
<TabPanel value={key} key={`tab-panel-${key}`}>
|
||||
<Component />
|
||||
</TabPanel>
|
||||
))}
|
||||
{tabs
|
||||
.filter((el) => !el.isHidden)
|
||||
.map(({ key, component: Component }) => (
|
||||
<TabPanel value={key} key={`tab-panel-${key}`}>
|
||||
<Component />
|
||||
</TabPanel>
|
||||
))}
|
||||
</Tabs>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user