feat: added v1 projects project-role-router

This commit is contained in:
=
2025-09-11 16:24:58 +05:30
parent ca877cb225
commit d3acc90d6a
6 changed files with 361 additions and 58 deletions

View File

@@ -0,0 +1,287 @@
import { packRules } from "@casl/ability/extra";
import { z } from "zod";
import { ProjectMembershipRole, ProjectRolesSchema } from "@app/db/schemas";
import {
backfillPermissionV1SchemaToV2Schema,
ProjectPermissionV1Schema
} from "@app/ee/services/permission/project-permission";
import { PROJECT_ROLE } from "@app/lib/api-docs";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { slugSchema } from "@app/server/lib/schemas";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { SanitizedRoleSchemaV1 } from "@app/server/routes/sanitizedSchemas";
import { AuthMode } from "@app/services/auth/auth-type";
import { ProjectRoleServiceIdentifierType } from "@app/services/project-role/project-role-types";
export const registerDepreciatedProjectRoleRouter = async (server: FastifyZodProvider) => {
server.route({
method: "POST",
url: "/:projectSlug/roles",
config: {
rateLimit: writeLimit
},
schema: {
description: "Create a project role",
security: [
{
bearerAuth: []
}
],
params: z.object({
projectSlug: z.string().trim().describe(PROJECT_ROLE.CREATE.projectSlug)
}),
body: z.object({
slug: slugSchema({ max: 64 })
.refine(
(val) => !Object.values(ProjectMembershipRole).includes(val as ProjectMembershipRole),
"Please choose a different slug, the slug you have entered is reserved"
)
.describe(PROJECT_ROLE.CREATE.slug),
name: z.string().min(1).trim().describe(PROJECT_ROLE.CREATE.name),
description: z.string().trim().nullish().describe(PROJECT_ROLE.CREATE.description),
permissions: ProjectPermissionV1Schema.array().describe(PROJECT_ROLE.CREATE.permissions)
}),
response: {
200: z.object({
role: SanitizedRoleSchemaV1
})
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const role = await server.services.projectRole.createRole({
actorAuthMethod: req.permission.authMethod,
actorId: req.permission.id,
actorOrgId: req.permission.orgId,
actor: req.permission.type,
filter: {
type: ProjectRoleServiceIdentifierType.SLUG,
projectSlug: req.params.projectSlug
},
data: {
...req.body,
permissions: JSON.stringify(packRules(backfillPermissionV1SchemaToV2Schema(req.body.permissions, true)))
}
});
return { role };
}
});
server.route({
method: "PATCH",
url: "/:projectSlug/roles/:roleId",
config: {
rateLimit: writeLimit
},
schema: {
description: "Update a project role",
security: [
{
bearerAuth: []
}
],
params: z.object({
projectSlug: z.string().trim().describe(PROJECT_ROLE.UPDATE.projectSlug),
roleId: z.string().trim().describe(PROJECT_ROLE.UPDATE.roleId)
}),
body: z.object({
slug: slugSchema({ max: 64 })
.refine(
(val) => !Object.values(ProjectMembershipRole).includes(val as ProjectMembershipRole),
"Please choose a different slug, the slug you have entered is reserved"
)
.describe(PROJECT_ROLE.UPDATE.slug)
.optional(),
name: z.string().trim().optional().describe(PROJECT_ROLE.UPDATE.name),
description: z.string().trim().nullish().describe(PROJECT_ROLE.UPDATE.description),
permissions: ProjectPermissionV1Schema.array().describe(PROJECT_ROLE.UPDATE.permissions).optional()
}),
response: {
200: z.object({
role: SanitizedRoleSchemaV1
})
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const role = await server.services.projectRole.updateRole({
actorAuthMethod: req.permission.authMethod,
actorId: req.permission.id,
actorOrgId: req.permission.orgId,
actor: req.permission.type,
roleId: req.params.roleId,
data: {
...req.body,
permissions: req.body.permissions
? JSON.stringify(packRules(backfillPermissionV1SchemaToV2Schema(req.body.permissions, true)))
: undefined
}
});
return { role };
}
});
server.route({
method: "DELETE",
url: "/:projectSlug/roles/:roleId",
config: {
rateLimit: writeLimit
},
schema: {
description: "Delete a project role",
security: [
{
bearerAuth: []
}
],
params: z.object({
projectSlug: z.string().trim().describe(PROJECT_ROLE.DELETE.projectSlug),
roleId: z.string().trim().describe(PROJECT_ROLE.DELETE.roleId)
}),
response: {
200: z.object({
role: SanitizedRoleSchemaV1
})
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const role = await server.services.projectRole.deleteRole({
actorAuthMethod: req.permission.authMethod,
actorId: req.permission.id,
actorOrgId: req.permission.orgId,
actor: req.permission.type,
roleId: req.params.roleId
});
return { role };
}
});
server.route({
method: "GET",
url: "/:projectSlug/roles",
config: {
rateLimit: readLimit
},
schema: {
description: "List project role",
security: [
{
bearerAuth: []
}
],
params: z.object({
projectSlug: z.string().trim().describe(PROJECT_ROLE.LIST.projectSlug)
}),
response: {
200: z.object({
roles: ProjectRolesSchema.omit({ permissions: true, version: true }).array()
})
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const roles = await server.services.projectRole.listRoles({
actorAuthMethod: req.permission.authMethod,
actorId: req.permission.id,
actorOrgId: req.permission.orgId,
actor: req.permission.type,
filter: {
type: ProjectRoleServiceIdentifierType.SLUG,
projectSlug: req.params.projectSlug
}
});
return { roles };
}
});
server.route({
method: "GET",
url: "/:projectSlug/roles/slug/:slug",
config: {
rateLimit: readLimit
},
schema: {
params: z.object({
projectSlug: z.string().trim().describe(PROJECT_ROLE.GET_ROLE_BY_SLUG.projectSlug),
slug: z.string().trim().describe(PROJECT_ROLE.GET_ROLE_BY_SLUG.roleSlug)
}),
response: {
200: z.object({
role: SanitizedRoleSchemaV1.omit({ version: true })
})
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const role = await server.services.projectRole.getRoleBySlug({
actorAuthMethod: req.permission.authMethod,
actorId: req.permission.id,
actorOrgId: req.permission.orgId,
actor: req.permission.type,
filter: {
type: ProjectRoleServiceIdentifierType.SLUG,
projectSlug: req.params.projectSlug
},
roleSlug: req.params.slug
});
return { role };
}
});
server.route({
method: "GET",
url: "/:projectId/permissions",
config: {
rateLimit: readLimit
},
schema: {
params: z.object({
projectId: z.string().trim()
}),
response: {
200: z.object({
data: z.object({
membership: z.object({
id: z.string(),
roles: z
.object({
role: z.string()
})
.array()
}),
assumedPrivilegeDetails: z
.object({
actorId: z.string(),
actorType: z.string(),
actorName: z.string(),
actorEmail: z.string().optional()
})
.optional(),
permissions: z.any().array()
})
})
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
const { permissions, membership, assumedPrivilegeDetails } = await server.services.projectRole.getUserPermission(
req.permission.id,
req.params.projectId,
req.permission.authMethod,
req.permission.orgId
);
return {
data: {
permissions,
membership,
assumedPrivilegeDetails
}
};
}
});
};

View File

@@ -22,6 +22,7 @@ import { registerOidcRouter } from "./oidc-router";
import { registerOrgRoleRouter } from "./org-role-router";
import { registerPITRouter } from "./pit-router";
import { registerProjectRoleRouter } from "./project-role-router";
import { registerDepreciatedProjectRoleRouter } from "./depreciated-project-role-router";
import { registerProjectRouter } from "./project-router";
import { registerRateLimitRouter } from "./rate-limit-router";
import { registerRelayRouter } from "./relay-router";
@@ -49,13 +50,21 @@ export const registerV1EERoutes = async (server: FastifyZodProvider) => {
await server.register(registerLicenseRouter, { prefix: "/organizations" });
await server.register(
async (projectRouter) => {
await projectRouter.register(registerProjectRoleRouter);
await projectRouter.register(registerDepreciatedProjectRoleRouter);
await projectRouter.register(registerProjectRouter);
await projectRouter.register(registerTrustedIpRouter);
await projectRouter.register(registerAssumePrivilegeRouter);
},
{ prefix: "/workspace" }
);
await server.register(
async (projectRouter) => {
await projectRouter.register(registerProjectRoleRouter);
},
{ prefix: "/projects" }
);
await server.register(registerSnapshotRouter, { prefix: "/secret-snapshot" });
await server.register(registerPITRouter, { prefix: "/pit" });
await server.register(registerSecretApprovalPolicyRouter, { prefix: "/secret-approvals" });

View File

@@ -2,26 +2,26 @@ import { packRules } from "@casl/ability/extra";
import { z } from "zod";
import { ProjectMembershipRole, ProjectRolesSchema } from "@app/db/schemas";
import {
backfillPermissionV1SchemaToV2Schema,
ProjectPermissionV1Schema
} from "@app/ee/services/permission/project-permission";
import { PROJECT_ROLE } from "@app/lib/api-docs";
import { checkForInvalidPermissionCombination } from "@app/ee/services/permission/permission-fns";
import { ProjectPermissionV2Schema } from "@app/ee/services/permission/project-permission";
import { ApiDocsTags, PROJECT_ROLE } from "@app/lib/api-docs";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { slugSchema } from "@app/server/lib/schemas";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { SanitizedRoleSchemaV1 } from "@app/server/routes/sanitizedSchemas";
import { SanitizedRoleSchema } from "@app/server/routes/sanitizedSchemas";
import { AuthMode } from "@app/services/auth/auth-type";
import { ProjectRoleServiceIdentifierType } from "@app/services/project-role/project-role-types";
export const registerProjectRoleRouter = async (server: FastifyZodProvider) => {
server.route({
method: "POST",
url: "/:projectSlug/roles",
url: "/:projectId/roles",
config: {
rateLimit: writeLimit
},
schema: {
hide: false,
tags: [ApiDocsTags.ProjectRoles],
description: "Create a project role",
security: [
{
@@ -29,10 +29,10 @@ export const registerProjectRoleRouter = async (server: FastifyZodProvider) => {
}
],
params: z.object({
projectSlug: z.string().trim().describe(PROJECT_ROLE.CREATE.projectSlug)
projectId: z.string().trim().describe(PROJECT_ROLE.CREATE.projectId)
}),
body: z.object({
slug: slugSchema({ max: 64 })
slug: slugSchema({ min: 1, max: 64 })
.refine(
(val) => !Object.values(ProjectMembershipRole).includes(val as ProjectMembershipRole),
"Please choose a different slug, the slug you have entered is reserved"
@@ -40,11 +40,13 @@ export const registerProjectRoleRouter = async (server: FastifyZodProvider) => {
.describe(PROJECT_ROLE.CREATE.slug),
name: z.string().min(1).trim().describe(PROJECT_ROLE.CREATE.name),
description: z.string().trim().nullish().describe(PROJECT_ROLE.CREATE.description),
permissions: ProjectPermissionV1Schema.array().describe(PROJECT_ROLE.CREATE.permissions)
permissions: ProjectPermissionV2Schema.array()
.describe(PROJECT_ROLE.CREATE.permissions)
.refine(checkForInvalidPermissionCombination)
}),
response: {
200: z.object({
role: SanitizedRoleSchemaV1
role: SanitizedRoleSchema
})
}
},
@@ -56,26 +58,27 @@ export const registerProjectRoleRouter = async (server: FastifyZodProvider) => {
actorOrgId: req.permission.orgId,
actor: req.permission.type,
filter: {
type: ProjectRoleServiceIdentifierType.SLUG,
projectSlug: req.params.projectSlug
type: ProjectRoleServiceIdentifierType.ID,
projectId: req.params.projectId
},
data: {
...req.body,
permissions: JSON.stringify(packRules(backfillPermissionV1SchemaToV2Schema(req.body.permissions, true)))
permissions: JSON.stringify(packRules(req.body.permissions))
}
});
return { role };
}
});
server.route({
method: "PATCH",
url: "/:projectSlug/roles/:roleId",
url: "/:projectId/roles/:roleId",
config: {
rateLimit: writeLimit
},
schema: {
hide: false,
tags: [ApiDocsTags.ProjectRoles],
description: "Update a project role",
security: [
{
@@ -83,24 +86,27 @@ export const registerProjectRoleRouter = async (server: FastifyZodProvider) => {
}
],
params: z.object({
projectSlug: z.string().trim().describe(PROJECT_ROLE.UPDATE.projectSlug),
projectId: z.string().trim().describe(PROJECT_ROLE.UPDATE.projectId),
roleId: z.string().trim().describe(PROJECT_ROLE.UPDATE.roleId)
}),
body: z.object({
slug: slugSchema({ max: 64 })
slug: slugSchema({ min: 1, max: 64 })
.refine(
(val) => !Object.values(ProjectMembershipRole).includes(val as ProjectMembershipRole),
"Please choose a different slug, the slug you have entered is reserved"
)
.describe(PROJECT_ROLE.UPDATE.slug)
.optional(),
.optional()
.describe(PROJECT_ROLE.UPDATE.slug),
name: z.string().trim().optional().describe(PROJECT_ROLE.UPDATE.name),
description: z.string().trim().nullish().describe(PROJECT_ROLE.UPDATE.description),
permissions: ProjectPermissionV1Schema.array().describe(PROJECT_ROLE.UPDATE.permissions).optional()
permissions: ProjectPermissionV2Schema.array()
.describe(PROJECT_ROLE.UPDATE.permissions)
.optional()
.superRefine(checkForInvalidPermissionCombination)
}),
response: {
200: z.object({
role: SanitizedRoleSchemaV1
role: SanitizedRoleSchema
})
}
},
@@ -114,9 +120,7 @@ export const registerProjectRoleRouter = async (server: FastifyZodProvider) => {
roleId: req.params.roleId,
data: {
...req.body,
permissions: req.body.permissions
? JSON.stringify(packRules(backfillPermissionV1SchemaToV2Schema(req.body.permissions, true)))
: undefined
permissions: req.body.permissions ? JSON.stringify(packRules(req.body.permissions)) : undefined
}
});
return { role };
@@ -125,11 +129,13 @@ export const registerProjectRoleRouter = async (server: FastifyZodProvider) => {
server.route({
method: "DELETE",
url: "/:projectSlug/roles/:roleId",
url: "/:projectId/roles/:roleId",
config: {
rateLimit: writeLimit
},
schema: {
hide: false,
tags: [ApiDocsTags.ProjectRoles],
description: "Delete a project role",
security: [
{
@@ -137,12 +143,12 @@ export const registerProjectRoleRouter = async (server: FastifyZodProvider) => {
}
],
params: z.object({
projectSlug: z.string().trim().describe(PROJECT_ROLE.DELETE.projectSlug),
projectId: z.string().trim().describe(PROJECT_ROLE.DELETE.projectId),
roleId: z.string().trim().describe(PROJECT_ROLE.DELETE.roleId)
}),
response: {
200: z.object({
role: SanitizedRoleSchemaV1
role: SanitizedRoleSchema
})
}
},
@@ -161,11 +167,13 @@ export const registerProjectRoleRouter = async (server: FastifyZodProvider) => {
server.route({
method: "GET",
url: "/:projectSlug/roles",
url: "/:projectId/roles",
config: {
rateLimit: readLimit
},
schema: {
hide: false,
tags: [ApiDocsTags.ProjectRoles],
description: "List project role",
security: [
{
@@ -173,7 +181,7 @@ export const registerProjectRoleRouter = async (server: FastifyZodProvider) => {
}
],
params: z.object({
projectSlug: z.string().trim().describe(PROJECT_ROLE.LIST.projectSlug)
projectId: z.string().trim().describe(PROJECT_ROLE.LIST.projectId)
}),
response: {
200: z.object({
@@ -189,8 +197,8 @@ export const registerProjectRoleRouter = async (server: FastifyZodProvider) => {
actorOrgId: req.permission.orgId,
actor: req.permission.type,
filter: {
type: ProjectRoleServiceIdentifierType.SLUG,
projectSlug: req.params.projectSlug
type: ProjectRoleServiceIdentifierType.ID,
projectId: req.params.projectId
}
});
return { roles };
@@ -199,18 +207,20 @@ export const registerProjectRoleRouter = async (server: FastifyZodProvider) => {
server.route({
method: "GET",
url: "/:projectSlug/roles/slug/:slug",
url: "/:projectId/roles/slug/:roleSlug",
config: {
rateLimit: readLimit
},
schema: {
hide: false,
tags: [ApiDocsTags.ProjectRoles],
params: z.object({
projectSlug: z.string().trim().describe(PROJECT_ROLE.GET_ROLE_BY_SLUG.projectSlug),
slug: z.string().trim().describe(PROJECT_ROLE.GET_ROLE_BY_SLUG.roleSlug)
projectId: z.string().trim().describe(PROJECT_ROLE.GET_ROLE_BY_SLUG.projectId),
roleSlug: z.string().trim().describe(PROJECT_ROLE.GET_ROLE_BY_SLUG.roleSlug)
}),
response: {
200: z.object({
role: SanitizedRoleSchemaV1.omit({ version: true })
role: SanitizedRoleSchema.omit({ version: true })
})
}
},
@@ -222,12 +232,11 @@ export const registerProjectRoleRouter = async (server: FastifyZodProvider) => {
actorOrgId: req.permission.orgId,
actor: req.permission.type,
filter: {
type: ProjectRoleServiceIdentifierType.SLUG,
projectSlug: req.params.projectSlug
type: ProjectRoleServiceIdentifierType.ID,
projectId: req.params.projectId
},
roleSlug: req.params.slug
roleSlug: req.params.roleSlug
});
return { role };
}
});

View File

@@ -22,7 +22,7 @@ export const useCreateProjectRole = () => {
mutationFn: async ({ projectId, ...dto }: TCreateProjectRoleDTO) => {
const {
data: { role }
} = await apiRequest.post(`/api/v2/workspace/${projectId}/roles`, dto);
} = await apiRequest.post(`/api/v1/projects/${projectId}/roles`, dto);
return role;
},
onSuccess: (_, { projectId }) => {
@@ -38,7 +38,7 @@ export const useUpdateProjectRole = () => {
mutationFn: async ({ id, projectId, ...dto }: TUpdateProjectRoleDTO) => {
const {
data: { role }
} = await apiRequest.patch(`/api/v2/workspace/${projectId}/roles/${id}`, dto);
} = await apiRequest.patch(`/api/v1/projects/${projectId}/roles/${id}`, dto);
return role;
},
onSuccess: (_, { projectId, slug }) => {
@@ -58,7 +58,7 @@ export const useDeleteProjectRole = () => {
mutationFn: async ({ projectId, id }: TDeleteProjectRoleDTO) => {
const {
data: { role }
} = await apiRequest.delete(`/api/v2/workspace/${projectId}/roles/${id}`);
} = await apiRequest.delete(`/api/v1/projects/${projectId}/roles/${id}`);
return role;
},
onSuccess: (_, { projectId }) => {

View File

@@ -46,13 +46,13 @@ export const roleQueryKeys = {
getOrgRole: (orgId: string, roleId: string) => [{ orgId, roleId }, "org-role"] as const,
getUserOrgPermissions: ({ orgId }: TGetUserOrgPermissionsDTO) =>
["user-permissions", { orgId }] as const,
getUserProjectPermissions: ({ workspaceId }: TGetUserProjectPermissionDTO) =>
["user-project-permissions", { workspaceId }] as const
getUserProjectPermissions: ({ projectId }: TGetUserProjectPermissionDTO) =>
["user-project-permissions", { projectId }] as const
};
export const getProjectRoles = async (projectId: string) => {
const { data } = await apiRequest.get<{ roles: Array<Omit<TProjectRole, "permissions">> }>(
`/api/v2/workspace/${projectId}/roles`
`/api/v1/projects/${projectId}/roles`
);
return data.roles;
};
@@ -69,7 +69,7 @@ export const useGetProjectRoleBySlug = (projectId: string, roleSlug: string) =>
queryKey: roleQueryKeys.getProjectRoleBySlug(projectId, roleSlug),
queryFn: async () => {
const { data } = await apiRequest.get<{ role: TProjectRole }>(
`/api/v2/workspace/${projectId}/roles/slug/${roleSlug}`
`/api/v1/projects/${projectId}/roles/slug/${roleSlug}`
);
return data.role;
},
@@ -131,9 +131,7 @@ export const useGetUserOrgPermissions = ({ orgId }: TGetUserOrgPermissionsDTO) =
}
});
export const fetchUserProjectPermissions = async ({
workspaceId
}: TGetUserProjectPermissionDTO) => {
export const fetchUserProjectPermissions = async ({ projectId }: TGetUserProjectPermissionDTO) => {
const { data } = await apiRequest.get<{
data: {
permissions: PackRule<RawRuleOf<MongoAbility<OrgPermissionSet>>>[];
@@ -145,16 +143,16 @@ export const fetchUserProjectPermissions = async ({
actorName: string;
};
};
}>(`/api/v1/workspace/${workspaceId}/permissions`, {});
}>(`/api/v1/projects/${projectId}/permissions`, {});
return data.data;
};
export const useGetUserProjectPermissions = ({ workspaceId }: TGetUserProjectPermissionDTO) =>
export const useGetUserProjectPermissions = ({ projectId }: TGetUserProjectPermissionDTO) =>
useQuery({
queryKey: roleQueryKeys.getUserProjectPermissions({ workspaceId }),
queryFn: () => fetchUserProjectPermissions({ workspaceId }),
enabled: Boolean(workspaceId),
queryKey: roleQueryKeys.getUserProjectPermissions({ projectId }),
queryFn: () => fetchUserProjectPermissions({ projectId }),
enabled: Boolean(projectId),
select: (data) => {
const rule = unpackRules<RawRuleOf<MongoAbility<ProjectPermissionSet>>>(data.permissions);
const negatedRules = groupBy(

View File

@@ -9,7 +9,7 @@ export enum ProjectMembershipRole {
}
export type TGetProjectRolesDTO = {
workspaceId?: string;
projectId?: string;
};
export type TProjectRole = {
@@ -52,7 +52,7 @@ export type TGetUserOrgPermissionsDTO = {
};
export type TGetUserProjectPermissionDTO = {
workspaceId: string;
projectId: string;
};
export type TCreateOrgRoleDTO = {