mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-06 14:27:59 +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,
|
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
|
||||||
|
|||||||
@@ -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 }
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -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
|
||||||
|
});
|
||||||
|
|||||||
+129
@@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
+44
-16
@@ -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: [
|
||||||
{
|
{
|
||||||
|
|||||||
+32
-14
@@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user