diff --git a/backend/src/db/migrations/20250808021941_access-policy-max-time-period.ts b/backend/src/db/migrations/20250808021941_access-policy-max-time-period.ts new file mode 100644 index 000000000..218260b3e --- /dev/null +++ b/backend/src/db/migrations/20250808021941_access-policy-max-time-period.ts @@ -0,0 +1,19 @@ +import { Knex } from "knex"; + +import { TableName } from "../schemas/models"; + +export async function up(knex: Knex): Promise { + if (!(await knex.schema.hasColumn(TableName.AccessApprovalPolicy, "maxTimePeriod"))) { + await knex.schema.alterTable(TableName.AccessApprovalPolicy, (t) => { + t.string("maxTimePeriod").nullable(); // Ex: 1h - Null is permanent + }); + } +} + +export async function down(knex: Knex): Promise { + if (await knex.schema.hasColumn(TableName.AccessApprovalPolicy, "maxTimePeriod")) { + await knex.schema.alterTable(TableName.AccessApprovalPolicy, (t) => { + t.dropColumn("maxTimePeriod"); + }); + } +} diff --git a/backend/src/db/schemas/access-approval-policies.ts b/backend/src/db/schemas/access-approval-policies.ts index ea57c54d2..66ed79ab7 100644 --- a/backend/src/db/schemas/access-approval-policies.ts +++ b/backend/src/db/schemas/access-approval-policies.ts @@ -17,7 +17,8 @@ export const AccessApprovalPoliciesSchema = z.object({ updatedAt: z.date(), enforcementLevel: z.string().default("hard"), deletedAt: z.date().nullable().optional(), - allowedSelfApprovals: z.boolean().default(true) + allowedSelfApprovals: z.boolean().default(true), + maxTimePeriod: z.string().nullable().optional() }); export type TAccessApprovalPolicies = z.infer; diff --git a/backend/src/ee/routes/v1/access-approval-policy-router.ts b/backend/src/ee/routes/v1/access-approval-policy-router.ts index ef44344de..d8d3bf9ed 100644 --- a/backend/src/ee/routes/v1/access-approval-policy-router.ts +++ b/backend/src/ee/routes/v1/access-approval-policy-router.ts @@ -3,12 +3,32 @@ import { z } from "zod"; import { ApproverType, BypasserType } from "@app/ee/services/access-approval-policy/access-approval-policy-types"; import { removeTrailingSlash } from "@app/lib/fn"; +import { ms } from "@app/lib/ms"; import { EnforcementLevel } from "@app/lib/types"; import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { sapPubSchema } from "@app/server/routes/sanitizedSchemas"; import { AuthMode } from "@app/services/auth/auth-type"; +const maxTimePeriodSchema = z + .string() + .trim() + .nullish() + .transform((val, ctx) => { + if (val === undefined) return undefined; + if (!val || val === "permanent") return null; + const parsedMs = ms(val); + + if (typeof parsedMs !== "number" || parsedMs <= 0) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: "Invalid time period format or value. Must be a positive duration (e.g., '1h', '30m', '2d')." + }); + return z.NEVER; + } + return val; + }); + export const registerAccessApprovalPolicyRouter = async (server: FastifyZodProvider) => { server.route({ url: "/", @@ -71,7 +91,8 @@ export const registerAccessApprovalPolicyRouter = async (server: FastifyZodProvi .optional(), approvals: z.number().min(1).default(1), enforcementLevel: z.nativeEnum(EnforcementLevel).default(EnforcementLevel.Hard), - allowedSelfApprovals: z.boolean().default(true) + allowedSelfApprovals: z.boolean().default(true), + maxTimePeriod: maxTimePeriodSchema }) .refine( (val) => Boolean(val.environment) || Boolean(val.environments), @@ -124,7 +145,8 @@ export const registerAccessApprovalPolicyRouter = async (server: FastifyZodProvi .array() .nullable() .optional(), - bypassers: z.object({ type: z.nativeEnum(BypasserType), id: z.string().nullable().optional() }).array() + bypassers: z.object({ type: z.nativeEnum(BypasserType), id: z.string().nullable().optional() }).array(), + maxTimePeriod: z.string().nullable().optional() }) .array() .nullable() @@ -233,7 +255,8 @@ export const registerAccessApprovalPolicyRouter = async (server: FastifyZodProvi stepNumber: z.number().int() }) .array() - .optional() + .optional(), + maxTimePeriod: maxTimePeriodSchema }), response: { 200: z.object({ @@ -314,7 +337,8 @@ export const registerAccessApprovalPolicyRouter = async (server: FastifyZodProvi }) .array() .nullable() - .optional() + .optional(), + maxTimePeriod: z.string().nullable().optional() }) }) } diff --git a/backend/src/ee/routes/v1/access-approval-request-router.ts b/backend/src/ee/routes/v1/access-approval-request-router.ts index a7e8e79cc..511c93cae 100644 --- a/backend/src/ee/routes/v1/access-approval-request-router.ts +++ b/backend/src/ee/routes/v1/access-approval-request-router.ts @@ -129,7 +129,8 @@ export const registerAccessApprovalRequestRouter = async (server: FastifyZodProv envId: z.string(), enforcementLevel: z.string(), deletedAt: z.date().nullish(), - allowedSelfApprovals: z.boolean() + allowedSelfApprovals: z.boolean(), + maxTimePeriod: z.string().nullable().optional() }), reviewers: z .object({ diff --git a/backend/src/ee/services/access-approval-policy/access-approval-policy-dal.ts b/backend/src/ee/services/access-approval-policy/access-approval-policy-dal.ts index 9baf762d6..78221d988 100644 --- a/backend/src/ee/services/access-approval-policy/access-approval-policy-dal.ts +++ b/backend/src/ee/services/access-approval-policy/access-approval-policy-dal.ts @@ -56,6 +56,7 @@ export interface TAccessApprovalPolicyDALFactory allowedSelfApprovals: boolean; secretPath: string; deletedAt?: Date | null | undefined; + maxTimePeriod?: string | null; projectId: string; bypassers: ( | { @@ -96,6 +97,7 @@ export interface TAccessApprovalPolicyDALFactory allowedSelfApprovals: boolean; secretPath: string; deletedAt?: Date | null | undefined; + maxTimePeriod?: string | null; environments: { id: string; name: string; @@ -141,6 +143,7 @@ export interface TAccessApprovalPolicyDALFactory allowedSelfApprovals: boolean; secretPath: string; deletedAt?: Date | null | undefined; + maxTimePeriod?: string | null; } | undefined >; diff --git a/backend/src/ee/services/access-approval-policy/access-approval-policy-service.ts b/backend/src/ee/services/access-approval-policy/access-approval-policy-service.ts index 0b3c4e128..95d1a9877 100644 --- a/backend/src/ee/services/access-approval-policy/access-approval-policy-service.ts +++ b/backend/src/ee/services/access-approval-policy/access-approval-policy-service.ts @@ -100,7 +100,8 @@ export const accessApprovalPolicyServiceFactory = ({ environments, enforcementLevel, allowedSelfApprovals, - approvalsRequired + approvalsRequired, + maxTimePeriod }) => { const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId); if (!project) throw new NotFoundError({ message: `Project with slug '${projectSlug}' not found` }); @@ -219,7 +220,8 @@ export const accessApprovalPolicyServiceFactory = ({ secretPath, name, enforcementLevel, - allowedSelfApprovals + allowedSelfApprovals, + maxTimePeriod }, tx ); @@ -318,7 +320,8 @@ export const accessApprovalPolicyServiceFactory = ({ enforcementLevel, allowedSelfApprovals, approvalsRequired, - environments + environments, + maxTimePeriod }: TUpdateAccessApprovalPolicy) => { const groupApprovers = approvers.filter((approver) => approver.type === ApproverType.Group); @@ -461,7 +464,8 @@ export const accessApprovalPolicyServiceFactory = ({ secretPath, name, enforcementLevel, - allowedSelfApprovals + allowedSelfApprovals, + maxTimePeriod }, tx ); diff --git a/backend/src/ee/services/access-approval-policy/access-approval-policy-types.ts b/backend/src/ee/services/access-approval-policy/access-approval-policy-types.ts index 27ec228f7..997849800 100644 --- a/backend/src/ee/services/access-approval-policy/access-approval-policy-types.ts +++ b/backend/src/ee/services/access-approval-policy/access-approval-policy-types.ts @@ -41,6 +41,7 @@ export type TCreateAccessApprovalPolicy = { enforcementLevel: EnforcementLevel; allowedSelfApprovals: boolean; approvalsRequired?: { numberOfApprovals: number; stepNumber: number }[]; + maxTimePeriod?: string | null; } & Omit; export type TUpdateAccessApprovalPolicy = { @@ -60,6 +61,7 @@ export type TUpdateAccessApprovalPolicy = { allowedSelfApprovals: boolean; approvalsRequired?: { numberOfApprovals: number; stepNumber: number }[]; environments?: string[]; + maxTimePeriod?: string | null; } & Omit; export type TDeleteAccessApprovalPolicy = { @@ -104,7 +106,8 @@ export interface TAccessApprovalPolicyServiceFactory { environment, enforcementLevel, allowedSelfApprovals, - approvalsRequired + approvalsRequired, + maxTimePeriod }: TCreateAccessApprovalPolicy) => Promise<{ environment: { name: string; @@ -135,6 +138,7 @@ export interface TAccessApprovalPolicyServiceFactory { allowedSelfApprovals: boolean; secretPath: string; deletedAt?: Date | null | undefined; + maxTimePeriod?: string | null; }>; deleteAccessApprovalPolicy: ({ policyId, @@ -159,6 +163,7 @@ export interface TAccessApprovalPolicyServiceFactory { allowedSelfApprovals: boolean; secretPath: string; deletedAt?: Date | null | undefined; + maxTimePeriod?: string | null; environment: { id: string; name: string; @@ -185,7 +190,8 @@ export interface TAccessApprovalPolicyServiceFactory { enforcementLevel, allowedSelfApprovals, approvalsRequired, - environments + environments, + maxTimePeriod }: TUpdateAccessApprovalPolicy) => Promise<{ environment: { id: string; @@ -208,6 +214,7 @@ export interface TAccessApprovalPolicyServiceFactory { allowedSelfApprovals: boolean; secretPath?: string | null | undefined; deletedAt?: Date | null | undefined; + maxTimePeriod?: string | null; }>; getAccessApprovalPolicyByProjectSlug: ({ actorId, @@ -242,6 +249,7 @@ export interface TAccessApprovalPolicyServiceFactory { allowedSelfApprovals: boolean; secretPath: string; deletedAt?: Date | null | undefined; + maxTimePeriod?: string | null; environment: { id: string; name: string; @@ -298,6 +306,7 @@ export interface TAccessApprovalPolicyServiceFactory { allowedSelfApprovals: boolean; secretPath: string; deletedAt?: Date | null | undefined; + maxTimePeriod?: string | null; environment: { id: string; name: string; diff --git a/backend/src/ee/services/access-approval-request/access-approval-request-dal.ts b/backend/src/ee/services/access-approval-request/access-approval-request-dal.ts index f930ceaf7..f3972fc1c 100644 --- a/backend/src/ee/services/access-approval-request/access-approval-request-dal.ts +++ b/backend/src/ee/services/access-approval-request/access-approval-request-dal.ts @@ -64,6 +64,7 @@ export interface TAccessApprovalRequestDALFactory extends Omit( db(TableName.OrgMembership).as("approverOrgMembership"), `${TableName.AccessApprovalPolicyApprover}.approverUserId`, @@ -324,10 +325,10 @@ export const accessApprovalRequestDALFactory = (db: TDbClient): TAccessApprovalR db.ref("envId").withSchema(TableName.AccessApprovalPolicy).as("policyEnvId"), db.ref("deletedAt").withSchema(TableName.AccessApprovalPolicy).as("policyDeletedAt"), - // Added: db.ref("isActive").withSchema("approverOrgMembership").as("approverIsOrgMembershipActive"), db.ref("isActive").withSchema("approverGroupOrgMembership").as("approverGroupIsOrgMembershipActive"), - db.ref("isActive").withSchema("reviewerOrgMembership").as("reviewerIsOrgMembershipActive") + db.ref("isActive").withSchema("reviewerOrgMembership").as("reviewerIsOrgMembershipActive"), + db.ref("maxTimePeriod").withSchema(TableName.AccessApprovalPolicy).as("policyMaxTimePeriod") ) .select(db.ref("approverUserId").withSchema(TableName.AccessApprovalPolicyApprover)) .select(db.ref("sequence").withSchema(TableName.AccessApprovalPolicyApprover).as("approverSequence")) @@ -394,7 +395,8 @@ export const accessApprovalRequestDALFactory = (db: TDbClient): TAccessApprovalR enforcementLevel: doc.policyEnforcementLevel, allowedSelfApprovals: doc.policyAllowedSelfApprovals, envId: doc.policyEnvId, - deletedAt: doc.policyDeletedAt + deletedAt: doc.policyDeletedAt, + maxTimePeriod: doc.policyMaxTimePeriod }, requestedByUser: { userId: doc.requestedByUserId, @@ -615,7 +617,8 @@ export const accessApprovalRequestDALFactory = (db: TDbClient): TAccessApprovalR tx.ref("enforcementLevel").withSchema(TableName.AccessApprovalPolicy).as("policyEnforcementLevel"), tx.ref("allowedSelfApprovals").withSchema(TableName.AccessApprovalPolicy).as("policyAllowedSelfApprovals"), tx.ref("approvals").withSchema(TableName.AccessApprovalPolicy).as("policyApprovals"), - tx.ref("deletedAt").withSchema(TableName.AccessApprovalPolicy).as("policyDeletedAt") + tx.ref("deletedAt").withSchema(TableName.AccessApprovalPolicy).as("policyDeletedAt"), + tx.ref("maxTimePeriod").withSchema(TableName.AccessApprovalPolicy).as("policyMaxTimePeriod") ); const findById: TAccessApprovalRequestDALFactory["findById"] = async (id, tx) => { @@ -636,7 +639,8 @@ export const accessApprovalRequestDALFactory = (db: TDbClient): TAccessApprovalR secretPath: el.policySecretPath, enforcementLevel: el.policyEnforcementLevel, allowedSelfApprovals: el.policyAllowedSelfApprovals, - deletedAt: el.policyDeletedAt + deletedAt: el.policyDeletedAt, + maxTimePeriod: el.policyMaxTimePeriod }, requestedByUser: { userId: el.requestedByUserId, diff --git a/backend/src/ee/services/access-approval-request/access-approval-request-service.ts b/backend/src/ee/services/access-approval-request/access-approval-request-service.ts index dcbe717da..58f5c57db 100644 --- a/backend/src/ee/services/access-approval-request/access-approval-request-service.ts +++ b/backend/src/ee/services/access-approval-request/access-approval-request-service.ts @@ -156,6 +156,15 @@ export const accessApprovalRequestServiceFactory = ({ throw new BadRequestError({ message: "The policy linked to this request has been deleted" }); } + // Check if the requested time falls under policy.maxTimePeriod + if (policy.maxTimePeriod) { + if (!temporaryRange || ms(temporaryRange) > ms(policy.maxTimePeriod)) { + throw new BadRequestError({ + message: `Requested access time range is limited to ${policy.maxTimePeriod} by policy` + }); + } + } + const approverIds: string[] = []; const approverGroupIds: string[] = []; diff --git a/backend/src/ee/services/access-approval-request/access-approval-request-types.ts b/backend/src/ee/services/access-approval-request/access-approval-request-types.ts index d56c64bfd..046ef027e 100644 --- a/backend/src/ee/services/access-approval-request/access-approval-request-types.ts +++ b/backend/src/ee/services/access-approval-request/access-approval-request-types.ts @@ -84,6 +84,7 @@ export interface TAccessApprovalRequestServiceFactory { allowedSelfApprovals: boolean; envId: string; deletedAt: Date | null | undefined; + maxTimePeriod?: string | null; }; projectId: string; environment: string; diff --git a/backend/src/server/routes/v1/secret-folder-router.ts b/backend/src/server/routes/v1/secret-folder-router.ts index b307347b8..871259147 100644 --- a/backend/src/server/routes/v1/secret-folder-router.ts +++ b/backend/src/server/routes/v1/secret-folder-router.ts @@ -45,7 +45,7 @@ export const registerSecretFolderRouter = async (server: FastifyZodProvider) => .transform(removeTrailingSlash) .describe(FOLDERS.CREATE.path) .optional(), - // backward compatiability with cli + // backward compatibility with cli directory: z .string() .trim() @@ -58,7 +58,9 @@ export const registerSecretFolderRouter = async (server: FastifyZodProvider) => }), response: { 200: z.object({ - folder: SecretFoldersSchema + folder: SecretFoldersSchema.extend({ + path: z.string() + }) }) } }, @@ -130,7 +132,7 @@ export const registerSecretFolderRouter = async (server: FastifyZodProvider) => .transform(removeTrailingSlash) .describe(FOLDERS.UPDATE.path) .optional(), - // backward compatiability with cli + // backward compatibility with cli directory: z .string() .trim() @@ -143,7 +145,9 @@ export const registerSecretFolderRouter = async (server: FastifyZodProvider) => }), response: { 200: z.object({ - folder: SecretFoldersSchema + folder: SecretFoldersSchema.extend({ + path: z.string() + }) }) } }, @@ -359,7 +363,7 @@ export const registerSecretFolderRouter = async (server: FastifyZodProvider) => .transform(removeTrailingSlash) .describe(FOLDERS.LIST.path) .optional(), - // backward compatiability with cli + // backward compatibility with cli directory: z .string() .trim() diff --git a/backend/src/services/resource-cleanup/resource-cleanup-queue.ts b/backend/src/services/resource-cleanup/resource-cleanup-queue.ts index 224eef4bf..dcfa7ea0d 100644 --- a/backend/src/services/resource-cleanup/resource-cleanup-queue.ts +++ b/backend/src/services/resource-cleanup/resource-cleanup-queue.ts @@ -49,6 +49,19 @@ export const dailyResourceCleanUpQueueServiceFactory = ({ } const init = async () => { + await queueService.stopRepeatableJob( + QueueName.AuditLogPrune, + QueueJobs.AuditLogPrune, + { pattern: "0 0 * * *", utc: true }, + QueueName.AuditLogPrune // just a job id + ); + await queueService.stopRepeatableJob( + QueueName.DailyResourceCleanUp, + QueueJobs.DailyResourceCleanUp, + { pattern: "0 0 * * *", utc: true }, + QueueName.DailyResourceCleanUp // just a job id + ); + await queueService.startPg( QueueJobs.DailyResourceCleanUp, async () => { diff --git a/backend/src/services/secret-folder/secret-folder-service.ts b/backend/src/services/secret-folder/secret-folder-service.ts index a60d29348..90cc25710 100644 --- a/backend/src/services/secret-folder/secret-folder-service.ts +++ b/backend/src/services/secret-folder/secret-folder-service.ts @@ -238,8 +238,16 @@ export const secretFolderServiceFactory = ({ return doc; }); + const [folderWithFullPath] = await folderDAL.findSecretPathByFolderIds(projectId, [folder.id]); + + if (!folderWithFullPath) { + throw new NotFoundError({ + message: `Failed to retrieve path for folder with ID '${folder.id}'` + }); + } + await snapshotService.performSnapshot(folder.parentId as string); - return folder; + return { ...folder, path: folderWithFullPath.path }; }; const updateManyFolders = async ({ @@ -496,8 +504,27 @@ export const secretFolderServiceFactory = ({ return doc; }); + const foldersWithFullPaths = await folderDAL.findSecretPathByFolderIds(projectId, [newFolder.id, folder.id]); + + const newFolderWithFullPath = foldersWithFullPaths.find((f) => f?.id === newFolder.id); + if (!newFolderWithFullPath) { + throw new NotFoundError({ + message: `Failed to retrieve path for folder with ID '${newFolder.id}'` + }); + } + + const folderWithFullPath = foldersWithFullPaths.find((f) => f?.id === folder.id); + if (!folderWithFullPath) { + throw new NotFoundError({ + message: `Failed to retrieve path for folder with ID '${folder.id}'` + }); + } + await snapshotService.performSnapshot(newFolder.parentId as string); - return { folder: newFolder, old: folder }; + return { + folder: { ...newFolder, path: newFolderWithFullPath.path }, + old: { ...folder, path: folderWithFullPath.path } + }; }; const $checkFolderPolicy = async ({ diff --git a/frontend/public/locales/en/translations.json b/frontend/public/locales/en/translations.json index 167e1c7b5..fdfefcf93 100644 --- a/frontend/public/locales/en/translations.json +++ b/frontend/public/locales/en/translations.json @@ -53,8 +53,8 @@ "project-id": "Project ID", "save-changes": "Save Changes", "saved": "Saved", - "drop-zone": "Drag and drop a .env, .json, or .yml file here.", - "drop-zone-keys": "Drag and drop a .env, .json, or .yml file here to add more secrets.", + "drop-zone": "Drag and drop a .env, .json, .csv, or .yml file here.", + "drop-zone-keys": "Drag and drop a .env, .json, .csv, or .yml file here to add more secrets.", "role": "Role", "role_admin": "admin", "display-name": "Display Name", diff --git a/frontend/src/components/utilities/parseSecrets.ts b/frontend/src/components/utilities/parseSecrets.ts index 3e5a57edf..e33f25ace 100644 --- a/frontend/src/components/utilities/parseSecrets.ts +++ b/frontend/src/components/utilities/parseSecrets.ts @@ -165,3 +165,61 @@ export function parseYaml(src: ArrayBuffer | string) { return result; } + +function detectSeparator(csvContent: string): string { + const firstLine = csvContent.split("\n")[0]; + const separators = [",", ";", "\t", "|"]; + + const counts = separators.map((sep) => ({ + separator: sep, + count: (firstLine.match(new RegExp(`\\${sep}`, "g")) || []).length + })); + + const detected = counts.reduce((max, curr) => (curr.count > max.count ? curr : max)); + + return detected.count > 0 ? detected.separator : ","; +} + +export function parseCsvToMatrix(src: ArrayBuffer | string): string[][] { + let csvContent: string; + if (typeof src === "string") { + csvContent = src; + } else { + csvContent = new TextDecoder("utf-8").decode(src); + } + + const separator = detectSeparator(csvContent); + const lines = csvContent.replace(/\r\n?/g, "\n").split("\n"); + const matrix: string[][] = []; + + lines.forEach((line) => { + if (line.trim() !== "") { + const cells: string[] = []; + let currentCell = ""; + let inQuote = false; + + for (let i = 0; i < line.length; i += 1) { + const char = line[i]; + const nextChar = line[i + 1]; + + if (char === '"') { + if (inQuote && nextChar === '"') { + currentCell += '"'; + i += 1; + } else { + inQuote = !inQuote; + } + } else if (char === separator && !inQuote) { + cells.push(currentCell.trim()); + currentCell = ""; + } else { + currentCell += char; + } + } + cells.push(currentCell.trim()); + matrix.push(cells); + } + }); + + return matrix; +} diff --git a/frontend/src/hooks/api/accessApproval/mutation.tsx b/frontend/src/hooks/api/accessApproval/mutation.tsx index ad7917a8f..69637243f 100644 --- a/frontend/src/hooks/api/accessApproval/mutation.tsx +++ b/frontend/src/hooks/api/accessApproval/mutation.tsx @@ -26,7 +26,8 @@ export const useCreateAccessApprovalPolicy = () => { secretPath, enforcementLevel, allowedSelfApprovals, - approvalsRequired + approvalsRequired, + maxTimePeriod }) => { const { data } = await apiRequest.post("/api/v1/access-approvals/policies", { environments, @@ -38,7 +39,8 @@ export const useCreateAccessApprovalPolicy = () => { name, enforcementLevel, allowedSelfApprovals, - approvalsRequired + approvalsRequired, + maxTimePeriod }); return data; }, @@ -64,7 +66,8 @@ export const useUpdateAccessApprovalPolicy = () => { enforcementLevel, allowedSelfApprovals, approvalsRequired, - environments + environments, + maxTimePeriod }) => { const { data } = await apiRequest.patch(`/api/v1/access-approvals/policies/${id}`, { approvals, @@ -75,7 +78,8 @@ export const useUpdateAccessApprovalPolicy = () => { enforcementLevel, allowedSelfApprovals, approvalsRequired, - environments + environments, + maxTimePeriod }); return data; }, diff --git a/frontend/src/hooks/api/accessApproval/types.ts b/frontend/src/hooks/api/accessApproval/types.ts index dde4ed70e..7f0877ce5 100644 --- a/frontend/src/hooks/api/accessApproval/types.ts +++ b/frontend/src/hooks/api/accessApproval/types.ts @@ -18,6 +18,7 @@ export type TAccessApprovalPolicy = { approvers?: Approver[]; bypassers?: Bypasser[]; allowedSelfApprovals: boolean; + maxTimePeriod?: string | null; }; export enum ApproverType { @@ -95,6 +96,7 @@ export type TAccessApprovalRequest = { enforcementLevel: EnforcementLevel; deletedAt: Date | null; allowedSelfApprovals: boolean; + maxTimePeriod?: string | null; }; reviewers: { @@ -176,6 +178,7 @@ export type TCreateAccessPolicyDTO = { enforcementLevel?: EnforcementLevel; allowedSelfApprovals: boolean; approvalsRequired?: { numberOfApprovals: number; stepNumber: number }[]; + maxTimePeriod?: string | null; }; export type TUpdateAccessPolicyDTO = { @@ -191,6 +194,7 @@ export type TUpdateAccessPolicyDTO = { // for invalidating list projectSlug: string; approvalsRequired?: { numberOfApprovals: number; stepNumber: number }[]; + maxTimePeriod?: string | null; }; export type TDeleteSecretPolicyDTO = { diff --git a/frontend/src/pages/project/AccessControlPage/components/MembersTab/components/MemberRoleForm/SpecificPrivilegeSection.tsx b/frontend/src/pages/project/AccessControlPage/components/MembersTab/components/MemberRoleForm/SpecificPrivilegeSection.tsx index 0256d3219..5dd439744 100644 --- a/frontend/src/pages/project/AccessControlPage/components/MembersTab/components/MemberRoleForm/SpecificPrivilegeSection.tsx +++ b/frontend/src/pages/project/AccessControlPage/components/MembersTab/components/MemberRoleForm/SpecificPrivilegeSection.tsx @@ -220,6 +220,24 @@ export const SpecificPrivilegeSecretForm = ({ return; } + const policy = policies.find( + (p) => + p.environments.find((e) => e.slug === selectedEnvironment) && p.secretPath === secretPath + ); + + if ( + policy?.maxTimePeriod && + (!data.temporaryAccess.isTemporary || + ms(data.temporaryAccess.temporaryRange) > ms(policy.maxTimePeriod)) + ) { + createNotification({ + type: "error", + text: `Requested access time range is limited to ${policy.maxTimePeriod} by policy`, + title: "Error" + }); + return; + } + const actions = [ { action: ProjectPermissionActions.Read, allowed: data.read }, { action: ProjectPermissionActions.Create, allowed: data.create }, diff --git a/frontend/src/pages/secret-manager/SecretApprovalsPage/components/ApprovalPolicyList/components/AccessPolicyModal.tsx b/frontend/src/pages/secret-manager/SecretApprovalsPage/components/ApprovalPolicyList/components/AccessPolicyModal.tsx index 7ece7d29a..648a6ab80 100644 --- a/frontend/src/pages/secret-manager/SecretApprovalsPage/components/ApprovalPolicyList/components/AccessPolicyModal.tsx +++ b/frontend/src/pages/secret-manager/SecretApprovalsPage/components/ApprovalPolicyList/components/AccessPolicyModal.tsx @@ -91,7 +91,8 @@ const formSchema = z }) .array() .default([]) - .optional() + .optional(), + maxTimePeriod: z.string().trim().optional() }) .superRefine((data, ctx) => { if (data.policyType === PolicyType.ChangePolicy) { @@ -444,6 +445,25 @@ const Form = ({ )} /> + + {isAccessPolicyType && ( + ( + + + + )} + /> + )} + {!isAccessPolicyType && ( >; + headers: string[]; + mapKey: keyof SecretMatrixMap; +}) => { + return ( + + + + + +
+ +
+ + +
+ + {mapKey === "key" && ( + <> + + Secret Key + + )} + {mapKey === "value" && ( + <> + + Secret Value + + )} + {mapKey === "comment" && ( + <> + + Comment + + )} + +
+ + + ); +}; + export const SecretDropzone = ({ isSmaller, environments = [], @@ -50,11 +149,14 @@ export const SecretDropzone = ({ const [isDragActive, setDragActive] = useToggle(); const [isLoading, setIsLoading] = useToggle(); - const { popUp, handlePopUpToggle, handlePopUpOpen, handlePopUpClose } = usePopUp([ - "importSecEnv", - "confirmUpload", - "pasteSecEnv" - ] as const); + // Maps matrix columns to parts of a secret + const [importSecretMatrixMap, setImportSecretMatrixMap] = useState({ + key: 0, + value: null, + comment: null + }); + + const { popUp, handlePopUpToggle, handlePopUpOpen, handlePopUpClose } = usePopUp(popupKeys); const queryClient = useQueryClient(); const { openPopUp } = usePopUpAction(); @@ -136,10 +238,17 @@ export const SecretDropzone = ({ }); return; } - // const fileType = file.name.split('.')[1]; + setIsLoading.on(); reader.onload = (event) => { - if (!event?.target?.result) return; + if (!event?.target?.result) { + createNotification({ + type: "error", + text: "Invalid file contents." + }); + setIsLoading.off(); + return; + } let env: TParsedEnv; @@ -154,7 +263,22 @@ export const SecretDropzone = ({ case "application/yaml": env = parseYaml(src); break; - + case "text/csv": { + const fullMatrix = parseCsvToMatrix(src); + if (!fullMatrix.length) { + createNotification({ + type: "error", + text: "Failed to find secrets in CSV file. File might be empty." + }); + setIsLoading.off(); + return; + } + const headers = fullMatrix[0]; + const matrix = fullMatrix.slice(1); + handlePopUpOpen("importMatrixMap", { headers, matrix }); + setIsLoading.off(); + return; + } default: env = parseDotEnv(src); break; @@ -171,6 +295,22 @@ export const SecretDropzone = ({ } }; + const finishMappedMatrixImport = (matrix: string[][]) => { + const env: TParsedEnv = {}; + matrix.forEach((row) => { + const key = row[importSecretMatrixMap.key]; + if (key) { + env[key] = { + value: importSecretMatrixMap.value ? row[importSecretMatrixMap.value] : "", + comments: importSecretMatrixMap.comment ? [row[importSecretMatrixMap.comment]] : [] + }; + } + }); + handlePopUpClose("importMatrixMap"); + setImportSecretMatrixMap({ key: 0, value: null, comment: null }); + handleParsedEnv(env); + }; + const handleDrop = (e: DragEvent) => { e.preventDefault(); e.stopPropagation(); @@ -293,7 +433,7 @@ export const SecretDropzone = ({ disabled={!isAllowed} type="file" className="absolute h-full w-full cursor-pointer opacity-0" - accept=".txt,.env,.yml,.yaml,.json" + accept=".txt,.env,.yml,.yaml,.json,.csv" onChange={handleFileUpload} /> )} @@ -407,6 +547,75 @@ export const SecretDropzone = ({ )} + + {/* Matrix Import Modal */} + handlePopUpToggle("importMatrixMap", open)} + > + +
+ + + + + + + + + {/* Key */} + + + {/* Value */} + + + {/* Comment */} + + +
+ + + + +
+
+ +
+ +
+
+
); }; diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretListView/SecretDetailSidebar.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretListView/SecretDetailSidebar.tsx index d3845fcbc..953176ff5 100644 --- a/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretListView/SecretDetailSidebar.tsx +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretListView/SecretDetailSidebar.tsx @@ -27,7 +27,10 @@ import { twMerge } from "tailwind-merge"; import { UpgradePlanModal } from "@app/components/license/UpgradePlanModal"; import { createNotification } from "@app/components/notifications"; import { ProjectPermissionCan } from "@app/components/permissions"; -import { hasSecretReference, SecretReferenceTree } from "@app/components/secrets/SecretReferenceDetails"; +import { + hasSecretReference, + SecretReferenceTree +} from "@app/components/secrets/SecretReferenceDetails"; import { Button, Drawer, @@ -49,8 +52,12 @@ import { Tooltip } from "@app/components/v2"; import { InfisicalSecretInput } from "@app/components/v2/InfisicalSecretInput"; -import { ProjectPermissionActions, ProjectPermissionSub, useProjectPermission, useWorkspace } from "@app/context"; - +import { + ProjectPermissionActions, + ProjectPermissionSub, + useProjectPermission, + useWorkspace +} from "@app/context"; import { ProjectPermissionSecretActions } from "@app/context/ProjectPermissionContext/types"; import { getProjectBaseURL } from "@app/helpers/project"; import { usePopUp } from "@app/hooks";