diff --git a/backend/src/ee/routes/v1/identity-project-additional-privilege-router.ts b/backend/src/ee/routes/v1/identity-project-additional-privilege-router.ts index aec3a9c97..df28a627d 100644 --- a/backend/src/ee/routes/v1/identity-project-additional-privilege-router.ts +++ b/backend/src/ee/routes/v1/identity-project-additional-privilege-router.ts @@ -7,6 +7,7 @@ import { z } from "zod"; import { IdentityProjectAdditionalPrivilegeSchema } from "@app/db/schemas"; import { IdentityProjectAdditionalPrivilegeTemporaryMode } from "@app/ee/services/identity-project-additional-privilege/identity-project-additional-privilege-types"; import { ProjectPermissionSet } from "@app/ee/services/permission/project-permission"; +import { IDENTITY_ADDITIONAL_PRIVILEGE } from "@app/lib/api-docs"; import { alphaNumericNanoId } from "@app/lib/nanoid"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { AuthMode } from "@app/services/auth/auth-type"; @@ -18,41 +19,59 @@ export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: F schema: { body: z.union([ z.object({ - identityId: z.string().min(1), - projectSlug: z.string().min(1), + identityId: z.string().min(1).describe(IDENTITY_ADDITIONAL_PRIVILEGE.CREATE.identityId), + projectSlug: z.string().min(1).describe(IDENTITY_ADDITIONAL_PRIVILEGE.CREATE.projectSlug), slug: z .string() .min(1) .max(60) .trim() - .optional() .default(`privilege-${slugify(alphaNumericNanoId(12))}`) + .refine((val) => val.toLowerCase() === val, "Must be lowercase") .refine((v) => slugify(v) === v, { message: "Slug must be a valid slug" - }), - permissions: z.any().array(), - isPackedPermission: z.boolean().optional().default(false), - isTemporary: z.literal(false).default(false) + }) + .describe(IDENTITY_ADDITIONAL_PRIVILEGE.CREATE.slug), + permissions: z.any().array().describe(IDENTITY_ADDITIONAL_PRIVILEGE.CREATE.permissions), + isPackedPermission: z + .boolean() + .optional() + .default(false) + .describe(IDENTITY_ADDITIONAL_PRIVILEGE.CREATE.isPackPermission), + isTemporary: z.literal(false).default(false).describe(IDENTITY_ADDITIONAL_PRIVILEGE.CREATE.isTemporary) }), z.object({ - identityId: z.string(), - projectSlug: z.string().min(1), + identityId: z.string().min(1).describe(IDENTITY_ADDITIONAL_PRIVILEGE.CREATE.identityId), + projectSlug: z.string().min(1).describe(IDENTITY_ADDITIONAL_PRIVILEGE.CREATE.projectSlug), slug: z .string() .min(1) .max(60) .trim() - .optional() .default(`privilege-${slugify(alphaNumericNanoId(12))}`) + .refine((val) => val.toLowerCase() === val, "Must be lowercase") .refine((v) => slugify(v) === v, { message: "Slug must be a valid slug" - }), - permissions: z.any().array(), - isTemporary: z.literal(true), - isPackedPermission: z.boolean().optional().default(false), - temporaryMode: z.nativeEnum(IdentityProjectAdditionalPrivilegeTemporaryMode), - temporaryRange: z.string().refine((val) => ms(val) > 0, "Temporary range must be a positive number"), - temporaryAccessStartTime: z.string().datetime() + }) + .describe(IDENTITY_ADDITIONAL_PRIVILEGE.CREATE.slug), + permissions: z.any().array().describe(IDENTITY_ADDITIONAL_PRIVILEGE.CREATE.permissions), + isPackedPermission: z + .boolean() + .optional() + .default(false) + .describe(IDENTITY_ADDITIONAL_PRIVILEGE.CREATE.isPackPermission), + isTemporary: z.literal(true).describe(IDENTITY_ADDITIONAL_PRIVILEGE.CREATE.isTemporary), + temporaryMode: z + .nativeEnum(IdentityProjectAdditionalPrivilegeTemporaryMode) + .describe(IDENTITY_ADDITIONAL_PRIVILEGE.CREATE.temporaryMode), + temporaryRange: z + .string() + .refine((val) => ms(val) > 0, "Temporary range must be a positive number") + .describe(IDENTITY_ADDITIONAL_PRIVILEGE.CREATE.temporaryRange), + temporaryAccessStartTime: z + .string() + .datetime() + .describe(IDENTITY_ADDITIONAL_PRIVILEGE.CREATE.temporaryAccessStartTime) }) ]), response: { @@ -83,9 +102,9 @@ export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: F schema: { body: z.object({ // disallow empty string - slug: z.string().min(1), - identityId: z.string().min(1), - projectSlug: z.string().min(1), + slug: z.string().min(1).describe(IDENTITY_ADDITIONAL_PRIVILEGE.UPDATE.slug), + identityId: z.string().min(1).describe(IDENTITY_ADDITIONAL_PRIVILEGE.UPDATE.identityId), + projectSlug: z.string().min(1).describe(IDENTITY_ADDITIONAL_PRIVILEGE.UPDATE.projectSlug), data: z .object({ slug: z @@ -93,15 +112,29 @@ export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: F .min(1) .max(60) .trim() + .refine((val) => val.toLowerCase() === val, "Must be lowercase") .refine((v) => slugify(v) === v, { message: "Slug must be a valid slug" - }), - isPackedPermission: z.boolean().optional().default(false), - permissions: z.any().array(), - isTemporary: z.boolean(), - temporaryMode: z.nativeEnum(IdentityProjectAdditionalPrivilegeTemporaryMode), - temporaryRange: z.string().refine((val) => ms(val) > 0, "Temporary range must be a positive number"), - temporaryAccessStartTime: z.string().datetime() + }) + .describe(IDENTITY_ADDITIONAL_PRIVILEGE.UPDATE.newSlug), + isPackedPermission: z + .boolean() + .optional() + .default(false) + .describe(IDENTITY_ADDITIONAL_PRIVILEGE.UPDATE.isPackPermission), + permissions: z.any().array().describe(IDENTITY_ADDITIONAL_PRIVILEGE.UPDATE.permissions), + isTemporary: z.boolean().describe(IDENTITY_ADDITIONAL_PRIVILEGE.UPDATE.isTemporary), + temporaryMode: z + .nativeEnum(IdentityProjectAdditionalPrivilegeTemporaryMode) + .describe(IDENTITY_ADDITIONAL_PRIVILEGE.UPDATE.temporaryMode), + temporaryRange: z + .string() + .refine((val) => ms(val) > 0, "Temporary range must be a positive number") + .describe(IDENTITY_ADDITIONAL_PRIVILEGE.UPDATE.temporaryRange), + temporaryAccessStartTime: z + .string() + .datetime() + .describe(IDENTITY_ADDITIONAL_PRIVILEGE.UPDATE.temporaryAccessStartTime) }) .partial() }), @@ -136,9 +169,9 @@ export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: F method: "DELETE", schema: { body: z.object({ - slug: z.string().min(1), - identityId: z.string().min(1), - projectSlug: z.string().min(1) + slug: z.string().min(1).describe(IDENTITY_ADDITIONAL_PRIVILEGE.DELETE.slug), + identityId: z.string().min(1).describe(IDENTITY_ADDITIONAL_PRIVILEGE.DELETE.identityId), + projectSlug: z.string().min(1).describe(IDENTITY_ADDITIONAL_PRIVILEGE.DELETE.projectSlug) }), response: { 200: z.object({ @@ -164,11 +197,11 @@ export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: F method: "GET", schema: { params: z.object({ - slug: z.string() + slug: z.string().min(1).describe(IDENTITY_ADDITIONAL_PRIVILEGE.GET_BY_SLUG.slug) }), querystring: z.object({ - identityId: z.string().min(1), - projectSlug: z.string().min(1) + identityId: z.string().min(1).describe(IDENTITY_ADDITIONAL_PRIVILEGE.GET_BY_SLUG.identityId), + projectSlug: z.string().min(1).describe(IDENTITY_ADDITIONAL_PRIVILEGE.GET_BY_SLUG.projectSlug) }), response: { 200: z.object({ @@ -195,12 +228,13 @@ export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: F method: "GET", schema: { querystring: z.object({ - identityId: z.string().min(1), - projectSlug: z.string().min(1), + identityId: z.string().min(1).describe(IDENTITY_ADDITIONAL_PRIVILEGE.LIST.identityId), + projectSlug: z.string().min(1).describe(IDENTITY_ADDITIONAL_PRIVILEGE.LIST.projectSlug), unpacked: z .enum(["false", "true"]) .transform((el) => el === "true") .default("true") + .describe(IDENTITY_ADDITIONAL_PRIVILEGE.LIST.unpacked) }), response: { 200: z.object({ diff --git a/backend/src/ee/routes/v1/user-additional-privilege-router.ts b/backend/src/ee/routes/v1/user-additional-privilege-router.ts index d0643b22c..c003971e4 100644 --- a/backend/src/ee/routes/v1/user-additional-privilege-router.ts +++ b/backend/src/ee/routes/v1/user-additional-privilege-router.ts @@ -4,6 +4,7 @@ import { z } from "zod"; import { ProjectUserAdditionalPrivilegeSchema } from "@app/db/schemas"; import { ProjectUserAdditionalPrivilegeTemporaryMode } from "@app/ee/services/project-user-additional-privilege/project-user-additional-privilege-types"; +import { PROJECT_USER_ADDITIONAL_PRIVILEGE } from "@app/lib/api-docs"; import { alphaNumericNanoId } from "@app/lib/nanoid"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { AuthMode } from "@app/services/auth/auth-type"; @@ -15,37 +16,47 @@ export const registerUserAdditionalPrivilegeRouter = async (server: FastifyZodPr schema: { body: z.union([ z.object({ - projectMembershipId: z.string(), + projectMembershipId: z.string().min(1).describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.CREATE.projectMembershipId), slug: z .string() .min(1) .max(60) .trim() .default(`privilege-${slugify(alphaNumericNanoId(12))}`) + .refine((v) => v.toLowerCase() === v, "Slug must be lowercase") .refine((v) => slugify(v) === v, { message: "Slug must be a valid slug" - }), - permissions: z.any().array(), - isTemporary: z.literal(false).default(false) + }) + .describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.CREATE.slug), + permissions: z.any().array().describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.CREATE.permissions), + isTemporary: z.literal(false).default(false).describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.CREATE.isTemporary) }), z.object({ - projectMembershipId: z.string(), + projectMembershipId: z.string().min(1).describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.CREATE.projectMembershipId), slug: z .string() .min(1) .max(60) .trim() .default(`privilege-${slugify(alphaNumericNanoId(12))}`) + .refine((v) => v.toLowerCase() === v, "Slug must be lowercase") .refine((v) => slugify(v) === v, { message: "Slug must be a valid slug" - }), - name: z.string().trim(), - description: z.string().trim().optional(), - permissions: z.any().array(), - isTemporary: z.literal(true), - temporaryMode: z.nativeEnum(ProjectUserAdditionalPrivilegeTemporaryMode), - temporaryRange: z.string().refine((val) => ms(val) > 0, "Temporary range must be a positive number"), - temporaryAccessStartTime: z.string().datetime() + }) + .describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.CREATE.slug), + permissions: z.any().array().describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.CREATE.permissions), + isTemporary: z.literal(true).describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.CREATE.isTemporary), + temporaryMode: z + .nativeEnum(ProjectUserAdditionalPrivilegeTemporaryMode) + .describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.CREATE.temporaryMode), + temporaryRange: z + .string() + .refine((val) => ms(val) > 0, "Temporary range must be a positive number") + .describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.CREATE.temporaryRange), + temporaryAccessStartTime: z + .string() + .datetime() + .describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.CREATE.temporaryAccessStartTime) }) ]), response: { @@ -73,7 +84,7 @@ export const registerUserAdditionalPrivilegeRouter = async (server: FastifyZodPr method: "PATCH", schema: { params: z.object({ - privilegeId: z.string() + privilegeId: z.string().min(1).describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.UPDATE.privilegeId) }), body: z .object({ @@ -84,12 +95,21 @@ export const registerUserAdditionalPrivilegeRouter = async (server: FastifyZodPr .refine((v) => v.toLowerCase() === v, "Slug must be lowercase") .refine((v) => slugify(v) === v, { message: "Slug must be a valid slug" - }), - permissions: z.any().array(), - isTemporary: z.boolean(), - temporaryMode: z.nativeEnum(ProjectUserAdditionalPrivilegeTemporaryMode), - temporaryRange: z.string().refine((val) => ms(val) > 0, "Temporary range must be a positive number"), - temporaryAccessStartTime: z.string().datetime() + }) + .describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.UPDATE.slug), + permissions: z.any().array().describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.UPDATE.permissions), + isTemporary: z.boolean().describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.UPDATE.isTemporary), + temporaryMode: z + .nativeEnum(ProjectUserAdditionalPrivilegeTemporaryMode) + .describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.UPDATE.temporaryMode), + temporaryRange: z + .string() + .refine((val) => ms(val) > 0, "Temporary range must be a positive number") + .describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.UPDATE.temporaryRange), + temporaryAccessStartTime: z + .string() + .datetime() + .describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.UPDATE.temporaryAccessStartTime) }) .partial(), response: { @@ -118,7 +138,7 @@ export const registerUserAdditionalPrivilegeRouter = async (server: FastifyZodPr method: "DELETE", schema: { params: z.object({ - privilegeId: z.string() + privilegeId: z.string().describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.DELETE.privilegeId) }), response: { 200: z.object({ @@ -144,7 +164,7 @@ export const registerUserAdditionalPrivilegeRouter = async (server: FastifyZodPr method: "GET", schema: { querystring: z.object({ - projectMembershipId: z.string() + projectMembershipId: z.string().describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.LIST.projectMembershipId) }), response: { 200: z.object({ @@ -170,7 +190,7 @@ export const registerUserAdditionalPrivilegeRouter = async (server: FastifyZodPr method: "GET", schema: { params: z.object({ - privilegeId: z.string() + privilegeId: z.string().describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.GET_BY_PRIVILEGEID.privilegeId) }), response: { 200: z.object({ diff --git a/backend/src/lib/api-docs/constants.ts b/backend/src/lib/api-docs/constants.ts index 8736e80c6..e0d82b2d1 100644 --- a/backend/src/lib/api-docs/constants.ts +++ b/backend/src/lib/api-docs/constants.ts @@ -397,3 +397,81 @@ export const SECRET_TAGS = { projectId: "The ID of the project to delete the tag from." } } as const; + +export const IDENTITY_ADDITIONAL_PRIVILEGE = { + CREATE: { + projectSlug: "The slug of the project of the dynamic secret in.", + identityId: "The ID of the identity to delete.", + slug: "The slug of the privilege to create.", + permissions: + "The permission object for the privilege. Refer https://casl.js.org/v6/en/guide/define-rules#the-shape-of-raw-rule to understand the shape", + isPackPermission: "Whether the server should pack(compact) the permission object.", + isTemporary: "Whether the privilege is temporary.", + temporaryMode: "Type of temporary access given. Types: relative", + temporaryRange: "TTL for the temporay time. Eg: 1m, 1h, 1d", + temporaryAccessStartTime: "ISO time for which temporary access should begin." + }, + UPDATE: { + projectSlug: "The slug of the project of the dynamic secret in.", + identityId: "The ID of the identity to delete.", + slug: "The slug of the privilege to create.", + newSlug: "The new slug of the privilege to create.", + permissions: + "The permission object for the privilege. Refer https://casl.js.org/v6/en/guide/define-rules#the-shape-of-raw-rule to understand the shape", + isPackPermission: "Whether the server should pack(compact) the permission object.", + isTemporary: "Whether the privilege is temporary.", + temporaryMode: "Type of temporary access given. Types: relative", + temporaryRange: "TTL for the temporay time. Eg: 1m, 1h, 1d", + temporaryAccessStartTime: "ISO time for which temporary access should begin." + }, + DELETE: { + projectSlug: "The slug of the project of the dynamic secret in.", + identityId: "The ID of the identity to delete.", + slug: "The slug of the privilege to create." + }, + GET_BY_SLUG: { + projectSlug: "The slug of the project of the dynamic secret in.", + identityId: "The ID of the identity to delete.", + slug: "The slug of the privilege to create." + }, + LIST: { + projectSlug: "The slug of the project of the dynamic secret in.", + identityId: "The ID of the identity to delete.", + unpacked: "Whether the system should send the permissions as unpacked" + } +}; + +export const PROJECT_USER_ADDITIONAL_PRIVILEGE = { + CREATE: { + projectMembershipId: "Project membership id of user", + slug: "The slug of the privilege to create.", + permissions: + "The permission object for the privilege. Refer https://casl.js.org/v6/en/guide/define-rules#the-shape-of-raw-rule to understand the shape", + isPackPermission: "Whether the server should pack(compact) the permission object.", + isTemporary: "Whether the privilege is temporary.", + temporaryMode: "Type of temporary access given. Types: relative", + temporaryRange: "TTL for the temporay time. Eg: 1m, 1h, 1d", + temporaryAccessStartTime: "ISO time for which temporary access should begin." + }, + UPDATE: { + privilegeId: "The id of privilege object", + slug: "The slug of the privilege to create.", + newSlug: "The new slug of the privilege to create.", + permissions: + "The permission object for the privilege. Refer https://casl.js.org/v6/en/guide/define-rules#the-shape-of-raw-rule to understand the shape", + isPackPermission: "Whether the server should pack(compact) the permission object.", + isTemporary: "Whether the privilege is temporary.", + temporaryMode: "Type of temporary access given. Types: relative", + temporaryRange: "TTL for the temporay time. Eg: 1m, 1h, 1d", + temporaryAccessStartTime: "ISO time for which temporary access should begin." + }, + DELETE: { + privilegeId: "The id of privilege object" + }, + GET_BY_PRIVILEGEID: { + privilegeId: "The id of privilege object" + }, + LIST: { + projectMembershipId: "Project membership id of user" + } +}; diff --git a/frontend/src/views/Project/MembersPage/components/IdentityTab/components/IdentityRoleForm/IdentityRbacSection.tsx b/frontend/src/views/Project/MembersPage/components/IdentityTab/components/IdentityRoleForm/IdentityRbacSection.tsx index 9d25c7ed0..fddc067bc 100644 --- a/frontend/src/views/Project/MembersPage/components/IdentityTab/components/IdentityRoleForm/IdentityRbacSection.tsx +++ b/frontend/src/views/Project/MembersPage/components/IdentityTab/components/IdentityRoleForm/IdentityRbacSection.tsx @@ -331,7 +331,7 @@ export const IdentityRbacSection = ({ identityProjectMember, onOpenUpgradeModal )} isLoading={roleForm.formState.isSubmitting} > - Save Changes + Save Roles diff --git a/frontend/src/views/Project/MembersPage/components/MemberListTab/MemberRoleForm/MemberRbacSection.tsx b/frontend/src/views/Project/MembersPage/components/MemberListTab/MemberRoleForm/MemberRbacSection.tsx index ecd1ee40e..73642344e 100644 --- a/frontend/src/views/Project/MembersPage/components/MemberListTab/MemberRoleForm/MemberRbacSection.tsx +++ b/frontend/src/views/Project/MembersPage/components/MemberListTab/MemberRoleForm/MemberRbacSection.tsx @@ -328,7 +328,7 @@ export const MemberRbacSection = ({ projectMember, onOpenUpgradeModal }: Props) )} isLoading={roleForm.formState.isSubmitting} > - Save Changes + Save Roles