feat: added create identity project membership to api reference and support for roles

This commit is contained in:
=
2024-05-17 17:09:35 +05:30
parent 21656a7ab6
commit 56c2e12760
9 changed files with 155 additions and 36 deletions
+21 -1
View File
@@ -211,11 +211,31 @@ export const PROJECT_IDENTITIES = {
UPDATE_IDENTITY_MEMBERSHIP: { UPDATE_IDENTITY_MEMBERSHIP: {
projectId: "The ID of the project to update the identity membership for.", projectId: "The ID of the project to update the identity membership for.",
identityId: "The ID of the identity to update the membership for.", identityId: "The ID of the identity to update the membership for.",
roles: "A list of roles to update the membership to." roles: {
description: "A list of role slugs to assign to the identity project membership.",
role: "The role slug to assign to the newly created identity project membership.",
isTemporary: "Whether the assigned role is temporary.",
temporaryMode: "Type of temporary expiry.",
temporaryRange: "Expiry time for temporary access. In relative mode it could be 1s,2m,3h",
temporaryAccessStartTime: "Time to which the temporary access starts"
}
}, },
DELETE_IDENTITY_MEMBERSHIP: { DELETE_IDENTITY_MEMBERSHIP: {
projectId: "The ID of the project to delete the identity membership from.", projectId: "The ID of the project to delete the identity membership from.",
identityId: "The ID of the identity to delete the membership from." identityId: "The ID of the identity to delete the membership from."
},
CREATE_IDENTITY_MEMBERSHIP: {
projectId: "The ID of the project to create the identity membership from.",
identityId: "The ID of the identity to create the membership from.",
role: "The role slug to assign to the newly created identity project membership.",
roles: {
description: "A list of role slugs to assign to the newly created identity project membership.",
role: "The role slug to assign to the newly created identity project membership.",
isTemporary: "Whether the assigned role is temporary.",
temporaryMode: "Type of temporary expiry.",
temporaryRange: "Expiry time for temporary access. In relative mode it could be 1s,2m,3h",
temporaryAccessStartTime: "Time to which the temporary access starts"
}
} }
}; };
@@ -8,6 +8,7 @@ import {
ProjectUserMembershipRolesSchema ProjectUserMembershipRolesSchema
} from "@app/db/schemas"; } from "@app/db/schemas";
import { PROJECT_IDENTITIES } from "@app/lib/api-docs"; import { PROJECT_IDENTITIES } from "@app/lib/api-docs";
import { BadRequestError } from "@app/lib/errors";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type"; import { AuthMode } from "@app/services/auth/auth-type";
@@ -22,12 +23,48 @@ export const registerIdentityProjectRouter = async (server: FastifyZodProvider)
}, },
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: { schema: {
description: "Create project identity membership",
security: [
{
bearerAuth: []
}
],
params: z.object({ params: z.object({
projectId: z.string().trim(), projectId: z.string().trim(),
identityId: z.string().trim() identityId: z.string().trim()
}), }),
body: z.object({ body: z.object({
role: z.string().trim().min(1).default(ProjectMembershipRole.NoAccess) // @depreciated
role: z.string().trim().optional().default(ProjectMembershipRole.NoAccess),
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(ProjectUserMembershipTemporaryMode)
.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)
.optional()
}), }),
response: { response: {
200: z.object({ 200: z.object({
@@ -36,6 +73,9 @@ export const registerIdentityProjectRouter = async (server: FastifyZodProvider)
} }
}, },
handler: async (req) => { handler: async (req) => {
const { role, roles } = req.body;
if (!role && !roles) throw new BadRequestError({ message: "You must provide either role or roles field" });
const identityMembership = await server.services.identityProject.createProjectIdentity({ const identityMembership = await server.services.identityProject.createProjectIdentity({
actor: req.permission.type, actor: req.permission.type,
actorId: req.permission.id, actorId: req.permission.id,
@@ -43,7 +83,7 @@ export const registerIdentityProjectRouter = async (server: FastifyZodProvider)
actorOrgId: req.permission.orgId, actorOrgId: req.permission.orgId,
identityId: req.params.identityId, identityId: req.params.identityId,
projectId: req.params.projectId, projectId: req.params.projectId,
role: req.body.role roles: roles || [{ role }]
}); });
return { identityMembership }; return { identityMembership };
} }
@@ -72,20 +112,31 @@ export const registerIdentityProjectRouter = async (server: FastifyZodProvider)
.array( .array(
z.union([ z.union([
z.object({ z.object({
role: z.string(), role: z.string().describe(PROJECT_IDENTITIES.UPDATE_IDENTITY_MEMBERSHIP.roles.role),
isTemporary: z.literal(false).default(false) isTemporary: z
.literal(false)
.default(false)
.describe(PROJECT_IDENTITIES.UPDATE_IDENTITY_MEMBERSHIP.roles.isTemporary)
}), }),
z.object({ z.object({
role: z.string(), role: z.string().describe(PROJECT_IDENTITIES.UPDATE_IDENTITY_MEMBERSHIP.roles.role),
isTemporary: z.literal(true), isTemporary: z.literal(true).describe(PROJECT_IDENTITIES.UPDATE_IDENTITY_MEMBERSHIP.roles.isTemporary),
temporaryMode: z.nativeEnum(ProjectUserMembershipTemporaryMode), temporaryMode: z
temporaryRange: z.string().refine((val) => ms(val) > 0, "Temporary range must be a positive number"), .nativeEnum(ProjectUserMembershipTemporaryMode)
temporaryAccessStartTime: z.string().datetime() .describe(PROJECT_IDENTITIES.UPDATE_IDENTITY_MEMBERSHIP.roles.temporaryMode),
temporaryRange: z
.string()
.refine((val) => ms(val) > 0, "Temporary range must be a positive number")
.describe(PROJECT_IDENTITIES.UPDATE_IDENTITY_MEMBERSHIP.roles.temporaryRange),
temporaryAccessStartTime: z
.string()
.datetime()
.describe(PROJECT_IDENTITIES.UPDATE_IDENTITY_MEMBERSHIP.roles.temporaryAccessStartTime)
}) })
]) ])
) )
.min(1) .min(1)
.describe(PROJECT_IDENTITIES.UPDATE_IDENTITY_MEMBERSHIP.roles) .describe(PROJECT_IDENTITIES.UPDATE_IDENTITY_MEMBERSHIP.roles.description)
}), }),
response: { response: {
200: z.object({ 200: z.object({
@@ -51,7 +51,7 @@ export const identityProjectServiceFactory = ({
actorOrgId, actorOrgId,
actorAuthMethod, actorAuthMethod,
projectId, projectId,
role roles
}: TCreateProjectIdentityDTO) => { }: TCreateProjectIdentityDTO) => {
const { permission } = await permissionService.getProjectPermission( const { permission } = await permissionService.getProjectPermission(
actor, actor,
@@ -78,18 +78,33 @@ export const identityProjectServiceFactory = ({
message: `Failed to find identity with id ${identityId}` message: `Failed to find identity with id ${identityId}`
}); });
const { permission: rolePermission, role: customRole } = await permissionService.getProjectPermissionByRole( for await (const { role: requestedRoleChange } of roles) {
role, const { permission: rolePermission } = await permissionService.getProjectPermissionByRole(
project.id requestedRoleChange,
projectId
);
const hasRequiredPriviledges = isAtLeastAsPrivileged(permission, rolePermission);
if (!hasRequiredPriviledges) {
throw new ForbiddenRequestError({ message: "Failed to change to a more privileged role" });
}
}
// validate custom roles input
const customInputRoles = roles.filter(
({ role }) => !Object.values(ProjectMembershipRole).includes(role as ProjectMembershipRole)
); );
const hasCustomRole = Boolean(customInputRoles.length);
const customRoles = hasCustomRole
? await projectRoleDAL.find({
projectId,
$in: { slug: customInputRoles.map(({ role }) => role) }
})
: [];
if (customRoles.length !== customInputRoles.length) throw new BadRequestError({ message: "Custom role not found" });
const hasPriviledge = isAtLeastAsPrivileged(permission, rolePermission); const customRolesGroupBySlug = groupBy(customRoles, ({ slug }) => slug);
if (!hasPriviledge)
throw new ForbiddenRequestError({
message: "Failed to add identity to project with more privileged role"
});
const isCustomRole = Boolean(customRole);
const projectIdentity = await identityProjectDAL.transaction(async (tx) => { const projectIdentity = await identityProjectDAL.transaction(async (tx) => {
const identityProjectMembership = await identityProjectDAL.create( const identityProjectMembership = await identityProjectDAL.create(
{ {
@@ -98,16 +113,32 @@ export const identityProjectServiceFactory = ({
}, },
tx tx
); );
const sanitizedProjectMembershipRoles = roles.map((inputRole) => {
const isCustomRole = Boolean(customRolesGroupBySlug?.[inputRole.role]?.[0]);
if (!inputRole.isTemporary) {
return {
projectMembershipId: identityProjectMembership.id,
role: isCustomRole ? ProjectMembershipRole.Custom : inputRole.role,
customRoleId: customRolesGroupBySlug[inputRole.role] ? customRolesGroupBySlug[inputRole.role][0].id : null
};
}
await identityProjectMembershipRoleDAL.create( // check cron or relative here later for now its just relative
{ const relativeTimeInMs = ms(inputRole.temporaryRange);
return {
projectMembershipId: identityProjectMembership.id, projectMembershipId: identityProjectMembership.id,
role: isCustomRole ? ProjectMembershipRole.Custom : role, role: isCustomRole ? ProjectMembershipRole.Custom : inputRole.role,
customRoleId: customRole?.id customRoleId: customRolesGroupBySlug[inputRole.role] ? customRolesGroupBySlug[inputRole.role][0].id : null,
}, isTemporary: true,
tx temporaryMode: ProjectUserMembershipTemporaryMode.Relative,
); temporaryRange: inputRole.temporaryRange,
return identityProjectMembership; temporaryAccessStartTime: new Date(inputRole.temporaryAccessStartTime),
temporaryAccessEndTime: new Date(new Date(inputRole.temporaryAccessStartTime).getTime() + relativeTimeInMs)
};
});
const identityRoles = await identityProjectMembershipRoleDAL.insertMany(sanitizedProjectMembershipRoles, tx);
return { ...identityProjectMembership, roles: identityRoles };
}); });
return projectIdentity; return projectIdentity;
}; };
@@ -4,7 +4,19 @@ import { ProjectUserMembershipTemporaryMode } from "../project-membership/projec
export type TCreateProjectIdentityDTO = { export type TCreateProjectIdentityDTO = {
identityId: string; identityId: string;
role: string; roles: (
| {
role: string;
isTemporary?: false;
}
| {
role: string;
isTemporary: true;
temporaryMode: ProjectUserMembershipTemporaryMode.Relative;
temporaryRange: string;
temporaryAccessStartTime: string;
}
)[];
} & TProjectPermission; } & TProjectPermission;
export type TUpdateProjectIdentityDTO = { export type TUpdateProjectIdentityDTO = {
@@ -0,0 +1,4 @@
---
title: "Add Identity Project Membership"
openapi: "POST /api/v2/workspace/{projectId}/identity-memberships/{identityId}"
---
@@ -1,4 +1,4 @@
--- ---
title: "Delete Identity Membership" title: "Delete Project Identity Membership"
openapi: "DELETE /api/v2/workspace/{projectId}/identity-memberships/{identityId}" openapi: "DELETE /api/v2/workspace/{projectId}/identity-memberships/{identityId}"
--- ---
@@ -1,4 +1,4 @@
--- ---
title: "List Identity Memberships" title: "List Project Identity Memberships"
openapi: "GET /api/v2/workspace/{projectId}/identity-memberships" openapi: "GET /api/v2/workspace/{projectId}/identity-memberships"
--- ---
@@ -1,4 +1,4 @@
--- ---
title: "Update Identity Membership" title: "Update Project Identity Membership"
openapi: "PATCH /api/v2/workspace/{projectId}/identity-memberships/{identityId}" openapi: "PATCH /api/v2/workspace/{projectId}/identity-memberships/{identityId}"
--- ---
+3 -2
View File
@@ -1,6 +1,6 @@
{ {
"name": "Infisical", "name": "Infisical",
"openapi": "https://app.infisical.com/api/docs/json", "openapi": "http://localhost:8080/api/docs/json",
"logo": { "logo": {
"dark": "/logo/dark.svg", "dark": "/logo/dark.svg",
"light": "/logo/light.svg", "light": "/logo/light.svg",
@@ -463,8 +463,9 @@
] ]
}, },
{ {
"group": "Projects", "group": "Project Identities",
"pages": [ "pages": [
"api-reference/endpoints/project-identities/add-identity-membership",
"api-reference/endpoints/project-identities/list-identity-memberships", "api-reference/endpoints/project-identities/list-identity-memberships",
"api-reference/endpoints/project-identities/update-identity-membership", "api-reference/endpoints/project-identities/update-identity-membership",
"api-reference/endpoints/project-identities/delete-identity-membership" "api-reference/endpoints/project-identities/delete-identity-membership"