mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-09-22 13:39:35 +00:00
Merge pull request #4336 from Infisical/ENG-3429
feat(access-policies): Allow policy limits on access request times
This commit is contained in:
@@ -0,0 +1,19 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TableName } from "../schemas/models";
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
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<void> {
|
||||
if (await knex.schema.hasColumn(TableName.AccessApprovalPolicy, "maxTimePeriod")) {
|
||||
await knex.schema.alterTable(TableName.AccessApprovalPolicy, (t) => {
|
||||
t.dropColumn("maxTimePeriod");
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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<typeof AccessApprovalPoliciesSchema>;
|
||||
|
||||
@@ -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()
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
@@ -128,7 +128,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({
|
||||
|
||||
@@ -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
|
||||
>;
|
||||
|
||||
@@ -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
|
||||
);
|
||||
|
||||
@@ -41,6 +41,7 @@ export type TCreateAccessApprovalPolicy = {
|
||||
enforcementLevel: EnforcementLevel;
|
||||
allowedSelfApprovals: boolean;
|
||||
approvalsRequired?: { numberOfApprovals: number; stepNumber: number }[];
|
||||
maxTimePeriod?: string | null;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
export type TUpdateAccessApprovalPolicy = {
|
||||
@@ -60,6 +61,7 @@ export type TUpdateAccessApprovalPolicy = {
|
||||
allowedSelfApprovals: boolean;
|
||||
approvalsRequired?: { numberOfApprovals: number; stepNumber: number }[];
|
||||
environments?: string[];
|
||||
maxTimePeriod?: string | null;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
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;
|
||||
|
||||
@@ -63,6 +63,7 @@ export interface TAccessApprovalRequestDALFactory extends Omit<TOrmify<TableName
|
||||
enforcementLevel: string;
|
||||
allowedSelfApprovals: boolean;
|
||||
deletedAt: Date | null | undefined;
|
||||
maxTimePeriod?: string | null;
|
||||
};
|
||||
projectId: string;
|
||||
environments: string[];
|
||||
@@ -161,6 +162,7 @@ export interface TAccessApprovalRequestDALFactory extends Omit<TOrmify<TableName
|
||||
allowedSelfApprovals: boolean;
|
||||
envId: string;
|
||||
deletedAt: Date | null | undefined;
|
||||
maxTimePeriod?: string | null;
|
||||
};
|
||||
projectId: string;
|
||||
environment: string;
|
||||
@@ -297,7 +299,8 @@ export const accessApprovalRequestDALFactory = (db: TDbClient): TAccessApprovalR
|
||||
db.ref("enforcementLevel").withSchema(TableName.AccessApprovalPolicy).as("policyEnforcementLevel"),
|
||||
db.ref("allowedSelfApprovals").withSchema(TableName.AccessApprovalPolicy).as("policyAllowedSelfApprovals"),
|
||||
db.ref("envId").withSchema(TableName.AccessApprovalPolicy).as("policyEnvId"),
|
||||
db.ref("deletedAt").withSchema(TableName.AccessApprovalPolicy).as("policyDeletedAt")
|
||||
db.ref("deletedAt").withSchema(TableName.AccessApprovalPolicy).as("policyDeletedAt"),
|
||||
db.ref("maxTimePeriod").withSchema(TableName.AccessApprovalPolicy).as("policyMaxTimePeriod")
|
||||
)
|
||||
.select(db.ref("approverUserId").withSchema(TableName.AccessApprovalPolicyApprover))
|
||||
.select(db.ref("sequence").withSchema(TableName.AccessApprovalPolicyApprover).as("approverSequence"))
|
||||
@@ -364,7 +367,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,
|
||||
@@ -574,7 +578,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) => {
|
||||
@@ -595,7 +600,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,
|
||||
|
||||
@@ -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[] = [];
|
||||
|
||||
|
||||
@@ -82,6 +82,7 @@ export interface TAccessApprovalRequestServiceFactory {
|
||||
allowedSelfApprovals: boolean;
|
||||
envId: string;
|
||||
deletedAt: Date | null | undefined;
|
||||
maxTimePeriod?: string | null;
|
||||
};
|
||||
projectId: string;
|
||||
environment: string;
|
||||
|
||||
@@ -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;
|
||||
},
|
||||
|
||||
@@ -18,6 +18,7 @@ export type TAccessApprovalPolicy = {
|
||||
approvers?: Approver[];
|
||||
bypassers?: Bypasser[];
|
||||
allowedSelfApprovals: boolean;
|
||||
maxTimePeriod?: string | null;
|
||||
};
|
||||
|
||||
export enum ApproverType {
|
||||
@@ -93,6 +94,7 @@ export type TAccessApprovalRequest = {
|
||||
enforcementLevel: EnforcementLevel;
|
||||
deletedAt: Date | null;
|
||||
allowedSelfApprovals: boolean;
|
||||
maxTimePeriod?: string | null;
|
||||
};
|
||||
|
||||
reviewers: {
|
||||
@@ -173,6 +175,7 @@ export type TCreateAccessPolicyDTO = {
|
||||
enforcementLevel?: EnforcementLevel;
|
||||
allowedSelfApprovals: boolean;
|
||||
approvalsRequired?: { numberOfApprovals: number; stepNumber: number }[];
|
||||
maxTimePeriod?: string | null;
|
||||
};
|
||||
|
||||
export type TUpdateAccessPolicyDTO = {
|
||||
@@ -188,6 +191,7 @@ export type TUpdateAccessPolicyDTO = {
|
||||
// for invalidating list
|
||||
projectSlug: string;
|
||||
approvalsRequired?: { numberOfApprovals: number; stepNumber: number }[];
|
||||
maxTimePeriod?: string | null;
|
||||
};
|
||||
|
||||
export type TDeleteSecretPolicyDTO = {
|
||||
|
||||
@@ -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 },
|
||||
|
||||
@@ -91,7 +91,8 @@ const formSchema = z
|
||||
})
|
||||
.array()
|
||||
.default([])
|
||||
.optional()
|
||||
.optional(),
|
||||
maxTimePeriod: z.string().trim().optional()
|
||||
})
|
||||
.superRefine((data, ctx) => {
|
||||
if (data.policyType === PolicyType.ChangePolicy) {
|
||||
@@ -440,6 +441,25 @@ const Form = ({
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
|
||||
{isAccessPolicyType && (
|
||||
<Controller
|
||||
control={control}
|
||||
name="maxTimePeriod"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Max. Time Period"
|
||||
tooltipText="The maximum amount of time someone can request access for. Ex: 1h, 3w, 30d"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
className="flex-shrink"
|
||||
>
|
||||
<Input {...field} value={field.value} placeholder="permanent" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{!isAccessPolicyType && (
|
||||
<Controller
|
||||
control={control}
|
||||
|
||||
@@ -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";
|
||||
|
||||
Reference in New Issue
Block a user