diff --git a/.github/workflows/release-k8-operator-helm.yml b/.github/workflows/release-k8-operator-helm.yml new file mode 100644 index 000000000..f3731fb46 --- /dev/null +++ b/.github/workflows/release-k8-operator-helm.yml @@ -0,0 +1,27 @@ +name: Release K8 Operator Helm Chart +on: + workflow_dispatch: + +jobs: + release-helm: + name: Release Helm Chart + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v2 + + - name: Install Helm + uses: azure/setup-helm@v3 + with: + version: v3.10.0 + + - name: Install python + uses: actions/setup-python@v4 + + - name: Install Cloudsmith CLI + run: pip install --upgrade cloudsmith-cli + + - name: Build and push helm package to CloudSmith + run: cd helm-charts && sh upload-k8s-operator-cloudsmith.sh + env: + CLOUDSMITH_API_KEY: ${{ secrets.CLOUDSMITH_API_KEY }} diff --git a/.github/workflows/release_docker_k8_operator.yaml b/.github/workflows/release_docker_k8_operator.yaml index 162cc9dc0..4af62df1c 100644 --- a/.github/workflows/release_docker_k8_operator.yaml +++ b/.github/workflows/release_docker_k8_operator.yaml @@ -1,52 +1,103 @@ -name: Release image + Helm chart K8s Operator +name: Release K8 Operator Docker Image on: - push: - tags: - - "infisical-k8-operator/v*.*.*" + push: + tags: + - "infisical-k8-operator/v*.*.*" jobs: - release: - runs-on: ubuntu-latest - steps: - - name: Extract version from tag - id: extract_version - run: echo "::set-output name=version::${GITHUB_REF_NAME#infisical-k8-operator/}" - - uses: actions/checkout@v2 + release-image: + name: Generate Helm Chart PR + runs-on: ubuntu-latest + outputs: + pr_number: ${{ steps.create-pr.outputs.pull-request-number }} + steps: + - name: Extract version from tag + id: extract_version + run: echo "::set-output name=version::${GITHUB_REF_NAME#infisical-k8-operator/}" - - name: 🔧 Set up QEMU - uses: docker/setup-qemu-action@v1 + - name: Checkout code + uses: actions/checkout@v2 - - name: 🔧 Set up Docker Buildx - uses: docker/setup-buildx-action@v1 + # Dependency for helm generation + - name: Install Helm + uses: azure/setup-helm@v3 + with: + version: v3.10.0 - - name: 🐋 Login to Docker Hub - uses: docker/login-action@v1 - with: - username: ${{ secrets.DOCKERHUB_USERNAME }} - password: ${{ secrets.DOCKERHUB_TOKEN }} + # Dependency for helm generation + - name: Install Go + uses: actions/setup-go@v4 + with: + go-version: 1.21 - - name: Build and push - id: docker_build - uses: docker/build-push-action@v2 - with: - context: k8-operator - push: true - platforms: linux/amd64,linux/arm64 - tags: | - infisical/kubernetes-operator:latest - infisical/kubernetes-operator:${{ steps.extract_version.outputs.version }} + # Install binaries for helm generation + - name: Install dependencies + working-directory: k8-operator + run: | + make helmify + make kustomize + make controller-gen - - name: Checkout - uses: actions/checkout@v2 - - name: Install Helm - uses: azure/setup-helm@v3 - with: - version: v3.10.0 - - name: Install python - uses: actions/setup-python@v4 - - name: Install Cloudsmith CLI - run: pip install --upgrade cloudsmith-cli - - name: Build and push helm package to Cloudsmith - run: cd helm-charts && sh upload-k8s-operator-cloudsmith.sh - env: - CLOUDSMITH_API_KEY: ${{ secrets.CLOUDSMITH_API_KEY }} + - name: Generate Helm Chart + working-directory: k8-operator + run: make helm + + - name: Update Helm Chart Version + run: ./k8-operator/scripts/update-version.sh ${{ steps.extract_version.outputs.version }} + + - name: Debug - Check file changes + run: | + echo "Current git status:" + git status + echo "" + echo "Modified files:" + git diff --name-only + + # If there is no diff, exit with error. Version should always be changed, so if there is no diff, something is wrong and we should exit. + if [ -z "$(git diff --name-only)" ]; then + echo "No helm changes or version changes. Invalid release detected, Exiting." + exit 1 + fi + + - name: Create Helm Chart PR + id: create-pr + uses: peter-evans/create-pull-request@v5 + with: + token: ${{ secrets.GITHUB_TOKEN }} + commit-message: "Update Helm chart to version ${{ steps.extract_version.outputs.version }}" + committer: GitHub + author: ${{ github.actor }} <${{ github.actor }}@users.noreply.github.com> + branch: helm-update-${{ steps.extract_version.outputs.version }} + delete-branch: true + title: "Update Helm chart to version ${{ steps.extract_version.outputs.version }}" + body: | + This PR updates the Helm chart to version `${{ steps.extract_version.outputs.version }}`. + Additionally the helm chart has been updated to match the latest operator code changes. + + Associated Release Workflow: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }} + + Once you have approved this PR, you can trigger the helm release workflow manually. + base: main + + - name: 🔧 Set up QEMU + uses: docker/setup-qemu-action@v1 + + - name: 🔧 Set up Docker Buildx + uses: docker/setup-buildx-action@v1 + + - name: 🐋 Login to Docker Hub + uses: docker/login-action@v1 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Build and push + id: docker_build + uses: docker/build-push-action@v2 + with: + context: k8-operator + push: true + platforms: linux/amd64,linux/arm64 + tags: | + infisical/kubernetes-operator:latest + infisical/kubernetes-operator:${{ steps.extract_version.outputs.version }} diff --git a/backend/src/db/migrations/20250324142102_add-self-approvals-to-secret-approval-policies.ts b/backend/src/db/migrations/20250324142102_add-self-approvals-to-secret-approval-policies.ts new file mode 100644 index 000000000..ee99fce67 --- /dev/null +++ b/backend/src/db/migrations/20250324142102_add-self-approvals-to-secret-approval-policies.ts @@ -0,0 +1,29 @@ +import { Knex } from "knex"; + +import { TableName } from "../schemas/models"; + +export async function up(knex: Knex): Promise { + if (!(await knex.schema.hasColumn(TableName.SecretApprovalPolicy, "allowedSelfApprovals"))) { + await knex.schema.alterTable(TableName.SecretApprovalPolicy, (t) => { + t.boolean("allowedSelfApprovals").notNullable().defaultTo(true); + }); + } + if (!(await knex.schema.hasColumn(TableName.AccessApprovalPolicy, "allowedSelfApprovals"))) { + await knex.schema.alterTable(TableName.AccessApprovalPolicy, (t) => { + t.boolean("allowedSelfApprovals").notNullable().defaultTo(true); + }); + } +} + +export async function down(knex: Knex): Promise { + if (await knex.schema.hasColumn(TableName.SecretApprovalPolicy, "allowedSelfApprovals")) { + await knex.schema.alterTable(TableName.SecretApprovalPolicy, (t) => { + t.dropColumn("allowedSelfApprovals"); + }); + } + if (await knex.schema.hasColumn(TableName.AccessApprovalPolicy, "allowedSelfApprovals")) { + await knex.schema.alterTable(TableName.AccessApprovalPolicy, (t) => { + t.dropColumn("allowedSelfApprovals"); + }); + } +} diff --git a/backend/src/db/schemas/access-approval-policies.ts b/backend/src/db/schemas/access-approval-policies.ts index 3650face9..19a98675f 100644 --- a/backend/src/db/schemas/access-approval-policies.ts +++ b/backend/src/db/schemas/access-approval-policies.ts @@ -16,7 +16,8 @@ export const AccessApprovalPoliciesSchema = z.object({ createdAt: z.date(), updatedAt: z.date(), enforcementLevel: z.string().default("hard"), - deletedAt: z.date().nullable().optional() + deletedAt: z.date().nullable().optional(), + allowedSelfApprovals: z.boolean().default(true) }); export type TAccessApprovalPolicies = z.infer; diff --git a/backend/src/db/schemas/secret-approval-policies.ts b/backend/src/db/schemas/secret-approval-policies.ts index 06ae3e5c4..8b9174456 100644 --- a/backend/src/db/schemas/secret-approval-policies.ts +++ b/backend/src/db/schemas/secret-approval-policies.ts @@ -16,7 +16,8 @@ export const SecretApprovalPoliciesSchema = z.object({ createdAt: z.date(), updatedAt: z.date(), enforcementLevel: z.string().default("hard"), - deletedAt: z.date().nullable().optional() + deletedAt: z.date().nullable().optional(), + allowedSelfApprovals: z.boolean().default(true) }); export type TSecretApprovalPolicies = 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 814d19841..97a819234 100644 --- a/backend/src/ee/routes/v1/access-approval-policy-router.ts +++ b/backend/src/ee/routes/v1/access-approval-policy-router.ts @@ -29,7 +29,8 @@ export const registerAccessApprovalPolicyRouter = async (server: FastifyZodProvi .array() .min(1, { message: "At least one approver should be provided" }), approvals: z.number().min(1).default(1), - enforcementLevel: z.nativeEnum(EnforcementLevel).default(EnforcementLevel.Hard) + enforcementLevel: z.nativeEnum(EnforcementLevel).default(EnforcementLevel.Hard), + allowedSelfApprovals: z.boolean().default(true) }), response: { 200: z.object({ @@ -147,7 +148,8 @@ export const registerAccessApprovalPolicyRouter = async (server: FastifyZodProvi .array() .min(1, { message: "At least one approver should be provided" }), approvals: z.number().min(1).optional(), - enforcementLevel: z.nativeEnum(EnforcementLevel).default(EnforcementLevel.Hard) + enforcementLevel: z.nativeEnum(EnforcementLevel).default(EnforcementLevel.Hard), + allowedSelfApprovals: z.boolean().default(true) }), response: { 200: z.object({ 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 4aa26eb36..6a6ec3c07 100644 --- a/backend/src/ee/routes/v1/access-approval-request-router.ts +++ b/backend/src/ee/routes/v1/access-approval-request-router.ts @@ -110,7 +110,8 @@ export const registerAccessApprovalRequestRouter = async (server: FastifyZodProv secretPath: z.string().nullish(), envId: z.string(), enforcementLevel: z.string(), - deletedAt: z.date().nullish() + deletedAt: z.date().nullish(), + allowedSelfApprovals: z.boolean() }), reviewers: z .object({ diff --git a/backend/src/ee/routes/v1/secret-approval-policy-router.ts b/backend/src/ee/routes/v1/secret-approval-policy-router.ts index 40f0a71bd..846b60923 100644 --- a/backend/src/ee/routes/v1/secret-approval-policy-router.ts +++ b/backend/src/ee/routes/v1/secret-approval-policy-router.ts @@ -35,7 +35,8 @@ export const registerSecretApprovalPolicyRouter = async (server: FastifyZodProvi .array() .min(1, { message: "At least one approver should be provided" }), approvals: z.number().min(1).default(1), - enforcementLevel: z.nativeEnum(EnforcementLevel).default(EnforcementLevel.Hard) + enforcementLevel: z.nativeEnum(EnforcementLevel).default(EnforcementLevel.Hard), + allowedSelfApprovals: z.boolean().default(true) }), response: { 200: z.object({ @@ -85,7 +86,8 @@ export const registerSecretApprovalPolicyRouter = async (server: FastifyZodProvi .nullable() .transform((val) => (val ? removeTrailingSlash(val) : val)) .transform((val) => (val === "" ? "/" : val)), - enforcementLevel: z.nativeEnum(EnforcementLevel).optional() + enforcementLevel: z.nativeEnum(EnforcementLevel).optional(), + allowedSelfApprovals: z.boolean().default(true) }), response: { 200: z.object({ diff --git a/backend/src/ee/routes/v1/secret-approval-request-router.ts b/backend/src/ee/routes/v1/secret-approval-request-router.ts index 1ceb6019b..98cb9244d 100644 --- a/backend/src/ee/routes/v1/secret-approval-request-router.ts +++ b/backend/src/ee/routes/v1/secret-approval-request-router.ts @@ -49,7 +49,8 @@ export const registerSecretApprovalRequestRouter = async (server: FastifyZodProv .array(), secretPath: z.string().optional().nullable(), enforcementLevel: z.string(), - deletedAt: z.date().nullish() + deletedAt: z.date().nullish(), + allowedSelfApprovals: z.boolean() }), committerUser: approvalRequestUser, commits: z.object({ op: z.string(), secretId: z.string().nullable().optional() }).array(), @@ -267,7 +268,8 @@ export const registerSecretApprovalRequestRouter = async (server: FastifyZodProv approvers: approvalRequestUser.array(), secretPath: z.string().optional().nullable(), enforcementLevel: z.string(), - deletedAt: z.date().nullish() + deletedAt: z.date().nullish(), + allowedSelfApprovals: z.boolean() }), environment: z.string(), statusChangedByUser: approvalRequestUser.optional(), 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 2313cdec8..6b5014acc 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 @@ -65,7 +65,8 @@ export const accessApprovalPolicyServiceFactory = ({ approvers, projectSlug, environment, - enforcementLevel + enforcementLevel, + allowedSelfApprovals }: TCreateAccessApprovalPolicy) => { const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId); if (!project) throw new NotFoundError({ message: `Project with slug '${projectSlug}' not found` }); @@ -153,7 +154,8 @@ export const accessApprovalPolicyServiceFactory = ({ approvals, secretPath, name, - enforcementLevel + enforcementLevel, + allowedSelfApprovals }, tx ); @@ -216,7 +218,8 @@ export const accessApprovalPolicyServiceFactory = ({ actorOrgId, actorAuthMethod, approvals, - enforcementLevel + enforcementLevel, + allowedSelfApprovals }: TUpdateAccessApprovalPolicy) => { const groupApprovers = approvers .filter((approver) => approver.type === ApproverType.Group) @@ -262,7 +265,8 @@ export const accessApprovalPolicyServiceFactory = ({ approvals, secretPath, name, - enforcementLevel + enforcementLevel, + allowedSelfApprovals }, 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 a42c89e7a..dde8ffbea 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 @@ -26,6 +26,7 @@ export type TCreateAccessApprovalPolicy = { projectSlug: string; name: string; enforcementLevel: EnforcementLevel; + allowedSelfApprovals: boolean; } & Omit; export type TUpdateAccessApprovalPolicy = { @@ -35,6 +36,7 @@ export type TUpdateAccessApprovalPolicy = { secretPath?: string; name?: string; enforcementLevel?: EnforcementLevel; + allowedSelfApprovals: boolean; } & Omit; export type TDeleteAccessApprovalPolicy = { 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 c1ccedff7..e2075af0a 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 @@ -61,6 +61,7 @@ export const accessApprovalRequestDALFactory = (db: TDbClient) => { db.ref("approvals").withSchema(TableName.AccessApprovalPolicy).as("policyApprovals"), db.ref("secretPath").withSchema(TableName.AccessApprovalPolicy).as("policySecretPath"), 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") ) @@ -119,6 +120,7 @@ export const accessApprovalRequestDALFactory = (db: TDbClient) => { approvals: doc.policyApprovals, secretPath: doc.policySecretPath, enforcementLevel: doc.policyEnforcementLevel, + allowedSelfApprovals: doc.policyAllowedSelfApprovals, envId: doc.policyEnvId, deletedAt: doc.policyDeletedAt }, @@ -254,6 +256,7 @@ export const accessApprovalRequestDALFactory = (db: TDbClient) => { tx.ref("slug").withSchema(TableName.Environment).as("environment"), tx.ref("secretPath").withSchema(TableName.AccessApprovalPolicy).as("policySecretPath"), 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") ); @@ -275,6 +278,7 @@ export const accessApprovalRequestDALFactory = (db: TDbClient) => { approvals: el.policyApprovals, secretPath: el.policySecretPath, enforcementLevel: el.policyEnforcementLevel, + allowedSelfApprovals: el.policyAllowedSelfApprovals, deletedAt: el.policyDeletedAt }, requestedByUser: { 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 29a151515..50670cb49 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 @@ -320,6 +320,11 @@ export const accessApprovalRequestServiceFactory = ({ message: "The policy associated with this access request has been deleted." }); } + if (!policy.allowedSelfApprovals && actorId === accessApprovalRequest.requestedByUserId) { + throw new BadRequestError({ + message: "Failed to review access approval request. Users are not authorized to review their own request." + }); + } const { membership, hasRole } = await permissionService.getProjectPermission({ actor, diff --git a/backend/src/ee/services/license/licence-enums.ts b/backend/src/ee/services/license/licence-enums.ts new file mode 100644 index 000000000..047eb0a38 --- /dev/null +++ b/backend/src/ee/services/license/licence-enums.ts @@ -0,0 +1,24 @@ +export const BillingPlanRows = { + MemberLimit: { name: "Organization member limit", field: "memberLimit" }, + IdentityLimit: { name: "Organization identity limit", field: "identityLimit" }, + WorkspaceLimit: { name: "Project limit", field: "workspaceLimit" }, + EnvironmentLimit: { name: "Environment limit", field: "environmentLimit" }, + SecretVersioning: { name: "Secret versioning", field: "secretVersioning" }, + PitRecovery: { name: "Point in time recovery", field: "pitRecovery" }, + Rbac: { name: "RBAC", field: "rbac" }, + CustomRateLimits: { name: "Custom rate limits", field: "customRateLimits" }, + CustomAlerts: { name: "Custom alerts", field: "customAlerts" }, + AuditLogs: { name: "Audit logs", field: "auditLogs" }, + SamlSSO: { name: "SAML SSO", field: "samlSSO" }, + Hsm: { name: "Hardware Security Module (HSM)", field: "hsm" }, + OidcSSO: { name: "OIDC SSO", field: "oidcSSO" }, + SecretApproval: { name: "Secret approvals", field: "secretApproval" }, + SecretRotation: { name: "Secret rotation", field: "secretRotation" }, + InstanceUserManagement: { name: "Instance User Management", field: "instanceUserManagement" }, + ExternalKms: { name: "External KMS", field: "externalKms" } +} as const; + +export const BillingPlanTableHead = { + Allowed: { name: "Allowed" }, + Used: { name: "Used" } +} as const; diff --git a/backend/src/ee/services/license/license-service.ts b/backend/src/ee/services/license/license-service.ts index 7835ccfae..29c36c7fe 100644 --- a/backend/src/ee/services/license/license-service.ts +++ b/backend/src/ee/services/license/license-service.ts @@ -12,10 +12,13 @@ import { getConfig } from "@app/lib/config/env"; import { verifyOfflineLicense } from "@app/lib/crypto"; import { NotFoundError } from "@app/lib/errors"; import { logger } from "@app/lib/logger"; +import { TIdentityOrgDALFactory } from "@app/services/identity/identity-org-dal"; import { TOrgDALFactory } from "@app/services/org/org-dal"; +import { TProjectDALFactory } from "@app/services/project/project-dal"; import { OrgPermissionActions, OrgPermissionSubjects } from "../permission/org-permission"; import { TPermissionServiceFactory } from "../permission/permission-service"; +import { BillingPlanRows, BillingPlanTableHead } from "./licence-enums"; import { TLicenseDALFactory } from "./license-dal"; import { getDefaultOnPremFeatures, setupLicenseRequestWithStore } from "./license-fns"; import { @@ -28,6 +31,7 @@ import { TFeatureSet, TGetOrgBillInfoDTO, TGetOrgTaxIdDTO, + TOfflineLicense, TOfflineLicenseContents, TOrgInvoiceDTO, TOrgLicensesDTO, @@ -39,10 +43,12 @@ import { } from "./license-types"; type TLicenseServiceFactoryDep = { - orgDAL: Pick; + orgDAL: Pick; permissionService: Pick; licenseDAL: TLicenseDALFactory; keyStore: Pick; + identityOrgMembershipDAL: TIdentityOrgDALFactory; + projectDAL: TProjectDALFactory; }; export type TLicenseServiceFactory = ReturnType; @@ -57,11 +63,14 @@ export const licenseServiceFactory = ({ orgDAL, permissionService, licenseDAL, - keyStore + keyStore, + identityOrgMembershipDAL, + projectDAL }: TLicenseServiceFactoryDep) => { let isValidLicense = false; let instanceType = InstanceType.OnPrem; let onPremFeatures: TFeatureSet = getDefaultOnPremFeatures(); + let selfHostedLicense: TOfflineLicense | null = null; const appCfg = getConfig(); const licenseServerCloudApi = setupLicenseRequestWithStore( @@ -125,6 +134,7 @@ export const licenseServiceFactory = ({ instanceType = InstanceType.EnterpriseOnPremOffline; logger.info(`Instance type: ${InstanceType.EnterpriseOnPremOffline}`); isValidLicense = true; + selfHostedLicense = contents.license; return; } } @@ -348,10 +358,21 @@ export const licenseServiceFactory = ({ message: `Organization with ID '${orgId}' not found` }); } - const { data } = await licenseServerCloudApi.request.get( - `/api/license-server/v1/customers/${organization.customerId}/cloud-plan/billing` - ); - return data; + if (instanceType !== InstanceType.OnPrem && instanceType !== InstanceType.EnterpriseOnPremOffline) { + const { data } = await licenseServerCloudApi.request.get( + `/api/license-server/v1/customers/${organization.customerId}/cloud-plan/billing` + ); + return data; + } + + return { + currentPeriodStart: selfHostedLicense?.issuedAt ? Date.parse(selfHostedLicense?.issuedAt) / 1000 : undefined, + currentPeriodEnd: selfHostedLicense?.expiresAt ? Date.parse(selfHostedLicense?.expiresAt) / 1000 : undefined, + interval: "month", + intervalCount: 1, + amount: 0, + quantity: 1 + }; }; // returns org current plan feature table @@ -365,10 +386,41 @@ export const licenseServiceFactory = ({ message: `Organization with ID '${orgId}' not found` }); } - const { data } = await licenseServerCloudApi.request.get( - `/api/license-server/v1/customers/${organization.customerId}/cloud-plan/table` + if (instanceType !== InstanceType.OnPrem && instanceType !== InstanceType.EnterpriseOnPremOffline) { + const { data } = await licenseServerCloudApi.request.get( + `/api/license-server/v1/customers/${organization.customerId}/cloud-plan/table` + ); + return data; + } + + const mappedRows = await Promise.all( + Object.values(BillingPlanRows).map(async ({ name, field }: { name: string; field: string }) => { + const allowed = onPremFeatures[field as keyof TFeatureSet]; + let used = "-"; + + if (field === BillingPlanRows.MemberLimit.field) { + const orgMemberships = await orgDAL.countAllOrgMembers(orgId); + used = orgMemberships.toString(); + } else if (field === BillingPlanRows.WorkspaceLimit.field) { + const projects = await projectDAL.find({ orgId }); + used = projects.length.toString(); + } else if (field === BillingPlanRows.IdentityLimit.field) { + const identities = await identityOrgMembershipDAL.countAllOrgIdentities({ orgId }); + used = identities.toString(); + } + + return { + name, + allowed, + used + }; + }) ); - return data; + + return { + head: Object.values(BillingPlanTableHead), + rows: mappedRows + }; }; const getOrgBillingDetails = async ({ orgId, actor, actorId, actorAuthMethod, actorOrgId }: TGetOrgBillInfoDTO) => { diff --git a/backend/src/ee/services/secret-approval-policy/secret-approval-policy-service.ts b/backend/src/ee/services/secret-approval-policy/secret-approval-policy-service.ts index 189ba5c1f..4c212e6cd 100644 --- a/backend/src/ee/services/secret-approval-policy/secret-approval-policy-service.ts +++ b/backend/src/ee/services/secret-approval-policy/secret-approval-policy-service.ts @@ -62,7 +62,8 @@ export const secretApprovalPolicyServiceFactory = ({ projectId, secretPath, environment, - enforcementLevel + enforcementLevel, + allowedSelfApprovals }: TCreateSapDTO) => { const groupApprovers = approvers ?.filter((approver) => approver.type === ApproverType.Group) @@ -113,7 +114,8 @@ export const secretApprovalPolicyServiceFactory = ({ approvals, secretPath, name, - enforcementLevel + enforcementLevel, + allowedSelfApprovals }, tx ); @@ -172,7 +174,8 @@ export const secretApprovalPolicyServiceFactory = ({ actorAuthMethod, approvals, secretPolicyId, - enforcementLevel + enforcementLevel, + allowedSelfApprovals }: TUpdateSapDTO) => { const groupApprovers = approvers ?.filter((approver) => approver.type === ApproverType.Group) @@ -218,7 +221,8 @@ export const secretApprovalPolicyServiceFactory = ({ approvals, secretPath, name, - enforcementLevel + enforcementLevel, + allowedSelfApprovals }, tx ); diff --git a/backend/src/ee/services/secret-approval-policy/secret-approval-policy-types.ts b/backend/src/ee/services/secret-approval-policy/secret-approval-policy-types.ts index 863f1c926..a6fea6956 100644 --- a/backend/src/ee/services/secret-approval-policy/secret-approval-policy-types.ts +++ b/backend/src/ee/services/secret-approval-policy/secret-approval-policy-types.ts @@ -10,6 +10,7 @@ export type TCreateSapDTO = { projectId: string; name: string; enforcementLevel: EnforcementLevel; + allowedSelfApprovals: boolean; } & Omit; export type TUpdateSapDTO = { @@ -19,6 +20,7 @@ export type TUpdateSapDTO = { approvers: ({ type: ApproverType.Group; id: string } | { type: ApproverType.User; id?: string; name?: string })[]; name?: string; enforcementLevel?: EnforcementLevel; + allowedSelfApprovals?: boolean; } & Omit; export type TDeleteSapDTO = { diff --git a/backend/src/ee/services/secret-approval-request/secret-approval-request-dal.ts b/backend/src/ee/services/secret-approval-request/secret-approval-request-dal.ts index 5fc869d12..be4a7ab3b 100644 --- a/backend/src/ee/services/secret-approval-request/secret-approval-request-dal.ts +++ b/backend/src/ee/services/secret-approval-request/secret-approval-request-dal.ts @@ -112,6 +112,7 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => { tx.ref("secretPath").withSchema(TableName.SecretApprovalPolicy).as("policySecretPath"), tx.ref("envId").withSchema(TableName.SecretApprovalPolicy).as("policyEnvId"), tx.ref("enforcementLevel").withSchema(TableName.SecretApprovalPolicy).as("policyEnforcementLevel"), + tx.ref("allowedSelfApprovals").withSchema(TableName.SecretApprovalPolicy).as("policyAllowedSelfApprovals"), tx.ref("approvals").withSchema(TableName.SecretApprovalPolicy).as("policyApprovals"), tx.ref("deletedAt").withSchema(TableName.SecretApprovalPolicy).as("policyDeletedAt") ); @@ -150,7 +151,8 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => { secretPath: el.policySecretPath, enforcementLevel: el.policyEnforcementLevel, envId: el.policyEnvId, - deletedAt: el.policyDeletedAt + deletedAt: el.policyDeletedAt, + allowedSelfApprovals: el.policyAllowedSelfApprovals } }), childrenMapper: [ @@ -336,6 +338,7 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => { ), db.ref("secretPath").withSchema(TableName.SecretApprovalPolicy).as("policySecretPath"), db.ref("enforcementLevel").withSchema(TableName.SecretApprovalPolicy).as("policyEnforcementLevel"), + db.ref("allowedSelfApprovals").withSchema(TableName.SecretApprovalPolicy).as("policyAllowedSelfApprovals"), db.ref("approvals").withSchema(TableName.SecretApprovalPolicy).as("policyApprovals"), db.ref("approverUserId").withSchema(TableName.SecretApprovalPolicyApprover), db.ref("userId").withSchema(TableName.UserGroupMembership).as("approverGroupUserId"), @@ -364,7 +367,8 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => { name: el.policyName, approvals: el.policyApprovals, secretPath: el.policySecretPath, - enforcementLevel: el.policyEnforcementLevel + enforcementLevel: el.policyEnforcementLevel, + allowedSelfApprovals: el.policyAllowedSelfApprovals }, committerUser: { userId: el.committerUserId, @@ -482,6 +486,7 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => { `DENSE_RANK() OVER (partition by ${TableName.Environment}."projectId" ORDER BY ${TableName.SecretApprovalRequest}."id" DESC) as rank` ), db.ref("secretPath").withSchema(TableName.SecretApprovalPolicy).as("policySecretPath"), + db.ref("allowedSelfApprovals").withSchema(TableName.SecretApprovalPolicy).as("policyAllowedSelfApprovals"), db.ref("approvals").withSchema(TableName.SecretApprovalPolicy).as("policyApprovals"), db.ref("enforcementLevel").withSchema(TableName.SecretApprovalPolicy).as("policyEnforcementLevel"), db.ref("approverUserId").withSchema(TableName.SecretApprovalPolicyApprover), @@ -511,7 +516,8 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => { name: el.policyName, approvals: el.policyApprovals, secretPath: el.policySecretPath, - enforcementLevel: el.policyEnforcementLevel + enforcementLevel: el.policyEnforcementLevel, + allowedSelfApprovals: el.policyAllowedSelfApprovals }, committerUser: { userId: el.committerUserId, diff --git a/backend/src/ee/services/secret-approval-request/secret-approval-request-service.ts b/backend/src/ee/services/secret-approval-request/secret-approval-request-service.ts index 98cb13865..879065ef5 100644 --- a/backend/src/ee/services/secret-approval-request/secret-approval-request-service.ts +++ b/backend/src/ee/services/secret-approval-request/secret-approval-request-service.ts @@ -352,6 +352,11 @@ export const secretApprovalRequestServiceFactory = ({ message: "The policy associated with this secret approval request has been deleted." }); } + if (!policy.allowedSelfApprovals && actorId === secretApprovalRequest.committerUserId) { + throw new BadRequestError({ + message: "Failed to review secret approval request. Users are not authorized to review their own request." + }); + } const { hasRole } = await permissionService.getProjectPermission({ actor: ActorType.USER, diff --git a/backend/src/server/routes/index.ts b/backend/src/server/routes/index.ts index 9aa60a631..dac88791f 100644 --- a/backend/src/server/routes/index.ts +++ b/backend/src/server/routes/index.ts @@ -413,7 +413,14 @@ export const registerRoutes = async ( serviceTokenDAL, projectDAL }); - const licenseService = licenseServiceFactory({ permissionService, orgDAL, licenseDAL, keyStore }); + const licenseService = licenseServiceFactory({ + permissionService, + orgDAL, + licenseDAL, + keyStore, + identityOrgMembershipDAL, + projectDAL + }); const hsmService = hsmServiceFactory({ hsmModule, diff --git a/backend/src/services/identity-ua/identity-ua-service.ts b/backend/src/services/identity-ua/identity-ua-service.ts index fdb7acd52..37aff1b1f 100644 --- a/backend/src/services/identity-ua/identity-ua-service.ts +++ b/backend/src/services/identity-ua/identity-ua-service.ts @@ -64,9 +64,11 @@ export const identityUaServiceFactory = ({ ipAddress: ip, trustedIps: identityUa.clientSecretTrustedIps as TIp[] }); + const clientSecretPrefix = clientSecret.slice(0, 4); const clientSecrtInfo = await identityUaClientSecretDAL.find({ identityUAId: identityUa.id, - isClientSecretRevoked: false + isClientSecretRevoked: false, + clientSecretPrefix }); let validClientSecretInfo: (typeof clientSecrtInfo)[0] | null = null; diff --git a/docs/documentation/platform/audit-logs.mdx b/docs/documentation/platform/audit-logs.mdx index 7059fad4a..ea6b09fa3 100644 --- a/docs/documentation/platform/audit-logs.mdx +++ b/docs/documentation/platform/audit-logs.mdx @@ -1,6 +1,6 @@ --- title: "Overview" -description: "Track evert event action performed within Infisical projects." +description: "Track all actions performed within Infisical" --- diff --git a/frontend/src/helpers/platform.ts b/frontend/src/helpers/platform.ts index 821febcb8..73aaeb4ea 100644 --- a/frontend/src/helpers/platform.ts +++ b/frontend/src/helpers/platform.ts @@ -1,4 +1,5 @@ export const isInfisicalCloud = () => window.location.origin.includes("https://app.infisical.com") || window.location.origin.includes("https://us.infisical.com") || - window.location.origin.includes("https://eu.infisical.com"); + window.location.origin.includes("https://eu.infisical.com") || + window.location.origin.includes("https://gamma.infisical.com"); diff --git a/frontend/src/hooks/api/accessApproval/mutation.tsx b/frontend/src/hooks/api/accessApproval/mutation.tsx index 115b73186..9fda2377a 100644 --- a/frontend/src/hooks/api/accessApproval/mutation.tsx +++ b/frontend/src/hooks/api/accessApproval/mutation.tsx @@ -23,7 +23,8 @@ export const useCreateAccessApprovalPolicy = () => { approvers, name, secretPath, - enforcementLevel + enforcementLevel, + allowedSelfApprovals }) => { const { data } = await apiRequest.post("/api/v1/access-approvals/policies", { environment, @@ -32,7 +33,8 @@ export const useCreateAccessApprovalPolicy = () => { approvers, secretPath, name, - enforcementLevel + enforcementLevel, + allowedSelfApprovals }); return data; }, @@ -48,13 +50,22 @@ export const useUpdateAccessApprovalPolicy = () => { const queryClient = useQueryClient(); return useMutation({ - mutationFn: async ({ id, approvers, approvals, name, secretPath, enforcementLevel }) => { + mutationFn: async ({ + id, + approvers, + approvals, + name, + secretPath, + enforcementLevel, + allowedSelfApprovals + }) => { const { data } = await apiRequest.patch(`/api/v1/access-approvals/policies/${id}`, { approvals, approvers, secretPath, name, - enforcementLevel + enforcementLevel, + allowedSelfApprovals }); return data; }, diff --git a/frontend/src/hooks/api/accessApproval/types.ts b/frontend/src/hooks/api/accessApproval/types.ts index bd6173d91..40da0207a 100644 --- a/frontend/src/hooks/api/accessApproval/types.ts +++ b/frontend/src/hooks/api/accessApproval/types.ts @@ -16,6 +16,7 @@ export type TAccessApprovalPolicy = { enforcementLevel: EnforcementLevel; updatedAt: Date; approvers?: Approver[]; + allowedSelfApprovals: boolean; }; export enum ApproverType { @@ -71,6 +72,7 @@ export type TAccessApprovalRequest = { envId: string; enforcementLevel: EnforcementLevel; deletedAt: Date | null; + allowedSelfApprovals: boolean; }; reviewers: { @@ -144,6 +146,7 @@ export type TCreateAccessPolicyDTO = { approvals?: number; secretPath?: string; enforcementLevel?: EnforcementLevel; + allowedSelfApprovals: boolean; }; export type TUpdateAccessPolicyDTO = { @@ -154,6 +157,7 @@ export type TUpdateAccessPolicyDTO = { environment?: string; approvals?: number; enforcementLevel?: EnforcementLevel; + allowedSelfApprovals: boolean; // for invalidating list projectSlug: string; }; diff --git a/frontend/src/hooks/api/secretApproval/mutation.tsx b/frontend/src/hooks/api/secretApproval/mutation.tsx index f5ffad7b0..f9aca5bee 100644 --- a/frontend/src/hooks/api/secretApproval/mutation.tsx +++ b/frontend/src/hooks/api/secretApproval/mutation.tsx @@ -16,7 +16,8 @@ export const useCreateSecretApprovalPolicy = () => { approvers, secretPath, name, - enforcementLevel + enforcementLevel, + allowedSelfApprovals }) => { const { data } = await apiRequest.post("/api/v1/secret-approvals", { environment, @@ -25,7 +26,8 @@ export const useCreateSecretApprovalPolicy = () => { approvers, secretPath, name, - enforcementLevel + enforcementLevel, + allowedSelfApprovals }); return data; }, @@ -41,13 +43,22 @@ export const useUpdateSecretApprovalPolicy = () => { const queryClient = useQueryClient(); return useMutation({ - mutationFn: async ({ id, approvers, approvals, secretPath, name, enforcementLevel }) => { + mutationFn: async ({ + id, + approvers, + approvals, + secretPath, + name, + enforcementLevel, + allowedSelfApprovals + }) => { const { data } = await apiRequest.patch(`/api/v1/secret-approvals/${id}`, { approvals, approvers, secretPath, name, - enforcementLevel + enforcementLevel, + allowedSelfApprovals }); return data; }, diff --git a/frontend/src/hooks/api/secretApproval/types.ts b/frontend/src/hooks/api/secretApproval/types.ts index 6df30b70c..0a4b0d495 100644 --- a/frontend/src/hooks/api/secretApproval/types.ts +++ b/frontend/src/hooks/api/secretApproval/types.ts @@ -12,6 +12,7 @@ export type TSecretApprovalPolicy = { approvers: Approver[]; updatedAt: Date; enforcementLevel: EnforcementLevel; + allowedSelfApprovals: boolean; }; export enum ApproverType { @@ -42,6 +43,7 @@ export type TCreateSecretPolicyDTO = { approvers?: Approver[]; approvals?: number; enforcementLevel: EnforcementLevel; + allowedSelfApprovals: boolean; }; export type TUpdateSecretPolicyDTO = { @@ -50,6 +52,7 @@ export type TUpdateSecretPolicyDTO = { approvers?: Approver[]; secretPath?: string | null; approvals?: number; + allowedSelfApprovals?: boolean; enforcementLevel?: EnforcementLevel; // for invalidating list workspaceId: string; diff --git a/frontend/src/layouts/OrganizationLayout/ProductsSideBar/DefaultSideBar.tsx b/frontend/src/layouts/OrganizationLayout/ProductsSideBar/DefaultSideBar.tsx index e78d86703..054a873a3 100644 --- a/frontend/src/layouts/OrganizationLayout/ProductsSideBar/DefaultSideBar.tsx +++ b/frontend/src/layouts/OrganizationLayout/ProductsSideBar/DefaultSideBar.tsx @@ -12,17 +12,13 @@ export const DefaultSideBar = () => ( )} - {(window.location.origin.includes("https://app.infisical.com") || - window.location.origin.includes("https://eu.infisical.com") || - window.location.origin.includes("https://gamma.infisical.com")) && ( - - {({ isActive }) => ( - - Usage & Billing - - )} - - )} + + {({ isActive }) => ( + + Usage & Billing + + )} + diff --git a/frontend/src/layouts/OrganizationLayout/components/MinimizedOrgSidebar/MinimizedOrgSidebar.tsx b/frontend/src/layouts/OrganizationLayout/components/MinimizedOrgSidebar/MinimizedOrgSidebar.tsx index 3cc104f48..f625a1627 100644 --- a/frontend/src/layouts/OrganizationLayout/components/MinimizedOrgSidebar/MinimizedOrgSidebar.tsx +++ b/frontend/src/layouts/OrganizationLayout/components/MinimizedOrgSidebar/MinimizedOrgSidebar.tsx @@ -370,17 +370,11 @@ export const MinimizedOrgSidebar = () => { Gateways - {(window.location.origin.includes("https://app.infisical.com") || - window.location.origin.includes("https://eu.infisical.com") || - window.location.origin.includes("https://gamma.infisical.com")) && ( - - } - > - Usage & Billing - - - )} + + }> + Usage & Billing + + }> Audit Logs diff --git a/frontend/src/pages/organization/BillingPage/components/BillingCloudTab/PreviewSection.tsx b/frontend/src/pages/organization/BillingPage/components/BillingCloudTab/PreviewSection.tsx index ab160e4c5..5741722bd 100644 --- a/frontend/src/pages/organization/BillingPage/components/BillingCloudTab/PreviewSection.tsx +++ b/frontend/src/pages/organization/BillingPage/components/BillingCloudTab/PreviewSection.tsx @@ -9,6 +9,7 @@ import { useOrganization, useSubscription } from "@app/context"; +import { isInfisicalCloud } from "@app/helpers/platform"; import { useCreateCustomerPortalSession, useGetOrgPlanBillingInfo, @@ -47,6 +48,9 @@ export const PreviewSection = () => { }; function formatPlanSlug(slug: string) { + if (!slug) { + return "-"; + } return slug.replace(/(\b[a-z])/g, (match) => match.toUpperCase()).replace(/-/g, " "); } @@ -54,6 +58,11 @@ export const PreviewSection = () => { try { if (!subscription || !currentOrg) return; + if (!isInfisicalCloud()) { + window.open("https://infisical.com/pricing", "_blank"); + return; + } + if (!subscription.has_used_trial) { // direct user to start pro trial const url = await getOrgTrialUrl.mutateAsync({ @@ -71,6 +80,19 @@ export const PreviewSection = () => { } }; + const getUpgradePlanLabel = () => { + if (!isInfisicalCloud()) { + return ( +
+ Go to Pricing + +
+ ); + } + + return !subscription.has_used_trial ? "Start Pro Free Trial" : "Upgrade Plan"; + }; + return (
{subscription && @@ -97,7 +119,7 @@ export const PreviewSection = () => { color="mineshaft" isDisabled={!isAllowed} > - {!subscription.has_used_trial ? "Start Pro Free Trial" : "Upgrade Plan"} + {getUpgradePlanLabel()} )} @@ -133,22 +155,24 @@ export const PreviewSection = () => { subscription.status === "trialing" ? "(Trial)" : "" }`}

- - {(isAllowed) => ( - - )} - + {isInfisicalCloud() && ( + + {(isAllowed) => ( + + )} + + )}

Price

@@ -161,7 +185,7 @@ export const PreviewSection = () => {

Subscription renews on

- {formatDate(data.currentPeriodEnd)} + {data.currentPeriodEnd ? formatDate(data.currentPeriodEnd) : "-"}

diff --git a/frontend/src/pages/organization/BillingPage/components/BillingTabGroup/BillingTabGroup.tsx b/frontend/src/pages/organization/BillingPage/components/BillingTabGroup/BillingTabGroup.tsx index 6d8b59d2a..9631f68da 100644 --- a/frontend/src/pages/organization/BillingPage/components/BillingTabGroup/BillingTabGroup.tsx +++ b/frontend/src/pages/organization/BillingPage/components/BillingTabGroup/BillingTabGroup.tsx @@ -1,5 +1,6 @@ import { Tab, TabList, TabPanel, Tabs } from "@app/components/v2"; import { OrgPermissionActions, OrgPermissionSubjects } from "@app/context"; +import { isInfisicalCloud } from "@app/helpers/platform"; import { withPermission } from "@app/hoc"; import { BillingCloudTab } from "../BillingCloudTab"; @@ -16,25 +17,33 @@ const tabs = [ export const BillingTabGroup = withPermission( () => { + const tabsFiltered = isInfisicalCloud() + ? tabs + : [{ name: "Infisical Self-Hosted", key: "tab-infisical-cloud" }]; + return ( - {tabs.map((tab) => ( + {tabsFiltered.map((tab) => ( {tab.name} ))} - - - - - - - - - + {isInfisicalCloud() && ( + <> + + + + + + + + + + + )} ); }, diff --git a/frontend/src/pages/secret-manager/SecretApprovalsPage/components/AccessApprovalRequest/AccessApprovalRequest.tsx b/frontend/src/pages/secret-manager/SecretApprovalsPage/components/AccessApprovalRequest/AccessApprovalRequest.tsx index ff762ad5d..b857e3038 100644 --- a/frontend/src/pages/secret-manager/SecretApprovalsPage/components/AccessApprovalRequest/AccessApprovalRequest.tsx +++ b/frontend/src/pages/secret-manager/SecretApprovalsPage/components/AccessApprovalRequest/AccessApprovalRequest.tsx @@ -152,7 +152,7 @@ export const AccessApprovalRequest = ({ const isAccepted = request.isApproved; const isSoftEnforcement = request.policy.enforcementLevel === EnforcementLevel.Soft; const isRequestedByCurrentUser = request.requestedByUserId === user.id; - + const isSelfApproveAllowed = request.policy.allowedSelfApprovals; const userReviewStatus = request.reviewers.find(({ member }) => member === user.id)?.status; let displayData: { label: string; type: "primary" | "danger" | "success" } = { @@ -189,7 +189,8 @@ export const AccessApprovalRequest = ({ userReviewStatus, isAccepted, isSoftEnforcement, - isRequestedByCurrentUser + isRequestedByCurrentUser, + isSelfApproveAllowed }; }; @@ -342,15 +343,16 @@ export const AccessApprovalRequest = ({ tabIndex={0} onClick={() => { if ( - (!details.isApprover || + ((!details.isApprover || details.isReviewedByUser || details.isRejectedByAnyone || details.isAccepted) && - !( - details.isSoftEnforcement && - details.isRequestedByCurrentUser && - !details.isAccepted - ) + !( + details.isSoftEnforcement && + details.isRequestedByCurrentUser && + !details.isAccepted + )) || + (request.requestedByUserId === user.id && !details.isSelfApproveAllowed) ) return; if (membersGroupById?.[request.requestedByUserId].user) { 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 9f5aeec5a..269d77543 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 @@ -12,7 +12,8 @@ import { Modal, ModalContent, Select, - SelectItem + SelectItem, + Switch } from "@app/components/v2"; import { useWorkspace } from "@app/context"; import { getMemberLabel } from "@app/helpers/members"; @@ -54,7 +55,8 @@ const formSchema = z .array() .default([]), policyType: z.nativeEnum(PolicyType), - enforcementLevel: z.nativeEnum(EnforcementLevel) + enforcementLevel: z.nativeEnum(EnforcementLevel), + allowedSelfApprovals: z.boolean().default(true) }) .superRefine((data, ctx) => { if (!(data.groupApprovers.length || data.userApprovers.length)) { @@ -101,7 +103,8 @@ export const AccessPolicyForm = ({ editValues?.approvers ?.filter((approver) => approver.type === ApproverType.Group) .map(({ id, type }) => ({ id, type: type as ApproverType.Group })) || [], - approvals: editValues?.approvals + approvals: editValues?.approvals, + allowedSelfApprovals: editValues?.allowedSelfApprovals } : undefined }); @@ -441,6 +444,27 @@ export const AccessPolicyForm = ({ )} /> + ( + + + Allow approvers to review their own requests + + + )} + />
- {!hasMerged && secretApprovalRequestDetails.status === "open" && ( - handlePopUpToggle("reviewChanges", isOpen)} - > - - - - -
-
-
Finish your review
- ( - -