mirror of
https://github.com/Infisical/infisical.git
synced 2025-04-10 07:25:40 +00:00
Compare commits
7 Commits
helm-updat
...
secret-rot
Author | SHA1 | Date | |
---|---|---|---|
bcf86aea90 | |||
06024065cd | |||
2d207147a6 | |||
4c04d0f871 | |||
484a9b28ef | |||
94f79ade4a | |||
5a63a6dbe4 |
27
.github/workflows/release-k8-operator-helm.yml
vendored
27
.github/workflows/release-k8-operator-helm.yml
vendored
@ -1,27 +0,0 @@
|
||||
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 }}
|
143
.github/workflows/release_docker_k8_operator.yaml
vendored
143
.github/workflows/release_docker_k8_operator.yaml
vendored
@ -1,107 +1,52 @@
|
||||
name: Release K8 Operator Docker Image
|
||||
name: Release image + Helm chart K8s Operator
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- "infisical-k8-operator/v*.*.*"
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
push:
|
||||
tags:
|
||||
- "infisical-k8-operator/v*.*.*"
|
||||
|
||||
jobs:
|
||||
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/}"
|
||||
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
|
||||
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v2
|
||||
- name: 🔧 Set up QEMU
|
||||
uses: docker/setup-qemu-action@v1
|
||||
|
||||
# Dependency for helm generation
|
||||
- name: Install Helm
|
||||
uses: azure/setup-helm@v3
|
||||
with:
|
||||
version: v3.10.0
|
||||
- name: 🔧 Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v1
|
||||
|
||||
# Dependency for helm generation
|
||||
- name: Install Go
|
||||
uses: actions/setup-go@v4
|
||||
with:
|
||||
go-version: 1.21
|
||||
- name: 🐋 Login to Docker Hub
|
||||
uses: docker/login-action@v1
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
# Install binaries for helm generation
|
||||
- name: Install dependencies
|
||||
working-directory: k8-operator
|
||||
run: |
|
||||
make helmify
|
||||
make kustomize
|
||||
make controller-gen
|
||||
- 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 }}
|
||||
|
||||
- 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 <noreply@github.com>
|
||||
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 }}
|
||||
- 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 }}
|
||||
|
@ -8,9 +8,3 @@ frontend/src/views/Project/MembersPage/components/MemberListTab/MemberRoleForm/S
|
||||
docs/mint.json:generic-api-key:651
|
||||
backend/src/ee/services/hsm/hsm-service.ts:generic-api-key:134
|
||||
docs/documentation/platform/audit-log-streams/audit-log-streams.mdx:generic-api-key:104
|
||||
docs/cli/commands/bootstrap.mdx:jwt:86
|
||||
docs/documentation/platform/audit-log-streams/audit-log-streams.mdx:generic-api-key:102
|
||||
docs/self-hosting/guides/automated-bootstrapping.mdx:jwt:74
|
||||
frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretListView/SecretDetailSidebar.tsx:generic-api-key:72
|
||||
k8-operator/config/samples/crd/pushsecret/source-secret-with-templating.yaml:private-key:11
|
||||
k8-operator/config/samples/crd/pushsecret/push-secret-with-template.yaml:private-key:52
|
||||
|
7
backend/src/@types/fastify-request-context.d.ts
vendored
Normal file
7
backend/src/@types/fastify-request-context.d.ts
vendored
Normal file
@ -0,0 +1,7 @@
|
||||
import "@fastify/request-context";
|
||||
|
||||
declare module "@fastify/request-context" {
|
||||
interface RequestContextData {
|
||||
reqId: string;
|
||||
}
|
||||
}
|
9
backend/src/@types/fastify.d.ts
vendored
9
backend/src/@types/fastify.d.ts
vendored
@ -33,6 +33,7 @@ import { TScimServiceFactory } from "@app/ee/services/scim/scim-service";
|
||||
import { TSecretApprovalPolicyServiceFactory } from "@app/ee/services/secret-approval-policy/secret-approval-policy-service";
|
||||
import { TSecretApprovalRequestServiceFactory } from "@app/ee/services/secret-approval-request/secret-approval-request-service";
|
||||
import { TSecretRotationServiceFactory } from "@app/ee/services/secret-rotation/secret-rotation-service";
|
||||
import { TSecretRotationV2ServiceFactory } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-service";
|
||||
import { TSecretScanningServiceFactory } from "@app/ee/services/secret-scanning/secret-scanning-service";
|
||||
import { TSecretSnapshotServiceFactory } from "@app/ee/services/secret-snapshot/secret-snapshot-service";
|
||||
import { TSshCertificateAuthorityServiceFactory } from "@app/ee/services/ssh/ssh-certificate-authority-service";
|
||||
@ -100,13 +101,6 @@ import { TWorkflowIntegrationServiceFactory } from "@app/services/workflow-integ
|
||||
declare module "@fastify/request-context" {
|
||||
interface RequestContextData {
|
||||
reqId: string;
|
||||
identityAuthInfo?: {
|
||||
identityId: string;
|
||||
oidc?: {
|
||||
claims: Record<string, string>;
|
||||
};
|
||||
};
|
||||
identityPermissionMetadata?: Record<string, unknown>; // filled by permission service
|
||||
}
|
||||
}
|
||||
|
||||
@ -237,6 +231,7 @@ declare module "fastify" {
|
||||
kmip: TKmipServiceFactory;
|
||||
kmipOperation: TKmipOperationServiceFactory;
|
||||
gateway: TGatewayServiceFactory;
|
||||
secretRotationV2: TSecretRotationV2ServiceFactory;
|
||||
};
|
||||
// this is exclusive use for middlewares in which we need to inject data
|
||||
// everywhere else access using service layer
|
||||
|
10
backend/src/@types/knex.d.ts
vendored
10
backend/src/@types/knex.d.ts
vendored
@ -393,6 +393,11 @@ import {
|
||||
TExternalGroupOrgRoleMappingsInsert,
|
||||
TExternalGroupOrgRoleMappingsUpdate
|
||||
} from "@app/db/schemas/external-group-org-role-mappings";
|
||||
import {
|
||||
TSecretRotationsV2,
|
||||
TSecretRotationsV2Insert,
|
||||
TSecretRotationsV2Update
|
||||
} from "@app/db/schemas/secret-rotations-v2";
|
||||
import { TSecretSyncs, TSecretSyncsInsert, TSecretSyncsUpdate } from "@app/db/schemas/secret-syncs";
|
||||
import {
|
||||
TSecretV2TagJunction,
|
||||
@ -950,5 +955,10 @@ declare module "knex/types/tables" {
|
||||
TOrgGatewayConfigInsert,
|
||||
TOrgGatewayConfigUpdate
|
||||
>;
|
||||
[TableName.SecretRotationV2]: KnexOriginal.CompositeTableType<
|
||||
TSecretRotationsV2,
|
||||
TSecretRotationsV2Insert,
|
||||
TSecretRotationsV2Update
|
||||
>;
|
||||
}
|
||||
}
|
||||
|
@ -16,7 +16,7 @@ const createAuditLogPartition = async (knex: Knex, startDate: Date, endDate: Dat
|
||||
const startDateStr = formatPartitionDate(startDate);
|
||||
const endDateStr = formatPartitionDate(endDate);
|
||||
|
||||
const partitionName = `${TableName.AuditLog}_${startDateStr.replaceAll("-", "")}_${endDateStr.replaceAll("-", "")}`;
|
||||
const partitionName = `${TableName.AuditLog}_${startDateStr.replace(/-/g, "")}_${endDateStr.replace(/-/g, "")}`;
|
||||
|
||||
await knex.schema.raw(
|
||||
`CREATE TABLE ${partitionName} PARTITION OF ${TableName.AuditLog} FOR VALUES FROM ('${startDateStr}') TO ('${endDateStr}')`
|
||||
|
@ -1,30 +0,0 @@
|
||||
import { Knex } from "knex";
|
||||
import { TableName } from "../schemas";
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
if (!(await knex.schema.hasColumn(TableName.Organization, "shouldUseNewPrivilegeSystem"))) {
|
||||
await knex.schema.alterTable(TableName.Organization, (t) => {
|
||||
t.boolean("shouldUseNewPrivilegeSystem");
|
||||
t.string("privilegeUpgradeInitiatedByUsername");
|
||||
t.dateTime("privilegeUpgradeInitiatedAt");
|
||||
});
|
||||
|
||||
await knex(TableName.Organization).update({
|
||||
shouldUseNewPrivilegeSystem: false
|
||||
});
|
||||
|
||||
await knex.schema.alterTable(TableName.Organization, (t) => {
|
||||
t.boolean("shouldUseNewPrivilegeSystem").defaultTo(true).notNullable().alter();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
if (await knex.schema.hasColumn(TableName.Organization, "shouldUseNewPrivilegeSystem")) {
|
||||
await knex.schema.alterTable(TableName.Organization, (t) => {
|
||||
t.dropColumn("shouldUseNewPrivilegeSystem");
|
||||
t.dropColumn("privilegeUpgradeInitiatedByUsername");
|
||||
t.dropColumn("privilegeUpgradeInitiatedAt");
|
||||
});
|
||||
}
|
||||
}
|
@ -1,21 +0,0 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TableName } from "../schemas";
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
const hasMappingField = await knex.schema.hasColumn(TableName.IdentityOidcAuth, "claimMetadataMapping");
|
||||
if (!hasMappingField) {
|
||||
await knex.schema.alterTable(TableName.IdentityOidcAuth, (t) => {
|
||||
t.jsonb("claimMetadataMapping");
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
const hasMappingField = await knex.schema.hasColumn(TableName.IdentityOidcAuth, "claimMetadataMapping");
|
||||
if (hasMappingField) {
|
||||
await knex.schema.alterTable(TableName.IdentityOidcAuth, (t) => {
|
||||
t.dropColumn("claimMetadataMapping");
|
||||
});
|
||||
}
|
||||
}
|
@ -1,19 +0,0 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TableName } from "../schemas/models";
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
if (!(await knex.schema.hasColumn(TableName.SuperAdmin, "adminIdentityIds"))) {
|
||||
await knex.schema.alterTable(TableName.SuperAdmin, (t) => {
|
||||
t.specificType("adminIdentityIds", "text[]");
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
if (await knex.schema.hasColumn(TableName.SuperAdmin, "adminIdentityIds")) {
|
||||
await knex.schema.alterTable(TableName.SuperAdmin, (t) => {
|
||||
t.dropColumn("adminIdentityIds");
|
||||
});
|
||||
}
|
||||
}
|
@ -0,0 +1,19 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TableName } from "@app/db/schemas";
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
if (!(await knex.schema.hasColumn(TableName.AppConnection, "isPlatformManaged"))) {
|
||||
await knex.schema.alterTable(TableName.AppConnection, (t) => {
|
||||
t.boolean("isPlatformManaged").defaultTo(false);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
if (await knex.schema.hasColumn(TableName.AppConnection, "isPlatformManaged")) {
|
||||
await knex.schema.alterTable(TableName.AppConnection, (t) => {
|
||||
t.dropColumn("isPlatformManaged");
|
||||
});
|
||||
}
|
||||
}
|
@ -0,0 +1,44 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TableName } from "@app/db/schemas";
|
||||
import { createOnUpdateTrigger, dropOnUpdateTrigger } from "@app/db/utils";
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
if (!(await knex.schema.hasTable(TableName.SecretRotationV2))) {
|
||||
await knex.schema.createTable(TableName.SecretRotationV2, (t) => {
|
||||
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
|
||||
t.string("name", 32).notNullable();
|
||||
t.string("description");
|
||||
t.string("type").notNullable();
|
||||
t.integer("interval").notNullable();
|
||||
t.jsonb("parameters").notNullable();
|
||||
t.binary("encryptedGeneratedCredentials").notNullable();
|
||||
t.boolean("isAutoRotationEnabled").notNullable().defaultTo(true);
|
||||
t.integer("activeIndex").notNullable().defaultTo(0);
|
||||
// we're including projectId in addition to folder ID because we allow folderId to be null (if the folder
|
||||
// is deleted), to preserve configuration
|
||||
t.string("projectId").notNullable();
|
||||
t.foreign("projectId").references("id").inTable(TableName.Project).onDelete("CASCADE");
|
||||
t.uuid("folderId");
|
||||
t.foreign("folderId").references("id").inTable(TableName.SecretFolder).onDelete("SET NULL");
|
||||
t.uuid("connectionId").notNullable();
|
||||
t.foreign("connectionId").references("id").inTable(TableName.AppConnection);
|
||||
t.timestamps(true, true, true);
|
||||
t.string("rotationStatus");
|
||||
t.string("lastRotationJobId");
|
||||
t.string("lastRotationMessage", 1024);
|
||||
t.datetime("lastRotatedAt");
|
||||
});
|
||||
|
||||
await createOnUpdateTrigger(knex, TableName.SecretRotationV2);
|
||||
|
||||
await knex.schema.alterTable(TableName.SecretRotationV2, (t) => {
|
||||
t.unique(["projectId", "name"]);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
await knex.schema.dropTableIfExists(TableName.SecretRotationV2);
|
||||
await dropOnUpdateTrigger(knex, TableName.SecretRotationV2);
|
||||
}
|
@ -1,23 +0,0 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TableName } from "../schemas";
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
const doesParentColumExist = await knex.schema.hasColumn(TableName.SecretFolder, "parentId");
|
||||
const doesNameColumnExist = await knex.schema.hasColumn(TableName.SecretFolder, "name");
|
||||
if (doesParentColumExist && doesNameColumnExist) {
|
||||
await knex.schema.alterTable(TableName.SecretFolder, (t) => {
|
||||
t.index(["parentId", "name"]);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
const doesParentColumExist = await knex.schema.hasColumn(TableName.SecretFolder, "parentId");
|
||||
const doesNameColumnExist = await knex.schema.hasColumn(TableName.SecretFolder, "name");
|
||||
if (doesParentColumExist && doesNameColumnExist) {
|
||||
await knex.schema.alterTable(TableName.SecretFolder, (t) => {
|
||||
t.dropIndex(["parentId", "name"]);
|
||||
});
|
||||
}
|
||||
}
|
@ -1,19 +0,0 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TableName } from "../schemas";
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
const hasReviewerJwtCol = await knex.schema.hasColumn(
|
||||
TableName.IdentityKubernetesAuth,
|
||||
"encryptedKubernetesTokenReviewerJwt"
|
||||
);
|
||||
if (hasReviewerJwtCol) {
|
||||
await knex.schema.alterTable(TableName.IdentityKubernetesAuth, (t) => {
|
||||
t.binary("encryptedKubernetesTokenReviewerJwt").nullable().alter();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export async function down(): Promise<void> {
|
||||
// we can't make it back to non nullable, it will fail
|
||||
}
|
@ -1,29 +0,0 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TableName } from "../schemas/models";
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
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<void> {
|
||||
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");
|
||||
});
|
||||
}
|
||||
}
|
@ -16,8 +16,7 @@ export const AccessApprovalPoliciesSchema = z.object({
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date(),
|
||||
enforcementLevel: z.string().default("hard"),
|
||||
deletedAt: z.date().nullable().optional(),
|
||||
allowedSelfApprovals: z.boolean().default(true)
|
||||
deletedAt: z.date().nullable().optional()
|
||||
});
|
||||
|
||||
export type TAccessApprovalPolicies = z.infer<typeof AccessApprovalPoliciesSchema>;
|
||||
|
@ -19,7 +19,8 @@ export const AppConnectionsSchema = z.object({
|
||||
version: z.number().default(1),
|
||||
orgId: z.string().uuid(),
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date()
|
||||
updatedAt: z.date(),
|
||||
isPlatformManaged: z.boolean().default(false).nullable().optional()
|
||||
});
|
||||
|
||||
export type TAppConnections = z.infer<typeof AppConnectionsSchema>;
|
||||
|
@ -28,7 +28,7 @@ export const IdentityKubernetesAuthsSchema = z.object({
|
||||
allowedNamespaces: z.string(),
|
||||
allowedNames: z.string(),
|
||||
allowedAudience: z.string(),
|
||||
encryptedKubernetesTokenReviewerJwt: zodBuffer.nullable().optional(),
|
||||
encryptedKubernetesTokenReviewerJwt: zodBuffer,
|
||||
encryptedKubernetesCaCertificate: zodBuffer.nullable().optional()
|
||||
});
|
||||
|
||||
|
@ -26,8 +26,7 @@ export const IdentityOidcAuthsSchema = z.object({
|
||||
boundSubject: z.string().nullable().optional(),
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date(),
|
||||
encryptedCaCertificate: zodBuffer.nullable().optional(),
|
||||
claimMetadataMapping: z.unknown().nullable().optional()
|
||||
encryptedCaCertificate: zodBuffer.nullable().optional()
|
||||
});
|
||||
|
||||
export type TIdentityOidcAuths = z.infer<typeof IdentityOidcAuthsSchema>;
|
||||
|
@ -140,7 +140,8 @@ export enum TableName {
|
||||
KmipClient = "kmip_clients",
|
||||
KmipOrgConfig = "kmip_org_configs",
|
||||
KmipOrgServerCertificates = "kmip_org_server_certificates",
|
||||
KmipClientCertificates = "kmip_client_certificates"
|
||||
KmipClientCertificates = "kmip_client_certificates",
|
||||
SecretRotationV2 = "secret_rotations_v2"
|
||||
}
|
||||
|
||||
export type TImmutableDBKeys = "id" | "createdAt" | "updatedAt";
|
||||
|
@ -23,9 +23,6 @@ export const OrganizationsSchema = z.object({
|
||||
defaultMembershipRole: z.string().default("member"),
|
||||
enforceMfa: z.boolean().default(false),
|
||||
selectedMfaMethod: z.string().nullable().optional(),
|
||||
shouldUseNewPrivilegeSystem: z.boolean().default(true),
|
||||
privilegeUpgradeInitiatedByUsername: z.string().nullable().optional(),
|
||||
privilegeUpgradeInitiatedAt: z.date().nullable().optional(),
|
||||
allowSecretSharingOutsideOrganization: z.boolean().default(true).nullable().optional()
|
||||
});
|
||||
|
||||
|
@ -16,8 +16,7 @@ export const SecretApprovalPoliciesSchema = z.object({
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date(),
|
||||
enforcementLevel: z.string().default("hard"),
|
||||
deletedAt: z.date().nullable().optional(),
|
||||
allowedSelfApprovals: z.boolean().default(true)
|
||||
deletedAt: z.date().nullable().optional()
|
||||
});
|
||||
|
||||
export type TSecretApprovalPolicies = z.infer<typeof SecretApprovalPoliciesSchema>;
|
||||
|
35
backend/src/db/schemas/secret-rotations-v2.ts
Normal file
35
backend/src/db/schemas/secret-rotations-v2.ts
Normal file
@ -0,0 +1,35 @@
|
||||
// Code generated by automation script, DO NOT EDIT.
|
||||
// Automated by pulling database and generating zod schema
|
||||
// To update. Just run npm run generate:schema
|
||||
// Written by akhilmhdh.
|
||||
|
||||
import { z } from "zod";
|
||||
|
||||
import { zodBuffer } from "@app/lib/zod";
|
||||
|
||||
import { TImmutableDBKeys } from "./models";
|
||||
|
||||
export const SecretRotationsV2Schema = z.object({
|
||||
id: z.string().uuid(),
|
||||
name: z.string(),
|
||||
description: z.string().nullable().optional(),
|
||||
type: z.string(),
|
||||
interval: z.number(),
|
||||
parameters: z.unknown(),
|
||||
encryptedGeneratedCredentials: zodBuffer,
|
||||
isAutoRotationEnabled: z.boolean().default(true),
|
||||
activeIndex: z.number().default(0),
|
||||
projectId: z.string(),
|
||||
folderId: z.string().uuid().nullable().optional(),
|
||||
connectionId: z.string().uuid(),
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date(),
|
||||
rotationStatus: z.string().nullable().optional(),
|
||||
lastRotationJobId: z.string().nullable().optional(),
|
||||
lastRotationMessage: z.string().nullable().optional(),
|
||||
lastRotatedAt: z.date().nullable().optional()
|
||||
});
|
||||
|
||||
export type TSecretRotationsV2 = z.infer<typeof SecretRotationsV2Schema>;
|
||||
export type TSecretRotationsV2Insert = Omit<z.input<typeof SecretRotationsV2Schema>, TImmutableDBKeys>;
|
||||
export type TSecretRotationsV2Update = Partial<Omit<z.input<typeof SecretRotationsV2Schema>, TImmutableDBKeys>>;
|
@ -25,8 +25,7 @@ export const SuperAdminSchema = z.object({
|
||||
encryptedSlackClientId: zodBuffer.nullable().optional(),
|
||||
encryptedSlackClientSecret: zodBuffer.nullable().optional(),
|
||||
authConsentContent: z.string().nullable().optional(),
|
||||
pageFrameContent: z.string().nullable().optional(),
|
||||
adminIdentityIds: z.string().array().nullable().optional()
|
||||
pageFrameContent: z.string().nullable().optional()
|
||||
});
|
||||
|
||||
export type TSuperAdmin = z.infer<typeof SuperAdminSchema>;
|
||||
|
@ -16,7 +16,7 @@ export const registerCertificateEstRouter = async (server: FastifyZodProvider) =
|
||||
// for CSRs sent in PEM, we leave them as is
|
||||
// for CSRs sent in base64, we preprocess them to remove new lines and spaces
|
||||
if (!csrBody.includes("BEGIN CERTIFICATE REQUEST")) {
|
||||
csrBody = csrBody.replaceAll("\n", "").replaceAll(" ", "");
|
||||
csrBody = csrBody.replace(/\n/g, "").replace(/ /g, "");
|
||||
}
|
||||
|
||||
done(null, csrBody);
|
||||
|
@ -29,8 +29,7 @@ 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),
|
||||
allowedSelfApprovals: z.boolean().default(true)
|
||||
enforcementLevel: z.nativeEnum(EnforcementLevel).default(EnforcementLevel.Hard)
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
@ -148,8 +147,7 @@ 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),
|
||||
allowedSelfApprovals: z.boolean().default(true)
|
||||
enforcementLevel: z.nativeEnum(EnforcementLevel).default(EnforcementLevel.Hard)
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
|
@ -110,8 +110,7 @@ export const registerAccessApprovalRequestRouter = async (server: FastifyZodProv
|
||||
secretPath: z.string().nullish(),
|
||||
envId: z.string(),
|
||||
enforcementLevel: z.string(),
|
||||
deletedAt: z.date().nullish(),
|
||||
allowedSelfApprovals: z.boolean()
|
||||
deletedAt: z.date().nullish()
|
||||
}),
|
||||
reviewers: z
|
||||
.object({
|
||||
|
@ -61,8 +61,8 @@ export const registerLdapRouter = async (server: FastifyZodProvider) => {
|
||||
if (ldapConfig.groupSearchBase) {
|
||||
const groupFilter = "(|(memberUid={{.Username}})(member={{.UserDN}})(uniqueMember={{.UserDN}}))";
|
||||
const groupSearchFilter = (ldapConfig.groupSearchFilter || groupFilter)
|
||||
.replaceAll("{{.Username}}", user.uid)
|
||||
.replaceAll("{{.UserDN}}", user.dn);
|
||||
.replace(/{{\.Username}}/g, user.uid)
|
||||
.replace(/{{\.UserDN}}/g, user.dn);
|
||||
|
||||
if (!isValidLdapFilter(groupSearchFilter)) {
|
||||
throw new Error("Generated LDAP search filter is invalid.");
|
||||
|
@ -35,8 +35,7 @@ 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),
|
||||
allowedSelfApprovals: z.boolean().default(true)
|
||||
enforcementLevel: z.nativeEnum(EnforcementLevel).default(EnforcementLevel.Hard)
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
@ -86,8 +85,7 @@ export const registerSecretApprovalPolicyRouter = async (server: FastifyZodProvi
|
||||
.nullable()
|
||||
.transform((val) => (val ? removeTrailingSlash(val) : val))
|
||||
.transform((val) => (val === "" ? "/" : val)),
|
||||
enforcementLevel: z.nativeEnum(EnforcementLevel).optional(),
|
||||
allowedSelfApprovals: z.boolean().default(true)
|
||||
enforcementLevel: z.nativeEnum(EnforcementLevel).optional()
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
|
@ -49,8 +49,7 @@ export const registerSecretApprovalRequestRouter = async (server: FastifyZodProv
|
||||
.array(),
|
||||
secretPath: z.string().optional().nullable(),
|
||||
enforcementLevel: z.string(),
|
||||
deletedAt: z.date().nullish(),
|
||||
allowedSelfApprovals: z.boolean()
|
||||
deletedAt: z.date().nullish()
|
||||
}),
|
||||
committerUser: approvalRequestUser,
|
||||
commits: z.object({ op: z.string(), secretId: z.string().nullable().optional() }).array(),
|
||||
@ -268,8 +267,7 @@ export const registerSecretApprovalRequestRouter = async (server: FastifyZodProv
|
||||
approvers: approvalRequestUser.array(),
|
||||
secretPath: z.string().optional().nullable(),
|
||||
enforcementLevel: z.string(),
|
||||
deletedAt: z.date().nullish(),
|
||||
allowedSelfApprovals: z.boolean()
|
||||
deletedAt: z.date().nullish()
|
||||
}),
|
||||
environment: z.string(),
|
||||
statusChangedByUser: approvalRequestUser.optional(),
|
||||
|
@ -5,11 +5,9 @@ import { SshCertType } from "@app/ee/services/ssh/ssh-certificate-authority-type
|
||||
import { SSH_CERTIFICATE_AUTHORITIES } from "@app/lib/api-docs";
|
||||
import { ms } from "@app/lib/ms";
|
||||
import { writeLimit } from "@app/server/config/rateLimiter";
|
||||
import { getTelemetryDistinctId } from "@app/server/lib/telemetry";
|
||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||
import { AuthMode } from "@app/services/auth/auth-type";
|
||||
import { CertKeyAlgorithm } from "@app/services/certificate/certificate-types";
|
||||
import { PostHogEventTypes } from "@app/services/telemetry/telemetry-types";
|
||||
|
||||
export const registerSshCertRouter = async (server: FastifyZodProvider) => {
|
||||
server.route({
|
||||
@ -75,16 +73,6 @@ export const registerSshCertRouter = async (server: FastifyZodProvider) => {
|
||||
}
|
||||
});
|
||||
|
||||
await server.services.telemetry.sendPostHogEvents({
|
||||
event: PostHogEventTypes.SignSshKey,
|
||||
distinctId: getTelemetryDistinctId(req),
|
||||
properties: {
|
||||
certificateTemplateId: req.body.certificateTemplateId,
|
||||
principals: req.body.principals,
|
||||
...req.auditLogInfo
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
serialNumber,
|
||||
signedKey: signedPublicKey
|
||||
@ -164,16 +152,6 @@ export const registerSshCertRouter = async (server: FastifyZodProvider) => {
|
||||
}
|
||||
});
|
||||
|
||||
await server.services.telemetry.sendPostHogEvents({
|
||||
event: PostHogEventTypes.IssueSshCreds,
|
||||
distinctId: getTelemetryDistinctId(req),
|
||||
properties: {
|
||||
certificateTemplateId: req.body.certificateTemplateId,
|
||||
principals: req.body.principals,
|
||||
...req.auditLogInfo
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
serialNumber,
|
||||
signedKey: signedPublicKey,
|
||||
|
@ -1,3 +1,8 @@
|
||||
import {
|
||||
registerSecretRotationV2Router,
|
||||
SECRET_ROTATION_REGISTER_ROUTER_MAP
|
||||
} from "@app/ee/routes/v2/secret-rotation-v2-routers";
|
||||
|
||||
import { registerIdentityProjectAdditionalPrivilegeRouter } from "./identity-project-additional-privilege-router";
|
||||
import { registerProjectRoleRouter } from "./project-role-router";
|
||||
|
||||
@ -13,4 +18,17 @@ export const registerV2EERoutes = async (server: FastifyZodProvider) => {
|
||||
await server.register(registerIdentityProjectAdditionalPrivilegeRouter, {
|
||||
prefix: "/identity-project-additional-privilege"
|
||||
});
|
||||
|
||||
await server.register(
|
||||
async (secretRotationV2Router) => {
|
||||
// register generic secret sync endpoints
|
||||
await secretRotationV2Router.register(registerSecretRotationV2Router);
|
||||
|
||||
// register service specific secret rotation endpoints (secret-rotations/postgres-credentials, etc.)
|
||||
for await (const [type, router] of Object.entries(SECRET_ROTATION_REGISTER_ROUTER_MAP)) {
|
||||
await secretRotationV2Router.register(router, { prefix: `/${type}` });
|
||||
}
|
||||
},
|
||||
{ prefix: "/secret-rotations" }
|
||||
);
|
||||
};
|
||||
|
12
backend/src/ee/routes/v2/secret-rotation-v2-routers/index.ts
Normal file
12
backend/src/ee/routes/v2/secret-rotation-v2-routers/index.ts
Normal file
@ -0,0 +1,12 @@
|
||||
import { SecretRotation } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-enums";
|
||||
|
||||
import { registerPostgresCredentialsRotationRouter } from "./postgres-credentials-rotation-router";
|
||||
|
||||
export * from "./secret-rotation-v2-router";
|
||||
|
||||
export const SECRET_ROTATION_REGISTER_ROUTER_MAP: Record<
|
||||
SecretRotation,
|
||||
(server: FastifyZodProvider) => Promise<void>
|
||||
> = {
|
||||
[SecretRotation.PostgresCredentials]: registerPostgresCredentialsRotationRouter
|
||||
};
|
@ -0,0 +1,17 @@
|
||||
import {
|
||||
CreatePostgresCredentialsRotationSchema,
|
||||
PostgresCredentialsRotationSchema,
|
||||
UpdatePostgresCredentialsRotationSchema
|
||||
} from "@app/ee/services/secret-rotation-v2/postgres-credentials";
|
||||
import { SecretRotation } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-enums";
|
||||
|
||||
import { registerSecretRotationEndpoints } from "./secret-rotation-v2-endpoints";
|
||||
|
||||
export const registerPostgresCredentialsRotationRouter = async (server: FastifyZodProvider) =>
|
||||
registerSecretRotationEndpoints({
|
||||
type: SecretRotation.PostgresCredentials,
|
||||
server,
|
||||
responseSchema: PostgresCredentialsRotationSchema,
|
||||
createSchema: CreatePostgresCredentialsRotationSchema,
|
||||
updateSchema: UpdatePostgresCredentialsRotationSchema
|
||||
});
|
@ -0,0 +1,402 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
||||
import { SecretRotation } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-enums";
|
||||
import { SECRET_ROTATION_NAME_MAP } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-maps";
|
||||
import {
|
||||
TSecretRotationV2,
|
||||
TSecretRotationV2Input
|
||||
} from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-types";
|
||||
import { SecretRotations } from "@app/lib/api-docs";
|
||||
import { startsWithVowel } from "@app/lib/fn";
|
||||
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||
import { AuthMode } from "@app/services/auth/auth-type";
|
||||
|
||||
export const registerSecretRotationEndpoints = <T extends TSecretRotationV2, I extends TSecretRotationV2Input>({
|
||||
server,
|
||||
type,
|
||||
createSchema,
|
||||
updateSchema,
|
||||
responseSchema
|
||||
}: {
|
||||
type: SecretRotation;
|
||||
server: FastifyZodProvider;
|
||||
createSchema: z.ZodType<{
|
||||
name: string;
|
||||
environment: string;
|
||||
secretPath: string;
|
||||
projectId: string;
|
||||
connectionId: string;
|
||||
parameters: I["parameters"];
|
||||
description?: string | null;
|
||||
isAutoRotationEnabled?: boolean;
|
||||
interval: number;
|
||||
}>;
|
||||
updateSchema: z.ZodType<{
|
||||
connectionId?: string;
|
||||
name?: string;
|
||||
environment?: string;
|
||||
secretPath?: string;
|
||||
parameters?: I["parameters"];
|
||||
description?: string | null;
|
||||
isAutoRotationEnabled?: boolean;
|
||||
interval?: number;
|
||||
}>;
|
||||
responseSchema: z.ZodTypeAny;
|
||||
}) => {
|
||||
const rotationType = SECRET_ROTATION_NAME_MAP[type];
|
||||
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: `/`,
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
description: `List the ${rotationType} Rotations for the specified project.`,
|
||||
querystring: z.object({
|
||||
projectId: z.string().trim().min(1, "Project ID required").describe(SecretRotations.LIST(type).projectId)
|
||||
}),
|
||||
response: {
|
||||
200: z.object({ secretRotations: responseSchema.array() })
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const {
|
||||
query: { projectId }
|
||||
} = req;
|
||||
|
||||
const secretRotations = (await server.services.secretRotationV2.listSecretRotationsByProjectId(
|
||||
{ projectId, type },
|
||||
req.permission
|
||||
)) as T[];
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
projectId,
|
||||
event: {
|
||||
type: EventType.GET_SECRET_ROTATIONS,
|
||||
metadata: {
|
||||
type,
|
||||
count: secretRotations.length,
|
||||
rotationIds: secretRotations.map((rotation) => rotation.id)
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return { secretRotations };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: "/:rotationId",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
description: `Get the specified ${rotationType} Rotation by ID.`,
|
||||
params: z.object({
|
||||
rotationId: z.string().uuid().describe(SecretRotations.GET_BY_ID(type).rotationId)
|
||||
}),
|
||||
response: {
|
||||
200: z.object({ secretRotation: responseSchema })
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const { rotationId } = req.params;
|
||||
|
||||
const secretRotation = (await server.services.secretRotationV2.findSecretRotationById(
|
||||
{ rotationId, type },
|
||||
req.permission
|
||||
)) as T;
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
projectId: secretRotation.projectId,
|
||||
event: {
|
||||
type: EventType.GET_SECRET_ROTATION,
|
||||
metadata: {
|
||||
rotationId,
|
||||
type
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return { secretRotation };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: `/rotation-name/:rotationName`,
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
description: `Get the specified ${rotationType} Rotation by name and project ID.`,
|
||||
params: z.object({
|
||||
rotationName: z
|
||||
.string()
|
||||
.trim()
|
||||
.min(1, "Rotation name required")
|
||||
.describe(SecretRotations.GET_BY_NAME(type).rotationName)
|
||||
}),
|
||||
querystring: z.object({
|
||||
projectId: z.string().trim().min(1, "Project ID required").describe(SecretRotations.GET_BY_NAME(type).projectId)
|
||||
}),
|
||||
response: {
|
||||
200: z.object({ secretRotation: responseSchema })
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const { rotationName } = req.params;
|
||||
const { projectId } = req.query;
|
||||
|
||||
const secretRotation = (await server.services.secretRotationV2.findSecretRotationByName(
|
||||
{ rotationName, projectId, type },
|
||||
req.permission
|
||||
)) as T;
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
projectId,
|
||||
event: {
|
||||
type: EventType.GET_SECRET_ROTATION,
|
||||
metadata: {
|
||||
rotationId: secretRotation.id,
|
||||
type
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return { secretRotation };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "POST",
|
||||
url: "/",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
schema: {
|
||||
description: `Create ${
|
||||
startsWithVowel(rotationType) ? "an" : "a"
|
||||
} ${rotationType} Rotation for the specified project.`,
|
||||
body: createSchema,
|
||||
response: {
|
||||
200: z.object({ secretRotation: responseSchema })
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const secretRotation = (await server.services.secretRotationV2.createSecretRotation(
|
||||
{ ...req.body, type },
|
||||
req.permission
|
||||
)) as T;
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
projectId: secretRotation.projectId,
|
||||
event: {
|
||||
type: EventType.CREATE_SECRET_ROTATION,
|
||||
metadata: {
|
||||
rotationId: secretRotation.id,
|
||||
type,
|
||||
...req.body
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return { secretRotation };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "PATCH",
|
||||
url: "/:rotationId",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
schema: {
|
||||
description: `Update the specified ${rotationType} Rotation.`,
|
||||
params: z.object({
|
||||
rotationId: z.string().uuid().describe(SecretRotations.UPDATE(type).rotationId)
|
||||
}),
|
||||
body: updateSchema,
|
||||
response: {
|
||||
200: z.object({ secretRotation: responseSchema })
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const { rotationId } = req.params;
|
||||
|
||||
const secretRotation = (await server.services.secretRotationV2.updateSecretRotation(
|
||||
{ ...req.body, rotationId, type },
|
||||
req.permission
|
||||
)) as T;
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
projectId: secretRotation.projectId,
|
||||
event: {
|
||||
type: EventType.UPDATE_SECRET_ROTATION,
|
||||
metadata: {
|
||||
rotationId,
|
||||
type,
|
||||
...req.body
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return { secretRotation };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "DELETE",
|
||||
url: `/:rotationId`,
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
schema: {
|
||||
description: `Delete the specified ${rotationType} Rotation.`,
|
||||
params: z.object({
|
||||
rotationId: z.string().uuid().describe(SecretRotations.DELETE(type).rotationId)
|
||||
}),
|
||||
querystring: z.object({
|
||||
removeSecrets: z
|
||||
.enum(["true", "false"])
|
||||
.default("false")
|
||||
.transform((value) => value === "true")
|
||||
.describe(SecretRotations.DELETE(type).removeSecrets)
|
||||
}),
|
||||
response: {
|
||||
200: z.object({ secretRotation: responseSchema })
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const { rotationId } = req.params;
|
||||
const { removeSecrets } = req.query;
|
||||
|
||||
const secretRotation = (await server.services.secretRotationV2.deleteSecretRotation(
|
||||
{ type, rotationId, removeSecrets },
|
||||
req.permission
|
||||
)) as T;
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
orgId: req.permission.orgId,
|
||||
event: {
|
||||
type: EventType.DELETE_SECRET_ROTATION,
|
||||
metadata: {
|
||||
type,
|
||||
rotationId,
|
||||
removeSecrets
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return { secretRotation };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: "/:rotationId/credentials",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
description: `Get the active and inactive credentials for the specified ${rotationType} Rotation.`,
|
||||
params: z.object({
|
||||
rotationId: z.string().uuid().describe(SecretRotations.GET_CREDENTIALS_BY_ID(type).rotationId)
|
||||
}),
|
||||
response: {
|
||||
200: z.object({ secretRotation: responseSchema })
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const { rotationId } = req.params;
|
||||
|
||||
// TODO!
|
||||
// const secretRotation = (await server.services.secretRotation.triggerSecretRotationRotationSecretsById(
|
||||
// {
|
||||
// rotationId,
|
||||
// type,
|
||||
// auditLogInfo: req.auditLogInfo
|
||||
// },
|
||||
// req.permission
|
||||
// )) as T;
|
||||
|
||||
// await server.services.auditLog.createAuditLog({
|
||||
// ...req.auditLogInfo,
|
||||
// orgId: req.permission.orgId,
|
||||
// event: {
|
||||
// type: EventType.DELETE_SECRET_ROTATION,
|
||||
// metadata: {
|
||||
// type,
|
||||
// rotationId,
|
||||
// removeSecrets
|
||||
// }
|
||||
// }
|
||||
// });
|
||||
|
||||
return { secretRotation: null };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "POST",
|
||||
url: "/:rotationId/rotate",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
schema: {
|
||||
description: `Rotate the credentials for the specified ${rotationType} Rotation.`,
|
||||
params: z.object({
|
||||
rotationId: z.string().uuid().describe(SecretRotations.ROTATE(type).rotationId)
|
||||
}),
|
||||
response: {
|
||||
200: z.object({ secretRotation: responseSchema })
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const { rotationId } = req.params;
|
||||
|
||||
// TODO!
|
||||
// const secretRotation = (await server.services.secretRotation.triggerSecretRotationRotationSecretsById(
|
||||
// {
|
||||
// rotationId,
|
||||
// type,
|
||||
// auditLogInfo: req.auditLogInfo
|
||||
// },
|
||||
// req.permission
|
||||
// )) as T;
|
||||
|
||||
// await server.services.auditLog.createAuditLog({
|
||||
// ...req.auditLogInfo,
|
||||
// orgId: req.permission.orgId,
|
||||
// event: {
|
||||
// type: EventType.DELETE_SECRET_ROTATION,
|
||||
// metadata: {
|
||||
// type,
|
||||
// rotationId,
|
||||
// removeSecrets
|
||||
// }
|
||||
// }
|
||||
// });
|
||||
|
||||
return { secretRotation: null };
|
||||
}
|
||||
});
|
||||
};
|
@ -0,0 +1,81 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
||||
import {
|
||||
PostgresCredentialsRotationListItemSchema,
|
||||
PostgresCredentialsRotationSchema
|
||||
} from "@app/ee/services/secret-rotation-v2/postgres-credentials";
|
||||
import { SecretRotations } from "@app/lib/api-docs";
|
||||
import { readLimit } from "@app/server/config/rateLimiter";
|
||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||
import { AuthMode } from "@app/services/auth/auth-type";
|
||||
|
||||
const SecretRotationV2Schema = z.discriminatedUnion("type", [PostgresCredentialsRotationSchema]);
|
||||
|
||||
const SecretRotationV2OptionsSchema = z.discriminatedUnion("type", [PostgresCredentialsRotationListItemSchema]);
|
||||
|
||||
export const registerSecretRotationV2Router = async (server: FastifyZodProvider) => {
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: "/options",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
description: "List the available Secret Rotation Options.",
|
||||
response: {
|
||||
200: z.object({
|
||||
secretRotationOptions: SecretRotationV2OptionsSchema.array()
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: () => {
|
||||
const secretRotationOptions = server.services.secretRotationV2.listSecretRotationOptions();
|
||||
return { secretRotationOptions };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: "/",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
description: "List all the Secret Rotations for the specified project.",
|
||||
querystring: z.object({
|
||||
projectId: z.string().trim().min(1, "Project ID required").describe(SecretRotations.LIST().projectId)
|
||||
}),
|
||||
response: {
|
||||
200: z.object({ secretRotations: SecretRotationV2Schema.array() })
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const {
|
||||
query: { projectId },
|
||||
permission
|
||||
} = req;
|
||||
|
||||
const secretRotations = await server.services.secretRotationV2.listSecretRotationsByProjectId(
|
||||
{ projectId },
|
||||
permission
|
||||
);
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
projectId,
|
||||
event: {
|
||||
type: EventType.GET_SECRET_ROTATIONS,
|
||||
metadata: {
|
||||
rotationIds: secretRotations.map((sync) => sync.id),
|
||||
count: secretRotations.length
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return { secretRotations };
|
||||
}
|
||||
});
|
||||
};
|
@ -65,8 +65,7 @@ export const accessApprovalPolicyServiceFactory = ({
|
||||
approvers,
|
||||
projectSlug,
|
||||
environment,
|
||||
enforcementLevel,
|
||||
allowedSelfApprovals
|
||||
enforcementLevel
|
||||
}: TCreateAccessApprovalPolicy) => {
|
||||
const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId);
|
||||
if (!project) throw new NotFoundError({ message: `Project with slug '${projectSlug}' not found` });
|
||||
@ -154,8 +153,7 @@ export const accessApprovalPolicyServiceFactory = ({
|
||||
approvals,
|
||||
secretPath,
|
||||
name,
|
||||
enforcementLevel,
|
||||
allowedSelfApprovals
|
||||
enforcementLevel
|
||||
},
|
||||
tx
|
||||
);
|
||||
@ -218,8 +216,7 @@ export const accessApprovalPolicyServiceFactory = ({
|
||||
actorOrgId,
|
||||
actorAuthMethod,
|
||||
approvals,
|
||||
enforcementLevel,
|
||||
allowedSelfApprovals
|
||||
enforcementLevel
|
||||
}: TUpdateAccessApprovalPolicy) => {
|
||||
const groupApprovers = approvers
|
||||
.filter((approver) => approver.type === ApproverType.Group)
|
||||
@ -265,8 +262,7 @@ export const accessApprovalPolicyServiceFactory = ({
|
||||
approvals,
|
||||
secretPath,
|
||||
name,
|
||||
enforcementLevel,
|
||||
allowedSelfApprovals
|
||||
enforcementLevel
|
||||
},
|
||||
tx
|
||||
);
|
||||
|
@ -26,7 +26,6 @@ export type TCreateAccessApprovalPolicy = {
|
||||
projectSlug: string;
|
||||
name: string;
|
||||
enforcementLevel: EnforcementLevel;
|
||||
allowedSelfApprovals: boolean;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
export type TUpdateAccessApprovalPolicy = {
|
||||
@ -36,7 +35,6 @@ export type TUpdateAccessApprovalPolicy = {
|
||||
secretPath?: string;
|
||||
name?: string;
|
||||
enforcementLevel?: EnforcementLevel;
|
||||
allowedSelfApprovals: boolean;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
export type TDeleteAccessApprovalPolicy = {
|
||||
|
@ -61,7 +61,6 @@ 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")
|
||||
)
|
||||
@ -120,7 +119,6 @@ export const accessApprovalRequestDALFactory = (db: TDbClient) => {
|
||||
approvals: doc.policyApprovals,
|
||||
secretPath: doc.policySecretPath,
|
||||
enforcementLevel: doc.policyEnforcementLevel,
|
||||
allowedSelfApprovals: doc.policyAllowedSelfApprovals,
|
||||
envId: doc.policyEnvId,
|
||||
deletedAt: doc.policyDeletedAt
|
||||
},
|
||||
@ -256,7 +254,6 @@ 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")
|
||||
);
|
||||
@ -278,7 +275,6 @@ export const accessApprovalRequestDALFactory = (db: TDbClient) => {
|
||||
approvals: el.policyApprovals,
|
||||
secretPath: el.policySecretPath,
|
||||
enforcementLevel: el.policyEnforcementLevel,
|
||||
allowedSelfApprovals: el.policyAllowedSelfApprovals,
|
||||
deletedAt: el.policyDeletedAt
|
||||
},
|
||||
requestedByUser: {
|
||||
|
@ -320,11 +320,6 @@ 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,
|
||||
|
@ -45,6 +45,7 @@ export const auditLogStreamServiceFactory = ({
|
||||
}: TCreateAuditLogStreamDTO) => {
|
||||
if (!actorOrgId) throw new UnauthorizedError({ message: "No organization ID attached to authentication token" });
|
||||
|
||||
const appCfg = getConfig();
|
||||
const plan = await licenseService.getPlan(actorOrgId);
|
||||
if (!plan.auditLogStreams) {
|
||||
throw new BadRequestError({
|
||||
@ -61,8 +62,9 @@ export const auditLogStreamServiceFactory = ({
|
||||
);
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Settings);
|
||||
|
||||
const appCfg = getConfig();
|
||||
if (appCfg.isCloud) await blockLocalAndPrivateIpAddresses(url);
|
||||
if (appCfg.isCloud) {
|
||||
blockLocalAndPrivateIpAddresses(url);
|
||||
}
|
||||
|
||||
const totalStreams = await auditLogStreamDAL.find({ orgId: actorOrgId });
|
||||
if (totalStreams.length >= plan.auditLogStreamLimit) {
|
||||
@ -133,8 +135,9 @@ export const auditLogStreamServiceFactory = ({
|
||||
const { orgId } = logStream;
|
||||
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Settings);
|
||||
|
||||
const appCfg = getConfig();
|
||||
if (url && appCfg.isCloud) await blockLocalAndPrivateIpAddresses(url);
|
||||
if (url && appCfg.isCloud) blockLocalAndPrivateIpAddresses(url);
|
||||
|
||||
// testing connection first
|
||||
const streamHeaders: RawAxiosRequestHeaders = { "Content-Type": "application/json" };
|
||||
|
@ -1,10 +1,8 @@
|
||||
import { ForbiddenError } from "@casl/ability";
|
||||
import { requestContext } from "@fastify/request-context";
|
||||
|
||||
import { ActionProjectType } from "@app/db/schemas";
|
||||
import { getConfig } from "@app/lib/config/env";
|
||||
import { BadRequestError } from "@app/lib/errors";
|
||||
import { ActorType } from "@app/services/auth/auth-type";
|
||||
|
||||
import { OrgPermissionActions, OrgPermissionSubjects } from "../permission/org-permission";
|
||||
import { TPermissionServiceFactory } from "../permission/permission-service";
|
||||
@ -83,12 +81,8 @@ export const auditLogServiceFactory = ({
|
||||
if (!data.projectId && !data.orgId)
|
||||
throw new BadRequestError({ message: "Must specify either project id or org id" });
|
||||
}
|
||||
const el = { ...data };
|
||||
if (el.actor.type === ActorType.USER || el.actor.type === ActorType.IDENTITY) {
|
||||
const permissionMetadata = requestContext.get("identityPermissionMetadata");
|
||||
el.actor.metadata.permission = permissionMetadata;
|
||||
}
|
||||
return auditLogQueue.pushToLog(el);
|
||||
|
||||
return auditLogQueue.pushToLog(data);
|
||||
};
|
||||
|
||||
return {
|
||||
|
@ -2,6 +2,13 @@ import {
|
||||
TCreateProjectTemplateDTO,
|
||||
TUpdateProjectTemplateDTO
|
||||
} from "@app/ee/services/project-template/project-template-types";
|
||||
import { SecretRotation } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-enums";
|
||||
import {
|
||||
TCreateSecretRotationV2DTO,
|
||||
TDeleteSecretRotationV2DTO,
|
||||
TSecretRotationV2,
|
||||
TUpdateSecretRotationV2DTO
|
||||
} from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-types";
|
||||
import { SshCaStatus, SshCertType } from "@app/ee/services/ssh/ssh-certificate-authority-types";
|
||||
import { SshCertTemplateStatus } from "@app/ee/services/ssh-certificate-template/ssh-certificate-template-types";
|
||||
import { SymmetricEncryption } from "@app/lib/crypto/cipher";
|
||||
@ -283,14 +290,21 @@ export enum EventType {
|
||||
KMIP_OPERATION_ACTIVATE = "kmip-operation-activate",
|
||||
KMIP_OPERATION_REVOKE = "kmip-operation-revoke",
|
||||
KMIP_OPERATION_LOCATE = "kmip-operation-locate",
|
||||
KMIP_OPERATION_REGISTER = "kmip-operation-register"
|
||||
KMIP_OPERATION_REGISTER = "kmip-operation-register",
|
||||
|
||||
GET_SECRET_ROTATIONS = "get-secret-rotations",
|
||||
GET_SECRET_ROTATION = "get-secret-rotation",
|
||||
GET_SECRET_ROTATION_CREDENTIALS = "get-secret-rotation-credentials",
|
||||
CREATE_SECRET_ROTATION = "create-secret-rotation",
|
||||
UPDATE_SECRET_ROTATION = "update-secret-rotation",
|
||||
DELETE_SECRET_ROTATION = "delete-secret-rotation",
|
||||
ROTATE_SECRET_ROTATION = "rotate-secret-rotation"
|
||||
}
|
||||
|
||||
interface UserActorMetadata {
|
||||
userId: string;
|
||||
email?: string | null;
|
||||
username: string;
|
||||
permission?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
interface ServiceActorMetadata {
|
||||
@ -301,7 +315,6 @@ interface ServiceActorMetadata {
|
||||
interface IdentityActorMetadata {
|
||||
identityId: string;
|
||||
name: string;
|
||||
permission?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
interface ScimClientActorMetadata {}
|
||||
@ -968,7 +981,6 @@ interface LoginIdentityOidcAuthEvent {
|
||||
identityId: string;
|
||||
identityOidcAuthId: string;
|
||||
identityAccessTokenId: string;
|
||||
oidcClaimsReceived: Record<string, unknown>;
|
||||
};
|
||||
}
|
||||
|
||||
@ -981,7 +993,6 @@ interface AddIdentityOidcAuthEvent {
|
||||
boundIssuer: string;
|
||||
boundAudiences: string;
|
||||
boundClaims: Record<string, string>;
|
||||
claimMetadataMapping: Record<string, string>;
|
||||
boundSubject: string;
|
||||
accessTokenTTL: number;
|
||||
accessTokenMaxTTL: number;
|
||||
@ -1006,7 +1017,6 @@ interface UpdateIdentityOidcAuthEvent {
|
||||
boundIssuer?: string;
|
||||
boundAudiences?: string;
|
||||
boundClaims?: Record<string, string>;
|
||||
claimMetadataMapping?: Record<string, string>;
|
||||
boundSubject?: string;
|
||||
accessTokenTTL?: number;
|
||||
accessTokenMaxTTL?: number;
|
||||
@ -2290,6 +2300,56 @@ interface RegisterKmipServerEvent {
|
||||
};
|
||||
}
|
||||
|
||||
interface GetSecretRotationsEvent {
|
||||
type: EventType.GET_SECRET_ROTATIONS;
|
||||
metadata: {
|
||||
type?: SecretRotation;
|
||||
count: number;
|
||||
rotationIds: string[];
|
||||
};
|
||||
}
|
||||
|
||||
interface GetSecretRotationEvent {
|
||||
type: EventType.GET_SECRET_ROTATION;
|
||||
metadata: {
|
||||
type: SecretRotation;
|
||||
rotationId: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface GetSecretRotationCredentialsEvent {
|
||||
type: EventType.GET_SECRET_ROTATION_CREDENTIALS;
|
||||
metadata: {
|
||||
type: SecretRotation;
|
||||
rotationId: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface CreateSecretRotationEvent {
|
||||
type: EventType.CREATE_SECRET_ROTATION;
|
||||
metadata: Omit<TCreateSecretRotationV2DTO, "projectId"> & { rotationId: string };
|
||||
}
|
||||
|
||||
interface UpdateSecretRotationEvent {
|
||||
type: EventType.UPDATE_SECRET_ROTATION;
|
||||
metadata: TUpdateSecretRotationV2DTO;
|
||||
}
|
||||
|
||||
interface DeleteSecretRotationEvent {
|
||||
type: EventType.DELETE_SECRET_ROTATION;
|
||||
metadata: TDeleteSecretRotationV2DTO;
|
||||
}
|
||||
|
||||
interface RotateSecretRotationEvent {
|
||||
type: EventType.ROTATE_SECRET_ROTATION;
|
||||
metadata: Pick<TSecretRotationV2, "parameters" | "type" | "rotationStatus" | "connectionId" | "folderId"> & {
|
||||
rotationId: string;
|
||||
rotationMessage: string | null;
|
||||
jobId?: string;
|
||||
rotatedAt: Date;
|
||||
};
|
||||
}
|
||||
|
||||
export type Event =
|
||||
| GetSecretsEvent
|
||||
| GetSecretEvent
|
||||
@ -2500,4 +2560,11 @@ export type Event =
|
||||
| KmipOperationLocateEvent
|
||||
| KmipOperationRegisterEvent
|
||||
| CreateSecretRequestEvent
|
||||
| SecretApprovalRequestReview;
|
||||
| SecretApprovalRequestReview
|
||||
| GetSecretRotationsEvent
|
||||
| GetSecretRotationEvent
|
||||
| GetSecretRotationCredentialsEvent
|
||||
| CreateSecretRotationEvent
|
||||
| UpdateSecretRotationEvent
|
||||
| DeleteSecretRotationEvent
|
||||
| RotateSecretRotationEvent;
|
||||
|
@ -1,6 +1,5 @@
|
||||
import * as x509 from "@peculiar/x509";
|
||||
|
||||
import { extractX509CertFromChain } from "@app/lib/certificates/extract-certificate";
|
||||
import { BadRequestError, NotFoundError, UnauthorizedError } from "@app/lib/errors";
|
||||
import { isCertChainValid } from "@app/services/certificate/certificate-fns";
|
||||
import { TCertificateAuthorityCertDALFactory } from "@app/services/certificate-authority/certificate-authority-cert-dal";
|
||||
@ -68,7 +67,9 @@ export const certificateEstServiceFactory = ({
|
||||
|
||||
const certTemplate = await certificateTemplateDAL.findById(certificateTemplateId);
|
||||
|
||||
const leafCertificate = extractX509CertFromChain(decodeURIComponent(sslClientCert))?.[0];
|
||||
const leafCertificate = decodeURIComponent(sslClientCert).match(
|
||||
/-----BEGIN CERTIFICATE-----[\s\S]+?-----END CERTIFICATE-----/g
|
||||
)?.[0];
|
||||
|
||||
if (!leafCertificate) {
|
||||
throw new UnauthorizedError({ message: "Missing client certificate" });
|
||||
@ -87,7 +88,10 @@ export const certificateEstServiceFactory = ({
|
||||
const verifiedChains = await Promise.all(
|
||||
caCertChains.map((chain) => {
|
||||
const caCert = new x509.X509Certificate(chain.certificate);
|
||||
const caChain = extractX509CertFromChain(chain.certificateChain)?.map((c) => new x509.X509Certificate(c)) || [];
|
||||
const caChain =
|
||||
chain.certificateChain
|
||||
.match(/-----BEGIN CERTIFICATE-----[\s\S]+?-----END CERTIFICATE-----/g)
|
||||
?.map((c) => new x509.X509Certificate(c)) || [];
|
||||
|
||||
return isCertChainValid([cert, caCert, ...caChain]);
|
||||
})
|
||||
@ -168,15 +172,19 @@ export const certificateEstServiceFactory = ({
|
||||
}
|
||||
|
||||
if (!estConfig.disableBootstrapCertValidation) {
|
||||
const caCerts = extractX509CertFromChain(estConfig.caChain)?.map((cert) => {
|
||||
return new x509.X509Certificate(cert);
|
||||
});
|
||||
const caCerts = estConfig.caChain
|
||||
.match(/-----BEGIN CERTIFICATE-----[\s\S]+?-----END CERTIFICATE-----/g)
|
||||
?.map((cert) => {
|
||||
return new x509.X509Certificate(cert);
|
||||
});
|
||||
|
||||
if (!caCerts) {
|
||||
throw new BadRequestError({ message: "Failed to parse certificate chain" });
|
||||
}
|
||||
|
||||
const leafCertificate = extractX509CertFromChain(decodeURIComponent(sslClientCert))?.[0];
|
||||
const leafCertificate = decodeURIComponent(sslClientCert).match(
|
||||
/-----BEGIN CERTIFICATE-----[\s\S]+?-----END CERTIFICATE-----/g
|
||||
)?.[0];
|
||||
|
||||
if (!leafCertificate) {
|
||||
throw new BadRequestError({ message: "Missing client certificate" });
|
||||
@ -242,7 +250,13 @@ export const certificateEstServiceFactory = ({
|
||||
kmsService
|
||||
});
|
||||
|
||||
const certificates = extractX509CertFromChain(caCertChain).map((cert) => new x509.X509Certificate(cert));
|
||||
const certificates = caCertChain
|
||||
.match(/-----BEGIN CERTIFICATE-----[\s\S]+?-----END CERTIFICATE-----/g)
|
||||
?.map((cert) => new x509.X509Certificate(cert));
|
||||
|
||||
if (!certificates) {
|
||||
throw new BadRequestError({ message: "Failed to parse certificate chain" });
|
||||
}
|
||||
|
||||
const caCertificate = new x509.X509Certificate(caCert);
|
||||
return convertRawCertsToPkcs7([caCertificate.rawData, ...certificates.map((cert) => cert.rawData)]);
|
||||
|
@ -183,7 +183,7 @@ export const dynamicSecretLeaseServiceFactory = ({
|
||||
});
|
||||
|
||||
const dynamicSecretLease = await dynamicSecretLeaseDAL.findById(leaseId);
|
||||
if (!dynamicSecretLease || dynamicSecretLease.dynamicSecret.folderId !== folder.id) {
|
||||
if (!dynamicSecretLease) {
|
||||
throw new NotFoundError({ message: `Dynamic secret lease with ID '${leaseId}' not found` });
|
||||
}
|
||||
|
||||
@ -256,7 +256,7 @@ export const dynamicSecretLeaseServiceFactory = ({
|
||||
});
|
||||
|
||||
const dynamicSecretLease = await dynamicSecretLeaseDAL.findById(leaseId);
|
||||
if (!dynamicSecretLease || dynamicSecretLease.dynamicSecret.folderId !== folder.id)
|
||||
if (!dynamicSecretLease)
|
||||
throw new NotFoundError({ message: `Dynamic secret lease with ID '${leaseId}' not found` });
|
||||
|
||||
const dynamicSecretCfg = dynamicSecretLease.dynamicSecret;
|
||||
|
@ -1,51 +1,31 @@
|
||||
import dns from "node:dns/promises";
|
||||
import net from "node:net";
|
||||
import crypto from "node:crypto";
|
||||
|
||||
import { getConfig } from "@app/lib/config/env";
|
||||
import { BadRequestError } from "@app/lib/errors";
|
||||
import { isPrivateIp } from "@app/lib/ip/ipRange";
|
||||
import { getDbConnectionHost } from "@app/lib/knex";
|
||||
|
||||
export const verifyHostInputValidity = async (host: string, isGateway = false) => {
|
||||
export const verifyHostInputValidity = (host: string, isGateway = false) => {
|
||||
const appCfg = getConfig();
|
||||
// if (appCfg.NODE_ENV === "development") return ["host.docker.internal"]; // incase you want to remove this check in dev
|
||||
const dbHost = appCfg.DB_HOST || getDbConnectionHost(appCfg.DB_CONNECTION_URI);
|
||||
// no need for validation when it's dev
|
||||
if (appCfg.NODE_ENV === "development") return;
|
||||
|
||||
const reservedHosts = [appCfg.DB_HOST || getDbConnectionHost(appCfg.DB_CONNECTION_URI)].concat(
|
||||
(appCfg.DB_READ_REPLICAS || []).map((el) => getDbConnectionHost(el.DB_CONNECTION_URI)),
|
||||
getDbConnectionHost(appCfg.REDIS_URL)
|
||||
);
|
||||
if (host === "host.docker.internal") throw new BadRequestError({ message: "Invalid db host" });
|
||||
|
||||
// get host db ip
|
||||
const exclusiveIps: string[] = [];
|
||||
for await (const el of reservedHosts) {
|
||||
if (el) {
|
||||
if (net.isIPv4(el)) {
|
||||
exclusiveIps.push(el);
|
||||
} else {
|
||||
const resolvedIps = await dns.resolve4(el);
|
||||
exclusiveIps.push(...resolvedIps);
|
||||
}
|
||||
}
|
||||
if (
|
||||
appCfg.isCloud &&
|
||||
!isGateway &&
|
||||
// localhost
|
||||
// internal ips
|
||||
(host.match(/^10\.\d+\.\d+\.\d+/) || host.match(/^192\.168\.\d+\.\d+/))
|
||||
)
|
||||
throw new BadRequestError({ message: "Invalid db host" });
|
||||
|
||||
if (
|
||||
host === "localhost" ||
|
||||
host === "127.0.0.1" ||
|
||||
(dbHost?.length === host.length && crypto.timingSafeEqual(Buffer.from(dbHost || ""), Buffer.from(host)))
|
||||
) {
|
||||
throw new BadRequestError({ message: "Invalid db host" });
|
||||
}
|
||||
|
||||
const normalizedHost = host.split(":")[0];
|
||||
const inputHostIps: string[] = [];
|
||||
if (net.isIPv4(host)) {
|
||||
inputHostIps.push(host);
|
||||
} else {
|
||||
if (normalizedHost === "localhost" || normalizedHost === "host.docker.internal") {
|
||||
throw new BadRequestError({ message: "Invalid db host" });
|
||||
}
|
||||
const resolvedIps = await dns.resolve4(host);
|
||||
inputHostIps.push(...resolvedIps);
|
||||
}
|
||||
|
||||
if (!isGateway) {
|
||||
const isInternalIp = inputHostIps.some((el) => isPrivateIp(el));
|
||||
if (isInternalIp) throw new BadRequestError({ message: "Invalid db host" });
|
||||
}
|
||||
|
||||
const isAppUsedIps = inputHostIps.some((el) => exclusiveIps.includes(el));
|
||||
if (isAppUsedIps) throw new BadRequestError({ message: "Invalid db host" });
|
||||
return inputHostIps;
|
||||
};
|
||||
|
@ -13,7 +13,6 @@ import { customAlphabet } from "nanoid";
|
||||
import { z } from "zod";
|
||||
|
||||
import { BadRequestError } from "@app/lib/errors";
|
||||
import { validateHandlebarTemplate } from "@app/lib/template/validate-handlebars";
|
||||
|
||||
import { DynamicSecretAwsElastiCacheSchema, TDynamicProviderFns } from "./models";
|
||||
|
||||
@ -145,14 +144,6 @@ export const AwsElastiCacheDatabaseProvider = (): TDynamicProviderFns => {
|
||||
// We can't return the parsed statements here because we need to use the handlebars template to generate the username and password, before we can use the parsed statements.
|
||||
CreateElastiCacheUserSchema.parse(JSON.parse(providerInputs.creationStatement));
|
||||
DeleteElasticCacheUserSchema.parse(JSON.parse(providerInputs.revocationStatement));
|
||||
validateHandlebarTemplate("AWS ElastiCache creation", providerInputs.creationStatement, {
|
||||
allowedExpressions: (val) => ["username", "password", "expiration"].includes(val)
|
||||
});
|
||||
if (providerInputs.revocationStatement) {
|
||||
validateHandlebarTemplate("AWS ElastiCache revoke", providerInputs.revocationStatement, {
|
||||
allowedExpressions: (val) => ["username"].includes(val)
|
||||
});
|
||||
}
|
||||
|
||||
return providerInputs;
|
||||
};
|
||||
|
@ -3,10 +3,9 @@ import handlebars from "handlebars";
|
||||
import { customAlphabet } from "nanoid";
|
||||
import { z } from "zod";
|
||||
|
||||
import { BadRequestError } from "@app/lib/errors";
|
||||
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
||||
import { validateHandlebarTemplate } from "@app/lib/template/validate-handlebars";
|
||||
|
||||
import { verifyHostInputValidity } from "../dynamic-secret-fns";
|
||||
import { DynamicSecretCassandraSchema, TDynamicProviderFns } from "./models";
|
||||
|
||||
const generatePassword = (size = 48) => {
|
||||
@ -21,28 +20,14 @@ const generateUsername = () => {
|
||||
export const CassandraProvider = (): TDynamicProviderFns => {
|
||||
const validateProviderInputs = async (inputs: unknown) => {
|
||||
const providerInputs = await DynamicSecretCassandraSchema.parseAsync(inputs);
|
||||
const hostIps = await Promise.all(
|
||||
providerInputs.host
|
||||
.split(",")
|
||||
.filter(Boolean)
|
||||
.map((el) => verifyHostInputValidity(el).then((ip) => ip[0]))
|
||||
);
|
||||
validateHandlebarTemplate("Cassandra creation", providerInputs.creationStatement, {
|
||||
allowedExpressions: (val) => ["username", "password", "expiration", "keyspace"].includes(val)
|
||||
});
|
||||
if (providerInputs.renewStatement) {
|
||||
validateHandlebarTemplate("Cassandra renew", providerInputs.renewStatement, {
|
||||
allowedExpressions: (val) => ["username", "expiration", "keyspace"].includes(val)
|
||||
});
|
||||
if (providerInputs.host === "localhost" || providerInputs.host === "127.0.0.1") {
|
||||
throw new BadRequestError({ message: "Invalid db host" });
|
||||
}
|
||||
validateHandlebarTemplate("Cassandra revoke", providerInputs.revocationStatement, {
|
||||
allowedExpressions: (val) => ["username"].includes(val)
|
||||
});
|
||||
|
||||
return { ...providerInputs, hostIps };
|
||||
return providerInputs;
|
||||
};
|
||||
|
||||
const $getClient = async (providerInputs: z.infer<typeof DynamicSecretCassandraSchema> & { hostIps: string[] }) => {
|
||||
const $getClient = async (providerInputs: z.infer<typeof DynamicSecretCassandraSchema>) => {
|
||||
const sslOptions = providerInputs.ca ? { rejectUnauthorized: false, ca: providerInputs.ca } : undefined;
|
||||
const client = new cassandra.Client({
|
||||
sslOptions,
|
||||
@ -55,7 +40,7 @@ export const CassandraProvider = (): TDynamicProviderFns => {
|
||||
},
|
||||
keyspace: providerInputs.keyspace,
|
||||
localDataCenter: providerInputs?.localDataCenter,
|
||||
contactPoints: providerInputs.hostIps
|
||||
contactPoints: providerInputs.host.split(",").filter(Boolean)
|
||||
});
|
||||
return client;
|
||||
};
|
||||
|
@ -19,14 +19,15 @@ const generateUsername = () => {
|
||||
export const ElasticSearchProvider = (): TDynamicProviderFns => {
|
||||
const validateProviderInputs = async (inputs: unknown) => {
|
||||
const providerInputs = await DynamicSecretElasticSearchSchema.parseAsync(inputs);
|
||||
const [hostIp] = await verifyHostInputValidity(providerInputs.host);
|
||||
return { ...providerInputs, hostIp };
|
||||
verifyHostInputValidity(providerInputs.host);
|
||||
|
||||
return providerInputs;
|
||||
};
|
||||
|
||||
const $getClient = async (providerInputs: z.infer<typeof DynamicSecretElasticSearchSchema> & { hostIp: string }) => {
|
||||
const $getClient = async (providerInputs: z.infer<typeof DynamicSecretElasticSearchSchema>) => {
|
||||
const connection = new ElasticSearchClient({
|
||||
node: {
|
||||
url: new URL(`${providerInputs.hostIp}:${providerInputs.port}`),
|
||||
url: new URL(`${providerInputs.host}:${providerInputs.port}`),
|
||||
...(providerInputs.ca && {
|
||||
ssl: {
|
||||
rejectUnauthorized: false,
|
||||
|
@ -19,15 +19,15 @@ const generateUsername = () => {
|
||||
export const MongoDBProvider = (): TDynamicProviderFns => {
|
||||
const validateProviderInputs = async (inputs: unknown) => {
|
||||
const providerInputs = await DynamicSecretMongoDBSchema.parseAsync(inputs);
|
||||
const [hostIp] = await verifyHostInputValidity(providerInputs.host);
|
||||
return { ...providerInputs, hostIp };
|
||||
verifyHostInputValidity(providerInputs.host);
|
||||
return providerInputs;
|
||||
};
|
||||
|
||||
const $getClient = async (providerInputs: z.infer<typeof DynamicSecretMongoDBSchema> & { hostIp: string }) => {
|
||||
const $getClient = async (providerInputs: z.infer<typeof DynamicSecretMongoDBSchema>) => {
|
||||
const isSrv = !providerInputs.port;
|
||||
const uri = isSrv
|
||||
? `mongodb+srv://${providerInputs.hostIp}`
|
||||
: `mongodb://${providerInputs.hostIp}:${providerInputs.port}`;
|
||||
? `mongodb+srv://${providerInputs.host}`
|
||||
: `mongodb://${providerInputs.host}:${providerInputs.port}`;
|
||||
|
||||
const client = new MongoClient(uri, {
|
||||
auth: {
|
||||
|
@ -3,6 +3,7 @@ import https from "https";
|
||||
import { customAlphabet } from "nanoid";
|
||||
import { z } from "zod";
|
||||
|
||||
import { removeTrailingSlash } from "@app/lib/fn";
|
||||
import { logger } from "@app/lib/logger";
|
||||
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
||||
|
||||
@ -78,13 +79,14 @@ async function deleteRabbitMqUser({ axiosInstance, usernameToDelete }: TDeleteRa
|
||||
export const RabbitMqProvider = (): TDynamicProviderFns => {
|
||||
const validateProviderInputs = async (inputs: unknown) => {
|
||||
const providerInputs = await DynamicSecretRabbitMqSchema.parseAsync(inputs);
|
||||
const [hostIp] = await verifyHostInputValidity(providerInputs.host);
|
||||
return { ...providerInputs, hostIp };
|
||||
verifyHostInputValidity(providerInputs.host);
|
||||
|
||||
return providerInputs;
|
||||
};
|
||||
|
||||
const $getClient = async (providerInputs: z.infer<typeof DynamicSecretRabbitMqSchema> & { hostIp: string }) => {
|
||||
const $getClient = async (providerInputs: z.infer<typeof DynamicSecretRabbitMqSchema>) => {
|
||||
const axiosInstance = axios.create({
|
||||
baseURL: `${providerInputs.hostIp}:${providerInputs.port}/api`,
|
||||
baseURL: `${removeTrailingSlash(providerInputs.host)}:${providerInputs.port}/api`,
|
||||
auth: {
|
||||
username: providerInputs.username,
|
||||
password: providerInputs.password
|
||||
|
@ -5,7 +5,6 @@ import { z } from "zod";
|
||||
|
||||
import { BadRequestError } from "@app/lib/errors";
|
||||
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
||||
import { validateHandlebarTemplate } from "@app/lib/template/validate-handlebars";
|
||||
|
||||
import { verifyHostInputValidity } from "../dynamic-secret-fns";
|
||||
import { DynamicSecretRedisDBSchema, TDynamicProviderFns } from "./models";
|
||||
@ -52,28 +51,16 @@ const executeTransactions = async (connection: Redis, commands: string[]): Promi
|
||||
export const RedisDatabaseProvider = (): TDynamicProviderFns => {
|
||||
const validateProviderInputs = async (inputs: unknown) => {
|
||||
const providerInputs = await DynamicSecretRedisDBSchema.parseAsync(inputs);
|
||||
const [hostIp] = await verifyHostInputValidity(providerInputs.host);
|
||||
validateHandlebarTemplate("Redis creation", providerInputs.creationStatement, {
|
||||
allowedExpressions: (val) => ["username", "password", "expiration"].includes(val)
|
||||
});
|
||||
if (providerInputs.renewStatement) {
|
||||
validateHandlebarTemplate("Redis renew", providerInputs.renewStatement, {
|
||||
allowedExpressions: (val) => ["username", "expiration"].includes(val)
|
||||
});
|
||||
}
|
||||
validateHandlebarTemplate("Redis revoke", providerInputs.revocationStatement, {
|
||||
allowedExpressions: (val) => ["username"].includes(val)
|
||||
});
|
||||
|
||||
return { ...providerInputs, hostIp };
|
||||
verifyHostInputValidity(providerInputs.host);
|
||||
return providerInputs;
|
||||
};
|
||||
|
||||
const $getClient = async (providerInputs: z.infer<typeof DynamicSecretRedisDBSchema> & { hostIp: string }) => {
|
||||
const $getClient = async (providerInputs: z.infer<typeof DynamicSecretRedisDBSchema>) => {
|
||||
let connection: Redis | null = null;
|
||||
try {
|
||||
connection = new Redis({
|
||||
username: providerInputs.username,
|
||||
host: providerInputs.hostIp,
|
||||
host: providerInputs.host,
|
||||
port: providerInputs.port,
|
||||
password: providerInputs.password,
|
||||
...(providerInputs.ca && {
|
||||
|
@ -5,7 +5,6 @@ import { z } from "zod";
|
||||
|
||||
import { BadRequestError } from "@app/lib/errors";
|
||||
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
||||
import { validateHandlebarTemplate } from "@app/lib/template/validate-handlebars";
|
||||
|
||||
import { verifyHostInputValidity } from "../dynamic-secret-fns";
|
||||
import { DynamicSecretSapAseSchema, TDynamicProviderFns } from "./models";
|
||||
@ -28,25 +27,14 @@ export const SapAseProvider = (): TDynamicProviderFns => {
|
||||
const validateProviderInputs = async (inputs: unknown) => {
|
||||
const providerInputs = await DynamicSecretSapAseSchema.parseAsync(inputs);
|
||||
|
||||
const [hostIp] = await verifyHostInputValidity(providerInputs.host);
|
||||
validateHandlebarTemplate("SAP ASE creation", providerInputs.creationStatement, {
|
||||
allowedExpressions: (val) => ["username", "password"].includes(val)
|
||||
});
|
||||
if (providerInputs.revocationStatement) {
|
||||
validateHandlebarTemplate("SAP ASE revoke", providerInputs.revocationStatement, {
|
||||
allowedExpressions: (val) => ["username"].includes(val)
|
||||
});
|
||||
}
|
||||
return { ...providerInputs, hostIp };
|
||||
verifyHostInputValidity(providerInputs.host);
|
||||
return providerInputs;
|
||||
};
|
||||
|
||||
const $getClient = async (
|
||||
providerInputs: z.infer<typeof DynamicSecretSapAseSchema> & { hostIp: string },
|
||||
useMaster?: boolean
|
||||
) => {
|
||||
const $getClient = async (providerInputs: z.infer<typeof DynamicSecretSapAseSchema>, useMaster?: boolean) => {
|
||||
const connectionString =
|
||||
`DRIVER={FreeTDS};` +
|
||||
`SERVER=${providerInputs.hostIp};` +
|
||||
`SERVER=${providerInputs.host};` +
|
||||
`PORT=${providerInputs.port};` +
|
||||
`DATABASE=${useMaster ? "master" : providerInputs.database};` +
|
||||
`UID=${providerInputs.username};` +
|
||||
@ -95,7 +83,7 @@ export const SapAseProvider = (): TDynamicProviderFns => {
|
||||
password
|
||||
});
|
||||
|
||||
const queries = creationStatement.trim().replaceAll("\n", "").split(";").filter(Boolean);
|
||||
const queries = creationStatement.trim().replace(/\n/g, "").split(";").filter(Boolean);
|
||||
|
||||
for await (const query of queries) {
|
||||
// If it's an adduser query, we need to first call sp_addlogin on the MASTER database.
|
||||
@ -116,7 +104,7 @@ export const SapAseProvider = (): TDynamicProviderFns => {
|
||||
username
|
||||
});
|
||||
|
||||
const queries = revokeStatement.trim().replaceAll("\n", "").split(";").filter(Boolean);
|
||||
const queries = revokeStatement.trim().replace(/\n/g, "").split(";").filter(Boolean);
|
||||
|
||||
const client = await $getClient(providerInputs);
|
||||
const masterClient = await $getClient(providerInputs, true);
|
||||
|
@ -11,7 +11,6 @@ import { z } from "zod";
|
||||
|
||||
import { BadRequestError } from "@app/lib/errors";
|
||||
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
||||
import { validateHandlebarTemplate } from "@app/lib/template/validate-handlebars";
|
||||
|
||||
import { verifyHostInputValidity } from "../dynamic-secret-fns";
|
||||
import { DynamicSecretSapHanaSchema, TDynamicProviderFns } from "./models";
|
||||
@ -29,24 +28,13 @@ export const SapHanaProvider = (): TDynamicProviderFns => {
|
||||
const validateProviderInputs = async (inputs: unknown) => {
|
||||
const providerInputs = await DynamicSecretSapHanaSchema.parseAsync(inputs);
|
||||
|
||||
const [hostIp] = await verifyHostInputValidity(providerInputs.host);
|
||||
validateHandlebarTemplate("SAP Hana creation", providerInputs.creationStatement, {
|
||||
allowedExpressions: (val) => ["username", "password", "expiration"].includes(val)
|
||||
});
|
||||
if (providerInputs.renewStatement) {
|
||||
validateHandlebarTemplate("SAP Hana renew", providerInputs.renewStatement, {
|
||||
allowedExpressions: (val) => ["username", "expiration"].includes(val)
|
||||
});
|
||||
}
|
||||
validateHandlebarTemplate("SAP Hana revoke", providerInputs.revocationStatement, {
|
||||
allowedExpressions: (val) => ["username"].includes(val)
|
||||
});
|
||||
return { ...providerInputs, hostIp };
|
||||
verifyHostInputValidity(providerInputs.host);
|
||||
return providerInputs;
|
||||
};
|
||||
|
||||
const $getClient = async (providerInputs: z.infer<typeof DynamicSecretSapHanaSchema> & { hostIp: string }) => {
|
||||
const $getClient = async (providerInputs: z.infer<typeof DynamicSecretSapHanaSchema>) => {
|
||||
const client = hdb.createClient({
|
||||
host: providerInputs.hostIp,
|
||||
host: providerInputs.host,
|
||||
port: providerInputs.port,
|
||||
user: providerInputs.username,
|
||||
password: providerInputs.password,
|
||||
|
@ -5,7 +5,6 @@ import { z } from "zod";
|
||||
|
||||
import { BadRequestError } from "@app/lib/errors";
|
||||
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
||||
import { validateHandlebarTemplate } from "@app/lib/template/validate-handlebars";
|
||||
|
||||
import { DynamicSecretSnowflakeSchema, TDynamicProviderFns } from "./models";
|
||||
|
||||
@ -32,18 +31,6 @@ const getDaysToExpiry = (expiryDate: Date) => {
|
||||
export const SnowflakeProvider = (): TDynamicProviderFns => {
|
||||
const validateProviderInputs = async (inputs: unknown) => {
|
||||
const providerInputs = await DynamicSecretSnowflakeSchema.parseAsync(inputs);
|
||||
validateHandlebarTemplate("Snowflake creation", providerInputs.creationStatement, {
|
||||
allowedExpressions: (val) => ["username", "password", "expiration"].includes(val)
|
||||
});
|
||||
if (providerInputs.renewStatement) {
|
||||
validateHandlebarTemplate("Snowflake renew", providerInputs.renewStatement, {
|
||||
allowedExpressions: (val) => ["username", "expiration"].includes(val)
|
||||
});
|
||||
}
|
||||
validateHandlebarTemplate("Snowflake revoke", providerInputs.revocationStatement, {
|
||||
allowedExpressions: (val) => ["username"].includes(val)
|
||||
});
|
||||
|
||||
return providerInputs;
|
||||
};
|
||||
|
||||
|
@ -5,7 +5,6 @@ import { z } from "zod";
|
||||
|
||||
import { withGatewayProxy } from "@app/lib/gateway";
|
||||
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
||||
import { validateHandlebarTemplate } from "@app/lib/template/validate-handlebars";
|
||||
|
||||
import { TGatewayServiceFactory } from "../../gateway/gateway-service";
|
||||
import { verifyHostInputValidity } from "../dynamic-secret-fns";
|
||||
@ -118,21 +117,8 @@ type TSqlDatabaseProviderDTO = {
|
||||
export const SqlDatabaseProvider = ({ gatewayService }: TSqlDatabaseProviderDTO): TDynamicProviderFns => {
|
||||
const validateProviderInputs = async (inputs: unknown) => {
|
||||
const providerInputs = await DynamicSecretSqlDBSchema.parseAsync(inputs);
|
||||
|
||||
const [hostIp] = await verifyHostInputValidity(providerInputs.host, Boolean(providerInputs.projectGatewayId));
|
||||
validateHandlebarTemplate("SQL creation", providerInputs.creationStatement, {
|
||||
allowedExpressions: (val) => ["username", "password", "expiration", "database"].includes(val)
|
||||
});
|
||||
if (providerInputs.renewStatement) {
|
||||
validateHandlebarTemplate("SQL renew", providerInputs.renewStatement, {
|
||||
allowedExpressions: (val) => ["username", "expiration", "database"].includes(val)
|
||||
});
|
||||
}
|
||||
validateHandlebarTemplate("SQL revoke", providerInputs.revocationStatement, {
|
||||
allowedExpressions: (val) => ["username", "database"].includes(val)
|
||||
});
|
||||
|
||||
return { ...providerInputs, hostIp };
|
||||
verifyHostInputValidity(providerInputs.host, Boolean(providerInputs.projectGatewayId));
|
||||
return providerInputs;
|
||||
};
|
||||
|
||||
const $getClient = async (providerInputs: z.infer<typeof DynamicSecretSqlDBSchema>) => {
|
||||
@ -158,8 +144,7 @@ export const SqlDatabaseProvider = ({ gatewayService }: TSqlDatabaseProviderDTO)
|
||||
}
|
||||
: undefined
|
||||
},
|
||||
acquireConnectionTimeout: EXTERNAL_REQUEST_TIMEOUT,
|
||||
pool: { min: 0, max: 7 }
|
||||
acquireConnectionTimeout: EXTERNAL_REQUEST_TIMEOUT
|
||||
});
|
||||
return db;
|
||||
};
|
||||
@ -193,7 +178,7 @@ export const SqlDatabaseProvider = ({ gatewayService }: TSqlDatabaseProviderDTO)
|
||||
const validateConnection = async (inputs: unknown) => {
|
||||
const providerInputs = await validateProviderInputs(inputs);
|
||||
let isConnected = false;
|
||||
const gatewayCallback = async (host = providerInputs.hostIp, port = providerInputs.port) => {
|
||||
const gatewayCallback = async (host = providerInputs.host, port = providerInputs.port) => {
|
||||
const db = await $getClient({ ...providerInputs, port, host });
|
||||
// oracle needs from keyword
|
||||
const testStatement = providerInputs.client === SqlProviders.Oracle ? "SELECT 1 FROM DUAL" : "SELECT 1";
|
||||
|
@ -3,7 +3,8 @@ import slugify from "@sindresorhus/slugify";
|
||||
|
||||
import { OrgMembershipRole, TOrgRoles } from "@app/db/schemas";
|
||||
import { TOidcConfigDALFactory } from "@app/ee/services/oidc/oidc-config-dal";
|
||||
import { BadRequestError, NotFoundError, PermissionBoundaryError, UnauthorizedError } from "@app/lib/errors";
|
||||
import { validatePermissionBoundary } from "@app/lib/casl/boundary";
|
||||
import { BadRequestError, ForbiddenRequestError, NotFoundError, UnauthorizedError } from "@app/lib/errors";
|
||||
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
||||
import { TGroupProjectDALFactory } from "@app/services/group-project/group-project-dal";
|
||||
import { TOrgDALFactory } from "@app/services/org/org-dal";
|
||||
@ -13,8 +14,7 @@ import { TProjectKeyDALFactory } from "@app/services/project-key/project-key-dal
|
||||
import { TUserDALFactory } from "@app/services/user/user-dal";
|
||||
|
||||
import { TLicenseServiceFactory } from "../license/license-service";
|
||||
import { OrgPermissionGroupActions, OrgPermissionSubjects } from "../permission/org-permission";
|
||||
import { constructPermissionErrorMessage, validatePrivilegeChangeOperation } from "../permission/permission-fns";
|
||||
import { OrgPermissionActions, OrgPermissionSubjects } from "../permission/org-permission";
|
||||
import { TPermissionServiceFactory } from "../permission/permission-service";
|
||||
import { TGroupDALFactory } from "./group-dal";
|
||||
import { addUsersToGroupByUserIds, removeUsersFromGroupByUserIds } from "./group-fns";
|
||||
@ -67,14 +67,14 @@ export const groupServiceFactory = ({
|
||||
const createGroup = async ({ name, slug, role, actor, actorId, actorAuthMethod, actorOrgId }: TCreateGroupDTO) => {
|
||||
if (!actorOrgId) throw new UnauthorizedError({ message: "No organization ID provided in request" });
|
||||
|
||||
const { permission, membership } = await permissionService.getOrgPermission(
|
||||
const { permission } = await permissionService.getOrgPermission(
|
||||
actor,
|
||||
actorId,
|
||||
actorOrgId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionGroupActions.Create, OrgPermissionSubjects.Groups);
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Groups);
|
||||
|
||||
const plan = await licenseService.getPlan(actorOrgId);
|
||||
if (!plan.groups)
|
||||
@ -87,26 +87,14 @@ export const groupServiceFactory = ({
|
||||
actorOrgId
|
||||
);
|
||||
const isCustomRole = Boolean(customRole);
|
||||
if (role !== OrgMembershipRole.NoAccess) {
|
||||
const permissionBoundary = validatePrivilegeChangeOperation(
|
||||
membership.shouldUseNewPrivilegeSystem,
|
||||
OrgPermissionGroupActions.GrantPrivileges,
|
||||
OrgPermissionSubjects.Groups,
|
||||
permission,
|
||||
rolePermission
|
||||
);
|
||||
|
||||
if (!permissionBoundary.isValid)
|
||||
throw new PermissionBoundaryError({
|
||||
message: constructPermissionErrorMessage(
|
||||
"Failed to create group",
|
||||
membership.shouldUseNewPrivilegeSystem,
|
||||
OrgPermissionGroupActions.GrantPrivileges,
|
||||
OrgPermissionSubjects.Groups
|
||||
),
|
||||
details: { missingPermissions: permissionBoundary.missingPermissions }
|
||||
});
|
||||
}
|
||||
const permissionBoundary = validatePermissionBoundary(permission, rolePermission);
|
||||
if (!permissionBoundary.isValid)
|
||||
throw new ForbiddenRequestError({
|
||||
name: "PermissionBoundaryError",
|
||||
message: "Failed to create a more privileged group",
|
||||
details: { missingPermissions: permissionBoundary.missingPermissions }
|
||||
});
|
||||
|
||||
const group = await groupDAL.transaction(async (tx) => {
|
||||
const existingGroup = await groupDAL.findOne({ orgId: actorOrgId, name }, tx);
|
||||
@ -145,15 +133,14 @@ export const groupServiceFactory = ({
|
||||
}: TUpdateGroupDTO) => {
|
||||
if (!actorOrgId) throw new UnauthorizedError({ message: "No organization ID provided in request" });
|
||||
|
||||
const { permission, membership } = await permissionService.getOrgPermission(
|
||||
const { permission } = await permissionService.getOrgPermission(
|
||||
actor,
|
||||
actorId,
|
||||
actorOrgId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionGroupActions.Edit, OrgPermissionSubjects.Groups);
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Groups);
|
||||
|
||||
const plan = await licenseService.getPlan(actorOrgId);
|
||||
if (!plan.groups)
|
||||
@ -174,21 +161,11 @@ export const groupServiceFactory = ({
|
||||
);
|
||||
|
||||
const isCustomRole = Boolean(customOrgRole);
|
||||
const permissionBoundary = validatePrivilegeChangeOperation(
|
||||
membership.shouldUseNewPrivilegeSystem,
|
||||
OrgPermissionGroupActions.GrantPrivileges,
|
||||
OrgPermissionSubjects.Groups,
|
||||
permission,
|
||||
rolePermission
|
||||
);
|
||||
const permissionBoundary = validatePermissionBoundary(permission, rolePermission);
|
||||
if (!permissionBoundary.isValid)
|
||||
throw new PermissionBoundaryError({
|
||||
message: constructPermissionErrorMessage(
|
||||
"Failed to update group",
|
||||
membership.shouldUseNewPrivilegeSystem,
|
||||
OrgPermissionGroupActions.GrantPrivileges,
|
||||
OrgPermissionSubjects.Groups
|
||||
),
|
||||
throw new ForbiddenRequestError({
|
||||
name: "PermissionBoundaryError",
|
||||
message: "Failed to update a more privileged group",
|
||||
details: { missingPermissions: permissionBoundary.missingPermissions }
|
||||
});
|
||||
if (isCustomRole) customRole = customOrgRole;
|
||||
@ -238,7 +215,7 @@ export const groupServiceFactory = ({
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionGroupActions.Delete, OrgPermissionSubjects.Groups);
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Delete, OrgPermissionSubjects.Groups);
|
||||
|
||||
const plan = await licenseService.getPlan(actorOrgId);
|
||||
|
||||
@ -265,7 +242,7 @@ export const groupServiceFactory = ({
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionGroupActions.Read, OrgPermissionSubjects.Groups);
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Groups);
|
||||
|
||||
const group = await groupDAL.findById(id);
|
||||
if (!group) {
|
||||
@ -298,7 +275,7 @@ export const groupServiceFactory = ({
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionGroupActions.Read, OrgPermissionSubjects.Groups);
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Groups);
|
||||
|
||||
const group = await groupDAL.findOne({
|
||||
orgId: actorOrgId,
|
||||
@ -326,14 +303,14 @@ export const groupServiceFactory = ({
|
||||
const addUserToGroup = async ({ id, username, actor, actorId, actorAuthMethod, actorOrgId }: TAddUserToGroupDTO) => {
|
||||
if (!actorOrgId) throw new UnauthorizedError({ message: "No organization ID provided in request" });
|
||||
|
||||
const { permission, membership } = await permissionService.getOrgPermission(
|
||||
const { permission } = await permissionService.getOrgPermission(
|
||||
actor,
|
||||
actorId,
|
||||
actorOrgId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionGroupActions.Edit, OrgPermissionSubjects.Groups);
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Groups);
|
||||
|
||||
// check if group with slug exists
|
||||
const group = await groupDAL.findOne({
|
||||
@ -361,22 +338,11 @@ export const groupServiceFactory = ({
|
||||
const { permission: groupRolePermission } = await permissionService.getOrgPermissionByRole(group.role, actorOrgId);
|
||||
|
||||
// check if user has broader or equal to privileges than group
|
||||
const permissionBoundary = validatePrivilegeChangeOperation(
|
||||
membership.shouldUseNewPrivilegeSystem,
|
||||
OrgPermissionGroupActions.AddMembers,
|
||||
OrgPermissionSubjects.Groups,
|
||||
permission,
|
||||
groupRolePermission
|
||||
);
|
||||
|
||||
const permissionBoundary = validatePermissionBoundary(permission, groupRolePermission);
|
||||
if (!permissionBoundary.isValid)
|
||||
throw new PermissionBoundaryError({
|
||||
message: constructPermissionErrorMessage(
|
||||
"Failed to add user to more privileged group",
|
||||
membership.shouldUseNewPrivilegeSystem,
|
||||
OrgPermissionGroupActions.AddMembers,
|
||||
OrgPermissionSubjects.Groups
|
||||
),
|
||||
throw new ForbiddenRequestError({
|
||||
name: "PermissionBoundaryError",
|
||||
message: "Failed to add user to more privileged group",
|
||||
details: { missingPermissions: permissionBoundary.missingPermissions }
|
||||
});
|
||||
|
||||
@ -408,14 +374,14 @@ export const groupServiceFactory = ({
|
||||
}: TRemoveUserFromGroupDTO) => {
|
||||
if (!actorOrgId) throw new UnauthorizedError({ message: "No organization ID provided in request" });
|
||||
|
||||
const { permission, membership } = await permissionService.getOrgPermission(
|
||||
const { permission } = await permissionService.getOrgPermission(
|
||||
actor,
|
||||
actorId,
|
||||
actorOrgId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionGroupActions.Edit, OrgPermissionSubjects.Groups);
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Groups);
|
||||
|
||||
// check if group with slug exists
|
||||
const group = await groupDAL.findOne({
|
||||
@ -443,21 +409,11 @@ export const groupServiceFactory = ({
|
||||
const { permission: groupRolePermission } = await permissionService.getOrgPermissionByRole(group.role, actorOrgId);
|
||||
|
||||
// check if user has broader or equal to privileges than group
|
||||
const permissionBoundary = validatePrivilegeChangeOperation(
|
||||
membership.shouldUseNewPrivilegeSystem,
|
||||
OrgPermissionGroupActions.RemoveMembers,
|
||||
OrgPermissionSubjects.Groups,
|
||||
permission,
|
||||
groupRolePermission
|
||||
);
|
||||
const permissionBoundary = validatePermissionBoundary(permission, groupRolePermission);
|
||||
if (!permissionBoundary.isValid)
|
||||
throw new PermissionBoundaryError({
|
||||
message: constructPermissionErrorMessage(
|
||||
"Failed to delete user from more privileged group",
|
||||
membership.shouldUseNewPrivilegeSystem,
|
||||
OrgPermissionGroupActions.RemoveMembers,
|
||||
OrgPermissionSubjects.Groups
|
||||
),
|
||||
throw new ForbiddenRequestError({
|
||||
name: "PermissionBoundaryError",
|
||||
message: "Failed to delete user from more privileged group",
|
||||
details: { missingPermissions: permissionBoundary.missingPermissions }
|
||||
});
|
||||
|
||||
|
@ -2,17 +2,16 @@ import { ForbiddenError, subject } from "@casl/ability";
|
||||
import { packRules } from "@casl/ability/extra";
|
||||
|
||||
import { ActionProjectType, TableName } from "@app/db/schemas";
|
||||
import { BadRequestError, NotFoundError, PermissionBoundaryError } from "@app/lib/errors";
|
||||
import { validatePermissionBoundary } from "@app/lib/casl/boundary";
|
||||
import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
|
||||
import { ms } from "@app/lib/ms";
|
||||
import { validateHandlebarTemplate } from "@app/lib/template/validate-handlebars";
|
||||
import { unpackPermissions } from "@app/server/routes/sanitizedSchema/permission";
|
||||
import { ActorType } from "@app/services/auth/auth-type";
|
||||
import { TIdentityProjectDALFactory } from "@app/services/identity-project/identity-project-dal";
|
||||
import { TProjectDALFactory } from "@app/services/project/project-dal";
|
||||
|
||||
import { constructPermissionErrorMessage, validatePrivilegeChangeOperation } from "../permission/permission-fns";
|
||||
import { TPermissionServiceFactory } from "../permission/permission-service";
|
||||
import { ProjectPermissionIdentityActions, ProjectPermissionSub } from "../permission/project-permission";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub } from "../permission/project-permission";
|
||||
import { TIdentityProjectAdditionalPrivilegeV2DALFactory } from "./identity-project-additional-privilege-v2-dal";
|
||||
import {
|
||||
IdentityProjectAdditionalPrivilegeTemporaryMode,
|
||||
@ -65,10 +64,10 @@ export const identityProjectAdditionalPrivilegeV2ServiceFactory = ({
|
||||
actionProjectType: ActionProjectType.Any
|
||||
});
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionIdentityActions.Edit,
|
||||
ProjectPermissionActions.Edit,
|
||||
subject(ProjectPermissionSub.Identity, { identityId })
|
||||
);
|
||||
const { permission: targetIdentityPermission, membership } = await permissionService.getProjectPermission({
|
||||
const { permission: targetIdentityPermission } = await permissionService.getProjectPermission({
|
||||
actor: ActorType.IDENTITY,
|
||||
actorId: identityId,
|
||||
projectId: identityProjectMembership.projectId,
|
||||
@ -80,26 +79,13 @@ export const identityProjectAdditionalPrivilegeV2ServiceFactory = ({
|
||||
// we need to validate that the privilege given is not higher than the assigning users permission
|
||||
// @ts-expect-error this is expected error because of one being really accurate rule definition other being a bit more broader. Both are valid casl rules
|
||||
targetIdentityPermission.update(targetIdentityPermission.rules.concat(customPermission));
|
||||
const permissionBoundary = validatePrivilegeChangeOperation(
|
||||
membership.shouldUseNewPrivilegeSystem,
|
||||
ProjectPermissionIdentityActions.GrantPrivileges,
|
||||
ProjectPermissionSub.Identity,
|
||||
permission,
|
||||
targetIdentityPermission
|
||||
);
|
||||
const permissionBoundary = validatePermissionBoundary(permission, targetIdentityPermission);
|
||||
if (!permissionBoundary.isValid)
|
||||
throw new PermissionBoundaryError({
|
||||
message: constructPermissionErrorMessage(
|
||||
"Failed to update more privileged identity",
|
||||
membership.shouldUseNewPrivilegeSystem,
|
||||
ProjectPermissionIdentityActions.GrantPrivileges,
|
||||
ProjectPermissionSub.Identity
|
||||
),
|
||||
throw new ForbiddenRequestError({
|
||||
name: "PermissionBoundaryError",
|
||||
message: "Failed to update more privileged identity",
|
||||
details: { missingPermissions: permissionBoundary.missingPermissions }
|
||||
});
|
||||
validateHandlebarTemplate("Identity Additional Privilege Create", JSON.stringify(customPermission || []), {
|
||||
allowedExpressions: (val) => val.includes("identity.")
|
||||
});
|
||||
|
||||
const existingSlug = await identityProjectAdditionalPrivilegeDAL.findOne({
|
||||
slug,
|
||||
@ -164,10 +150,10 @@ export const identityProjectAdditionalPrivilegeV2ServiceFactory = ({
|
||||
actionProjectType: ActionProjectType.Any
|
||||
});
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionIdentityActions.Edit,
|
||||
ProjectPermissionActions.Edit,
|
||||
subject(ProjectPermissionSub.Identity, { identityId: identityProjectMembership.identityId })
|
||||
);
|
||||
const { permission: targetIdentityPermission, membership } = await permissionService.getProjectPermission({
|
||||
const { permission: targetIdentityPermission } = await permissionService.getProjectPermission({
|
||||
actor: ActorType.IDENTITY,
|
||||
actorId: identityProjectMembership.identityId,
|
||||
projectId: identityProjectMembership.projectId,
|
||||
@ -179,28 +165,14 @@ export const identityProjectAdditionalPrivilegeV2ServiceFactory = ({
|
||||
// we need to validate that the privilege given is not higher than the assigning users permission
|
||||
// @ts-expect-error this is expected error because of one being really accurate rule definition other being a bit more broader. Both are valid casl rules
|
||||
targetIdentityPermission.update(targetIdentityPermission.rules.concat(data.permissions || []));
|
||||
const permissionBoundary = validatePrivilegeChangeOperation(
|
||||
membership.shouldUseNewPrivilegeSystem,
|
||||
ProjectPermissionIdentityActions.GrantPrivileges,
|
||||
ProjectPermissionSub.Identity,
|
||||
permission,
|
||||
targetIdentityPermission
|
||||
);
|
||||
const permissionBoundary = validatePermissionBoundary(permission, targetIdentityPermission);
|
||||
if (!permissionBoundary.isValid)
|
||||
throw new PermissionBoundaryError({
|
||||
message: constructPermissionErrorMessage(
|
||||
"Failed to update more privileged identity",
|
||||
membership.shouldUseNewPrivilegeSystem,
|
||||
ProjectPermissionIdentityActions.GrantPrivileges,
|
||||
ProjectPermissionSub.Identity
|
||||
),
|
||||
throw new ForbiddenRequestError({
|
||||
name: "PermissionBoundaryError",
|
||||
message: "Failed to update more privileged identity",
|
||||
details: { missingPermissions: permissionBoundary.missingPermissions }
|
||||
});
|
||||
|
||||
validateHandlebarTemplate("Identity Additional Privilege Update", JSON.stringify(data.permissions || []), {
|
||||
allowedExpressions: (val) => val.includes("identity.")
|
||||
});
|
||||
|
||||
if (data?.slug) {
|
||||
const existingSlug = await identityProjectAdditionalPrivilegeDAL.findOne({
|
||||
slug: data.slug,
|
||||
@ -255,7 +227,7 @@ export const identityProjectAdditionalPrivilegeV2ServiceFactory = ({
|
||||
message: `Failed to find identity with membership ${identityPrivilege.projectMembershipId}`
|
||||
});
|
||||
|
||||
const { permission, membership } = await permissionService.getProjectPermission({
|
||||
const { permission } = await permissionService.getProjectPermission({
|
||||
actor,
|
||||
actorId,
|
||||
projectId: identityProjectMembership.projectId,
|
||||
@ -264,7 +236,7 @@ export const identityProjectAdditionalPrivilegeV2ServiceFactory = ({
|
||||
actionProjectType: ActionProjectType.Any
|
||||
});
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionIdentityActions.Edit,
|
||||
ProjectPermissionActions.Edit,
|
||||
subject(ProjectPermissionSub.Identity, { identityId: identityProjectMembership.identityId })
|
||||
);
|
||||
const { permission: identityRolePermission } = await permissionService.getProjectPermission({
|
||||
@ -275,21 +247,11 @@ export const identityProjectAdditionalPrivilegeV2ServiceFactory = ({
|
||||
actorOrgId,
|
||||
actionProjectType: ActionProjectType.Any
|
||||
});
|
||||
const permissionBoundary = validatePrivilegeChangeOperation(
|
||||
membership.shouldUseNewPrivilegeSystem,
|
||||
ProjectPermissionIdentityActions.GrantPrivileges,
|
||||
ProjectPermissionSub.Identity,
|
||||
permission,
|
||||
identityRolePermission
|
||||
);
|
||||
const permissionBoundary = validatePermissionBoundary(permission, identityRolePermission);
|
||||
if (!permissionBoundary.isValid)
|
||||
throw new PermissionBoundaryError({
|
||||
message: constructPermissionErrorMessage(
|
||||
"Failed to update more privileged identity",
|
||||
membership.shouldUseNewPrivilegeSystem,
|
||||
ProjectPermissionIdentityActions.GrantPrivileges,
|
||||
ProjectPermissionSub.Identity
|
||||
),
|
||||
throw new ForbiddenRequestError({
|
||||
name: "PermissionBoundaryError",
|
||||
message: "Failed to update more privileged identity",
|
||||
details: { missingPermissions: permissionBoundary.missingPermissions }
|
||||
});
|
||||
|
||||
@ -325,7 +287,7 @@ export const identityProjectAdditionalPrivilegeV2ServiceFactory = ({
|
||||
actionProjectType: ActionProjectType.Any
|
||||
});
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionIdentityActions.Read,
|
||||
ProjectPermissionActions.Read,
|
||||
subject(ProjectPermissionSub.Identity, { identityId: identityProjectMembership.identityId })
|
||||
);
|
||||
|
||||
@ -360,7 +322,7 @@ export const identityProjectAdditionalPrivilegeV2ServiceFactory = ({
|
||||
actionProjectType: ActionProjectType.Any
|
||||
});
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionIdentityActions.Read,
|
||||
ProjectPermissionActions.Read,
|
||||
subject(ProjectPermissionSub.Identity, { identityId: identityProjectMembership.identityId })
|
||||
);
|
||||
|
||||
@ -396,7 +358,7 @@ export const identityProjectAdditionalPrivilegeV2ServiceFactory = ({
|
||||
actionProjectType: ActionProjectType.Any
|
||||
});
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionIdentityActions.Read,
|
||||
ProjectPermissionActions.Read,
|
||||
subject(ProjectPermissionSub.Identity, { identityId: identityProjectMembership.identityId })
|
||||
);
|
||||
|
||||
|
@ -2,21 +2,16 @@ import { ForbiddenError, MongoAbility, RawRuleOf, subject } from "@casl/ability"
|
||||
import { PackRule, packRules, unpackRules } from "@casl/ability/extra";
|
||||
|
||||
import { ActionProjectType } from "@app/db/schemas";
|
||||
import { BadRequestError, NotFoundError, PermissionBoundaryError } from "@app/lib/errors";
|
||||
import { validatePermissionBoundary } from "@app/lib/casl/boundary";
|
||||
import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
|
||||
import { ms } from "@app/lib/ms";
|
||||
import { validateHandlebarTemplate } from "@app/lib/template/validate-handlebars";
|
||||
import { UnpackedPermissionSchema } from "@app/server/routes/sanitizedSchema/permission";
|
||||
import { ActorType } from "@app/services/auth/auth-type";
|
||||
import { TIdentityProjectDALFactory } from "@app/services/identity-project/identity-project-dal";
|
||||
import { TProjectDALFactory } from "@app/services/project/project-dal";
|
||||
|
||||
import { constructPermissionErrorMessage, validatePrivilegeChangeOperation } from "../permission/permission-fns";
|
||||
import { TPermissionServiceFactory } from "../permission/permission-service";
|
||||
import {
|
||||
ProjectPermissionIdentityActions,
|
||||
ProjectPermissionSet,
|
||||
ProjectPermissionSub
|
||||
} from "../permission/project-permission";
|
||||
import { ProjectPermissionActions, ProjectPermissionSet, ProjectPermissionSub } from "../permission/project-permission";
|
||||
import { TIdentityProjectAdditionalPrivilegeDALFactory } from "./identity-project-additional-privilege-dal";
|
||||
import {
|
||||
IdentityProjectAdditionalPrivilegeTemporaryMode,
|
||||
@ -68,7 +63,7 @@ export const identityProjectAdditionalPrivilegeServiceFactory = ({
|
||||
if (!identityProjectMembership)
|
||||
throw new NotFoundError({ message: `Failed to find identity with id ${identityId}` });
|
||||
|
||||
const { permission, membership } = await permissionService.getProjectPermission({
|
||||
const { permission } = await permissionService.getProjectPermission({
|
||||
actor,
|
||||
actorId,
|
||||
projectId: identityProjectMembership.projectId,
|
||||
@ -76,9 +71,8 @@ export const identityProjectAdditionalPrivilegeServiceFactory = ({
|
||||
actorOrgId,
|
||||
actionProjectType: ActionProjectType.Any
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionIdentityActions.Edit,
|
||||
ProjectPermissionActions.Edit,
|
||||
subject(ProjectPermissionSub.Identity, { identityId })
|
||||
);
|
||||
|
||||
@ -94,21 +88,11 @@ export const identityProjectAdditionalPrivilegeServiceFactory = ({
|
||||
// we need to validate that the privilege given is not higher than the assigning users permission
|
||||
// @ts-expect-error this is expected error because of one being really accurate rule definition other being a bit more broader. Both are valid casl rules
|
||||
targetIdentityPermission.update(targetIdentityPermission.rules.concat(customPermission));
|
||||
const permissionBoundary = validatePrivilegeChangeOperation(
|
||||
membership.shouldUseNewPrivilegeSystem,
|
||||
ProjectPermissionIdentityActions.GrantPrivileges,
|
||||
ProjectPermissionSub.Identity,
|
||||
permission,
|
||||
targetIdentityPermission
|
||||
);
|
||||
const permissionBoundary = validatePermissionBoundary(permission, targetIdentityPermission);
|
||||
if (!permissionBoundary.isValid)
|
||||
throw new PermissionBoundaryError({
|
||||
message: constructPermissionErrorMessage(
|
||||
"Failed to update more privileged identity",
|
||||
membership.shouldUseNewPrivilegeSystem,
|
||||
ProjectPermissionIdentityActions.GrantPrivileges,
|
||||
ProjectPermissionSub.Identity
|
||||
),
|
||||
throw new ForbiddenRequestError({
|
||||
name: "PermissionBoundaryError",
|
||||
message: "Failed to update more privileged identity",
|
||||
details: { missingPermissions: permissionBoundary.missingPermissions }
|
||||
});
|
||||
|
||||
@ -118,10 +102,6 @@ export const identityProjectAdditionalPrivilegeServiceFactory = ({
|
||||
});
|
||||
if (existingSlug) throw new BadRequestError({ message: "Additional privilege of provided slug exist" });
|
||||
|
||||
validateHandlebarTemplate("Identity Additional Privilege Create", JSON.stringify(customPermission || []), {
|
||||
allowedExpressions: (val) => val.includes("identity.")
|
||||
});
|
||||
|
||||
const packedPermission = JSON.stringify(packRules(customPermission));
|
||||
if (!dto.isTemporary) {
|
||||
const additionalPrivilege = await identityProjectAdditionalPrivilegeDAL.create({
|
||||
@ -170,7 +150,7 @@ export const identityProjectAdditionalPrivilegeServiceFactory = ({
|
||||
if (!identityProjectMembership)
|
||||
throw new NotFoundError({ message: `Failed to find identity with id ${identityId}` });
|
||||
|
||||
const { permission, membership } = await permissionService.getProjectPermission({
|
||||
const { permission } = await permissionService.getProjectPermission({
|
||||
actor,
|
||||
actorId,
|
||||
projectId: identityProjectMembership.projectId,
|
||||
@ -180,7 +160,7 @@ export const identityProjectAdditionalPrivilegeServiceFactory = ({
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionIdentityActions.Edit,
|
||||
ProjectPermissionActions.Edit,
|
||||
subject(ProjectPermissionSub.Identity, { identityId })
|
||||
);
|
||||
|
||||
@ -196,21 +176,11 @@ export const identityProjectAdditionalPrivilegeServiceFactory = ({
|
||||
// we need to validate that the privilege given is not higher than the assigning users permission
|
||||
// @ts-expect-error this is expected error because of one being really accurate rule definition other being a bit more broader. Both are valid casl rules
|
||||
targetIdentityPermission.update(targetIdentityPermission.rules.concat(data.permissions || []));
|
||||
const permissionBoundary = validatePrivilegeChangeOperation(
|
||||
membership.shouldUseNewPrivilegeSystem,
|
||||
ProjectPermissionIdentityActions.GrantPrivileges,
|
||||
ProjectPermissionSub.Identity,
|
||||
permission,
|
||||
targetIdentityPermission
|
||||
);
|
||||
const permissionBoundary = validatePermissionBoundary(permission, targetIdentityPermission);
|
||||
if (!permissionBoundary.isValid)
|
||||
throw new PermissionBoundaryError({
|
||||
message: constructPermissionErrorMessage(
|
||||
"Failed to update more privileged identity",
|
||||
membership.shouldUseNewPrivilegeSystem,
|
||||
ProjectPermissionIdentityActions.GrantPrivileges,
|
||||
ProjectPermissionSub.Identity
|
||||
),
|
||||
throw new ForbiddenRequestError({
|
||||
name: "PermissionBoundaryError",
|
||||
message: "Failed to update more privileged identity",
|
||||
details: { missingPermissions: permissionBoundary.missingPermissions }
|
||||
});
|
||||
|
||||
@ -233,9 +203,6 @@ export const identityProjectAdditionalPrivilegeServiceFactory = ({
|
||||
}
|
||||
|
||||
const isTemporary = typeof data?.isTemporary !== "undefined" ? data.isTemporary : identityPrivilege.isTemporary;
|
||||
validateHandlebarTemplate("Identity Additional Privilege Update", JSON.stringify(data.permissions || []), {
|
||||
allowedExpressions: (val) => val.includes("identity.")
|
||||
});
|
||||
|
||||
const packedPermission = data.permissions ? JSON.stringify(packRules(data.permissions)) : undefined;
|
||||
if (isTemporary) {
|
||||
@ -288,7 +255,7 @@ export const identityProjectAdditionalPrivilegeServiceFactory = ({
|
||||
if (!identityProjectMembership)
|
||||
throw new NotFoundError({ message: `Failed to find identity with id ${identityId}` });
|
||||
|
||||
const { permission, membership } = await permissionService.getProjectPermission({
|
||||
const { permission } = await permissionService.getProjectPermission({
|
||||
actor,
|
||||
actorId,
|
||||
projectId: identityProjectMembership.projectId,
|
||||
@ -297,7 +264,7 @@ export const identityProjectAdditionalPrivilegeServiceFactory = ({
|
||||
actionProjectType: ActionProjectType.Any
|
||||
});
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionIdentityActions.Edit,
|
||||
ProjectPermissionActions.Edit,
|
||||
subject(ProjectPermissionSub.Identity, { identityId })
|
||||
);
|
||||
|
||||
@ -309,21 +276,11 @@ export const identityProjectAdditionalPrivilegeServiceFactory = ({
|
||||
actorOrgId,
|
||||
actionProjectType: ActionProjectType.Any
|
||||
});
|
||||
const permissionBoundary = validatePrivilegeChangeOperation(
|
||||
membership.shouldUseNewPrivilegeSystem,
|
||||
ProjectPermissionIdentityActions.GrantPrivileges,
|
||||
ProjectPermissionSub.Identity,
|
||||
permission,
|
||||
identityRolePermission
|
||||
);
|
||||
const permissionBoundary = validatePermissionBoundary(permission, identityRolePermission);
|
||||
if (!permissionBoundary.isValid)
|
||||
throw new PermissionBoundaryError({
|
||||
message: constructPermissionErrorMessage(
|
||||
"Failed to edit more privileged identity",
|
||||
membership.shouldUseNewPrivilegeSystem,
|
||||
ProjectPermissionIdentityActions.GrantPrivileges,
|
||||
ProjectPermissionSub.Identity
|
||||
),
|
||||
throw new ForbiddenRequestError({
|
||||
name: "PermissionBoundaryError",
|
||||
message: "Failed to edit more privileged identity",
|
||||
details: { missingPermissions: permissionBoundary.missingPermissions }
|
||||
});
|
||||
|
||||
@ -370,7 +327,7 @@ export const identityProjectAdditionalPrivilegeServiceFactory = ({
|
||||
actionProjectType: ActionProjectType.Any
|
||||
});
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionIdentityActions.Read,
|
||||
ProjectPermissionActions.Read,
|
||||
subject(ProjectPermissionSub.Identity, { identityId })
|
||||
);
|
||||
|
||||
@ -414,7 +371,7 @@ export const identityProjectAdditionalPrivilegeServiceFactory = ({
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionIdentityActions.Read,
|
||||
ProjectPermissionActions.Read,
|
||||
subject(ProjectPermissionSub.Identity, { identityId })
|
||||
);
|
||||
|
||||
|
@ -4,9 +4,8 @@ import crypto, { KeyObject } from "crypto";
|
||||
|
||||
import { ActionProjectType } from "@app/db/schemas";
|
||||
import { BadRequestError, InternalServerError, NotFoundError } from "@app/lib/errors";
|
||||
import { isValidIp } from "@app/lib/ip";
|
||||
import { isValidHostname, isValidIp } from "@app/lib/ip";
|
||||
import { ms } from "@app/lib/ms";
|
||||
import { isFQDN } from "@app/lib/validator/validate-url";
|
||||
import { constructPemChainFromCerts } from "@app/services/certificate/certificate-fns";
|
||||
import { CertExtendedKeyUsage, CertKeyAlgorithm, CertKeyUsage } from "@app/services/certificate/certificate-types";
|
||||
import {
|
||||
@ -666,7 +665,7 @@ export const kmipServiceFactory = ({
|
||||
.split(",")
|
||||
.map((name) => name.trim())
|
||||
.map((altName) => {
|
||||
if (isFQDN(altName, { allow_wildcard: true })) {
|
||||
if (isValidHostname(altName)) {
|
||||
return {
|
||||
type: "dns",
|
||||
value: altName
|
||||
|
@ -97,14 +97,12 @@ export const searchGroups = async (
|
||||
|
||||
res.on("searchEntry", (entry) => {
|
||||
const dn = entry.dn.toString();
|
||||
const cnStartIndex = dn.indexOf("cn=");
|
||||
const regex = /cn=([^,]+)/;
|
||||
const match = dn.match(regex);
|
||||
// parse the cn from the dn
|
||||
const cn = (match && match[1]) as string;
|
||||
|
||||
if (cnStartIndex !== -1) {
|
||||
const valueStartIndex = cnStartIndex + 3;
|
||||
const commaIndex = dn.indexOf(",", valueStartIndex);
|
||||
const cn = dn.substring(valueStartIndex, commaIndex === -1 ? undefined : commaIndex);
|
||||
groups.push({ dn, cn });
|
||||
}
|
||||
groups.push({ dn, cn });
|
||||
});
|
||||
res.on("error", (error) => {
|
||||
ldapClient.unbind();
|
||||
|
@ -1,24 +0,0 @@
|
||||
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;
|
@ -12,13 +12,10 @@ 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 {
|
||||
@ -31,7 +28,6 @@ import {
|
||||
TFeatureSet,
|
||||
TGetOrgBillInfoDTO,
|
||||
TGetOrgTaxIdDTO,
|
||||
TOfflineLicense,
|
||||
TOfflineLicenseContents,
|
||||
TOrgInvoiceDTO,
|
||||
TOrgLicensesDTO,
|
||||
@ -43,12 +39,10 @@ import {
|
||||
} from "./license-types";
|
||||
|
||||
type TLicenseServiceFactoryDep = {
|
||||
orgDAL: Pick<TOrgDALFactory, "findOrgById" | "countAllOrgMembers">;
|
||||
orgDAL: Pick<TOrgDALFactory, "findOrgById">;
|
||||
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">;
|
||||
licenseDAL: TLicenseDALFactory;
|
||||
keyStore: Pick<TKeyStoreFactory, "setItemWithExpiry" | "getItem" | "deleteItem">;
|
||||
identityOrgMembershipDAL: TIdentityOrgDALFactory;
|
||||
projectDAL: TProjectDALFactory;
|
||||
};
|
||||
|
||||
export type TLicenseServiceFactory = ReturnType<typeof licenseServiceFactory>;
|
||||
@ -56,21 +50,18 @@ export type TLicenseServiceFactory = ReturnType<typeof licenseServiceFactory>;
|
||||
const LICENSE_SERVER_CLOUD_LOGIN = "/api/auth/v1/license-server-login";
|
||||
const LICENSE_SERVER_ON_PREM_LOGIN = "/api/auth/v1/license-login";
|
||||
|
||||
const LICENSE_SERVER_CLOUD_PLAN_TTL = 5 * 60; // 5 mins
|
||||
const LICENSE_SERVER_CLOUD_PLAN_TTL = 30; // 30 second
|
||||
const FEATURE_CACHE_KEY = (orgId: string) => `infisical-cloud-plan-${orgId}`;
|
||||
|
||||
export const licenseServiceFactory = ({
|
||||
orgDAL,
|
||||
permissionService,
|
||||
licenseDAL,
|
||||
keyStore,
|
||||
identityOrgMembershipDAL,
|
||||
projectDAL
|
||||
keyStore
|
||||
}: TLicenseServiceFactoryDep) => {
|
||||
let isValidLicense = false;
|
||||
let instanceType = InstanceType.OnPrem;
|
||||
let onPremFeatures: TFeatureSet = getDefaultOnPremFeatures();
|
||||
let selfHostedLicense: TOfflineLicense | null = null;
|
||||
|
||||
const appCfg = getConfig();
|
||||
const licenseServerCloudApi = setupLicenseRequestWithStore(
|
||||
@ -134,7 +125,6 @@ export const licenseServiceFactory = ({
|
||||
instanceType = InstanceType.EnterpriseOnPremOffline;
|
||||
logger.info(`Instance type: ${InstanceType.EnterpriseOnPremOffline}`);
|
||||
isValidLicense = true;
|
||||
selfHostedLicense = contents.license;
|
||||
return;
|
||||
}
|
||||
}
|
||||
@ -152,10 +142,7 @@ export const licenseServiceFactory = ({
|
||||
try {
|
||||
if (instanceType === InstanceType.Cloud) {
|
||||
const cachedPlan = await keyStore.getItem(FEATURE_CACHE_KEY(orgId));
|
||||
if (cachedPlan) {
|
||||
logger.info(`getPlan: plan fetched from cache [orgId=${orgId}] [projectId=${projectId}]`);
|
||||
return JSON.parse(cachedPlan) as TFeatureSet;
|
||||
}
|
||||
if (cachedPlan) return JSON.parse(cachedPlan) as TFeatureSet;
|
||||
|
||||
const org = await orgDAL.findOrgById(orgId);
|
||||
if (!org) throw new NotFoundError({ message: `Organization with ID '${orgId}' not found` });
|
||||
@ -183,8 +170,6 @@ export const licenseServiceFactory = ({
|
||||
JSON.stringify(onPremFeatures)
|
||||
);
|
||||
return onPremFeatures;
|
||||
} finally {
|
||||
logger.info(`getPlan: Process done for [orgId=${orgId}] [projectId=${projectId}]`);
|
||||
}
|
||||
return onPremFeatures;
|
||||
};
|
||||
@ -358,21 +343,10 @@ export const licenseServiceFactory = ({
|
||||
message: `Organization with ID '${orgId}' not found`
|
||||
});
|
||||
}
|
||||
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
|
||||
};
|
||||
const { data } = await licenseServerCloudApi.request.get(
|
||||
`/api/license-server/v1/customers/${organization.customerId}/cloud-plan/billing`
|
||||
);
|
||||
return data;
|
||||
};
|
||||
|
||||
// returns org current plan feature table
|
||||
@ -386,41 +360,10 @@ export const licenseServiceFactory = ({
|
||||
message: `Organization with ID '${orgId}' not found`
|
||||
});
|
||||
}
|
||||
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
|
||||
};
|
||||
})
|
||||
const { data } = await licenseServerCloudApi.request.get(
|
||||
`/api/license-server/v1/customers/${organization.customerId}/cloud-plan/table`
|
||||
);
|
||||
|
||||
return {
|
||||
head: Object.values(BillingPlanTableHead),
|
||||
rows: mappedRows
|
||||
};
|
||||
return data;
|
||||
};
|
||||
|
||||
const getOrgBillingDetails = async ({ orgId, actor, actorId, actorAuthMethod, actorOrgId }: TGetOrgBillInfoDTO) => {
|
||||
|
@ -44,28 +44,6 @@ export enum OrgPermissionGatewayActions {
|
||||
DeleteGateways = "delete-gateways"
|
||||
}
|
||||
|
||||
export enum OrgPermissionIdentityActions {
|
||||
Read = "read",
|
||||
Create = "create",
|
||||
Edit = "edit",
|
||||
Delete = "delete",
|
||||
GrantPrivileges = "grant-privileges",
|
||||
RevokeAuth = "revoke-auth",
|
||||
CreateToken = "create-token",
|
||||
GetToken = "get-token",
|
||||
DeleteToken = "delete-token"
|
||||
}
|
||||
|
||||
export enum OrgPermissionGroupActions {
|
||||
Read = "read",
|
||||
Create = "create",
|
||||
Edit = "edit",
|
||||
Delete = "delete",
|
||||
GrantPrivileges = "grant-privileges",
|
||||
AddMembers = "add-members",
|
||||
RemoveMembers = "remove-members"
|
||||
}
|
||||
|
||||
export enum OrgPermissionSubjects {
|
||||
Workspace = "workspace",
|
||||
Role = "role",
|
||||
@ -102,10 +80,10 @@ export type OrgPermissionSet =
|
||||
| [OrgPermissionActions, OrgPermissionSubjects.Sso]
|
||||
| [OrgPermissionActions, OrgPermissionSubjects.Scim]
|
||||
| [OrgPermissionActions, OrgPermissionSubjects.Ldap]
|
||||
| [OrgPermissionGroupActions, OrgPermissionSubjects.Groups]
|
||||
| [OrgPermissionActions, OrgPermissionSubjects.Groups]
|
||||
| [OrgPermissionActions, OrgPermissionSubjects.SecretScanning]
|
||||
| [OrgPermissionActions, OrgPermissionSubjects.Billing]
|
||||
| [OrgPermissionIdentityActions, OrgPermissionSubjects.Identity]
|
||||
| [OrgPermissionActions, OrgPermissionSubjects.Identity]
|
||||
| [OrgPermissionActions, OrgPermissionSubjects.Kms]
|
||||
| [OrgPermissionActions, OrgPermissionSubjects.AuditLogs]
|
||||
| [OrgPermissionActions, OrgPermissionSubjects.ProjectTemplates]
|
||||
@ -278,28 +256,20 @@ const buildAdminPermission = () => {
|
||||
can(OrgPermissionActions.Edit, OrgPermissionSubjects.Ldap);
|
||||
can(OrgPermissionActions.Delete, OrgPermissionSubjects.Ldap);
|
||||
|
||||
can(OrgPermissionGroupActions.Read, OrgPermissionSubjects.Groups);
|
||||
can(OrgPermissionGroupActions.Create, OrgPermissionSubjects.Groups);
|
||||
can(OrgPermissionGroupActions.Edit, OrgPermissionSubjects.Groups);
|
||||
can(OrgPermissionGroupActions.Delete, OrgPermissionSubjects.Groups);
|
||||
can(OrgPermissionGroupActions.GrantPrivileges, OrgPermissionSubjects.Groups);
|
||||
can(OrgPermissionGroupActions.AddMembers, OrgPermissionSubjects.Groups);
|
||||
can(OrgPermissionGroupActions.RemoveMembers, OrgPermissionSubjects.Groups);
|
||||
can(OrgPermissionActions.Read, OrgPermissionSubjects.Groups);
|
||||
can(OrgPermissionActions.Create, OrgPermissionSubjects.Groups);
|
||||
can(OrgPermissionActions.Edit, OrgPermissionSubjects.Groups);
|
||||
can(OrgPermissionActions.Delete, OrgPermissionSubjects.Groups);
|
||||
|
||||
can(OrgPermissionActions.Read, OrgPermissionSubjects.Billing);
|
||||
can(OrgPermissionActions.Create, OrgPermissionSubjects.Billing);
|
||||
can(OrgPermissionActions.Edit, OrgPermissionSubjects.Billing);
|
||||
can(OrgPermissionActions.Delete, OrgPermissionSubjects.Billing);
|
||||
|
||||
can(OrgPermissionIdentityActions.Read, OrgPermissionSubjects.Identity);
|
||||
can(OrgPermissionIdentityActions.Create, OrgPermissionSubjects.Identity);
|
||||
can(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity);
|
||||
can(OrgPermissionIdentityActions.Delete, OrgPermissionSubjects.Identity);
|
||||
can(OrgPermissionIdentityActions.GrantPrivileges, OrgPermissionSubjects.Identity);
|
||||
can(OrgPermissionIdentityActions.RevokeAuth, OrgPermissionSubjects.Identity);
|
||||
can(OrgPermissionIdentityActions.CreateToken, OrgPermissionSubjects.Identity);
|
||||
can(OrgPermissionIdentityActions.GetToken, OrgPermissionSubjects.Identity);
|
||||
can(OrgPermissionIdentityActions.DeleteToken, OrgPermissionSubjects.Identity);
|
||||
can(OrgPermissionActions.Read, OrgPermissionSubjects.Identity);
|
||||
can(OrgPermissionActions.Create, OrgPermissionSubjects.Identity);
|
||||
can(OrgPermissionActions.Edit, OrgPermissionSubjects.Identity);
|
||||
can(OrgPermissionActions.Delete, OrgPermissionSubjects.Identity);
|
||||
|
||||
can(OrgPermissionActions.Read, OrgPermissionSubjects.Kms);
|
||||
can(OrgPermissionActions.Create, OrgPermissionSubjects.Kms);
|
||||
@ -346,7 +316,7 @@ const buildMemberPermission = () => {
|
||||
|
||||
can(OrgPermissionActions.Create, OrgPermissionSubjects.Workspace);
|
||||
can(OrgPermissionActions.Read, OrgPermissionSubjects.Member);
|
||||
can(OrgPermissionGroupActions.Read, OrgPermissionSubjects.Groups);
|
||||
can(OrgPermissionActions.Read, OrgPermissionSubjects.Groups);
|
||||
can(OrgPermissionActions.Read, OrgPermissionSubjects.Role);
|
||||
can(OrgPermissionActions.Read, OrgPermissionSubjects.Settings);
|
||||
can(OrgPermissionActions.Read, OrgPermissionSubjects.Billing);
|
||||
@ -357,10 +327,10 @@ const buildMemberPermission = () => {
|
||||
can(OrgPermissionActions.Edit, OrgPermissionSubjects.SecretScanning);
|
||||
can(OrgPermissionActions.Delete, OrgPermissionSubjects.SecretScanning);
|
||||
|
||||
can(OrgPermissionIdentityActions.Read, OrgPermissionSubjects.Identity);
|
||||
can(OrgPermissionIdentityActions.Create, OrgPermissionSubjects.Identity);
|
||||
can(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity);
|
||||
can(OrgPermissionIdentityActions.Delete, OrgPermissionSubjects.Identity);
|
||||
can(OrgPermissionActions.Read, OrgPermissionSubjects.Identity);
|
||||
can(OrgPermissionActions.Create, OrgPermissionSubjects.Identity);
|
||||
can(OrgPermissionActions.Edit, OrgPermissionSubjects.Identity);
|
||||
can(OrgPermissionActions.Delete, OrgPermissionSubjects.Identity);
|
||||
|
||||
can(OrgPermissionActions.Read, OrgPermissionSubjects.AuditLogs);
|
||||
|
||||
|
@ -49,7 +49,6 @@ export const permissionDALFactory = (db: TDbClient) => {
|
||||
.join(TableName.Organization, `${TableName.Organization}.id`, `${TableName.OrgMembership}.orgId`)
|
||||
.select(
|
||||
selectAllTableCols(TableName.OrgMembership),
|
||||
db.ref("shouldUseNewPrivilegeSystem").withSchema(TableName.Organization),
|
||||
db.ref("slug").withSchema(TableName.OrgRoles).withSchema(TableName.OrgRoles).as("customRoleSlug"),
|
||||
db.ref("permissions").withSchema(TableName.OrgRoles),
|
||||
db.ref("authEnforced").withSchema(TableName.Organization).as("orgAuthEnforced"),
|
||||
@ -71,8 +70,7 @@ export const permissionDALFactory = (db: TDbClient) => {
|
||||
OrgMembershipsSchema.extend({
|
||||
permissions: z.unknown(),
|
||||
orgAuthEnforced: z.boolean().optional().nullable(),
|
||||
customRoleSlug: z.string().optional().nullable(),
|
||||
shouldUseNewPrivilegeSystem: z.boolean()
|
||||
customRoleSlug: z.string().optional().nullable()
|
||||
}).parse(el),
|
||||
childrenMapper: [
|
||||
{
|
||||
@ -120,9 +118,7 @@ export const permissionDALFactory = (db: TDbClient) => {
|
||||
.select(selectAllTableCols(TableName.IdentityOrgMembership))
|
||||
.select(db.ref("authEnforced").withSchema(TableName.Organization).as("orgAuthEnforced"))
|
||||
.select("permissions")
|
||||
.select(db.ref("shouldUseNewPrivilegeSystem").withSchema(TableName.Organization))
|
||||
.first();
|
||||
|
||||
return membership;
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: "GetOrgIdentityPermission" });
|
||||
@ -672,8 +668,7 @@ export const permissionDALFactory = (db: TDbClient) => {
|
||||
db.ref("authEnforced").withSchema(TableName.Organization).as("orgAuthEnforced"),
|
||||
db.ref("orgId").withSchema(TableName.Project),
|
||||
db.ref("type").withSchema(TableName.Project).as("projectType"),
|
||||
db.ref("id").withSchema(TableName.Project).as("projectId"),
|
||||
db.ref("shouldUseNewPrivilegeSystem").withSchema(TableName.Organization)
|
||||
db.ref("id").withSchema(TableName.Project).as("projectId")
|
||||
);
|
||||
|
||||
const [userPermission] = sqlNestRelationships({
|
||||
@ -689,8 +684,7 @@ export const permissionDALFactory = (db: TDbClient) => {
|
||||
groupMembershipCreatedAt,
|
||||
groupMembershipUpdatedAt,
|
||||
membershipUpdatedAt,
|
||||
projectType,
|
||||
shouldUseNewPrivilegeSystem
|
||||
projectType
|
||||
}) => ({
|
||||
orgId,
|
||||
orgAuthEnforced,
|
||||
@ -700,8 +694,7 @@ export const permissionDALFactory = (db: TDbClient) => {
|
||||
projectType,
|
||||
id: membershipId || groupMembershipId,
|
||||
createdAt: membershipCreatedAt || groupMembershipCreatedAt,
|
||||
updatedAt: membershipUpdatedAt || groupMembershipUpdatedAt,
|
||||
shouldUseNewPrivilegeSystem
|
||||
updatedAt: membershipUpdatedAt || groupMembershipUpdatedAt
|
||||
}),
|
||||
childrenMapper: [
|
||||
{
|
||||
@ -1002,7 +995,6 @@ export const permissionDALFactory = (db: TDbClient) => {
|
||||
`${TableName.IdentityProjectMembership}.projectId`,
|
||||
`${TableName.Project}.id`
|
||||
)
|
||||
.join(TableName.Organization, `${TableName.Project}.orgId`, `${TableName.Organization}.id`)
|
||||
.leftJoin(TableName.IdentityMetadata, (queryBuilder) => {
|
||||
void queryBuilder
|
||||
.on(`${TableName.Identity}.id`, `${TableName.IdentityMetadata}.identityId`)
|
||||
@ -1020,7 +1012,6 @@ export const permissionDALFactory = (db: TDbClient) => {
|
||||
db.ref("updatedAt").withSchema(TableName.IdentityProjectMembership).as("membershipUpdatedAt"),
|
||||
db.ref("slug").withSchema(TableName.ProjectRoles).as("customRoleSlug"),
|
||||
db.ref("permissions").withSchema(TableName.ProjectRoles),
|
||||
db.ref("shouldUseNewPrivilegeSystem").withSchema(TableName.Organization),
|
||||
db.ref("id").withSchema(TableName.IdentityProjectAdditionalPrivilege).as("identityApId"),
|
||||
db.ref("permissions").withSchema(TableName.IdentityProjectAdditionalPrivilege).as("identityApPermissions"),
|
||||
db
|
||||
@ -1054,8 +1045,7 @@ export const permissionDALFactory = (db: TDbClient) => {
|
||||
membershipUpdatedAt,
|
||||
orgId,
|
||||
identityName,
|
||||
projectType,
|
||||
shouldUseNewPrivilegeSystem
|
||||
projectType
|
||||
}) => ({
|
||||
id: membershipId,
|
||||
identityId,
|
||||
@ -1065,7 +1055,6 @@ export const permissionDALFactory = (db: TDbClient) => {
|
||||
updatedAt: membershipUpdatedAt,
|
||||
orgId,
|
||||
projectType,
|
||||
shouldUseNewPrivilegeSystem,
|
||||
// just a prefilled value
|
||||
orgAuthEnforced: false
|
||||
}),
|
||||
|
@ -3,11 +3,9 @@ import { ForbiddenError, MongoAbility, PureAbility, subject } from "@casl/abilit
|
||||
import { z } from "zod";
|
||||
|
||||
import { TOrganizations } from "@app/db/schemas";
|
||||
import { validatePermissionBoundary } from "@app/lib/casl/boundary";
|
||||
import { BadRequestError, ForbiddenRequestError, UnauthorizedError } from "@app/lib/errors";
|
||||
import { ActorAuthMethod, AuthMethod } from "@app/services/auth/auth-type";
|
||||
|
||||
import { OrgPermissionSet } from "./org-permission";
|
||||
import {
|
||||
ProjectPermissionSecretActions,
|
||||
ProjectPermissionSet,
|
||||
@ -133,12 +131,12 @@ function validateOrgSSO(actorAuthMethod: ActorAuthMethod, isOrgSsoEnforced: TOrg
|
||||
}
|
||||
}
|
||||
|
||||
const escapeHandlebarsMissingDict = (obj: Record<string, string>, key: string) => {
|
||||
const escapeHandlebarsMissingMetadata = (obj: Record<string, string>) => {
|
||||
const handler = {
|
||||
get(target: Record<string, string>, prop: string) {
|
||||
if (!Object.hasOwn(target, prop)) {
|
||||
if (!(prop in target)) {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
target[prop] = `{{${key}.${prop}}}`; // Add missing key as an "own" property
|
||||
target[prop] = `{{identity.metadata.${prop}}}`; // Add missing key as an "own" property
|
||||
}
|
||||
return target[prop];
|
||||
}
|
||||
@ -147,57 +145,4 @@ const escapeHandlebarsMissingDict = (obj: Record<string, string>, key: string) =
|
||||
return new Proxy(obj, handler);
|
||||
};
|
||||
|
||||
// This function serves as a transition layer between the old and new privilege management system
|
||||
// the old privilege management system is based on the actor having more privileges than the managed permission
|
||||
// the new privilege management system is based on the actor having the appropriate permission to perform the privilege change,
|
||||
// regardless of the actor's privilege level.
|
||||
const validatePrivilegeChangeOperation = (
|
||||
shouldUseNewPrivilegeSystem: boolean,
|
||||
opAction: OrgPermissionSet[0] | ProjectPermissionSet[0],
|
||||
opSubject: OrgPermissionSet[1] | ProjectPermissionSet[1],
|
||||
actorPermission: MongoAbility,
|
||||
managedPermission: MongoAbility
|
||||
) => {
|
||||
if (shouldUseNewPrivilegeSystem) {
|
||||
if (actorPermission.can(opAction, opSubject)) {
|
||||
return {
|
||||
isValid: true,
|
||||
missingPermissions: []
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
isValid: false,
|
||||
missingPermissions: [
|
||||
{
|
||||
action: opAction,
|
||||
subject: opSubject
|
||||
}
|
||||
]
|
||||
};
|
||||
}
|
||||
|
||||
// if not, we check if the actor is indeed more privileged than the managed permission - this is the old system
|
||||
return validatePermissionBoundary(actorPermission, managedPermission);
|
||||
};
|
||||
|
||||
const constructPermissionErrorMessage = (
|
||||
baseMessage: string,
|
||||
shouldUseNewPrivilegeSystem: boolean,
|
||||
opAction: OrgPermissionSet[0] | ProjectPermissionSet[0],
|
||||
opSubject: OrgPermissionSet[1] | ProjectPermissionSet[1]
|
||||
) => {
|
||||
return `${baseMessage}${
|
||||
shouldUseNewPrivilegeSystem
|
||||
? `. Actor is missing permission ${opAction as string} on ${opSubject as string}`
|
||||
: ". Actor privilege level is not high enough to perform this action"
|
||||
}`;
|
||||
};
|
||||
|
||||
export {
|
||||
constructPermissionErrorMessage,
|
||||
escapeHandlebarsMissingDict,
|
||||
isAuthMethodSaml,
|
||||
validateOrgSSO,
|
||||
validatePrivilegeChangeOperation
|
||||
};
|
||||
export { escapeHandlebarsMissingMetadata, isAuthMethodSaml, validateOrgSSO };
|
||||
|
@ -1,6 +1,5 @@
|
||||
import { createMongoAbility, MongoAbility, RawRuleOf } from "@casl/ability";
|
||||
import { PackRule, unpackRules } from "@casl/ability/extra";
|
||||
import { requestContext } from "@fastify/request-context";
|
||||
import { MongoQuery } from "@ucast/mongo2js";
|
||||
import handlebars from "handlebars";
|
||||
|
||||
@ -23,7 +22,7 @@ import { TServiceTokenDALFactory } from "@app/services/service-token/service-tok
|
||||
|
||||
import { orgAdminPermissions, orgMemberPermissions, orgNoAccessPermissions, OrgPermissionSet } from "./org-permission";
|
||||
import { TPermissionDALFactory } from "./permission-dal";
|
||||
import { escapeHandlebarsMissingDict, validateOrgSSO } from "./permission-fns";
|
||||
import { escapeHandlebarsMissingMetadata, validateOrgSSO } from "./permission-fns";
|
||||
import {
|
||||
TBuildOrgPermissionDTO,
|
||||
TBuildProjectPermissionDTO,
|
||||
@ -244,13 +243,13 @@ export const permissionServiceFactory = ({
|
||||
|
||||
const rules = buildProjectPermissionRules(rolePermissions.concat(additionalPrivileges));
|
||||
const templatedRules = handlebars.compile(JSON.stringify(rules), { data: false });
|
||||
const unescapedMetadata = objectify(
|
||||
userProjectPermission.metadata,
|
||||
(i) => i.key,
|
||||
(i) => i.value
|
||||
const metadataKeyValuePair = escapeHandlebarsMissingMetadata(
|
||||
objectify(
|
||||
userProjectPermission.metadata,
|
||||
(i) => i.key,
|
||||
(i) => i.value
|
||||
)
|
||||
);
|
||||
const metadataKeyValuePair = escapeHandlebarsMissingDict(unescapedMetadata, "identity.metadata");
|
||||
requestContext.set("identityPermissionMetadata", { metadata: unescapedMetadata });
|
||||
const interpolateRules = templatedRules(
|
||||
{
|
||||
identity: {
|
||||
@ -318,26 +317,20 @@ export const permissionServiceFactory = ({
|
||||
|
||||
const rules = buildProjectPermissionRules(rolePermissions.concat(additionalPrivileges));
|
||||
const templatedRules = handlebars.compile(JSON.stringify(rules), { data: false });
|
||||
const unescapedIdentityAuthInfo = requestContext.get("identityAuthInfo");
|
||||
const unescapedMetadata = objectify(
|
||||
identityProjectPermission.metadata,
|
||||
(i) => i.key,
|
||||
(i) => i.value
|
||||
const metadataKeyValuePair = escapeHandlebarsMissingMetadata(
|
||||
objectify(
|
||||
identityProjectPermission.metadata,
|
||||
(i) => i.key,
|
||||
(i) => i.value
|
||||
)
|
||||
);
|
||||
const identityAuthInfo =
|
||||
unescapedIdentityAuthInfo?.identityId === identityId && unescapedIdentityAuthInfo
|
||||
? escapeHandlebarsMissingDict(unescapedIdentityAuthInfo as never, "identity.auth")
|
||||
: {};
|
||||
const metadataKeyValuePair = escapeHandlebarsMissingDict(unescapedMetadata, "identity.metadata");
|
||||
|
||||
requestContext.set("identityPermissionMetadata", { metadata: unescapedMetadata, auth: unescapedIdentityAuthInfo });
|
||||
const interpolateRules = templatedRules(
|
||||
{
|
||||
identity: {
|
||||
id: identityProjectPermission.identityId,
|
||||
username: identityProjectPermission.username,
|
||||
metadata: metadataKeyValuePair,
|
||||
auth: identityAuthInfo
|
||||
metadata: metadataKeyValuePair
|
||||
}
|
||||
},
|
||||
{ data: false }
|
||||
@ -397,18 +390,14 @@ export const permissionServiceFactory = ({
|
||||
const scopes = ServiceTokenScopes.parse(serviceToken.scopes || []);
|
||||
return {
|
||||
permission: buildServiceTokenProjectPermission(scopes, serviceToken.permissions),
|
||||
membership: {
|
||||
shouldUseNewPrivilegeSystem: true
|
||||
}
|
||||
membership: undefined
|
||||
};
|
||||
};
|
||||
|
||||
type TProjectPermissionRT<T extends ActorType> = T extends ActorType.SERVICE
|
||||
? {
|
||||
permission: MongoAbility<ProjectPermissionSet, MongoQuery>;
|
||||
membership: {
|
||||
shouldUseNewPrivilegeSystem: boolean;
|
||||
};
|
||||
membership: undefined;
|
||||
hasRole: (arg: string) => boolean;
|
||||
} // service token doesn't have both membership and roles
|
||||
: {
|
||||
@ -417,7 +406,6 @@ export const permissionServiceFactory = ({
|
||||
orgAuthEnforced: boolean | null | undefined;
|
||||
orgId: string;
|
||||
roles: Array<{ role: string }>;
|
||||
shouldUseNewPrivilegeSystem: boolean;
|
||||
};
|
||||
hasRole: (role: string) => boolean;
|
||||
};
|
||||
@ -436,13 +424,12 @@ export const permissionServiceFactory = ({
|
||||
|
||||
const rules = buildProjectPermissionRules(rolePermissions.concat(additionalPrivileges));
|
||||
const templatedRules = handlebars.compile(JSON.stringify(rules), { data: false });
|
||||
const metadataKeyValuePair = escapeHandlebarsMissingDict(
|
||||
const metadataKeyValuePair = escapeHandlebarsMissingMetadata(
|
||||
objectify(
|
||||
userProjectPermission.metadata,
|
||||
(i) => i.key,
|
||||
(i) => i.value
|
||||
),
|
||||
"identity.metadata"
|
||||
)
|
||||
);
|
||||
const interpolateRules = templatedRules(
|
||||
{
|
||||
@ -482,14 +469,14 @@ export const permissionServiceFactory = ({
|
||||
|
||||
const rules = buildProjectPermissionRules(rolePermissions.concat(additionalPrivileges));
|
||||
const templatedRules = handlebars.compile(JSON.stringify(rules), { data: false });
|
||||
const metadataKeyValuePair = escapeHandlebarsMissingDict(
|
||||
const metadataKeyValuePair = escapeHandlebarsMissingMetadata(
|
||||
objectify(
|
||||
identityProjectPermission.metadata,
|
||||
(i) => i.key,
|
||||
(i) => i.value
|
||||
),
|
||||
"identity.metadata"
|
||||
)
|
||||
);
|
||||
|
||||
const interpolateRules = templatedRules(
|
||||
{
|
||||
identity: {
|
||||
|
@ -43,30 +43,6 @@ export enum ProjectPermissionDynamicSecretActions {
|
||||
Lease = "lease"
|
||||
}
|
||||
|
||||
export enum ProjectPermissionIdentityActions {
|
||||
Read = "read",
|
||||
Create = "create",
|
||||
Edit = "edit",
|
||||
Delete = "delete",
|
||||
GrantPrivileges = "grant-privileges"
|
||||
}
|
||||
|
||||
export enum ProjectPermissionMemberActions {
|
||||
Read = "read",
|
||||
Create = "create",
|
||||
Edit = "edit",
|
||||
Delete = "delete",
|
||||
GrantPrivileges = "grant-privileges"
|
||||
}
|
||||
|
||||
export enum ProjectPermissionGroupActions {
|
||||
Read = "read",
|
||||
Create = "create",
|
||||
Edit = "edit",
|
||||
Delete = "delete",
|
||||
GrantPrivileges = "grant-privileges"
|
||||
}
|
||||
|
||||
export enum ProjectPermissionSecretSyncActions {
|
||||
Read = "read",
|
||||
Create = "create",
|
||||
@ -77,6 +53,15 @@ export enum ProjectPermissionSecretSyncActions {
|
||||
RemoveSecrets = "remove-secrets"
|
||||
}
|
||||
|
||||
export enum ProjectPermissionSecretRotationActions {
|
||||
Read = "read",
|
||||
ReadCredentials = "read-credentials",
|
||||
Create = "create",
|
||||
Edit = "edit",
|
||||
Delete = "delete",
|
||||
Rotate = "rotate"
|
||||
}
|
||||
|
||||
export enum ProjectPermissionKmipActions {
|
||||
CreateClients = "create-clients",
|
||||
UpdateClients = "update-clients",
|
||||
@ -174,8 +159,8 @@ export type ProjectPermissionSet =
|
||||
]
|
||||
| [ProjectPermissionActions, ProjectPermissionSub.Role]
|
||||
| [ProjectPermissionActions, ProjectPermissionSub.Tags]
|
||||
| [ProjectPermissionMemberActions, ProjectPermissionSub.Member]
|
||||
| [ProjectPermissionGroupActions, ProjectPermissionSub.Groups]
|
||||
| [ProjectPermissionActions, ProjectPermissionSub.Member]
|
||||
| [ProjectPermissionActions, ProjectPermissionSub.Groups]
|
||||
| [ProjectPermissionActions, ProjectPermissionSub.Integrations]
|
||||
| [ProjectPermissionActions, ProjectPermissionSub.Webhooks]
|
||||
| [ProjectPermissionActions, ProjectPermissionSub.AuditLogs]
|
||||
@ -184,9 +169,9 @@ export type ProjectPermissionSet =
|
||||
| [ProjectPermissionActions, ProjectPermissionSub.Settings]
|
||||
| [ProjectPermissionActions, ProjectPermissionSub.ServiceTokens]
|
||||
| [ProjectPermissionActions, ProjectPermissionSub.SecretApproval]
|
||||
| [ProjectPermissionActions, ProjectPermissionSub.SecretRotation]
|
||||
| [ProjectPermissionSecretRotationActions, ProjectPermissionSub.SecretRotation]
|
||||
| [
|
||||
ProjectPermissionIdentityActions,
|
||||
ProjectPermissionActions,
|
||||
ProjectPermissionSub.Identity | (ForcedSubject<ProjectPermissionSub.Identity> & IdentityManagementSubjectFields)
|
||||
]
|
||||
| [ProjectPermissionActions, ProjectPermissionSub.CertificateAuthorities]
|
||||
@ -302,7 +287,7 @@ const GeneralPermissionSchema = [
|
||||
}),
|
||||
z.object({
|
||||
subject: z.literal(ProjectPermissionSub.SecretRotation).describe("The entity this permission pertains to."),
|
||||
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionActions).describe(
|
||||
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionSecretRotationActions).describe(
|
||||
"Describe what action an entity can take."
|
||||
)
|
||||
}),
|
||||
@ -314,13 +299,13 @@ const GeneralPermissionSchema = [
|
||||
}),
|
||||
z.object({
|
||||
subject: z.literal(ProjectPermissionSub.Member).describe("The entity this permission pertains to."),
|
||||
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionMemberActions).describe(
|
||||
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionActions).describe(
|
||||
"Describe what action an entity can take."
|
||||
)
|
||||
}),
|
||||
z.object({
|
||||
subject: z.literal(ProjectPermissionSub.Groups).describe("The entity this permission pertains to."),
|
||||
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionGroupActions).describe(
|
||||
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionActions).describe(
|
||||
"Describe what action an entity can take."
|
||||
)
|
||||
}),
|
||||
@ -534,7 +519,7 @@ export const ProjectPermissionV2Schema = z.discriminatedUnion("subject", [
|
||||
z.object({
|
||||
subject: z.literal(ProjectPermissionSub.Identity).describe("The entity this permission pertains to."),
|
||||
inverted: z.boolean().optional().describe("Whether rule allows or forbids."),
|
||||
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionIdentityActions).describe(
|
||||
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionActions).describe(
|
||||
"Describe what action an entity can take."
|
||||
),
|
||||
conditions: IdentityManagementConditionSchema.describe(
|
||||
@ -554,10 +539,12 @@ const buildAdminPermissionRules = () => {
|
||||
ProjectPermissionSub.SecretFolders,
|
||||
ProjectPermissionSub.SecretImports,
|
||||
ProjectPermissionSub.SecretApproval,
|
||||
ProjectPermissionSub.SecretRotation,
|
||||
ProjectPermissionSub.Member,
|
||||
ProjectPermissionSub.Groups,
|
||||
ProjectPermissionSub.Role,
|
||||
ProjectPermissionSub.Integrations,
|
||||
ProjectPermissionSub.Webhooks,
|
||||
ProjectPermissionSub.Identity,
|
||||
ProjectPermissionSub.ServiceTokens,
|
||||
ProjectPermissionSub.Settings,
|
||||
ProjectPermissionSub.Environments,
|
||||
@ -584,39 +571,6 @@ const buildAdminPermissionRules = () => {
|
||||
);
|
||||
});
|
||||
|
||||
can(
|
||||
[
|
||||
ProjectPermissionMemberActions.Create,
|
||||
ProjectPermissionMemberActions.Edit,
|
||||
ProjectPermissionMemberActions.Delete,
|
||||
ProjectPermissionMemberActions.Read,
|
||||
ProjectPermissionMemberActions.GrantPrivileges
|
||||
],
|
||||
ProjectPermissionSub.Member
|
||||
);
|
||||
|
||||
can(
|
||||
[
|
||||
ProjectPermissionGroupActions.Create,
|
||||
ProjectPermissionGroupActions.Edit,
|
||||
ProjectPermissionGroupActions.Delete,
|
||||
ProjectPermissionGroupActions.Read,
|
||||
ProjectPermissionGroupActions.GrantPrivileges
|
||||
],
|
||||
ProjectPermissionSub.Groups
|
||||
);
|
||||
|
||||
can(
|
||||
[
|
||||
ProjectPermissionIdentityActions.Create,
|
||||
ProjectPermissionIdentityActions.Edit,
|
||||
ProjectPermissionIdentityActions.Delete,
|
||||
ProjectPermissionIdentityActions.Read,
|
||||
ProjectPermissionIdentityActions.GrantPrivileges
|
||||
],
|
||||
ProjectPermissionSub.Identity
|
||||
);
|
||||
|
||||
can(
|
||||
[
|
||||
ProjectPermissionSecretActions.DescribeAndReadValue,
|
||||
@ -678,6 +632,18 @@ const buildAdminPermissionRules = () => {
|
||||
ProjectPermissionSub.Kmip
|
||||
);
|
||||
|
||||
can(
|
||||
[
|
||||
ProjectPermissionSecretRotationActions.Create,
|
||||
ProjectPermissionSecretRotationActions.Edit,
|
||||
ProjectPermissionSecretRotationActions.Delete,
|
||||
ProjectPermissionSecretRotationActions.Read,
|
||||
ProjectPermissionSecretRotationActions.ReadCredentials,
|
||||
ProjectPermissionSecretRotationActions.Rotate
|
||||
],
|
||||
ProjectPermissionSub.SecretRotation
|
||||
);
|
||||
|
||||
return rules;
|
||||
};
|
||||
|
||||
@ -727,13 +693,13 @@ const buildMemberPermissionRules = () => {
|
||||
);
|
||||
|
||||
can([ProjectPermissionActions.Read], ProjectPermissionSub.SecretApproval);
|
||||
can([ProjectPermissionActions.Read], ProjectPermissionSub.SecretRotation);
|
||||
can([ProjectPermissionSecretRotationActions.Read], ProjectPermissionSub.SecretRotation);
|
||||
|
||||
can([ProjectPermissionActions.Read, ProjectPermissionActions.Create], ProjectPermissionSub.SecretRollback);
|
||||
|
||||
can([ProjectPermissionMemberActions.Read, ProjectPermissionMemberActions.Create], ProjectPermissionSub.Member);
|
||||
can([ProjectPermissionActions.Read, ProjectPermissionActions.Create], ProjectPermissionSub.Member);
|
||||
|
||||
can([ProjectPermissionGroupActions.Read], ProjectPermissionSub.Groups);
|
||||
can([ProjectPermissionActions.Read], ProjectPermissionSub.Groups);
|
||||
|
||||
can(
|
||||
[
|
||||
@ -757,10 +723,10 @@ const buildMemberPermissionRules = () => {
|
||||
|
||||
can(
|
||||
[
|
||||
ProjectPermissionIdentityActions.Read,
|
||||
ProjectPermissionIdentityActions.Edit,
|
||||
ProjectPermissionIdentityActions.Create,
|
||||
ProjectPermissionIdentityActions.Delete
|
||||
ProjectPermissionActions.Read,
|
||||
ProjectPermissionActions.Edit,
|
||||
ProjectPermissionActions.Create,
|
||||
ProjectPermissionActions.Delete
|
||||
],
|
||||
ProjectPermissionSub.Identity
|
||||
);
|
||||
@ -873,13 +839,13 @@ const buildViewerPermissionRules = () => {
|
||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretImports);
|
||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretApproval);
|
||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback);
|
||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretRotation);
|
||||
can(ProjectPermissionMemberActions.Read, ProjectPermissionSub.Member);
|
||||
can(ProjectPermissionGroupActions.Read, ProjectPermissionSub.Groups);
|
||||
can(ProjectPermissionSecretRotationActions.Read, ProjectPermissionSub.SecretRotation);
|
||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.Member);
|
||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.Groups);
|
||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.Role);
|
||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.Integrations);
|
||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.Webhooks);
|
||||
can(ProjectPermissionIdentityActions.Read, ProjectPermissionSub.Identity);
|
||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.Identity);
|
||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.ServiceTokens);
|
||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.Settings);
|
||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.Environments);
|
||||
|
@ -2,20 +2,15 @@ import { ForbiddenError, MongoAbility, RawRuleOf } from "@casl/ability";
|
||||
import { PackRule, packRules, unpackRules } from "@casl/ability/extra";
|
||||
|
||||
import { ActionProjectType, TableName } from "@app/db/schemas";
|
||||
import { BadRequestError, NotFoundError, PermissionBoundaryError } from "@app/lib/errors";
|
||||
import { validatePermissionBoundary } from "@app/lib/casl/boundary";
|
||||
import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
|
||||
import { ms } from "@app/lib/ms";
|
||||
import { validateHandlebarTemplate } from "@app/lib/template/validate-handlebars";
|
||||
import { UnpackedPermissionSchema } from "@app/server/routes/sanitizedSchema/permission";
|
||||
import { ActorType } from "@app/services/auth/auth-type";
|
||||
import { TProjectMembershipDALFactory } from "@app/services/project-membership/project-membership-dal";
|
||||
|
||||
import { constructPermissionErrorMessage, validatePrivilegeChangeOperation } from "../permission/permission-fns";
|
||||
import { TPermissionServiceFactory } from "../permission/permission-service";
|
||||
import {
|
||||
ProjectPermissionMemberActions,
|
||||
ProjectPermissionSet,
|
||||
ProjectPermissionSub
|
||||
} from "../permission/project-permission";
|
||||
import { ProjectPermissionActions, ProjectPermissionSet, ProjectPermissionSub } from "../permission/project-permission";
|
||||
import { TProjectUserAdditionalPrivilegeDALFactory } from "./project-user-additional-privilege-dal";
|
||||
import {
|
||||
ProjectUserAdditionalPrivilegeTemporaryMode,
|
||||
@ -68,8 +63,8 @@ export const projectUserAdditionalPrivilegeServiceFactory = ({
|
||||
actorOrgId,
|
||||
actionProjectType: ActionProjectType.Any
|
||||
});
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionMemberActions.Edit, ProjectPermissionSub.Member);
|
||||
const { permission: targetUserPermission, membership } = await permissionService.getProjectPermission({
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Member);
|
||||
const { permission: targetUserPermission } = await permissionService.getProjectPermission({
|
||||
actor: ActorType.USER,
|
||||
actorId: projectMembership.userId,
|
||||
projectId: projectMembership.projectId,
|
||||
@ -81,21 +76,11 @@ export const projectUserAdditionalPrivilegeServiceFactory = ({
|
||||
// we need to validate that the privilege given is not higher than the assigning users permission
|
||||
// @ts-expect-error this is expected error because of one being really accurate rule definition other being a bit more broader. Both are valid casl rules
|
||||
targetUserPermission.update(targetUserPermission.rules.concat(customPermission));
|
||||
const permissionBoundary = validatePrivilegeChangeOperation(
|
||||
membership.shouldUseNewPrivilegeSystem,
|
||||
ProjectPermissionMemberActions.GrantPrivileges,
|
||||
ProjectPermissionSub.Member,
|
||||
permission,
|
||||
targetUserPermission
|
||||
);
|
||||
const permissionBoundary = validatePermissionBoundary(permission, targetUserPermission);
|
||||
if (!permissionBoundary.isValid)
|
||||
throw new PermissionBoundaryError({
|
||||
message: constructPermissionErrorMessage(
|
||||
"Failed to update more privileged user",
|
||||
membership.shouldUseNewPrivilegeSystem,
|
||||
ProjectPermissionMemberActions.GrantPrivileges,
|
||||
ProjectPermissionSub.Member
|
||||
),
|
||||
throw new ForbiddenRequestError({
|
||||
name: "PermissionBoundaryError",
|
||||
message: "Failed to update more privileged user",
|
||||
details: { missingPermissions: permissionBoundary.missingPermissions }
|
||||
});
|
||||
|
||||
@ -107,10 +92,6 @@ export const projectUserAdditionalPrivilegeServiceFactory = ({
|
||||
if (existingSlug)
|
||||
throw new BadRequestError({ message: `Additional privilege with provided slug ${slug} already exists` });
|
||||
|
||||
validateHandlebarTemplate("User Additional Privilege Create", JSON.stringify(customPermission || []), {
|
||||
allowedExpressions: (val) => val.includes("identity.")
|
||||
});
|
||||
|
||||
const packedPermission = JSON.stringify(packRules(customPermission));
|
||||
if (!dto.isTemporary) {
|
||||
const additionalPrivilege = await projectUserAdditionalPrivilegeDAL.create({
|
||||
@ -165,7 +146,7 @@ export const projectUserAdditionalPrivilegeServiceFactory = ({
|
||||
message: `Project membership for user with ID '${userPrivilege.userId}' not found in project with ID '${userPrivilege.projectId}'`
|
||||
});
|
||||
|
||||
const { permission, membership } = await permissionService.getProjectPermission({
|
||||
const { permission } = await permissionService.getProjectPermission({
|
||||
actor,
|
||||
actorId,
|
||||
projectId: projectMembership.projectId,
|
||||
@ -173,7 +154,7 @@ export const projectUserAdditionalPrivilegeServiceFactory = ({
|
||||
actorOrgId,
|
||||
actionProjectType: ActionProjectType.Any
|
||||
});
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionMemberActions.Edit, ProjectPermissionSub.Member);
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Member);
|
||||
const { permission: targetUserPermission } = await permissionService.getProjectPermission({
|
||||
actor: ActorType.USER,
|
||||
actorId: projectMembership.userId,
|
||||
@ -186,21 +167,11 @@ export const projectUserAdditionalPrivilegeServiceFactory = ({
|
||||
// we need to validate that the privilege given is not higher than the assigning users permission
|
||||
// @ts-expect-error this is expected error because of one being really accurate rule definition other being a bit more broader. Both are valid casl rules
|
||||
targetUserPermission.update(targetUserPermission.rules.concat(dto.permissions || []));
|
||||
const permissionBoundary = validatePrivilegeChangeOperation(
|
||||
membership.shouldUseNewPrivilegeSystem,
|
||||
ProjectPermissionMemberActions.GrantPrivileges,
|
||||
ProjectPermissionSub.Member,
|
||||
permission,
|
||||
targetUserPermission
|
||||
);
|
||||
const permissionBoundary = validatePermissionBoundary(permission, targetUserPermission);
|
||||
if (!permissionBoundary.isValid)
|
||||
throw new PermissionBoundaryError({
|
||||
message: constructPermissionErrorMessage(
|
||||
"Failed to update more privileged user",
|
||||
membership.shouldUseNewPrivilegeSystem,
|
||||
ProjectPermissionMemberActions.GrantPrivileges,
|
||||
ProjectPermissionSub.Member
|
||||
),
|
||||
throw new ForbiddenRequestError({
|
||||
name: "PermissionBoundaryError",
|
||||
message: "Failed to update more privileged identity",
|
||||
details: { missingPermissions: permissionBoundary.missingPermissions }
|
||||
});
|
||||
|
||||
@ -214,10 +185,6 @@ export const projectUserAdditionalPrivilegeServiceFactory = ({
|
||||
throw new BadRequestError({ message: `Additional privilege with provided slug ${dto.slug} already exists` });
|
||||
}
|
||||
|
||||
validateHandlebarTemplate("User Additional Privilege Update", JSON.stringify(dto.permissions || []), {
|
||||
allowedExpressions: (val) => val.includes("identity.")
|
||||
});
|
||||
|
||||
const isTemporary = typeof dto?.isTemporary !== "undefined" ? dto.isTemporary : userPrivilege.isTemporary;
|
||||
|
||||
const packedPermission = dto.permissions && JSON.stringify(packRules(dto.permissions));
|
||||
@ -277,7 +244,7 @@ export const projectUserAdditionalPrivilegeServiceFactory = ({
|
||||
actorOrgId,
|
||||
actionProjectType: ActionProjectType.Any
|
||||
});
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionMemberActions.Edit, ProjectPermissionSub.Member);
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Member);
|
||||
|
||||
const deletedPrivilege = await projectUserAdditionalPrivilegeDAL.deleteById(userPrivilege.id);
|
||||
return {
|
||||
@ -314,7 +281,7 @@ export const projectUserAdditionalPrivilegeServiceFactory = ({
|
||||
actorOrgId,
|
||||
actionProjectType: ActionProjectType.Any
|
||||
});
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionMemberActions.Read, ProjectPermissionSub.Member);
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Member);
|
||||
|
||||
return {
|
||||
...userPrivilege,
|
||||
@ -341,7 +308,7 @@ export const projectUserAdditionalPrivilegeServiceFactory = ({
|
||||
actorOrgId,
|
||||
actionProjectType: ActionProjectType.Any
|
||||
});
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionMemberActions.Read, ProjectPermissionSub.Member);
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Member);
|
||||
|
||||
const userPrivileges = await projectUserAdditionalPrivilegeDAL.find(
|
||||
{
|
||||
|
@ -29,9 +29,15 @@ export const parseScimFilter = (filterToParse: string | undefined) => {
|
||||
attributeName = "name";
|
||||
}
|
||||
|
||||
return { [attributeName]: parsedValue.replaceAll('"', "") };
|
||||
return { [attributeName]: parsedValue.replace(/"/g, "") };
|
||||
};
|
||||
|
||||
export function extractScimValueFromPath(path: string): string | null {
|
||||
const regex = /members\[value eq "([^"]+)"\]/;
|
||||
const match = path.match(regex);
|
||||
return match ? match[1] : null;
|
||||
}
|
||||
|
||||
export const buildScimUser = ({
|
||||
orgMembershipId,
|
||||
username,
|
||||
|
@ -62,8 +62,7 @@ export const secretApprovalPolicyServiceFactory = ({
|
||||
projectId,
|
||||
secretPath,
|
||||
environment,
|
||||
enforcementLevel,
|
||||
allowedSelfApprovals
|
||||
enforcementLevel
|
||||
}: TCreateSapDTO) => {
|
||||
const groupApprovers = approvers
|
||||
?.filter((approver) => approver.type === ApproverType.Group)
|
||||
@ -114,8 +113,7 @@ export const secretApprovalPolicyServiceFactory = ({
|
||||
approvals,
|
||||
secretPath,
|
||||
name,
|
||||
enforcementLevel,
|
||||
allowedSelfApprovals
|
||||
enforcementLevel
|
||||
},
|
||||
tx
|
||||
);
|
||||
@ -174,8 +172,7 @@ export const secretApprovalPolicyServiceFactory = ({
|
||||
actorAuthMethod,
|
||||
approvals,
|
||||
secretPolicyId,
|
||||
enforcementLevel,
|
||||
allowedSelfApprovals
|
||||
enforcementLevel
|
||||
}: TUpdateSapDTO) => {
|
||||
const groupApprovers = approvers
|
||||
?.filter((approver) => approver.type === ApproverType.Group)
|
||||
@ -221,8 +218,7 @@ export const secretApprovalPolicyServiceFactory = ({
|
||||
approvals,
|
||||
secretPath,
|
||||
name,
|
||||
enforcementLevel,
|
||||
allowedSelfApprovals
|
||||
enforcementLevel
|
||||
},
|
||||
tx
|
||||
);
|
||||
|
@ -10,7 +10,6 @@ export type TCreateSapDTO = {
|
||||
projectId: string;
|
||||
name: string;
|
||||
enforcementLevel: EnforcementLevel;
|
||||
allowedSelfApprovals: boolean;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
export type TUpdateSapDTO = {
|
||||
@ -20,7 +19,6 @@ export type TUpdateSapDTO = {
|
||||
approvers: ({ type: ApproverType.Group; id: string } | { type: ApproverType.User; id?: string; name?: string })[];
|
||||
name?: string;
|
||||
enforcementLevel?: EnforcementLevel;
|
||||
allowedSelfApprovals?: boolean;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
export type TDeleteSapDTO = {
|
||||
|
@ -112,7 +112,6 @@ 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")
|
||||
);
|
||||
@ -151,8 +150,7 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => {
|
||||
secretPath: el.policySecretPath,
|
||||
enforcementLevel: el.policyEnforcementLevel,
|
||||
envId: el.policyEnvId,
|
||||
deletedAt: el.policyDeletedAt,
|
||||
allowedSelfApprovals: el.policyAllowedSelfApprovals
|
||||
deletedAt: el.policyDeletedAt
|
||||
}
|
||||
}),
|
||||
childrenMapper: [
|
||||
@ -338,7 +336,6 @@ 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"),
|
||||
@ -367,8 +364,7 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => {
|
||||
name: el.policyName,
|
||||
approvals: el.policyApprovals,
|
||||
secretPath: el.policySecretPath,
|
||||
enforcementLevel: el.policyEnforcementLevel,
|
||||
allowedSelfApprovals: el.policyAllowedSelfApprovals
|
||||
enforcementLevel: el.policyEnforcementLevel
|
||||
},
|
||||
committerUser: {
|
||||
userId: el.committerUserId,
|
||||
@ -486,7 +482,6 @@ 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),
|
||||
@ -516,8 +511,7 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => {
|
||||
name: el.policyName,
|
||||
approvals: el.policyApprovals,
|
||||
secretPath: el.policySecretPath,
|
||||
enforcementLevel: el.policyEnforcementLevel,
|
||||
allowedSelfApprovals: el.policyAllowedSelfApprovals
|
||||
enforcementLevel: el.policyEnforcementLevel
|
||||
},
|
||||
committerUser: {
|
||||
userId: el.committerUserId,
|
||||
|
@ -352,11 +352,6 @@ 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,
|
||||
|
@ -0,0 +1,4 @@
|
||||
export * from "./postgres-credentials-rotation-constants";
|
||||
export * from "./postgres-credentials-rotation-fns";
|
||||
export * from "./postgres-credentials-rotation-schemas";
|
||||
export * from "./postgres-credentials-rotation-types";
|
@ -0,0 +1,9 @@
|
||||
import { SecretRotation } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-enums";
|
||||
import { TSecretRotationV2ListItem } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-types";
|
||||
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
||||
|
||||
export const POSTGRES_CREDENTIALS_ROTATION_LIST_OPTION: TSecretRotationV2ListItem = {
|
||||
name: "PostgreSQL Credentials",
|
||||
type: SecretRotation.PostgresCredentials,
|
||||
connection: AppConnection.Postgres
|
||||
};
|
@ -0,0 +1,66 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { SecretRotation } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-enums";
|
||||
import {
|
||||
BaseCreateSecretRotationSchema,
|
||||
BaseSecretRotationSchema,
|
||||
BaseUpdateSecretRotationSchema
|
||||
} from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-schemas";
|
||||
import { SecretRotations } from "@app/lib/api-docs";
|
||||
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
||||
|
||||
const PostgresCredentialsRotationParametersSchema = z.object({
|
||||
usernameSecretKey: z
|
||||
.string()
|
||||
.trim()
|
||||
.min(1, "Username Secret Key Required")
|
||||
.describe(SecretRotations.PARAMETERS.POSTGRES_CREDENTIALS.usernameSecretKey),
|
||||
passwordSecretKey: z
|
||||
.string()
|
||||
.trim()
|
||||
.min(1, "Username Secret Key Required")
|
||||
.describe(SecretRotations.PARAMETERS.POSTGRES_CREDENTIALS.passwordSecretKey),
|
||||
issueStatement: z
|
||||
.string()
|
||||
.trim()
|
||||
.min(1, "Issue Credentials SQL Statement Required")
|
||||
.describe(SecretRotations.PARAMETERS.POSTGRES_CREDENTIALS.issueStatement),
|
||||
revokeStatement: z
|
||||
.string()
|
||||
.trim()
|
||||
.min(1, "Revoke Credentials SQL Statement Required")
|
||||
.describe(SecretRotations.PARAMETERS.POSTGRES_CREDENTIALS.revokeStatement)
|
||||
});
|
||||
|
||||
const PostgresCredentialsRotationGeneratedCredentialsSchema = z
|
||||
.object({
|
||||
username: z.string(),
|
||||
password: z.string()
|
||||
})
|
||||
.array()
|
||||
.min(1)
|
||||
.max(2);
|
||||
|
||||
export const PostgresCredentialsRotationSchema = BaseSecretRotationSchema(SecretRotation.PostgresCredentials).extend({
|
||||
type: z.literal(SecretRotation.PostgresCredentials),
|
||||
parameters: PostgresCredentialsRotationParametersSchema
|
||||
// generatedCredentials: PostgresCredentialsRotationGeneratedCredentialsSchema
|
||||
});
|
||||
|
||||
export const CreatePostgresCredentialsRotationSchema = BaseCreateSecretRotationSchema(
|
||||
SecretRotation.PostgresCredentials
|
||||
).extend({
|
||||
parameters: PostgresCredentialsRotationParametersSchema
|
||||
});
|
||||
|
||||
export const UpdatePostgresCredentialsRotationSchema = BaseUpdateSecretRotationSchema(
|
||||
SecretRotation.PostgresCredentials
|
||||
).extend({
|
||||
parameters: PostgresCredentialsRotationParametersSchema.optional()
|
||||
});
|
||||
|
||||
export const PostgresCredentialsRotationListItemSchema = z.object({
|
||||
name: z.literal("PostgreSQL Credentials"),
|
||||
connection: z.literal(AppConnection.Postgres),
|
||||
type: z.literal(SecretRotation.PostgresCredentials)
|
||||
});
|
@ -0,0 +1,19 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { TPostgresConnection } from "@app/services/app-connection/postgres";
|
||||
|
||||
import {
|
||||
CreatePostgresCredentialsRotationSchema,
|
||||
PostgresCredentialsRotationListItemSchema,
|
||||
PostgresCredentialsRotationSchema
|
||||
} from "./postgres-credentials-rotation-schemas";
|
||||
|
||||
export type TPostgresCredentialsRotation = z.infer<typeof PostgresCredentialsRotationSchema>;
|
||||
|
||||
export type TPostgresCredentialsRotationInput = z.infer<typeof CreatePostgresCredentialsRotationSchema>;
|
||||
|
||||
export type TPostgresCredentialsRotationListItem = z.infer<typeof PostgresCredentialsRotationListItemSchema>;
|
||||
|
||||
export type TPostgresCredentialsRotationWithConnection = TPostgresCredentialsRotation & {
|
||||
connection: TPostgresConnection;
|
||||
};
|
@ -0,0 +1,206 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TDbClient } from "@app/db";
|
||||
import { TableName } from "@app/db/schemas";
|
||||
import { TSecretRotationsV2 } from "@app/db/schemas/secret-rotations-v2";
|
||||
import { DatabaseError } from "@app/lib/errors";
|
||||
import { buildFindFilter, ormify, prependTableNameToFindFilter, selectAllTableCols } from "@app/lib/knex";
|
||||
import { TSecretFolderDALFactory } from "@app/services/secret-folder/secret-folder-dal";
|
||||
|
||||
export type TSecretRotationV2DALFactory = ReturnType<typeof secretRotationV2DALFactory>;
|
||||
|
||||
type TSecretRotationFindFilter = Parameters<typeof buildFindFilter<TSecretRotationsV2>>[0];
|
||||
|
||||
const baseSecretRotationV2Query = ({
|
||||
filter,
|
||||
db,
|
||||
tx
|
||||
}: {
|
||||
db: TDbClient;
|
||||
filter?: TSecretRotationFindFilter;
|
||||
tx?: Knex;
|
||||
}) => {
|
||||
const query = (tx || db.replicaNode())(TableName.SecretRotationV2)
|
||||
.leftJoin(TableName.SecretFolder, `${TableName.SecretRotationV2}.folderId`, `${TableName.SecretFolder}.id`)
|
||||
.leftJoin(TableName.Environment, `${TableName.SecretFolder}.envId`, `${TableName.Environment}.id`)
|
||||
.join(TableName.AppConnection, `${TableName.SecretRotationV2}.connectionId`, `${TableName.AppConnection}.id`)
|
||||
.select(selectAllTableCols(TableName.SecretRotationV2))
|
||||
.select(
|
||||
// environment
|
||||
db.ref("name").withSchema(TableName.Environment).as("envName"),
|
||||
db.ref("id").withSchema(TableName.Environment).as("envId"),
|
||||
db.ref("slug").withSchema(TableName.Environment).as("envSlug"),
|
||||
// entire connection
|
||||
db.ref("name").withSchema(TableName.AppConnection).as("connectionName"),
|
||||
db.ref("method").withSchema(TableName.AppConnection).as("connectionMethod"),
|
||||
db.ref("app").withSchema(TableName.AppConnection).as("connectionApp"),
|
||||
db.ref("orgId").withSchema(TableName.AppConnection).as("connectionOrgId"),
|
||||
db.ref("encryptedCredentials").withSchema(TableName.AppConnection).as("connectionEncryptedCredentials"),
|
||||
db.ref("description").withSchema(TableName.AppConnection).as("connectionDescription"),
|
||||
db.ref("version").withSchema(TableName.AppConnection).as("connectionVersion"),
|
||||
db.ref("createdAt").withSchema(TableName.AppConnection).as("connectionCreatedAt"),
|
||||
db.ref("updatedAt").withSchema(TableName.AppConnection).as("connectionUpdatedAt"),
|
||||
db.ref("isPlatformManaged").withSchema(TableName.AppConnection).as("connectionIsPlatformManaged")
|
||||
);
|
||||
|
||||
if (filter) {
|
||||
/* eslint-disable @typescript-eslint/no-misused-promises */
|
||||
void query.where(buildFindFilter(prependTableNameToFindFilter(TableName.SecretRotationV2, filter)));
|
||||
}
|
||||
|
||||
return query;
|
||||
};
|
||||
|
||||
const expandSecretRotation = (
|
||||
secretRotation: Awaited<ReturnType<typeof baseSecretRotationV2Query>>[number],
|
||||
folder?: Awaited<ReturnType<TSecretFolderDALFactory["findSecretPathByFolderIds"]>>[number]
|
||||
) => {
|
||||
const {
|
||||
envId,
|
||||
envName,
|
||||
envSlug,
|
||||
connectionApp,
|
||||
connectionName,
|
||||
connectionId,
|
||||
connectionOrgId,
|
||||
connectionEncryptedCredentials,
|
||||
connectionMethod,
|
||||
connectionDescription,
|
||||
connectionCreatedAt,
|
||||
connectionUpdatedAt,
|
||||
connectionVersion,
|
||||
connectionIsPlatformManaged,
|
||||
...el
|
||||
} = secretRotation;
|
||||
|
||||
return {
|
||||
...el,
|
||||
connectionId,
|
||||
environment: envId ? { id: envId, name: envName, slug: envSlug } : null,
|
||||
connection: {
|
||||
app: connectionApp,
|
||||
id: connectionId,
|
||||
name: connectionName,
|
||||
orgId: connectionOrgId,
|
||||
encryptedCredentials: connectionEncryptedCredentials,
|
||||
method: connectionMethod,
|
||||
description: connectionDescription,
|
||||
createdAt: connectionCreatedAt,
|
||||
updatedAt: connectionUpdatedAt,
|
||||
version: connectionVersion,
|
||||
isPlatformManaged: connectionIsPlatformManaged
|
||||
},
|
||||
folder: folder
|
||||
? {
|
||||
id: folder.id,
|
||||
path: folder.path
|
||||
}
|
||||
: null
|
||||
};
|
||||
};
|
||||
|
||||
export const secretRotationV2DALFactory = (
|
||||
db: TDbClient,
|
||||
folderDAL: Pick<TSecretFolderDALFactory, "findSecretPathByFolderIds">
|
||||
) => {
|
||||
const secretRotationV2Orm = ormify(db, TableName.SecretRotationV2);
|
||||
|
||||
const find = async (
|
||||
filter: Parameters<(typeof secretRotationV2Orm)["find"]>[0] & { projectId: string },
|
||||
tx?: Knex
|
||||
) => {
|
||||
try {
|
||||
const secretRotations = await baseSecretRotationV2Query({ filter, db, tx });
|
||||
|
||||
if (!secretRotations.length) return [];
|
||||
|
||||
const foldersWithPath = await folderDAL.findSecretPathByFolderIds(
|
||||
filter.projectId,
|
||||
secretRotations.filter((rotation) => Boolean(rotation.folderId)).map((rotation) => rotation.folderId!)
|
||||
);
|
||||
|
||||
const folderRecord: Record<string, (typeof foldersWithPath)[number]> = {};
|
||||
|
||||
foldersWithPath.forEach((folder) => {
|
||||
if (folder) folderRecord[folder.id] = folder;
|
||||
});
|
||||
|
||||
return secretRotations.map((rotation) =>
|
||||
expandSecretRotation(rotation, rotation.folderId ? folderRecord[rotation.folderId] : undefined)
|
||||
);
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: "Find - Secret Rotation V2" });
|
||||
}
|
||||
};
|
||||
|
||||
const findById = async (id: string, tx?: Knex) => {
|
||||
try {
|
||||
const secretRotation = await baseSecretRotationV2Query({
|
||||
filter: { id },
|
||||
db,
|
||||
tx
|
||||
}).first();
|
||||
|
||||
if (secretRotation) {
|
||||
const [folderWithPath] = secretRotation.folderId
|
||||
? await folderDAL.findSecretPathByFolderIds(secretRotation.projectId, [secretRotation.folderId])
|
||||
: [];
|
||||
return expandSecretRotation(secretRotation, folderWithPath);
|
||||
}
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: "Find by ID - Secret Rotation V2" });
|
||||
}
|
||||
};
|
||||
|
||||
const create = async (data: Parameters<(typeof secretRotationV2Orm)["create"]>[0]) => {
|
||||
const secretRotation = (await secretRotationV2Orm.transaction(async (tx) => {
|
||||
const rotation = await secretRotationV2Orm.create(data, tx);
|
||||
|
||||
return baseSecretRotationV2Query({
|
||||
filter: { id: rotation.id },
|
||||
db,
|
||||
tx
|
||||
}).first();
|
||||
}))!;
|
||||
|
||||
const [folderWithPath] = secretRotation.folderId
|
||||
? await folderDAL.findSecretPathByFolderIds(secretRotation.projectId, [secretRotation.folderId])
|
||||
: [];
|
||||
|
||||
return expandSecretRotation(secretRotation, folderWithPath);
|
||||
};
|
||||
|
||||
const updateById = async (rotationId: string, data: Parameters<(typeof secretRotationV2Orm)["updateById"]>[1]) => {
|
||||
const secretRotation = (await secretRotationV2Orm.transaction(async (tx) => {
|
||||
const rotation = await secretRotationV2Orm.updateById(rotationId, data, tx);
|
||||
|
||||
return baseSecretRotationV2Query({
|
||||
filter: { id: rotation.id },
|
||||
db,
|
||||
tx
|
||||
}).first();
|
||||
}))!;
|
||||
|
||||
const [folderWithPath] = secretRotation.folderId
|
||||
? await folderDAL.findSecretPathByFolderIds(secretRotation.projectId, [secretRotation.folderId])
|
||||
: [];
|
||||
return expandSecretRotation(secretRotation, folderWithPath);
|
||||
};
|
||||
|
||||
const findOne = async (filter: Parameters<(typeof secretRotationV2Orm)["findOne"]>[0], tx?: Knex) => {
|
||||
try {
|
||||
const secretRotation = await baseSecretRotationV2Query({ filter, db, tx }).first();
|
||||
|
||||
if (secretRotation) {
|
||||
const [folderWithPath] = secretRotation.folderId
|
||||
? await folderDAL.findSecretPathByFolderIds(secretRotation.projectId, [secretRotation.folderId])
|
||||
: [];
|
||||
return expandSecretRotation(secretRotation, folderWithPath);
|
||||
}
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: "Find One - Secret Rotation V2" });
|
||||
}
|
||||
};
|
||||
|
||||
return { ...secretRotationV2Orm, find, create, findById, updateById, findOne };
|
||||
};
|
@ -0,0 +1,4 @@
|
||||
export enum SecretRotation {
|
||||
PostgresCredentials = "postgres-credentials",
|
||||
MsSqlCredentials = "mssql-login-credentials"
|
||||
}
|
@ -0,0 +1,237 @@
|
||||
// import { AxiosError } from "axios";
|
||||
//
|
||||
// import {
|
||||
// AWS_PARAMETER_STORE_SYNC_LIST_OPTION,
|
||||
// AwsParameterStoreSyncFns
|
||||
// } from "@app/services/secret-sync/aws-parameter-store";
|
||||
// import {
|
||||
// AWS_SECRETS_MANAGER_SYNC_LIST_OPTION,
|
||||
// AwsSecretsManagerSyncFns
|
||||
// } from "@app/services/secret-sync/aws-secrets-manager";
|
||||
// import { DATABRICKS_SYNC_LIST_OPTION, databricksSyncFactory } from "@app/services/secret-sync/databricks";
|
||||
// import { GITHUB_SYNC_LIST_OPTION, GithubSyncFns } from "@app/services/secret-sync/github";
|
||||
// import { SecretSync } from "@app/services/secret-sync/secret-sync-enums";
|
||||
// import { SecretSyncError } from "@app/services/secret-sync/secret-sync-errors";
|
||||
// import {
|
||||
// TSecretMap,
|
||||
// TSecretSyncListItem,
|
||||
// TSecretSyncWithCredentials
|
||||
// } from "@app/services/secret-sync/secret-sync-types";
|
||||
//
|
||||
// import { TAppConnectionDALFactory } from "../app-connection/app-connection-dal";
|
||||
// import { TKmsServiceFactory } from "../kms/kms-service";
|
||||
// import { AZURE_APP_CONFIGURATION_SYNC_LIST_OPTION, azureAppConfigurationSyncFactory } from "./azure-app-configuration";
|
||||
// import { AZURE_KEY_VAULT_SYNC_LIST_OPTION, azureKeyVaultSyncFactory } from "./azure-key-vault";
|
||||
// import { GCP_SYNC_LIST_OPTION } from "./gcp";
|
||||
// import { GcpSyncFns } from "./gcp/gcp-sync-fns";
|
||||
// import { HUMANITEC_SYNC_LIST_OPTION } from "./humanitec";
|
||||
// import { HumanitecSyncFns } from "./humanitec/humanitec-sync-fns";
|
||||
//
|
||||
import { POSTGRES_CREDENTIALS_ROTATION_LIST_OPTION } from "./postgres-credentials";
|
||||
import { SecretRotation } from "./secret-rotation-v2-enums";
|
||||
import { TSecretRotationV2ListItem } from "./secret-rotation-v2-types";
|
||||
|
||||
const SECRET_ROTATION_LIST_OPTIONS: Record<SecretRotation, TSecretRotationV2ListItem> = {
|
||||
[SecretRotation.PostgresCredentials]: POSTGRES_CREDENTIALS_ROTATION_LIST_OPTION,
|
||||
[SecretRotation.MsSqlCredentials]: POSTGRES_CREDENTIALS_ROTATION_LIST_OPTION // TODO: replace
|
||||
};
|
||||
|
||||
export const listSecretRotationOptions = () => {
|
||||
return Object.values(SECRET_ROTATION_LIST_OPTIONS).sort((a, b) => a.name.localeCompare(b.name));
|
||||
};
|
||||
//
|
||||
// type TSyncSecretDeps = {
|
||||
// appConnectionDAL: Pick<TAppConnectionDALFactory, "findById" | "update" | "updateById">;
|
||||
// kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
|
||||
// };
|
||||
//
|
||||
// // const addAffixes = (secretSync: TSecretSyncWithCredentials, unprocessedSecretMap: TSecretMap) => {
|
||||
// // let secretMap = { ...unprocessedSecretMap };
|
||||
// //
|
||||
// // const { appendSuffix, prependPrefix } = secretSync.syncOptions;
|
||||
// //
|
||||
// // if (appendSuffix || prependPrefix) {
|
||||
// // secretMap = {};
|
||||
// // Object.entries(unprocessedSecretMap).forEach(([key, value]) => {
|
||||
// // secretMap[`${prependPrefix || ""}${key}${appendSuffix || ""}`] = value;
|
||||
// // });
|
||||
// // }
|
||||
// //
|
||||
// // return secretMap;
|
||||
// // };
|
||||
// //
|
||||
// // const stripAffixes = (secretSync: TSecretSyncWithCredentials, unprocessedSecretMap: TSecretMap) => {
|
||||
// // let secretMap = { ...unprocessedSecretMap };
|
||||
// //
|
||||
// // const { appendSuffix, prependPrefix } = secretSync.syncOptions;
|
||||
// //
|
||||
// // if (appendSuffix || prependPrefix) {
|
||||
// // secretMap = {};
|
||||
// // Object.entries(unprocessedSecretMap).forEach(([key, value]) => {
|
||||
// // let processedKey = key;
|
||||
// //
|
||||
// // if (prependPrefix && processedKey.startsWith(prependPrefix)) {
|
||||
// // processedKey = processedKey.slice(prependPrefix.length);
|
||||
// // }
|
||||
// //
|
||||
// // if (appendSuffix && processedKey.endsWith(appendSuffix)) {
|
||||
// // processedKey = processedKey.slice(0, -appendSuffix.length);
|
||||
// // }
|
||||
// //
|
||||
// // secretMap[processedKey] = value;
|
||||
// // });
|
||||
// // }
|
||||
// //
|
||||
// // return secretMap;
|
||||
// // };
|
||||
//
|
||||
// export const SecretRotationV2Fns = {
|
||||
// syncSecrets: (
|
||||
// secretSync: TSecretSyncWithCredentials,
|
||||
// secretMap: TSecretMap,
|
||||
// { kmsService, appConnectionDAL }: TSyncSecretDeps
|
||||
// ): Promise<void> => {
|
||||
// // const affixedSecretMap = addAffixes(secretSync, secretMap);
|
||||
//
|
||||
// switch (secretSync.destination) {
|
||||
// case SecretSync.AWSParameterStore:
|
||||
// return AwsParameterStoreSyncFns.syncSecrets(secretSync, secretMap);
|
||||
// case SecretSync.AWSSecretsManager:
|
||||
// return AwsSecretsManagerSyncFns.syncSecrets(secretSync, secretMap);
|
||||
// case SecretSync.GitHub:
|
||||
// return GithubSyncFns.syncSecrets(secretSync, secretMap);
|
||||
// case SecretSync.GCPSecretManager:
|
||||
// return GcpSyncFns.syncSecrets(secretSync, secretMap);
|
||||
// case SecretSync.AzureKeyVault:
|
||||
// return azureKeyVaultSyncFactory({
|
||||
// appConnectionDAL,
|
||||
// kmsService
|
||||
// }).syncSecrets(secretSync, secretMap);
|
||||
// case SecretSync.AzureAppConfiguration:
|
||||
// return azureAppConfigurationSyncFactory({
|
||||
// appConnectionDAL,
|
||||
// kmsService
|
||||
// }).syncSecrets(secretSync, secretMap);
|
||||
// case SecretSync.Databricks:
|
||||
// return databricksSyncFactory({
|
||||
// appConnectionDAL,
|
||||
// kmsService
|
||||
// }).syncSecrets(secretSync, secretMap);
|
||||
// case SecretSync.Humanitec:
|
||||
// return HumanitecSyncFns.syncSecrets(secretSync, secretMap);
|
||||
// default:
|
||||
// throw new Error(
|
||||
// `Unhandled sync destination for sync secrets fns: ${(secretSync as TSecretSyncWithCredentials).destination}`
|
||||
// );
|
||||
// }
|
||||
// },
|
||||
// getSecrets: async (
|
||||
// secretSync: TSecretSyncWithCredentials,
|
||||
// { kmsService, appConnectionDAL }: TSyncSecretDeps
|
||||
// ): Promise<TSecretMap> => {
|
||||
// let secretMap: TSecretMap;
|
||||
// switch (secretSync.destination) {
|
||||
// case SecretSync.AWSParameterStore:
|
||||
// secretMap = await AwsParameterStoreSyncFns.getSecrets(secretSync);
|
||||
// break;
|
||||
// case SecretSync.AWSSecretsManager:
|
||||
// secretMap = await AwsSecretsManagerSyncFns.getSecrets(secretSync);
|
||||
// break;
|
||||
// case SecretSync.GitHub:
|
||||
// secretMap = await GithubSyncFns.getSecrets(secretSync);
|
||||
// break;
|
||||
// case SecretSync.GCPSecretManager:
|
||||
// secretMap = await GcpSyncFns.getSecrets(secretSync);
|
||||
// break;
|
||||
// case SecretSync.AzureKeyVault:
|
||||
// secretMap = await azureKeyVaultSyncFactory({
|
||||
// appConnectionDAL,
|
||||
// kmsService
|
||||
// }).getSecrets(secretSync);
|
||||
// break;
|
||||
// case SecretSync.AzureAppConfiguration:
|
||||
// secretMap = await azureAppConfigurationSyncFactory({
|
||||
// appConnectionDAL,
|
||||
// kmsService
|
||||
// }).getSecrets(secretSync);
|
||||
// break;
|
||||
// case SecretSync.Databricks:
|
||||
// return databricksSyncFactory({
|
||||
// appConnectionDAL,
|
||||
// kmsService
|
||||
// }).getSecrets(secretSync);
|
||||
// case SecretSync.Humanitec:
|
||||
// secretMap = await HumanitecSyncFns.getSecrets(secretSync);
|
||||
// break;
|
||||
// default:
|
||||
// throw new Error(
|
||||
// `Unhandled sync destination for get secrets fns: ${(secretSync as TSecretSyncWithCredentials).destination}`
|
||||
// );
|
||||
// }
|
||||
//
|
||||
// return secretMap;
|
||||
// // return stripAffixes(secretSync, secretMap);
|
||||
// },
|
||||
// removeSecrets: (
|
||||
// secretSync: TSecretSyncWithCredentials,
|
||||
// secretMap: TSecretMap,
|
||||
// { kmsService, appConnectionDAL }: TSyncSecretDeps
|
||||
// ): Promise<void> => {
|
||||
// // const affixedSecretMap = addAffixes(secretSync, secretMap);
|
||||
//
|
||||
// switch (secretSync.destination) {
|
||||
// case SecretSync.AWSParameterStore:
|
||||
// return AwsParameterStoreSyncFns.removeSecrets(secretSync, secretMap);
|
||||
// case SecretSync.AWSSecretsManager:
|
||||
// return AwsSecretsManagerSyncFns.removeSecrets(secretSync, secretMap);
|
||||
// case SecretSync.GitHub:
|
||||
// return GithubSyncFns.removeSecrets(secretSync, secretMap);
|
||||
// case SecretSync.GCPSecretManager:
|
||||
// return GcpSyncFns.removeSecrets(secretSync, secretMap);
|
||||
// case SecretSync.AzureKeyVault:
|
||||
// return azureKeyVaultSyncFactory({
|
||||
// appConnectionDAL,
|
||||
// kmsService
|
||||
// }).removeSecrets(secretSync, secretMap);
|
||||
// case SecretSync.AzureAppConfiguration:
|
||||
// return azureAppConfigurationSyncFactory({
|
||||
// appConnectionDAL,
|
||||
// kmsService
|
||||
// }).removeSecrets(secretSync, secretMap);
|
||||
// case SecretSync.Databricks:
|
||||
// return databricksSyncFactory({
|
||||
// appConnectionDAL,
|
||||
// kmsService
|
||||
// }).removeSecrets(secretSync, secretMap);
|
||||
// case SecretSync.Humanitec:
|
||||
// return HumanitecSyncFns.removeSecrets(secretSync, secretMap);
|
||||
// default:
|
||||
// throw new Error(
|
||||
// `Unhandled sync destination for remove secrets fns: ${(secretSync as TSecretSyncWithCredentials).destination}`
|
||||
// );
|
||||
// }
|
||||
// }
|
||||
// };
|
||||
//
|
||||
// const MAX_MESSAGE_LENGTH = 1024;
|
||||
//
|
||||
// export const parseSyncErrorMessage = (err: unknown): string => {
|
||||
// let errorMessage: string;
|
||||
//
|
||||
// if (err instanceof SecretSyncError) {
|
||||
// errorMessage = JSON.stringify({
|
||||
// secretKey: err.secretKey,
|
||||
// error: err.message || parseSyncErrorMessage(err.error)
|
||||
// });
|
||||
// } else if (err instanceof AxiosError) {
|
||||
// errorMessage = err?.response?.data
|
||||
// ? JSON.stringify(err?.response?.data)
|
||||
// : err?.message ?? "An unknown error occurred.";
|
||||
// } else {
|
||||
// errorMessage = (err as Error)?.message || "An unknown error occurred.";
|
||||
// }
|
||||
//
|
||||
// return errorMessage.length <= MAX_MESSAGE_LENGTH
|
||||
// ? errorMessage
|
||||
// : `${errorMessage.substring(0, MAX_MESSAGE_LENGTH - 3)}...`;
|
||||
// };
|
@ -0,0 +1,12 @@
|
||||
import { SecretRotation } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-enums";
|
||||
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
||||
|
||||
export const SECRET_ROTATION_NAME_MAP: Record<SecretRotation, string> = {
|
||||
[SecretRotation.PostgresCredentials]: "PostgreSQL Credentials",
|
||||
[SecretRotation.MsSqlCredentials]: "Microsoft SQL Sever Credentials"
|
||||
};
|
||||
|
||||
export const SECRET_ROTATION_CONNECTION_MAP: Record<SecretRotation, AppConnection> = {
|
||||
[SecretRotation.PostgresCredentials]: AppConnection.Postgres,
|
||||
[SecretRotation.MsSqlCredentials]: AppConnection.MsSql
|
||||
};
|
@ -0,0 +1,972 @@
|
||||
// import opentelemetry from "@opentelemetry/api";
|
||||
// import { AxiosError } from "axios";
|
||||
// import { Job } from "bullmq";
|
||||
//
|
||||
// import { ProjectMembershipRole, SecretType } from "@app/db/schemas";
|
||||
// import { TAuditLogServiceFactory } from "@app/ee/services/audit-log/audit-log-service";
|
||||
// import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
||||
// import { KeyStorePrefixes, TKeyStoreFactory } from "@app/keystore/keystore";
|
||||
// import { getConfig } from "@app/lib/config/env";
|
||||
// import { logger } from "@app/lib/logger";
|
||||
// import { QueueJobs, QueueName, TQueueServiceFactory } from "@app/queue";
|
||||
// import { decryptAppConnectionCredentials } from "@app/services/app-connection/app-connection-fns";
|
||||
// import { ActorType } from "@app/services/auth/auth-type";
|
||||
// import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
||||
// import { KmsDataKey } from "@app/services/kms/kms-types";
|
||||
// import { TProjectDALFactory } from "@app/services/project/project-dal";
|
||||
// import { TProjectBotDALFactory } from "@app/services/project-bot/project-bot-dal";
|
||||
// import { TProjectMembershipDALFactory } from "@app/services/project-membership/project-membership-dal";
|
||||
// import { TResourceMetadataDALFactory } from "@app/services/resource-metadata/resource-metadata-dal";
|
||||
// import { TSecretDALFactory } from "@app/services/secret/secret-dal";
|
||||
// import { createManySecretsRawFnFactory, updateManySecretsRawFnFactory } from "@app/services/secret/secret-fns";
|
||||
// import { TSecretVersionDALFactory } from "@app/services/secret/secret-version-dal";
|
||||
// import { TSecretVersionTagDALFactory } from "@app/services/secret/secret-version-tag-dal";
|
||||
// import { TSecretBlindIndexDALFactory } from "@app/services/secret-blind-index/secret-blind-index-dal";
|
||||
// import { TSecretFolderDALFactory } from "@app/services/secret-folder/secret-folder-dal";
|
||||
// import { TSecretImportDALFactory } from "@app/services/secret-import/secret-import-dal";
|
||||
// import { fnSecretsV2FromImports } from "@app/services/secret-import/secret-import-fns";
|
||||
// import { TSecretSyncDALFactory } from "@app/services/secret-sync/secret-sync-dal";
|
||||
// import {
|
||||
// SecretSync,
|
||||
// SecretSyncImportBehavior,
|
||||
// SecretSyncInitialSyncBehavior
|
||||
// } from "@app/services/secret-sync/secret-sync-enums";
|
||||
// import { SecretSyncError } from "@app/services/secret-sync/secret-sync-errors";
|
||||
// import { parseSyncErrorMessage, SecretSyncFns } from "@app/services/secret-sync/secret-sync-fns";
|
||||
// import { SECRET_SYNC_NAME_MAP } from "@app/services/secret-sync/secret-sync-maps";
|
||||
// import {
|
||||
// SecretSyncAction,
|
||||
// SecretSyncStatus,
|
||||
// TQueueSecretSyncImportSecretsByIdDTO,
|
||||
// TQueueSecretSyncRemoveSecretsByIdDTO,
|
||||
// TQueueSecretSyncsByPathDTO,
|
||||
// TQueueSecretSyncSyncSecretsByIdDTO,
|
||||
// TQueueSendSecretSyncActionFailedNotificationsDTO,
|
||||
// TSecretMap,
|
||||
// TSecretSyncImportSecretsDTO,
|
||||
// TSecretSyncRaw,
|
||||
// TSecretSyncRemoveSecretsDTO,
|
||||
// TSecretSyncSyncSecretsDTO,
|
||||
// TSecretSyncWithCredentials,
|
||||
// TSendSecretSyncFailedNotificationsJobDTO
|
||||
// } from "@app/services/secret-sync/secret-sync-types";
|
||||
// import { TSecretTagDALFactory } from "@app/services/secret-tag/secret-tag-dal";
|
||||
// import { TSecretV2BridgeDALFactory } from "@app/services/secret-v2-bridge/secret-v2-bridge-dal";
|
||||
// import { expandSecretReferencesFactory } from "@app/services/secret-v2-bridge/secret-v2-bridge-fns";
|
||||
// import { TSecretVersionV2DALFactory } from "@app/services/secret-v2-bridge/secret-version-dal";
|
||||
// import { TSecretVersionV2TagDALFactory } from "@app/services/secret-v2-bridge/secret-version-tag-dal";
|
||||
// import { SmtpTemplates, TSmtpService } from "@app/services/smtp/smtp-service";
|
||||
//
|
||||
// import { TAppConnectionDALFactory } from "../app-connection/app-connection-dal";
|
||||
//
|
||||
// export type TSecretSyncQueueFactory = ReturnType<typeof secretSyncQueueFactory>;
|
||||
//
|
||||
// type TSecretSyncQueueFactoryDep = {
|
||||
// queueService: Pick<TQueueServiceFactory, "queue" | "start">;
|
||||
// kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
|
||||
// appConnectionDAL: Pick<TAppConnectionDALFactory, "findById" | "update" | "updateById">;
|
||||
// keyStore: Pick<TKeyStoreFactory, "acquireLock" | "setItemWithExpiry" | "getItem">;
|
||||
// folderDAL: TSecretFolderDALFactory;
|
||||
// secretV2BridgeDAL: Pick<
|
||||
// TSecretV2BridgeDALFactory,
|
||||
// | "findByFolderId"
|
||||
// | "find"
|
||||
// | "insertMany"
|
||||
// | "upsertSecretReferences"
|
||||
// | "findBySecretKeys"
|
||||
// | "bulkUpdate"
|
||||
// | "deleteMany"
|
||||
// >;
|
||||
// secretImportDAL: Pick<TSecretImportDALFactory, "find" | "findByFolderIds">;
|
||||
// secretSyncDAL: Pick<TSecretSyncDALFactory, "findById" | "find" | "updateById" | "deleteById">;
|
||||
// auditLogService: Pick<TAuditLogServiceFactory, "createAuditLog">;
|
||||
// projectMembershipDAL: Pick<TProjectMembershipDALFactory, "findAllProjectMembers">;
|
||||
// projectDAL: TProjectDALFactory;
|
||||
// smtpService: Pick<TSmtpService, "sendMail">;
|
||||
// projectBotDAL: TProjectBotDALFactory;
|
||||
// secretDAL: TSecretDALFactory;
|
||||
// secretVersionDAL: TSecretVersionDALFactory;
|
||||
// secretBlindIndexDAL: TSecretBlindIndexDALFactory;
|
||||
// secretTagDAL: TSecretTagDALFactory;
|
||||
// secretVersionTagDAL: TSecretVersionTagDALFactory;
|
||||
// secretVersionV2BridgeDAL: Pick<TSecretVersionV2DALFactory, "insertMany" | "findLatestVersionMany">;
|
||||
// secretVersionTagV2BridgeDAL: Pick<TSecretVersionV2TagDALFactory, "insertMany">;
|
||||
// resourceMetadataDAL: Pick<TResourceMetadataDALFactory, "insertMany" | "delete">;
|
||||
// };
|
||||
//
|
||||
// type SecretSyncActionJob = Job<
|
||||
// TQueueSecretSyncSyncSecretsByIdDTO | TQueueSecretSyncImportSecretsByIdDTO | TQueueSecretSyncRemoveSecretsByIdDTO
|
||||
// >;
|
||||
//
|
||||
// const getRequeueDelay = (failureCount?: number) => {
|
||||
// if (!failureCount) return 0;
|
||||
//
|
||||
// const baseDelay = 1000;
|
||||
// const maxDelay = 30000;
|
||||
//
|
||||
// const delay = Math.min(baseDelay * 2 ** failureCount, maxDelay);
|
||||
//
|
||||
// const jitter = delay * (0.5 + Math.random() * 0.5);
|
||||
//
|
||||
// return jitter;
|
||||
// };
|
||||
//
|
||||
// export const secretSyncQueueFactory = ({
|
||||
// queueService,
|
||||
// kmsService,
|
||||
// appConnectionDAL,
|
||||
// keyStore,
|
||||
// folderDAL,
|
||||
// secretV2BridgeDAL,
|
||||
// secretImportDAL,
|
||||
// secretSyncDAL,
|
||||
// auditLogService,
|
||||
// projectMembershipDAL,
|
||||
// projectDAL,
|
||||
// smtpService,
|
||||
// projectBotDAL,
|
||||
// secretDAL,
|
||||
// secretVersionDAL,
|
||||
// secretBlindIndexDAL,
|
||||
// secretTagDAL,
|
||||
// secretVersionTagDAL,
|
||||
// secretVersionV2BridgeDAL,
|
||||
// secretVersionTagV2BridgeDAL,
|
||||
// resourceMetadataDAL
|
||||
// }: TSecretSyncQueueFactoryDep) => {
|
||||
// const appCfg = getConfig();
|
||||
//
|
||||
// const integrationMeter = opentelemetry.metrics.getMeter("SecretSyncs");
|
||||
// const syncSecretsErrorHistogram = integrationMeter.createHistogram("secret_sync_sync_secrets_errors", {
|
||||
// description: "Secret Sync - sync secrets errors",
|
||||
// unit: "1"
|
||||
// });
|
||||
// const importSecretsErrorHistogram = integrationMeter.createHistogram("secret_sync_import_secrets_errors", {
|
||||
// description: "Secret Sync - import secrets errors",
|
||||
// unit: "1"
|
||||
// });
|
||||
// const removeSecretsErrorHistogram = integrationMeter.createHistogram("secret_sync_remove_secrets_errors", {
|
||||
// description: "Secret Sync - remove secrets errors",
|
||||
// unit: "1"
|
||||
// });
|
||||
//
|
||||
// const $createManySecretsRawFn = createManySecretsRawFnFactory({
|
||||
// projectDAL,
|
||||
// projectBotDAL,
|
||||
// secretDAL,
|
||||
// secretVersionDAL,
|
||||
// secretBlindIndexDAL,
|
||||
// secretTagDAL,
|
||||
// secretVersionTagDAL,
|
||||
// folderDAL,
|
||||
// kmsService,
|
||||
// secretVersionV2BridgeDAL,
|
||||
// secretV2BridgeDAL,
|
||||
// secretVersionTagV2BridgeDAL,
|
||||
// resourceMetadataDAL
|
||||
// });
|
||||
//
|
||||
// const $updateManySecretsRawFn = updateManySecretsRawFnFactory({
|
||||
// projectDAL,
|
||||
// projectBotDAL,
|
||||
// secretDAL,
|
||||
// secretVersionDAL,
|
||||
// secretBlindIndexDAL,
|
||||
// secretTagDAL,
|
||||
// secretVersionTagDAL,
|
||||
// folderDAL,
|
||||
// kmsService,
|
||||
// secretVersionV2BridgeDAL,
|
||||
// secretV2BridgeDAL,
|
||||
// secretVersionTagV2BridgeDAL,
|
||||
// resourceMetadataDAL
|
||||
// });
|
||||
//
|
||||
// const $getInfisicalSecrets = async (
|
||||
// secretSync: TSecretSyncRaw | TSecretSyncWithCredentials,
|
||||
// includeImports = true
|
||||
// ) => {
|
||||
// const { projectId, folderId, environment, folder } = secretSync;
|
||||
//
|
||||
// if (!folderId || !environment || !folder)
|
||||
// throw new SecretSyncError({
|
||||
// message:
|
||||
// "Invalid Secret Sync source configuration: folder no longer exists. Please update source environment and secret path.",
|
||||
// shouldRetry: false
|
||||
// });
|
||||
//
|
||||
// const secretMap: TSecretMap = {};
|
||||
//
|
||||
// const { decryptor: secretManagerDecryptor } = await kmsService.createCipherPairWithDataKey({
|
||||
// type: KmsDataKey.SecretManager,
|
||||
// projectId
|
||||
// });
|
||||
//
|
||||
// const decryptSecretValue = (value?: Buffer | undefined | null) =>
|
||||
// value ? secretManagerDecryptor({ cipherTextBlob: value }).toString() : "";
|
||||
//
|
||||
// const { expandSecretReferences } = expandSecretReferencesFactory({
|
||||
// decryptSecretValue,
|
||||
// secretDAL: secretV2BridgeDAL,
|
||||
// folderDAL,
|
||||
// projectId,
|
||||
// canExpandValue: () => true
|
||||
// });
|
||||
//
|
||||
// const secrets = await secretV2BridgeDAL.findByFolderId(folderId);
|
||||
//
|
||||
// await Promise.allSettled(
|
||||
// secrets.map(async (secret) => {
|
||||
// const secretKey = secret.key;
|
||||
// const secretValue = decryptSecretValue(secret.encryptedValue);
|
||||
// const expandedSecretValue = await expandSecretReferences({
|
||||
// environment: environment.slug,
|
||||
// secretPath: folder.path,
|
||||
// skipMultilineEncoding: secret.skipMultilineEncoding,
|
||||
// value: secretValue
|
||||
// });
|
||||
// secretMap[secretKey] = { value: expandedSecretValue || "" };
|
||||
//
|
||||
// if (secret.encryptedComment) {
|
||||
// const commentValue = decryptSecretValue(secret.encryptedComment);
|
||||
// secretMap[secretKey].comment = commentValue;
|
||||
// }
|
||||
//
|
||||
// secretMap[secretKey].skipMultilineEncoding = Boolean(secret.skipMultilineEncoding);
|
||||
// secretMap[secretKey].secretMetadata = secret.secretMetadata;
|
||||
// })
|
||||
// );
|
||||
//
|
||||
// if (!includeImports) return secretMap;
|
||||
//
|
||||
// const secretImports = await secretImportDAL.find({ folderId, isReplication: false });
|
||||
//
|
||||
// if (secretImports.length) {
|
||||
// const importedSecrets = await fnSecretsV2FromImports({
|
||||
// decryptor: decryptSecretValue,
|
||||
// folderDAL,
|
||||
// secretDAL: secretV2BridgeDAL,
|
||||
// expandSecretReferences,
|
||||
// secretImportDAL,
|
||||
// secretImports,
|
||||
// hasSecretAccess: () => true,
|
||||
// viewSecretValue: true
|
||||
// });
|
||||
//
|
||||
// for (let i = importedSecrets.length - 1; i >= 0; i -= 1) {
|
||||
// for (let j = 0; j < importedSecrets[i].secrets.length; j += 1) {
|
||||
// const importedSecret = importedSecrets[i].secrets[j];
|
||||
// if (!secretMap[importedSecret.key]) {
|
||||
// secretMap[importedSecret.key] = {
|
||||
// skipMultilineEncoding: importedSecret.skipMultilineEncoding,
|
||||
// comment: importedSecret.secretComment,
|
||||
// value: importedSecret.secretValue || "",
|
||||
// secretMetadata: importedSecret.secretMetadata
|
||||
// };
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// return secretMap;
|
||||
// };
|
||||
//
|
||||
// const queueSecretSyncSyncSecretsById = async (payload: TQueueSecretSyncSyncSecretsByIdDTO) =>
|
||||
// queueService.queue(QueueName.AppConnectionSecretSync, QueueJobs.SecretSyncSyncSecrets, payload, {
|
||||
// delay: getRequeueDelay(payload.failedToAcquireLockCount), // this is for delaying re-queued jobs if sync is locked
|
||||
// attempts: 5,
|
||||
// backoff: {
|
||||
// type: "exponential",
|
||||
// delay: 3000
|
||||
// },
|
||||
// removeOnComplete: true,
|
||||
// removeOnFail: true
|
||||
// });
|
||||
//
|
||||
// const queueSecretSyncImportSecretsById = async (payload: TQueueSecretSyncImportSecretsByIdDTO) =>
|
||||
// queueService.queue(QueueName.AppConnectionSecretSync, QueueJobs.SecretSyncImportSecrets, payload, {
|
||||
// attempts: 1,
|
||||
// removeOnComplete: true,
|
||||
// removeOnFail: true
|
||||
// });
|
||||
//
|
||||
// const queueSecretSyncRemoveSecretsById = async (payload: TQueueSecretSyncRemoveSecretsByIdDTO) =>
|
||||
// queueService.queue(QueueName.AppConnectionSecretSync, QueueJobs.SecretSyncRemoveSecrets, payload, {
|
||||
// attempts: 1,
|
||||
// removeOnComplete: true,
|
||||
// removeOnFail: true
|
||||
// });
|
||||
//
|
||||
// const $queueSendSecretSyncFailedNotifications = async (payload: TQueueSendSecretSyncActionFailedNotificationsDTO) => {
|
||||
// if (!appCfg.isSmtpConfigured) return;
|
||||
//
|
||||
// await queueService.queue(
|
||||
// QueueName.AppConnectionSecretSync,
|
||||
// QueueJobs.SecretSyncSendActionFailedNotifications,
|
||||
// payload,
|
||||
// {
|
||||
// jobId: `secret-sync-${payload.secretSync.id}-failed-notifications`,
|
||||
// attempts: 5,
|
||||
// delay: 1000 * 60,
|
||||
// backoff: {
|
||||
// type: "exponential",
|
||||
// delay: 3000
|
||||
// },
|
||||
// removeOnFail: true,
|
||||
// removeOnComplete: true
|
||||
// }
|
||||
// );
|
||||
// };
|
||||
//
|
||||
// const $importSecrets = async (
|
||||
// secretSync: TSecretSyncWithCredentials,
|
||||
// importBehavior: SecretSyncImportBehavior
|
||||
// ): Promise<TSecretMap> => {
|
||||
// const { projectId, environment, folder } = secretSync;
|
||||
//
|
||||
// if (!environment || !folder)
|
||||
// throw new Error(
|
||||
// "Invalid Secret Sync source configuration: folder no longer exists. Please update source environment and secret path."
|
||||
// );
|
||||
//
|
||||
// const importedSecrets = await SecretSyncFns.getSecrets(secretSync, {
|
||||
// appConnectionDAL,
|
||||
// kmsService
|
||||
// });
|
||||
//
|
||||
// if (!Object.keys(importedSecrets).length) return {};
|
||||
//
|
||||
// const importedSecretMap: TSecretMap = {};
|
||||
//
|
||||
// const secretMap = await $getInfisicalSecrets(secretSync, false);
|
||||
//
|
||||
// const secretsToCreate: Parameters<typeof $createManySecretsRawFn>[0]["secrets"] = [];
|
||||
// const secretsToUpdate: Parameters<typeof $updateManySecretsRawFn>[0]["secrets"] = [];
|
||||
//
|
||||
// Object.entries(importedSecrets).forEach(([key, secretData]) => {
|
||||
// const { value, comment = "", skipMultilineEncoding } = secretData;
|
||||
//
|
||||
// const secret = {
|
||||
// secretName: key,
|
||||
// secretValue: value,
|
||||
// type: SecretType.Shared,
|
||||
// secretComment: comment,
|
||||
// skipMultilineEncoding: skipMultilineEncoding ?? undefined
|
||||
// };
|
||||
//
|
||||
// if (Object.hasOwn(secretMap, key)) {
|
||||
// secretsToUpdate.push(secret);
|
||||
// if (importBehavior === SecretSyncImportBehavior.PrioritizeDestination) importedSecretMap[key] = secretData;
|
||||
// } else {
|
||||
// secretsToCreate.push(secret);
|
||||
// importedSecretMap[key] = secretData;
|
||||
// }
|
||||
// });
|
||||
//
|
||||
// if (secretsToCreate.length) {
|
||||
// await $createManySecretsRawFn({
|
||||
// projectId,
|
||||
// path: folder.path,
|
||||
// environment: environment.slug,
|
||||
// secrets: secretsToCreate
|
||||
// });
|
||||
// }
|
||||
//
|
||||
// if (importBehavior === SecretSyncImportBehavior.PrioritizeDestination && secretsToUpdate.length) {
|
||||
// await $updateManySecretsRawFn({
|
||||
// projectId,
|
||||
// path: folder.path,
|
||||
// environment: environment.slug,
|
||||
// secrets: secretsToUpdate
|
||||
// });
|
||||
// }
|
||||
//
|
||||
// return importedSecretMap;
|
||||
// };
|
||||
//
|
||||
// const $handleSyncSecretsJob = async (job: TSecretSyncSyncSecretsDTO) => {
|
||||
// const {
|
||||
// data: { syncId, auditLogInfo }
|
||||
// } = job;
|
||||
//
|
||||
// const secretSync = await secretSyncDAL.findById(syncId);
|
||||
//
|
||||
// if (!secretSync) throw new Error(`Cannot find secret sync with ID ${syncId}`);
|
||||
//
|
||||
// await secretSyncDAL.updateById(syncId, {
|
||||
// syncStatus: SecretSyncStatus.Running
|
||||
// });
|
||||
//
|
||||
// logger.info(
|
||||
// `SecretSync Sync [syncId=${secretSync.id}] [destination=${secretSync.destination}] [projectId=${secretSync.projectId}] [folderId=${secretSync.folderId}] [connectionId=${secretSync.connectionId}]`
|
||||
// );
|
||||
//
|
||||
// let isSynced = false;
|
||||
// let syncMessage: string | null = null;
|
||||
// let isFinalAttempt = job.attemptsStarted === job.opts.attempts;
|
||||
//
|
||||
// try {
|
||||
// const {
|
||||
// connection: { orgId, encryptedCredentials }
|
||||
// } = secretSync;
|
||||
//
|
||||
// const credentials = await decryptAppConnectionCredentials({
|
||||
// orgId,
|
||||
// encryptedCredentials,
|
||||
// kmsService
|
||||
// });
|
||||
//
|
||||
// const secretSyncWithCredentials = {
|
||||
// ...secretSync,
|
||||
// connection: {
|
||||
// ...secretSync.connection,
|
||||
// credentials
|
||||
// }
|
||||
// } as TSecretSyncWithCredentials;
|
||||
//
|
||||
// const {
|
||||
// lastSyncedAt,
|
||||
// syncOptions: { initialSyncBehavior }
|
||||
// } = secretSyncWithCredentials;
|
||||
//
|
||||
// const secretMap = await $getInfisicalSecrets(secretSync);
|
||||
//
|
||||
// if (!lastSyncedAt && initialSyncBehavior !== SecretSyncInitialSyncBehavior.OverwriteDestination) {
|
||||
// const importedSecretMap = await $importSecrets(
|
||||
// secretSyncWithCredentials,
|
||||
// initialSyncBehavior === SecretSyncInitialSyncBehavior.ImportPrioritizeSource
|
||||
// ? SecretSyncImportBehavior.PrioritizeSource
|
||||
// : SecretSyncImportBehavior.PrioritizeDestination
|
||||
// );
|
||||
//
|
||||
// Object.entries(importedSecretMap).forEach(([key, secretData]) => {
|
||||
// secretMap[key] = secretData;
|
||||
// });
|
||||
// }
|
||||
//
|
||||
// await SecretSyncFns.syncSecrets(secretSyncWithCredentials, secretMap, {
|
||||
// appConnectionDAL,
|
||||
// kmsService
|
||||
// });
|
||||
//
|
||||
// isSynced = true;
|
||||
// } catch (err) {
|
||||
// logger.error(
|
||||
// err,
|
||||
// `SecretSync Sync Error [syncId=${secretSync.id}] [destination=${secretSync.destination}] [projectId=${secretSync.projectId}] [folderId=${secretSync.folderId}] [connectionId=${secretSync.connectionId}]`
|
||||
// );
|
||||
//
|
||||
// if (appCfg.OTEL_TELEMETRY_COLLECTION_ENABLED) {
|
||||
// syncSecretsErrorHistogram.record(1, {
|
||||
// version: 1,
|
||||
// destination: secretSync.destination,
|
||||
// syncId: secretSync.id,
|
||||
// projectId: secretSync.projectId,
|
||||
// type: err instanceof AxiosError ? "AxiosError" : err?.constructor?.name || "UnknownError",
|
||||
// status: err instanceof AxiosError ? err.response?.status : undefined,
|
||||
// name: err instanceof Error ? err.name : undefined
|
||||
// });
|
||||
// }
|
||||
//
|
||||
// syncMessage = parseSyncErrorMessage(err);
|
||||
//
|
||||
// if (err instanceof SecretSyncError && !err.shouldRetry) {
|
||||
// isFinalAttempt = true;
|
||||
// } else {
|
||||
// // re-throw so job fails
|
||||
// throw err;
|
||||
// }
|
||||
// } finally {
|
||||
// const ranAt = new Date();
|
||||
// const syncStatus = isSynced ? SecretSyncStatus.Succeeded : SecretSyncStatus.Failed;
|
||||
//
|
||||
// await auditLogService.createAuditLog({
|
||||
// projectId: secretSync.projectId,
|
||||
// ...(auditLogInfo ?? {
|
||||
// actor: {
|
||||
// type: ActorType.PLATFORM,
|
||||
// metadata: {}
|
||||
// }
|
||||
// }),
|
||||
// event: {
|
||||
// type: EventType.SECRET_SYNC_SYNC_SECRETS,
|
||||
// metadata: {
|
||||
// syncId: secretSync.id,
|
||||
// syncOptions: secretSync.syncOptions,
|
||||
// destination: secretSync.destination,
|
||||
// destinationConfig: secretSync.destinationConfig,
|
||||
// folderId: secretSync.folderId,
|
||||
// connectionId: secretSync.connectionId,
|
||||
// jobRanAt: ranAt,
|
||||
// jobId: job.id!,
|
||||
// syncStatus,
|
||||
// syncMessage
|
||||
// }
|
||||
// }
|
||||
// });
|
||||
//
|
||||
// if (isSynced || isFinalAttempt) {
|
||||
// const updatedSecretSync = await secretSyncDAL.updateById(secretSync.id, {
|
||||
// syncStatus,
|
||||
// lastSyncJobId: job.id,
|
||||
// lastSyncMessage: syncMessage,
|
||||
// lastSyncedAt: isSynced ? ranAt : undefined
|
||||
// });
|
||||
//
|
||||
// if (!isSynced) {
|
||||
// await $queueSendSecretSyncFailedNotifications({
|
||||
// secretSync: updatedSecretSync,
|
||||
// action: SecretSyncAction.SyncSecrets,
|
||||
// auditLogInfo
|
||||
// });
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// logger.info("SecretSync Sync Job with ID %s Completed", job.id);
|
||||
// };
|
||||
//
|
||||
// const $handleImportSecretsJob = async (job: TSecretSyncImportSecretsDTO) => {
|
||||
// const {
|
||||
// data: { syncId, auditLogInfo, importBehavior }
|
||||
// } = job;
|
||||
//
|
||||
// const secretSync = await secretSyncDAL.findById(syncId);
|
||||
//
|
||||
// if (!secretSync) throw new Error(`Cannot find secret sync with ID ${syncId}`);
|
||||
//
|
||||
// await secretSyncDAL.updateById(syncId, {
|
||||
// importStatus: SecretSyncStatus.Running
|
||||
// });
|
||||
//
|
||||
// logger.info(
|
||||
// `SecretSync Import [syncId=${secretSync.id}] [destination=${secretSync.destination}] [projectId=${secretSync.projectId}] [folderId=${secretSync.folderId}] [connectionId=${secretSync.connectionId}]`
|
||||
// );
|
||||
//
|
||||
// let isSuccess = false;
|
||||
// let importMessage: string | null = null;
|
||||
// const isFinalAttempt = job.attemptsStarted === job.opts.attempts;
|
||||
//
|
||||
// try {
|
||||
// const {
|
||||
// connection: { orgId, encryptedCredentials }
|
||||
// } = secretSync;
|
||||
//
|
||||
// const credentials = await decryptAppConnectionCredentials({
|
||||
// orgId,
|
||||
// encryptedCredentials,
|
||||
// kmsService
|
||||
// });
|
||||
//
|
||||
// await $importSecrets(
|
||||
// {
|
||||
// ...secretSync,
|
||||
// connection: {
|
||||
// ...secretSync.connection,
|
||||
// credentials
|
||||
// }
|
||||
// } as TSecretSyncWithCredentials,
|
||||
// importBehavior
|
||||
// );
|
||||
//
|
||||
// isSuccess = true;
|
||||
// } catch (err) {
|
||||
// logger.error(
|
||||
// err,
|
||||
// `SecretSync Import Error [syncId=${secretSync.id}] [destination=${secretSync.destination}] [projectId=${secretSync.projectId}] [folderId=${secretSync.folderId}] [connectionId=${secretSync.connectionId}]`
|
||||
// );
|
||||
//
|
||||
// if (appCfg.OTEL_TELEMETRY_COLLECTION_ENABLED) {
|
||||
// importSecretsErrorHistogram.record(1, {
|
||||
// version: 1,
|
||||
// destination: secretSync.destination,
|
||||
// syncId: secretSync.id,
|
||||
// projectId: secretSync.projectId,
|
||||
// type: err instanceof AxiosError ? "AxiosError" : err?.constructor?.name || "UnknownError",
|
||||
// status: err instanceof AxiosError ? err.response?.status : undefined,
|
||||
// name: err instanceof Error ? err.name : undefined
|
||||
// });
|
||||
// }
|
||||
//
|
||||
// importMessage = parseSyncErrorMessage(err);
|
||||
//
|
||||
// // re-throw so job fails
|
||||
// throw err;
|
||||
// } finally {
|
||||
// const ranAt = new Date();
|
||||
// const importStatus = isSuccess ? SecretSyncStatus.Succeeded : SecretSyncStatus.Failed;
|
||||
//
|
||||
// await auditLogService.createAuditLog({
|
||||
// projectId: secretSync.projectId,
|
||||
// ...(auditLogInfo ?? {
|
||||
// actor: {
|
||||
// type: ActorType.PLATFORM,
|
||||
// metadata: {}
|
||||
// }
|
||||
// }),
|
||||
// event: {
|
||||
// type: EventType.SECRET_SYNC_IMPORT_SECRETS,
|
||||
// metadata: {
|
||||
// syncId: secretSync.id,
|
||||
// syncOptions: secretSync.syncOptions,
|
||||
// destination: secretSync.destination,
|
||||
// destinationConfig: secretSync.destinationConfig,
|
||||
// folderId: secretSync.folderId,
|
||||
// connectionId: secretSync.connectionId,
|
||||
// jobRanAt: ranAt,
|
||||
// jobId: job.id!,
|
||||
// importStatus,
|
||||
// importMessage,
|
||||
// importBehavior
|
||||
// }
|
||||
// }
|
||||
// });
|
||||
//
|
||||
// if (isSuccess || isFinalAttempt) {
|
||||
// const updatedSecretSync = await secretSyncDAL.updateById(secretSync.id, {
|
||||
// importStatus,
|
||||
// lastImportJobId: job.id,
|
||||
// lastImportMessage: importMessage,
|
||||
// lastImportedAt: isSuccess ? ranAt : undefined
|
||||
// });
|
||||
//
|
||||
// if (!isSuccess) {
|
||||
// await $queueSendSecretSyncFailedNotifications({
|
||||
// secretSync: updatedSecretSync,
|
||||
// action: SecretSyncAction.ImportSecrets,
|
||||
// auditLogInfo
|
||||
// });
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// logger.info("SecretSync Import Job with ID %s Completed", job.id);
|
||||
// };
|
||||
//
|
||||
// const $handleRemoveSecretsJob = async (job: TSecretSyncRemoveSecretsDTO) => {
|
||||
// const {
|
||||
// data: { syncId, auditLogInfo, deleteSyncOnComplete }
|
||||
// } = job;
|
||||
//
|
||||
// const secretSync = await secretSyncDAL.findById(syncId);
|
||||
//
|
||||
// if (!secretSync) throw new Error(`Cannot find secret sync with ID ${syncId}`);
|
||||
//
|
||||
// await secretSyncDAL.updateById(syncId, {
|
||||
// removeStatus: SecretSyncStatus.Running
|
||||
// });
|
||||
//
|
||||
// logger.info(
|
||||
// `SecretSync Remove [syncId=${secretSync.id}] [destination=${secretSync.destination}] [projectId=${secretSync.projectId}] [folderId=${secretSync.folderId}] [connectionId=${secretSync.connectionId}]`
|
||||
// );
|
||||
//
|
||||
// let isSuccess = false;
|
||||
// let removeMessage: string | null = null;
|
||||
// const isFinalAttempt = job.attemptsStarted === job.opts.attempts;
|
||||
//
|
||||
// try {
|
||||
// const {
|
||||
// connection: { orgId, encryptedCredentials }
|
||||
// } = secretSync;
|
||||
//
|
||||
// const credentials = await decryptAppConnectionCredentials({
|
||||
// orgId,
|
||||
// encryptedCredentials,
|
||||
// kmsService
|
||||
// });
|
||||
//
|
||||
// const secretMap = await $getInfisicalSecrets(secretSync);
|
||||
//
|
||||
// await SecretSyncFns.removeSecrets(
|
||||
// {
|
||||
// ...secretSync,
|
||||
// connection: {
|
||||
// ...secretSync.connection,
|
||||
// credentials
|
||||
// }
|
||||
// } as TSecretSyncWithCredentials,
|
||||
// secretMap,
|
||||
// {
|
||||
// appConnectionDAL,
|
||||
// kmsService
|
||||
// }
|
||||
// );
|
||||
//
|
||||
// isSuccess = true;
|
||||
// } catch (err) {
|
||||
// logger.error(
|
||||
// err,
|
||||
// `SecretSync Remove Error [syncId=${secretSync.id}] [destination=${secretSync.destination}] [projectId=${secretSync.projectId}] [folderId=${secretSync.folderId}] [connectionId=${secretSync.connectionId}]`
|
||||
// );
|
||||
//
|
||||
// if (appCfg.OTEL_TELEMETRY_COLLECTION_ENABLED) {
|
||||
// removeSecretsErrorHistogram.record(1, {
|
||||
// version: 1,
|
||||
// destination: secretSync.destination,
|
||||
// syncId: secretSync.id,
|
||||
// projectId: secretSync.projectId,
|
||||
// type: err instanceof AxiosError ? "AxiosError" : err?.constructor?.name || "UnknownError",
|
||||
// status: err instanceof AxiosError ? err.response?.status : undefined,
|
||||
// name: err instanceof Error ? err.name : undefined
|
||||
// });
|
||||
// }
|
||||
//
|
||||
// removeMessage = parseSyncErrorMessage(err);
|
||||
//
|
||||
// // re-throw so job fails
|
||||
// throw err;
|
||||
// } finally {
|
||||
// const ranAt = new Date();
|
||||
// const removeStatus = isSuccess ? SecretSyncStatus.Succeeded : SecretSyncStatus.Failed;
|
||||
//
|
||||
// await auditLogService.createAuditLog({
|
||||
// projectId: secretSync.projectId,
|
||||
// ...(auditLogInfo ?? {
|
||||
// actor: {
|
||||
// type: ActorType.PLATFORM,
|
||||
// metadata: {}
|
||||
// }
|
||||
// }),
|
||||
// event: {
|
||||
// type: EventType.SECRET_SYNC_REMOVE_SECRETS,
|
||||
// metadata: {
|
||||
// syncId: secretSync.id,
|
||||
// syncOptions: secretSync.syncOptions,
|
||||
// destination: secretSync.destination,
|
||||
// destinationConfig: secretSync.destinationConfig,
|
||||
// folderId: secretSync.folderId,
|
||||
// connectionId: secretSync.connectionId,
|
||||
// jobRanAt: ranAt,
|
||||
// jobId: job.id!,
|
||||
// removeStatus,
|
||||
// removeMessage
|
||||
// }
|
||||
// }
|
||||
// });
|
||||
//
|
||||
// if (isSuccess || isFinalAttempt) {
|
||||
// if (isSuccess && deleteSyncOnComplete) {
|
||||
// await secretSyncDAL.deleteById(secretSync.id);
|
||||
// } else {
|
||||
// const updatedSecretSync = await secretSyncDAL.updateById(secretSync.id, {
|
||||
// removeStatus,
|
||||
// lastRemoveJobId: job.id,
|
||||
// lastRemoveMessage: removeMessage,
|
||||
// lastRemovedAt: isSuccess ? ranAt : undefined
|
||||
// });
|
||||
//
|
||||
// if (!isSuccess) {
|
||||
// await $queueSendSecretSyncFailedNotifications({
|
||||
// secretSync: updatedSecretSync,
|
||||
// action: SecretSyncAction.RemoveSecrets,
|
||||
// auditLogInfo
|
||||
// });
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// logger.info("SecretSync Remove Job with ID %s Completed", job.id);
|
||||
// };
|
||||
//
|
||||
// const $sendSecretSyncFailedNotifications = async (job: TSendSecretSyncFailedNotificationsJobDTO) => {
|
||||
// const {
|
||||
// data: { secretSync, auditLogInfo, action }
|
||||
// } = job;
|
||||
//
|
||||
// const { projectId, destination, name, folder, lastSyncMessage, lastRemoveMessage, lastImportMessage, environment } =
|
||||
// secretSync;
|
||||
//
|
||||
// const projectMembers = await projectMembershipDAL.findAllProjectMembers(projectId);
|
||||
// const project = await projectDAL.findById(projectId);
|
||||
//
|
||||
// let projectAdmins = projectMembers.filter((member) =>
|
||||
// member.roles.some((role) => role.role === ProjectMembershipRole.Admin)
|
||||
// );
|
||||
//
|
||||
// const triggeredByUserId =
|
||||
// auditLogInfo && auditLogInfo.actor.type === ActorType.USER && auditLogInfo.actor.metadata.userId;
|
||||
//
|
||||
// // only notify triggering user if triggered by admin
|
||||
// if (triggeredByUserId && projectAdmins.map((admin) => admin.userId).includes(triggeredByUserId)) {
|
||||
// projectAdmins = projectAdmins.filter((admin) => admin.userId === triggeredByUserId);
|
||||
// }
|
||||
//
|
||||
// const syncDestination = SECRET_SYNC_NAME_MAP[destination as SecretSync];
|
||||
//
|
||||
// let actionLabel: string;
|
||||
// let failureMessage: string | null | undefined;
|
||||
//
|
||||
// switch (action) {
|
||||
// case SecretSyncAction.ImportSecrets:
|
||||
// actionLabel = "Import";
|
||||
// failureMessage = lastImportMessage;
|
||||
//
|
||||
// break;
|
||||
// case SecretSyncAction.RemoveSecrets:
|
||||
// actionLabel = "Remove";
|
||||
// failureMessage = lastRemoveMessage;
|
||||
//
|
||||
// break;
|
||||
// case SecretSyncAction.SyncSecrets:
|
||||
// default:
|
||||
// actionLabel = `Sync`;
|
||||
// failureMessage = lastSyncMessage;
|
||||
// break;
|
||||
// }
|
||||
//
|
||||
// await smtpService.sendMail({
|
||||
// recipients: projectAdmins.map((member) => member.user.email!).filter(Boolean),
|
||||
// template: SmtpTemplates.SecretSyncFailed,
|
||||
// subjectLine: `Secret Sync Failed to ${actionLabel} Secrets`,
|
||||
// substitutions: {
|
||||
// syncName: name,
|
||||
// syncDestination,
|
||||
// content: `Your ${syncDestination} Sync named "${name}" failed while attempting to ${action.toLowerCase()} secrets.`,
|
||||
// failureMessage,
|
||||
// secretPath: folder?.path,
|
||||
// environment: environment?.name,
|
||||
// projectName: project.name,
|
||||
// syncUrl: `${appCfg.SITE_URL}/integrations/secret-syncs/${destination}/${secretSync.id}`
|
||||
// }
|
||||
// });
|
||||
// };
|
||||
//
|
||||
// const queueSecretSyncsSyncSecretsByPath = async ({
|
||||
// secretPath,
|
||||
// projectId,
|
||||
// environmentSlug
|
||||
// }: TQueueSecretSyncsByPathDTO) => {
|
||||
// const folder = await folderDAL.findBySecretPath(projectId, environmentSlug, secretPath);
|
||||
//
|
||||
// if (!folder)
|
||||
// throw new Error(
|
||||
// `Could not find folder at path "${secretPath}" for environment with slug "${environmentSlug}" in project with ID "${projectId}"`
|
||||
// );
|
||||
//
|
||||
// const secretSyncs = await secretSyncDAL.find({ folderId: folder.id, isAutoSyncEnabled: true });
|
||||
//
|
||||
// await Promise.all(secretSyncs.map((secretSync) => queueSecretSyncSyncSecretsById({ syncId: secretSync.id })));
|
||||
// };
|
||||
//
|
||||
// const $handleAcquireLockFailure = async (job: SecretSyncActionJob) => {
|
||||
// const { syncId, auditLogInfo } = job.data;
|
||||
//
|
||||
// switch (job.name) {
|
||||
// case QueueJobs.SecretSyncSyncSecrets: {
|
||||
// const { failedToAcquireLockCount = 0, ...rest } = job.data as TQueueSecretSyncSyncSecretsByIdDTO;
|
||||
//
|
||||
// if (failedToAcquireLockCount < 10) {
|
||||
// await queueSecretSyncSyncSecretsById({ ...rest, failedToAcquireLockCount: failedToAcquireLockCount + 1 });
|
||||
// return;
|
||||
// }
|
||||
//
|
||||
// const secretSync = await secretSyncDAL.updateById(syncId, {
|
||||
// syncStatus: SecretSyncStatus.Failed,
|
||||
// lastSyncMessage:
|
||||
// "Failed to run job. This typically happens when a sync is already in progress. Please try again.",
|
||||
// lastSyncJobId: job.id
|
||||
// });
|
||||
//
|
||||
// await $queueSendSecretSyncFailedNotifications({
|
||||
// secretSync,
|
||||
// action: SecretSyncAction.SyncSecrets,
|
||||
// auditLogInfo
|
||||
// });
|
||||
//
|
||||
// break;
|
||||
// }
|
||||
// // Scott: the two cases below are unlikely to happen as we check the lock at the API level but including this as a fallback
|
||||
// case QueueJobs.SecretSyncImportSecrets: {
|
||||
// const secretSync = await secretSyncDAL.updateById(syncId, {
|
||||
// importStatus: SecretSyncStatus.Failed,
|
||||
// lastImportMessage:
|
||||
// "Failed to run job. This typically happens when a sync is already in progress. Please try again.",
|
||||
// lastImportJobId: job.id
|
||||
// });
|
||||
//
|
||||
// await $queueSendSecretSyncFailedNotifications({
|
||||
// secretSync,
|
||||
// action: SecretSyncAction.ImportSecrets,
|
||||
// auditLogInfo
|
||||
// });
|
||||
//
|
||||
// break;
|
||||
// }
|
||||
// case QueueJobs.SecretSyncRemoveSecrets: {
|
||||
// const secretSync = await secretSyncDAL.updateById(syncId, {
|
||||
// removeStatus: SecretSyncStatus.Failed,
|
||||
// lastRemoveMessage:
|
||||
// "Failed to run job. This typically happens when a sync is already in progress. Please try again.",
|
||||
// lastRemoveJobId: job.id
|
||||
// });
|
||||
//
|
||||
// await $queueSendSecretSyncFailedNotifications({
|
||||
// secretSync,
|
||||
// action: SecretSyncAction.RemoveSecrets,
|
||||
// auditLogInfo
|
||||
// });
|
||||
//
|
||||
// break;
|
||||
// }
|
||||
// default:
|
||||
// // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
|
||||
// throw new Error(`Unhandled Secret Sync Job ${job.name}`);
|
||||
// }
|
||||
// };
|
||||
//
|
||||
// queueService.start(QueueName.AppConnectionSecretSync, async (job) => {
|
||||
// if (job.name === QueueJobs.SecretSyncSendActionFailedNotifications) {
|
||||
// await $sendSecretSyncFailedNotifications(job as TSendSecretSyncFailedNotificationsJobDTO);
|
||||
// return;
|
||||
// }
|
||||
//
|
||||
// const { syncId } = job.data as
|
||||
// | TQueueSecretSyncSyncSecretsByIdDTO
|
||||
// | TQueueSecretSyncImportSecretsByIdDTO
|
||||
// | TQueueSecretSyncRemoveSecretsByIdDTO;
|
||||
//
|
||||
// let lock: Awaited<ReturnType<typeof keyStore.acquireLock>>;
|
||||
//
|
||||
// try {
|
||||
// lock = await keyStore.acquireLock(
|
||||
// [KeyStorePrefixes.SecretSyncLock(syncId)],
|
||||
// // scott: not sure on this duration; syncs can take excessive amounts of time so we need to keep it locked,
|
||||
// // but should always release below...
|
||||
// 5 * 60 * 1000
|
||||
// );
|
||||
// } catch (e) {
|
||||
// logger.info(`SecretSync Failed to acquire lock [syncId=${syncId}] [job=${job.name}]`);
|
||||
//
|
||||
// await $handleAcquireLockFailure(job as SecretSyncActionJob);
|
||||
//
|
||||
// return;
|
||||
// }
|
||||
//
|
||||
// try {
|
||||
// switch (job.name) {
|
||||
// case QueueJobs.SecretSyncSyncSecrets:
|
||||
// await $handleSyncSecretsJob(job as TSecretSyncSyncSecretsDTO);
|
||||
// break;
|
||||
// case QueueJobs.SecretSyncImportSecrets:
|
||||
// await $handleImportSecretsJob(job as TSecretSyncImportSecretsDTO);
|
||||
// break;
|
||||
// case QueueJobs.SecretSyncRemoveSecrets:
|
||||
// await $handleRemoveSecretsJob(job as TSecretSyncRemoveSecretsDTO);
|
||||
// break;
|
||||
// default:
|
||||
// // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
|
||||
// throw new Error(`Unhandled Secret Sync Job ${job.name}`);
|
||||
// }
|
||||
// } finally {
|
||||
// await lock.release();
|
||||
// }
|
||||
// });
|
||||
//
|
||||
// return {
|
||||
// queueSecretSyncSyncSecretsById,
|
||||
// queueSecretSyncImportSecretsById,
|
||||
// queueSecretSyncRemoveSecretsById,
|
||||
// queueSecretSyncsSyncSecretsByPath
|
||||
// };
|
||||
// };
|
@ -0,0 +1,69 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { SecretRotationsV2Schema } from "@app/db/schemas/secret-rotations-v2";
|
||||
import { SecretRotation } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-enums";
|
||||
import { SECRET_ROTATION_CONNECTION_MAP } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-maps";
|
||||
import { SecretRotations } from "@app/lib/api-docs";
|
||||
import { removeTrailingSlash } from "@app/lib/fn";
|
||||
import { slugSchema } from "@app/server/lib/schemas";
|
||||
|
||||
export const BaseSecretRotationSchema = (type: SecretRotation) =>
|
||||
SecretRotationsV2Schema.omit({
|
||||
type: true,
|
||||
parameters: true,
|
||||
encryptedGeneratedCredentials: true
|
||||
}).extend({
|
||||
connection: z.object({
|
||||
app: z.literal(SECRET_ROTATION_CONNECTION_MAP[type]),
|
||||
name: z.string(),
|
||||
id: z.string().uuid()
|
||||
}),
|
||||
environment: z.object({ slug: z.string(), name: z.string(), id: z.string().uuid() }).nullable(),
|
||||
folder: z.object({ id: z.string(), path: z.string() }).nullable()
|
||||
});
|
||||
|
||||
export const BaseCreateSecretRotationSchema = (type: SecretRotation) =>
|
||||
z.object({
|
||||
name: slugSchema({ field: "name" }).describe(SecretRotations.CREATE(type).name),
|
||||
projectId: z.string().trim().min(1, "Project ID required").describe(SecretRotations.CREATE(type).projectId),
|
||||
description: z
|
||||
.string()
|
||||
.trim()
|
||||
.max(256, "Description cannot exceed 256 characters")
|
||||
.nullish()
|
||||
.describe(SecretRotations.CREATE(type).description),
|
||||
connectionId: z.string().uuid().describe(SecretRotations.CREATE(type).connectionId),
|
||||
environment: slugSchema({ field: "environment", max: 64 }).describe(SecretRotations.CREATE(type).environment),
|
||||
secretPath: z
|
||||
.string()
|
||||
.trim()
|
||||
.min(1, "Secret path required")
|
||||
.transform(removeTrailingSlash)
|
||||
.describe(SecretRotations.CREATE(type).secretPath),
|
||||
isAutoRotationEnabled: z
|
||||
.boolean()
|
||||
.optional()
|
||||
.default(true)
|
||||
.describe(SecretRotations.CREATE(type).isAutoRotationEnabled),
|
||||
interval: z.coerce.number().describe(SecretRotations.CREATE(type).interval)
|
||||
});
|
||||
|
||||
export const BaseUpdateSecretRotationSchema = (type: SecretRotation) =>
|
||||
z.object({
|
||||
name: slugSchema({ field: "name" }).describe(SecretRotations.UPDATE(type).name),
|
||||
description: z
|
||||
.string()
|
||||
.trim()
|
||||
.max(256, "Description cannot exceed 256 characters")
|
||||
.nullish()
|
||||
.describe(SecretRotations.UPDATE(type).description),
|
||||
environment: slugSchema({ field: "environment", max: 64 }).describe(SecretRotations.UPDATE(type).environment),
|
||||
secretPath: z
|
||||
.string()
|
||||
.trim()
|
||||
.min(1, "Secret path required")
|
||||
.transform(removeTrailingSlash)
|
||||
.describe(SecretRotations.UPDATE(type).secretPath),
|
||||
isAutoRotationEnabled: z.boolean().default(true).describe(SecretRotations.UPDATE(type).isAutoRotationEnabled),
|
||||
interval: z.coerce.number().describe(SecretRotations.UPDATE(type).interval)
|
||||
});
|
@ -0,0 +1,548 @@
|
||||
import { ForbiddenError, subject } from "@casl/ability";
|
||||
|
||||
import { ActionProjectType } from "@app/db/schemas";
|
||||
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
|
||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||
import {
|
||||
ProjectPermissionSecretActions,
|
||||
ProjectPermissionSecretRotationActions,
|
||||
ProjectPermissionSub
|
||||
} from "@app/ee/services/permission/project-permission";
|
||||
import { listSecretRotationOptions } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-fns";
|
||||
import {
|
||||
SECRET_ROTATION_CONNECTION_MAP,
|
||||
SECRET_ROTATION_NAME_MAP
|
||||
} from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-maps";
|
||||
import {
|
||||
TCreateSecretRotationV2DTO,
|
||||
TDeleteSecretRotationV2DTO,
|
||||
TFindSecretRotationV2ByIdDTO,
|
||||
TFindSecretRotationV2ByNameDTO,
|
||||
TListSecretRotationsV2ByProjectId,
|
||||
TSecretRotationV2,
|
||||
TUpdateSecretRotationV2DTO
|
||||
} from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-types";
|
||||
import { DatabaseErrorCode } from "@app/lib/error-codes";
|
||||
import { BadRequestError, DatabaseError, NotFoundError } from "@app/lib/errors";
|
||||
import { OrgServiceActor } from "@app/lib/types";
|
||||
import { TAppConnectionServiceFactory } from "@app/services/app-connection/app-connection-service";
|
||||
import { TProjectBotServiceFactory } from "@app/services/project-bot/project-bot-service";
|
||||
import { TSecretFolderDALFactory } from "@app/services/secret-folder/secret-folder-dal";
|
||||
|
||||
import { TSecretRotationV2DALFactory } from "./secret-rotation-v2-dal";
|
||||
|
||||
type TSecretRotationV2ServiceFactoryDep = {
|
||||
secretRotationV2DAL: TSecretRotationV2DALFactory;
|
||||
appConnectionService: Pick<TAppConnectionServiceFactory, "connectAppConnectionById">;
|
||||
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission" | "getOrgPermission">;
|
||||
projectBotService: Pick<TProjectBotServiceFactory, "getBotKey">;
|
||||
folderDAL: Pick<TSecretFolderDALFactory, "findByProjectId" | "findById" | "findBySecretPath">;
|
||||
// keyStore: Pick<TKeyStoreFactory, "getItem">;
|
||||
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
|
||||
};
|
||||
|
||||
export type TSecretRotationV2ServiceFactory = ReturnType<typeof secretRotationV2ServiceFactory>;
|
||||
|
||||
export const secretRotationV2ServiceFactory = ({
|
||||
secretRotationV2DAL,
|
||||
folderDAL,
|
||||
permissionService,
|
||||
appConnectionService,
|
||||
projectBotService,
|
||||
licenseService
|
||||
}: TSecretRotationV2ServiceFactoryDep) => {
|
||||
const listSecretRotationsByProjectId = async (
|
||||
{ projectId, type }: TListSecretRotationsV2ByProjectId,
|
||||
actor: OrgServiceActor
|
||||
) => {
|
||||
const plan = await licenseService.getPlan(actor.orgId);
|
||||
|
||||
if (!plan.secretRotation)
|
||||
throw new BadRequestError({
|
||||
message: "Failed to access secret rotations due to plan restriction. Upgrade plan to access secret rotations."
|
||||
});
|
||||
|
||||
const { permission } = await permissionService.getProjectPermission({
|
||||
actor: actor.type,
|
||||
actorId: actor.id,
|
||||
actorAuthMethod: actor.authMethod,
|
||||
actorOrgId: actor.orgId,
|
||||
actionProjectType: ActionProjectType.SecretManager,
|
||||
projectId
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionSecretRotationActions.Read,
|
||||
ProjectPermissionSub.SecretRotation
|
||||
);
|
||||
|
||||
const secretRotations = await secretRotationV2DAL.find({
|
||||
...(type && { type }),
|
||||
projectId
|
||||
});
|
||||
|
||||
return secretRotations as TSecretRotationV2[];
|
||||
};
|
||||
|
||||
const findSecretRotationById = async ({ type, rotationId }: TFindSecretRotationV2ByIdDTO, actor: OrgServiceActor) => {
|
||||
const plan = await licenseService.getPlan(actor.orgId);
|
||||
|
||||
if (!plan.secretRotation)
|
||||
throw new BadRequestError({
|
||||
message: "Failed to access secret rotation due to plan restriction. Upgrade plan to access secret rotations."
|
||||
});
|
||||
|
||||
const secretRotation = await secretRotationV2DAL.findById(rotationId);
|
||||
|
||||
if (!secretRotation)
|
||||
throw new NotFoundError({
|
||||
message: `Could not find ${SECRET_ROTATION_NAME_MAP[type]} Rotation with ID "${rotationId}"`
|
||||
});
|
||||
|
||||
const { permission } = await permissionService.getProjectPermission({
|
||||
actor: actor.type,
|
||||
actorId: actor.id,
|
||||
actorAuthMethod: actor.authMethod,
|
||||
actorOrgId: actor.orgId,
|
||||
actionProjectType: ActionProjectType.SecretManager,
|
||||
projectId: secretRotation.projectId
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionSecretRotationActions.Read,
|
||||
ProjectPermissionSub.SecretRotation
|
||||
);
|
||||
|
||||
if (secretRotation.connection.app !== SECRET_ROTATION_CONNECTION_MAP[type])
|
||||
throw new BadRequestError({
|
||||
message: `Secret Rotation with ID "${secretRotation.id}" is not configured for ${SECRET_ROTATION_NAME_MAP[type]}`
|
||||
});
|
||||
|
||||
return secretRotation as TSecretRotationV2;
|
||||
};
|
||||
|
||||
const findSecretRotationByName = async (
|
||||
{ type, rotationName, projectId }: TFindSecretRotationV2ByNameDTO,
|
||||
actor: OrgServiceActor
|
||||
) => {
|
||||
const plan = await licenseService.getPlan(actor.orgId);
|
||||
|
||||
if (!plan.secretRotation)
|
||||
throw new BadRequestError({
|
||||
message: "Failed to access secret rotation due to plan restriction. Upgrade plan to access secret rotations."
|
||||
});
|
||||
|
||||
// we prevent conflicting names within a project
|
||||
const secretRotation = await secretRotationV2DAL.findOne({
|
||||
name: rotationName,
|
||||
projectId
|
||||
});
|
||||
|
||||
if (!secretRotation)
|
||||
throw new NotFoundError({
|
||||
message: `Could not find ${SECRET_ROTATION_NAME_MAP[type]} Rotation with name "${rotationName}"`
|
||||
});
|
||||
|
||||
const { permission } = await permissionService.getProjectPermission({
|
||||
actor: actor.type,
|
||||
actorId: actor.id,
|
||||
actorAuthMethod: actor.authMethod,
|
||||
actorOrgId: actor.orgId,
|
||||
actionProjectType: ActionProjectType.SecretManager,
|
||||
projectId: secretRotation.projectId
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionSecretRotationActions.Read,
|
||||
ProjectPermissionSub.SecretRotation
|
||||
);
|
||||
|
||||
if (secretRotation.connection.app !== SECRET_ROTATION_CONNECTION_MAP[type])
|
||||
throw new BadRequestError({
|
||||
message: `Secret Rotation with ID "${secretRotation.id}" is not configured for ${SECRET_ROTATION_NAME_MAP[type]}`
|
||||
});
|
||||
|
||||
return secretRotation as TSecretRotationV2;
|
||||
};
|
||||
|
||||
const createSecretRotation = async (
|
||||
{ projectId, secretPath, environment, ...params }: TCreateSecretRotationV2DTO,
|
||||
actor: OrgServiceActor
|
||||
) => {
|
||||
const plan = await licenseService.getPlan(actor.orgId);
|
||||
|
||||
if (!plan.secretRotation)
|
||||
throw new BadRequestError({
|
||||
message: "Failed to create secret rotation due to plan restriction. Upgrade plan to create secret rotations."
|
||||
});
|
||||
|
||||
const { permission } = await permissionService.getProjectPermission({
|
||||
actor: actor.type,
|
||||
actorId: actor.id,
|
||||
actorAuthMethod: actor.authMethod,
|
||||
actorOrgId: actor.orgId,
|
||||
actionProjectType: ActionProjectType.SecretManager,
|
||||
projectId
|
||||
});
|
||||
|
||||
const { shouldUseSecretV2Bridge } = await projectBotService.getBotKey(projectId);
|
||||
|
||||
if (!shouldUseSecretV2Bridge)
|
||||
throw new BadRequestError({ message: "Project version does not support Secret Rotation V2" });
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionSecretRotationActions.Create,
|
||||
ProjectPermissionSub.SecretRotation
|
||||
);
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionSecretActions.Create,
|
||||
subject(ProjectPermissionSub.Secrets, { environment, secretPath })
|
||||
);
|
||||
|
||||
const folder = await folderDAL.findBySecretPath(projectId, environment, secretPath);
|
||||
|
||||
if (!folder)
|
||||
throw new BadRequestError({
|
||||
message: `Could not find folder with path "${secretPath}" in environment "${environment}" for project with ID "${projectId}"`
|
||||
});
|
||||
|
||||
const typeApp = SECRET_ROTATION_CONNECTION_MAP[params.type];
|
||||
|
||||
// validates permission to connect and app is valid for sync type
|
||||
await appConnectionService.connectAppConnectionById(typeApp, params.connectionId, actor);
|
||||
|
||||
// TODO: initialize credentials
|
||||
|
||||
try {
|
||||
const secretRotation = await secretRotationV2DAL.create({
|
||||
folderId: folder.id,
|
||||
...params,
|
||||
encryptedGeneratedCredentials: Buffer.from([]),
|
||||
projectId
|
||||
});
|
||||
|
||||
return secretRotation as TSecretRotationV2;
|
||||
} catch (err) {
|
||||
if (err instanceof DatabaseError && (err.error as { code: string })?.code === DatabaseErrorCode.UniqueViolation) {
|
||||
throw new BadRequestError({
|
||||
message: `A Secret Rotation with the name "${params.name}" already exists for the project with ID "${folder.projectId}"`
|
||||
});
|
||||
}
|
||||
|
||||
throw err;
|
||||
}
|
||||
};
|
||||
|
||||
const updateSecretRotation = async (
|
||||
{ type, rotationId, secretPath, environment, ...params }: TUpdateSecretRotationV2DTO,
|
||||
actor: OrgServiceActor
|
||||
) => {
|
||||
const plan = await licenseService.getPlan(actor.orgId);
|
||||
|
||||
if (!plan.secretRotation)
|
||||
throw new BadRequestError({
|
||||
message: "Failed to update secret rotation due to plan restriction. Upgrade plan to update secret rotations."
|
||||
});
|
||||
|
||||
const secretRotation = await secretRotationV2DAL.findById(rotationId);
|
||||
|
||||
if (!secretRotation)
|
||||
throw new NotFoundError({
|
||||
message: `Could not find ${SECRET_ROTATION_NAME_MAP[type]} Rotation with ID ${rotationId}`
|
||||
});
|
||||
|
||||
const { permission } = await permissionService.getProjectPermission({
|
||||
actor: actor.type,
|
||||
actorId: actor.id,
|
||||
actorAuthMethod: actor.authMethod,
|
||||
actorOrgId: actor.orgId,
|
||||
actionProjectType: ActionProjectType.SecretManager,
|
||||
projectId: secretRotation.projectId
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionSecretRotationActions.Edit,
|
||||
ProjectPermissionSub.SecretRotation
|
||||
);
|
||||
|
||||
if (secretRotation.connection.app !== SECRET_ROTATION_CONNECTION_MAP[type])
|
||||
throw new BadRequestError({
|
||||
message: `Secret sync with ID "${secretRotation.id}" is not configured for ${SECRET_ROTATION_NAME_MAP[type]}`
|
||||
});
|
||||
|
||||
let { folderId } = secretRotation;
|
||||
|
||||
if (
|
||||
(secretPath && secretPath !== secretRotation.folder?.path) ||
|
||||
(environment && environment !== secretRotation.environment?.slug)
|
||||
) {
|
||||
const updatedEnvironment = environment ?? secretRotation.environment?.slug;
|
||||
const updatedSecretPath = secretPath ?? secretRotation.folder?.path;
|
||||
|
||||
if (!updatedEnvironment || !updatedSecretPath)
|
||||
throw new BadRequestError({ message: "Must specify both source environment and secret path" });
|
||||
|
||||
// TODO: get secrets to determine delete permission
|
||||
|
||||
// ForbiddenError.from(permission).throwUnlessCan(
|
||||
// ProjectPermissionSecretActions.Create,
|
||||
// subject(ProjectPermissionSub.Secrets, { environment, secretPath })
|
||||
// );
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionSecretActions.Create,
|
||||
subject(ProjectPermissionSub.Secrets, { environment: updatedEnvironment, secretPath: updatedSecretPath })
|
||||
);
|
||||
|
||||
const newFolder = await folderDAL.findBySecretPath(
|
||||
secretRotation.projectId,
|
||||
updatedEnvironment,
|
||||
updatedSecretPath
|
||||
);
|
||||
|
||||
if (!newFolder)
|
||||
throw new BadRequestError({
|
||||
message: `Could not find folder with path "${secretPath}" in environment "${environment}" for project with ID "${secretRotation.projectId}"`
|
||||
});
|
||||
|
||||
folderId = newFolder.id;
|
||||
}
|
||||
|
||||
try {
|
||||
const updatedSecretRotation = await secretRotationV2DAL.updateById(rotationId, {
|
||||
...params,
|
||||
folderId
|
||||
});
|
||||
|
||||
return updatedSecretRotation as TSecretRotationV2;
|
||||
} catch (err) {
|
||||
if (err instanceof DatabaseError && (err.error as { code: string })?.code === DatabaseErrorCode.UniqueViolation) {
|
||||
throw new BadRequestError({
|
||||
message: `A Secret Rotation with the name "${params.name}" already exists for the project with ID "${secretRotation.projectId}"`
|
||||
});
|
||||
}
|
||||
|
||||
throw err;
|
||||
}
|
||||
};
|
||||
|
||||
const deleteSecretRotation = async (
|
||||
{ type, rotationId, removeSecrets }: TDeleteSecretRotationV2DTO,
|
||||
actor: OrgServiceActor
|
||||
) => {
|
||||
const plan = await licenseService.getPlan(actor.orgId);
|
||||
|
||||
if (!plan.secretRotation)
|
||||
throw new BadRequestError({
|
||||
message: "Failed to access secret rotation due to plan restriction. Upgrade plan to access secret rotations."
|
||||
});
|
||||
|
||||
const secretRotation = await secretRotationV2DAL.findById(rotationId);
|
||||
|
||||
if (!secretRotation)
|
||||
throw new NotFoundError({
|
||||
message: `Could not find ${SECRET_ROTATION_NAME_MAP[type]} Rotation with ID "${rotationId}"`
|
||||
});
|
||||
|
||||
const { permission } = await permissionService.getProjectPermission({
|
||||
actor: actor.type,
|
||||
actorId: actor.id,
|
||||
actorAuthMethod: actor.authMethod,
|
||||
actorOrgId: actor.orgId,
|
||||
actionProjectType: ActionProjectType.SecretManager,
|
||||
projectId: secretRotation.projectId
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionSecretRotationActions.Delete,
|
||||
ProjectPermissionSub.SecretRotation
|
||||
);
|
||||
|
||||
if (secretRotation.connection.app !== SECRET_ROTATION_CONNECTION_MAP[type])
|
||||
throw new BadRequestError({
|
||||
message: `Secret sync with ID "${secretRotation.id}" is not configured for ${SECRET_ROTATION_NAME_MAP[type]}`
|
||||
});
|
||||
|
||||
if (removeSecrets) {
|
||||
// TODO: get secrets to determine remove permissions
|
||||
// ForbiddenError.from(permission).throwUnlessCan(
|
||||
// ProjectPermissionSecretRotationActions.RemoveSecrets,
|
||||
// ProjectPermissionSub.SecretRotations
|
||||
// );
|
||||
// TODO: remove secrets
|
||||
} else {
|
||||
// TODO delete relations
|
||||
}
|
||||
|
||||
await secretRotationV2DAL.deleteById(rotationId);
|
||||
|
||||
return secretRotation as TSecretRotationV2;
|
||||
};
|
||||
//
|
||||
// const triggerSecretRotationRotationSecretsById = async (
|
||||
// { rotationId, type, ...params }: TTriggerSecretRotationRotationSecretsByIdDTO,
|
||||
// actor: OrgServiceActor
|
||||
// ) => {
|
||||
// const secretRotation = await secretRotationDAL.findById(rotationId);
|
||||
//
|
||||
// if (!secretRotation)
|
||||
// throw new NotFoundError({
|
||||
// message: `Could not find ${SECRET_ROTATION_NAME_MAP[type]} Rotation with ID "${rotationId}"`
|
||||
// });
|
||||
//
|
||||
// const { permission } = await permissionService.getProjectPermission({
|
||||
// actor: actor.type,
|
||||
// actorId: actor.id,
|
||||
// actorAuthMethod: actor.authMethod,
|
||||
// actorOrgId: actor.orgId,
|
||||
// actionProjectType: ActionProjectType.SecretManager,
|
||||
// projectId: secretRotation.projectId
|
||||
// });
|
||||
//
|
||||
// ForbiddenError.from(permission).throwUnlessCan(
|
||||
// ProjectPermissionSecretRotationActions.RotationSecrets,
|
||||
// ProjectPermissionSub.SecretRotations
|
||||
// );
|
||||
//
|
||||
// if (secretRotation.connection.app !== SECRET_ROTATION_CONNECTION_MAP[type])
|
||||
// throw new BadRequestError({
|
||||
// message: `Secret sync with ID "${secretRotation.id}" is not configured for ${SECRET_ROTATION_NAME_MAP[type]}`
|
||||
// });
|
||||
//
|
||||
// if (!secretRotation.folderId)
|
||||
// throw new BadRequestError({
|
||||
// message: `Invalid source configuration: folder no longer exists. Please configure a valid source and try again.`
|
||||
// });
|
||||
//
|
||||
// const isRotationJobRunning = Boolean(await keyStore.getItem(KeyStorePrefixes.SecretRotationLock(rotationId)));
|
||||
//
|
||||
// if (isRotationJobRunning)
|
||||
// throw new BadRequestError({ message: `A job for this sync is already in progress. Please try again shortly.` });
|
||||
//
|
||||
// await secretRotationQueue.queueSecretRotationRotationSecretsById({ rotationId, ...params });
|
||||
//
|
||||
// const updatedSecretRotation = await secretRotationDAL.updateById(rotationId, {
|
||||
// syncStatus: SecretRotationStatus.Pending
|
||||
// });
|
||||
//
|
||||
// return updatedSecretRotation as TSecretRotation;
|
||||
// };
|
||||
//
|
||||
// const triggerSecretRotationImportSecretsById = async (
|
||||
// { rotationId, type, ...params }: TTriggerSecretRotationImportSecretsByIdDTO,
|
||||
// actor: OrgServiceActor
|
||||
// ) => {
|
||||
// if (!listSecretRotationOptions().find((option) => option.type === type)?.canImportSecrets) {
|
||||
// throw new BadRequestError({
|
||||
// message: `${SECRET_ROTATION_NAME_MAP[type]} does not support importing secrets.`
|
||||
// });
|
||||
// }
|
||||
//
|
||||
// const secretRotation = await secretRotationDAL.findById(rotationId);
|
||||
//
|
||||
// if (!secretRotation)
|
||||
// throw new NotFoundError({
|
||||
// message: `Could not find ${SECRET_ROTATION_NAME_MAP[type]} Rotation with ID "${rotationId}"`
|
||||
// });
|
||||
//
|
||||
// const { permission } = await permissionService.getProjectPermission({
|
||||
// actor: actor.type,
|
||||
// actorId: actor.id,
|
||||
// actorAuthMethod: actor.authMethod,
|
||||
// actorOrgId: actor.orgId,
|
||||
// actionProjectType: ActionProjectType.SecretManager,
|
||||
// projectId: secretRotation.projectId
|
||||
// });
|
||||
//
|
||||
// ForbiddenError.from(permission).throwUnlessCan(
|
||||
// ProjectPermissionSecretRotationActions.ImportSecrets,
|
||||
// ProjectPermissionSub.SecretRotations
|
||||
// );
|
||||
//
|
||||
// if (secretRotation.connection.app !== SECRET_ROTATION_CONNECTION_MAP[type])
|
||||
// throw new BadRequestError({
|
||||
// message: `Secret sync with ID "${secretRotation.id}" is not configured for ${SECRET_ROTATION_NAME_MAP[type]}`
|
||||
// });
|
||||
//
|
||||
// if (!secretRotation.folderId)
|
||||
// throw new BadRequestError({
|
||||
// message: `Invalid source configuration: folder no longer exists. Please configure a valid source and try again.`
|
||||
// });
|
||||
//
|
||||
// const isRotationJobRunning = Boolean(await keyStore.getItem(KeyStorePrefixes.SecretRotationLock(rotationId)));
|
||||
//
|
||||
// if (isRotationJobRunning)
|
||||
// throw new BadRequestError({ message: `A job for this sync is already in progress. Please try again shortly.` });
|
||||
//
|
||||
// await secretRotationQueue.queueSecretRotationImportSecretsById({ rotationId, ...params });
|
||||
//
|
||||
// const updatedSecretRotation = await secretRotationDAL.updateById(rotationId, {
|
||||
// importStatus: SecretRotationStatus.Pending
|
||||
// });
|
||||
//
|
||||
// return updatedSecretRotation as TSecretRotation;
|
||||
// };
|
||||
//
|
||||
// const triggerSecretRotationRemoveSecretsById = async (
|
||||
// { rotationId, type, ...params }: TTriggerSecretRotationRemoveSecretsByIdDTO,
|
||||
// actor: OrgServiceActor
|
||||
// ) => {
|
||||
// const secretRotation = await secretRotationDAL.findById(rotationId);
|
||||
//
|
||||
// if (!secretRotation)
|
||||
// throw new NotFoundError({
|
||||
// message: `Could not find ${SECRET_ROTATION_NAME_MAP[type]} Rotation with ID "${rotationId}"`
|
||||
// });
|
||||
//
|
||||
// const { permission } = await permissionService.getProjectPermission({
|
||||
// actor: actor.type,
|
||||
// actorId: actor.id,
|
||||
// actorAuthMethod: actor.authMethod,
|
||||
// actorOrgId: actor.orgId,
|
||||
// actionProjectType: ActionProjectType.SecretManager,
|
||||
// projectId: secretRotation.projectId
|
||||
// });
|
||||
//
|
||||
// ForbiddenError.from(permission).throwUnlessCan(
|
||||
// ProjectPermissionSecretRotationActions.RemoveSecrets,
|
||||
// ProjectPermissionSub.SecretRotations
|
||||
// );
|
||||
//
|
||||
// if (secretRotation.connection.app !== SECRET_ROTATION_CONNECTION_MAP[type])
|
||||
// throw new BadRequestError({
|
||||
// message: `Secret sync with ID "${secretRotation.id}" is not configured for ${SECRET_ROTATION_NAME_MAP[type]}`
|
||||
// });
|
||||
//
|
||||
// if (!secretRotation.folderId)
|
||||
// throw new BadRequestError({
|
||||
// message: `Invalid source configuration: folder no longer exists. Please configure a valid source and try again.`
|
||||
// });
|
||||
//
|
||||
// const isRotationJobRunning = Boolean(await keyStore.getItem(KeyStorePrefixes.SecretRotationLock(rotationId)));
|
||||
//
|
||||
// if (isRotationJobRunning)
|
||||
// throw new BadRequestError({ message: `A job for this sync is already in progress. Please try again shortly.` });
|
||||
//
|
||||
// await secretRotationQueue.queueSecretRotationRemoveSecretsById({ rotationId, ...params });
|
||||
//
|
||||
// const updatedSecretRotation = await secretRotationDAL.updateById(rotationId, {
|
||||
// removeStatus: SecretRotationStatus.Pending
|
||||
// });
|
||||
//
|
||||
// return updatedSecretRotation as TSecretRotation;
|
||||
// };
|
||||
|
||||
return {
|
||||
listSecretRotationOptions,
|
||||
listSecretRotationsByProjectId,
|
||||
createSecretRotation,
|
||||
updateSecretRotation,
|
||||
findSecretRotationById,
|
||||
findSecretRotationByName,
|
||||
deleteSecretRotation
|
||||
// triggerSecretRotationRotationSecretsById,
|
||||
// triggerSecretRotationImportSecretsById,
|
||||
// triggerSecretRotationRemoveSecretsById
|
||||
};
|
||||
};
|
@ -0,0 +1,52 @@
|
||||
import {
|
||||
TPostgresCredentialsRotation,
|
||||
TPostgresCredentialsRotationInput,
|
||||
TPostgresCredentialsRotationListItem,
|
||||
TPostgresCredentialsRotationWithConnection
|
||||
} from "./postgres-credentials";
|
||||
import { SecretRotation } from "./secret-rotation-v2-enums";
|
||||
|
||||
export type TSecretRotationV2 = TPostgresCredentialsRotation;
|
||||
|
||||
export type TSecretRotationV2WithConnection = TPostgresCredentialsRotationWithConnection;
|
||||
|
||||
export type TSecretRotationV2Input = TPostgresCredentialsRotationInput;
|
||||
|
||||
export type TSecretRotationV2ListItem = TPostgresCredentialsRotationListItem;
|
||||
|
||||
export type TListSecretRotationsV2ByProjectId = {
|
||||
projectId: string;
|
||||
type?: SecretRotation;
|
||||
};
|
||||
|
||||
export type TFindSecretRotationV2ByIdDTO = {
|
||||
rotationId: string;
|
||||
type: SecretRotation;
|
||||
};
|
||||
|
||||
export type TFindSecretRotationV2ByNameDTO = {
|
||||
rotationName: string;
|
||||
projectId: string;
|
||||
type: SecretRotation;
|
||||
};
|
||||
|
||||
export type TCreateSecretRotationV2DTO = Pick<
|
||||
TSecretRotationV2,
|
||||
"parameters" | "description" | "interval" | "name" | "connectionId" | "projectId"
|
||||
> & {
|
||||
type: SecretRotation;
|
||||
secretPath: string;
|
||||
environment: string;
|
||||
isAutoRotationEnabled?: boolean;
|
||||
};
|
||||
|
||||
export type TUpdateSecretRotationV2DTO = Partial<Omit<TCreateSecretRotationV2DTO, "projectId" | "connectionId">> & {
|
||||
rotationId: string;
|
||||
type: SecretRotation;
|
||||
};
|
||||
|
||||
export type TDeleteSecretRotationV2DTO = {
|
||||
type: SecretRotation;
|
||||
rotationId: string;
|
||||
removeSecrets: boolean;
|
||||
};
|
@ -8,49 +8,23 @@ import axios from "axios";
|
||||
import jmespath from "jmespath";
|
||||
import knex from "knex";
|
||||
|
||||
import { getConfig } from "@app/lib/config/env";
|
||||
import { getDbConnectionHost } from "@app/lib/knex";
|
||||
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
||||
|
||||
import { verifyHostInputValidity } from "../../dynamic-secret/dynamic-secret-fns";
|
||||
import { TAssignOp, TDbProviderClients, TDirectAssignOp, THttpProviderFunction } from "../templates/types";
|
||||
import { TSecretRotationData, TSecretRotationDbFn } from "./secret-rotation-queue-types";
|
||||
|
||||
const REGEX = /\${([^}]+)}/g;
|
||||
const EXTERNAL_REQUEST_TIMEOUT = 10 * 1000;
|
||||
|
||||
const replaceTemplateVariables = (str: string, getValue: (key: string) => unknown) => {
|
||||
// Use array to collect pieces and join at the end (more efficient for large strings)
|
||||
const parts: string[] = [];
|
||||
let pos = 0;
|
||||
|
||||
while (pos < str.length) {
|
||||
const start = str.indexOf("${", pos);
|
||||
if (start === -1) {
|
||||
parts.push(str.slice(pos));
|
||||
break;
|
||||
}
|
||||
|
||||
parts.push(str.slice(pos, start));
|
||||
const end = str.indexOf("}", start + 2);
|
||||
|
||||
if (end === -1) {
|
||||
parts.push(str.slice(start));
|
||||
break;
|
||||
}
|
||||
|
||||
const varName = str.slice(start + 2, end);
|
||||
parts.push(String(getValue(varName)));
|
||||
pos = end + 1;
|
||||
}
|
||||
|
||||
return parts.join("");
|
||||
};
|
||||
|
||||
export const interpolate = (data: any, getValue: (key: string) => unknown) => {
|
||||
if (!data) return;
|
||||
|
||||
if (typeof data === "number") return data;
|
||||
|
||||
if (typeof data === "string") {
|
||||
return replaceTemplateVariables(data, getValue);
|
||||
return data.replace(REGEX, (_a, b) => getValue(b) as string);
|
||||
}
|
||||
|
||||
if (typeof data === "object" && Array.isArray(data)) {
|
||||
@ -114,14 +88,32 @@ export const secretRotationDbFn = async ({
|
||||
variables,
|
||||
options
|
||||
}: TSecretRotationDbFn) => {
|
||||
const appCfg = getConfig();
|
||||
|
||||
const ssl = ca ? { rejectUnauthorized: false, ca } : undefined;
|
||||
const [hostIp] = await verifyHostInputValidity(host);
|
||||
const isCloud = Boolean(appCfg.LICENSE_SERVER_KEY); // quick and dirty way to check if its cloud or not
|
||||
const dbHost = appCfg.DB_HOST || getDbConnectionHost(appCfg.DB_CONNECTION_URI);
|
||||
|
||||
if (
|
||||
isCloud &&
|
||||
// internal ips
|
||||
(host === "host.docker.internal" || host.match(/^10\.\d+\.\d+\.\d+/) || host.match(/^192\.168\.\d+\.\d+/))
|
||||
)
|
||||
throw new Error("Invalid db host");
|
||||
if (
|
||||
host === "localhost" ||
|
||||
host === "127.0.0.1" ||
|
||||
// database infisical uses
|
||||
dbHost === host
|
||||
)
|
||||
throw new Error("Invalid db host");
|
||||
|
||||
const db = knex({
|
||||
client,
|
||||
connection: {
|
||||
database,
|
||||
port,
|
||||
host: hostIp,
|
||||
host,
|
||||
user: username,
|
||||
password,
|
||||
connectionTimeoutMillis: EXTERNAL_REQUEST_TIMEOUT,
|
||||
|
@ -8,18 +8,7 @@ type GetFullFolderPath = {
|
||||
|
||||
export const getFullFolderPath = async ({ folderDAL, folderId, envId }: GetFullFolderPath): Promise<string> => {
|
||||
// Helper function to remove duplicate slashes
|
||||
const removeDuplicateSlashes = (path: string) => {
|
||||
const chars = [];
|
||||
let lastWasSlash = false;
|
||||
|
||||
for (let i = 0; i < path.length; i += 1) {
|
||||
const char = path[i];
|
||||
if (char !== "/" || !lastWasSlash) chars.push(char);
|
||||
lastWasSlash = char === "/";
|
||||
}
|
||||
|
||||
return chars.join("");
|
||||
};
|
||||
const removeDuplicateSlashes = (path: string) => path.replace(/\/{2,}/g, "/");
|
||||
|
||||
// Fetch all folders at once based on environment ID to avoid multiple queries
|
||||
const folders = await folderDAL.find({ envId });
|
||||
|
@ -1,34 +1,14 @@
|
||||
import { isIP } from "net";
|
||||
|
||||
import { isFQDN } from "@app/lib/validator/validate-url";
|
||||
|
||||
// Validates usernames or wildcard (*)
|
||||
export const isValidUserPattern = (value: string): boolean => {
|
||||
// Length check before regex to prevent ReDoS
|
||||
if (typeof value !== "string") return false;
|
||||
if (value.length > 32) return false; // Maximum Linux username length
|
||||
if (value === "*") return true; // Handle wildcard separately
|
||||
|
||||
// Simpler, more specific pattern for usernames
|
||||
const userRegex = /^[a-z_][a-z0-9_-]*$/i;
|
||||
// Matches valid Linux usernames or a wildcard (*)
|
||||
const userRegex = /^(?:\*|[a-z_][a-z0-9_-]{0,31})$/;
|
||||
return userRegex.test(value);
|
||||
};
|
||||
|
||||
// Validates hostnames, wildcard domains, or IP addresses
|
||||
export const isValidHostPattern = (value: string): boolean => {
|
||||
// Input validation
|
||||
if (typeof value !== "string") return false;
|
||||
|
||||
// Length check
|
||||
if (value.length > 255) return false;
|
||||
|
||||
// Handle the wildcard case separately
|
||||
if (value === "*") return true;
|
||||
|
||||
// Check for IP addresses using Node.js built-in functions
|
||||
if (isIP(value)) return true;
|
||||
|
||||
return isFQDN(value, {
|
||||
allow_wildcard: true
|
||||
});
|
||||
// Matches FQDNs, wildcard domains (*.example.com), IPv4, and IPv6 addresses
|
||||
const hostRegex =
|
||||
/^(?:\*|\*\.[a-z0-9-]+(?:\.[a-z0-9-]+)*|[a-z0-9-]+(?:\.[a-z0-9-]+)*|\d{1,3}(\.\d{1,3}){3}|([a-fA-F0-9:]+:+)+[a-fA-F0-9]+(?:%[a-zA-Z0-9]+)?)$/;
|
||||
return hostRegex.test(value);
|
||||
};
|
||||
|
@ -8,7 +8,6 @@ import { promisify } from "util";
|
||||
import { TSshCertificateTemplates } from "@app/db/schemas";
|
||||
import { BadRequestError } from "@app/lib/errors";
|
||||
import { ms } from "@app/lib/ms";
|
||||
import { CharacterType, characterValidator } from "@app/lib/validator/validate-string";
|
||||
import { CertKeyAlgorithm } from "@app/services/certificate/certificate-types";
|
||||
|
||||
import {
|
||||
@ -19,7 +18,6 @@ import { SshCertType, TCreateSshCertDTO } from "./ssh-certificate-authority-type
|
||||
|
||||
const execFileAsync = promisify(execFile);
|
||||
|
||||
const EXEC_TIMEOUT_MS = 10000; // 10 seconds
|
||||
/* eslint-disable no-bitwise */
|
||||
export const createSshCertSerialNumber = () => {
|
||||
const randomBytes = crypto.randomBytes(8); // 8 bytes = 64 bits
|
||||
@ -66,9 +64,7 @@ export const createSshKeyPair = async (keyAlgorithm: CertKeyAlgorithm) => {
|
||||
// Generate the SSH key pair
|
||||
// The "-N ''" sets an empty passphrase
|
||||
// The keys are created in the temporary directory
|
||||
await execFileAsync("ssh-keygen", ["-t", keyType, "-b", keyBits, "-f", privateKeyFile, "-N", ""], {
|
||||
timeout: EXEC_TIMEOUT_MS
|
||||
});
|
||||
await execFileAsync("ssh-keygen", ["-t", keyType, "-b", keyBits, "-f", privateKeyFile, "-N", ""]);
|
||||
|
||||
// Read the generated keys
|
||||
const publicKey = await fs.readFile(publicKeyFile, "utf8");
|
||||
@ -91,10 +87,7 @@ export const getSshPublicKey = async (privateKey: string) => {
|
||||
await fs.writeFile(privateKeyFile, privateKey, { mode: 0o600 });
|
||||
|
||||
// Run ssh-keygen to extract the public key
|
||||
const { stdout } = await execFileAsync("ssh-keygen", ["-y", "-f", privateKeyFile], {
|
||||
encoding: "utf8",
|
||||
timeout: EXEC_TIMEOUT_MS
|
||||
});
|
||||
const { stdout } = await execFileAsync("ssh-keygen", ["-y", "-f", privateKeyFile], { encoding: "utf8" });
|
||||
return stdout.trim();
|
||||
} finally {
|
||||
// Ensure that files and the temporary directory are cleaned up
|
||||
@ -150,14 +143,7 @@ export const validateSshCertificatePrincipals = (
|
||||
}
|
||||
|
||||
// restrict allowed characters to letters, digits, dot, underscore, and hyphen
|
||||
if (
|
||||
!characterValidator([
|
||||
CharacterType.AlphaNumeric,
|
||||
CharacterType.Period,
|
||||
CharacterType.Underscore,
|
||||
CharacterType.Hyphen
|
||||
])(sanitized)
|
||||
) {
|
||||
if (!/^[A-Za-z0-9._-]+$/.test(sanitized)) {
|
||||
throw new BadRequestError({
|
||||
message: `Principal '${sanitized}' contains invalid characters. Allowed: alphanumeric, '.', '_', '-'.`
|
||||
});
|
||||
@ -280,8 +266,8 @@ export const validateSshCertificateTtl = (template: TSshCertificateTemplates, tt
|
||||
* that it only contains alphanumeric characters with no spaces.
|
||||
*/
|
||||
export const validateSshCertificateKeyId = (keyId: string) => {
|
||||
const regex = characterValidator([CharacterType.AlphaNumeric, CharacterType.Hyphen]);
|
||||
if (!regex(keyId)) {
|
||||
const regex = /^[A-Za-z0-9-]+$/;
|
||||
if (!regex.test(keyId)) {
|
||||
throw new BadRequestError({
|
||||
message:
|
||||
"Failed to validate Key ID because it can only contain alphanumeric characters and hyphens, with no spaces."
|
||||
@ -312,7 +298,7 @@ const validateSshPublicKey = async (publicKey: string) => {
|
||||
|
||||
try {
|
||||
await fs.writeFile(pubKeyFile, publicKey, { mode: 0o600 });
|
||||
await execFileAsync("ssh-keygen", ["-l", "-f", pubKeyFile], { timeout: EXEC_TIMEOUT_MS });
|
||||
await execFileAsync("ssh-keygen", ["-l", "-f", pubKeyFile]);
|
||||
} catch (error) {
|
||||
throw new BadRequestError({
|
||||
message: "Failed to validate SSH public key format: could not be parsed."
|
||||
@ -377,7 +363,7 @@ export const createSshCert = async ({
|
||||
await fs.writeFile(privateKeyFile, caPrivateKey, { mode: 0o600 });
|
||||
|
||||
// Execute the signing process
|
||||
await execFileAsync("ssh-keygen", sshKeygenArgs, { encoding: "utf8", timeout: EXEC_TIMEOUT_MS });
|
||||
await execFileAsync("ssh-keygen", sshKeygenArgs, { encoding: "utf8" });
|
||||
|
||||
// Read the signed public key from the generated cert file
|
||||
const signedPublicKey = await fs.readFile(signedPublicKeyFile, "utf8");
|
||||
|
@ -1,3 +1,8 @@
|
||||
import { SecretRotation } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-enums";
|
||||
import {
|
||||
SECRET_ROTATION_CONNECTION_MAP,
|
||||
SECRET_ROTATION_NAME_MAP
|
||||
} from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-maps";
|
||||
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
||||
import { APP_CONNECTION_NAME_MAP } from "@app/services/app-connection/app-connection-maps";
|
||||
import { SecretSync } from "@app/services/secret-sync/secret-sync-enums";
|
||||
@ -244,7 +249,7 @@ export const KUBERNETES_AUTH = {
|
||||
kubernetesHost: "The host string, host:port pair, or URL to the base of the Kubernetes API server.",
|
||||
caCert: "The PEM-encoded CA cert for the Kubernetes API server.",
|
||||
tokenReviewerJwt:
|
||||
"Optional JWT token for accessing Kubernetes TokenReview API. If provided, this long-lived token will be used to validate service account tokens during authentication. If omitted, the client's own JWT will be used instead, which requires the client to have the system:auth-delegator ClusterRole binding.",
|
||||
"The long-lived service account JWT token for Infisical to access the TokenReview API to validate other service account JWT tokens submitted by applications/pods.",
|
||||
allowedNamespaces:
|
||||
"The comma-separated list of trusted namespaces that service accounts must belong to authenticate with Infisical.",
|
||||
allowedNames: "The comma-separated list of trusted service account names that can authenticate with Infisical.",
|
||||
@ -260,7 +265,7 @@ export const KUBERNETES_AUTH = {
|
||||
kubernetesHost: "The new host string, host:port pair, or URL to the base of the Kubernetes API server.",
|
||||
caCert: "The new PEM-encoded CA cert for the Kubernetes API server.",
|
||||
tokenReviewerJwt:
|
||||
"Optional JWT token for accessing Kubernetes TokenReview API. If provided, this long-lived token will be used to validate service account tokens during authentication. If omitted, the client's own JWT will be used instead, which requires the client to have the system:auth-delegator ClusterRole binding.",
|
||||
"The new long-lived service account JWT token for Infisical to access the TokenReview API to validate other service account JWT tokens submitted by applications/pods.",
|
||||
allowedNamespaces:
|
||||
"The new comma-separated list of trusted namespaces that service accounts must belong to authenticate with Infisical.",
|
||||
allowedNames: "The new comma-separated list of trusted service account names that can authenticate with Infisical.",
|
||||
@ -329,7 +334,6 @@ export const OIDC_AUTH = {
|
||||
boundIssuer: "The unique identifier of the identity provider issuing the JWT.",
|
||||
boundAudiences: "The list of intended recipients.",
|
||||
boundClaims: "The attributes that should be present in the JWT for it to be valid.",
|
||||
claimMetadataMapping: "The attributes that should be present in the permission metadata from the JWT.",
|
||||
boundSubject: "The expected principal that is the subject of the JWT.",
|
||||
accessTokenTrustedIps: "The IPs or CIDR ranges that access tokens can be used from.",
|
||||
accessTokenTTL: "The lifetime for an access token in seconds.",
|
||||
@ -343,7 +347,6 @@ export const OIDC_AUTH = {
|
||||
boundIssuer: "The new unique identifier of the identity provider issuing the JWT.",
|
||||
boundAudiences: "The new list of intended recipients.",
|
||||
boundClaims: "The new attributes that should be present in the JWT for it to be valid.",
|
||||
claimMetadataMapping: "The new attributes that should be present in the permission metadata from the JWT.",
|
||||
boundSubject: "The new expected principal that is the subject of the JWT.",
|
||||
accessTokenTrustedIps: "The new IPs or CIDR ranges that access tokens can be used from.",
|
||||
accessTokenTTL: "The new lifetime for an access token in seconds.",
|
||||
@ -631,8 +634,7 @@ export const FOLDERS = {
|
||||
workspaceId: "The ID of the project to list folders from.",
|
||||
environment: "The slug of the environment to list folders from.",
|
||||
path: "The path to list folders from.",
|
||||
directory: "The directory to list folders from. (Deprecated in favor of path)",
|
||||
recursive: "Whether or not to fetch all folders from the specified base path, and all of its subdirectories."
|
||||
directory: "The directory to list folders from. (Deprecated in favor of path)"
|
||||
},
|
||||
GET_BY_ID: {
|
||||
folderId: "The ID of the folder to get details."
|
||||
@ -816,8 +818,7 @@ export const DASHBOARD = {
|
||||
search: "The text string to filter secret keys and folder names by.",
|
||||
includeSecrets: "Whether to include project secrets in the response.",
|
||||
includeFolders: "Whether to include project folders in the response.",
|
||||
includeDynamicSecrets: "Whether to include dynamic project secrets in the response.",
|
||||
includeImports: "Whether to include project secret imports in the response."
|
||||
includeDynamicSecrets: "Whether to include dynamic project secrets in the response."
|
||||
},
|
||||
SECRET_DETAILS_LIST: {
|
||||
projectId: "The ID of the project to list secrets/folders from.",
|
||||
@ -1653,7 +1654,8 @@ export const AppConnections = {
|
||||
name: `The name of the ${appName} Connection to create. Must be slug-friendly.`,
|
||||
description: `An optional description for the ${appName} Connection.`,
|
||||
credentials: `The credentials used to connect with ${appName}.`,
|
||||
method: `The method used to authenticate with ${appName}.`
|
||||
method: `The method used to authenticate with ${appName}.`,
|
||||
isPlatformManaged: `Whether or not the ${appName} Connection should be managed by Infisical.`
|
||||
};
|
||||
},
|
||||
UPDATE: (app: AppConnection) => {
|
||||
@ -1663,7 +1665,8 @@ export const AppConnections = {
|
||||
name: `The updated name of the ${appName} Connection. Must be slug-friendly.`,
|
||||
description: `The updated description of the ${appName} Connection.`,
|
||||
credentials: `The credentials used to connect with ${appName}.`,
|
||||
method: `The method used to authenticate with ${appName}.`
|
||||
method: `The method used to authenticate with ${appName}.`,
|
||||
isPlatformManaged: `Whether or not the ${appName} Connection should be managed by Infisical.`
|
||||
};
|
||||
},
|
||||
DELETE: (app: AppConnection) => ({
|
||||
@ -1729,8 +1732,7 @@ export const SecretSyncs = {
|
||||
SYNC_OPTIONS: (destination: SecretSync) => {
|
||||
const destinationName = SECRET_SYNC_NAME_MAP[destination];
|
||||
return {
|
||||
initialSyncBehavior: `Specify how Infisical should resolve the initial sync to the ${destinationName} destination.`,
|
||||
disableSecretDeletion: `Enable this flag to prevent removal of secrets from the ${destinationName} destination when syncing.`
|
||||
initialSyncBehavior: `Specify how Infisical should resolve the initial sync to the ${destinationName} destination.`
|
||||
};
|
||||
},
|
||||
ADDITIONAL_SYNC_OPTIONS: {
|
||||
@ -1785,3 +1787,67 @@ export const SecretSyncs = {
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const SecretRotations = {
|
||||
LIST: (type?: SecretRotation) => ({
|
||||
projectId: `The ID of the project to list ${type ? SECRET_ROTATION_NAME_MAP[type] : "Secret"} Rotations from.`
|
||||
}),
|
||||
GET_BY_ID: (type: SecretRotation) => ({
|
||||
rotationId: `The ID of the ${SECRET_ROTATION_NAME_MAP[type]} Rotation to retrieve.`
|
||||
}),
|
||||
GET_CREDENTIALS_BY_ID: (type: SecretRotation) => ({
|
||||
rotationId: `The ID of the ${SECRET_ROTATION_NAME_MAP[type]} Rotation to retrieve the credentials for.`
|
||||
}),
|
||||
GET_BY_NAME: (type: SecretRotation) => ({
|
||||
rotationName: `The name of the ${SECRET_ROTATION_NAME_MAP[type]} Rotation to retrieve.`,
|
||||
projectId: `The ID of the project the ${SECRET_ROTATION_NAME_MAP[type]} Rotation is associated with.`
|
||||
}),
|
||||
CREATE: (type: SecretRotation) => {
|
||||
const destinationName = SECRET_ROTATION_NAME_MAP[type];
|
||||
return {
|
||||
name: `The name of the ${destinationName} Rotation to create. Must be slug-friendly.`,
|
||||
description: `An optional description for the ${destinationName} Rotation.`,
|
||||
projectId: "The ID of the project to create the rotation in.",
|
||||
environment: `The slug of the project environment to create the rotation in.`,
|
||||
secretPath: `The folder path to of the project to create the rotation in.`,
|
||||
connectionId: `The ID of the ${
|
||||
APP_CONNECTION_NAME_MAP[SECRET_ROTATION_CONNECTION_MAP[type]]
|
||||
} Connection to use for rotation.`,
|
||||
isAutoRotationEnabled: `Whether secrets should be automatically rotated when the specified interval has elapsed.`,
|
||||
interval: `The interval, in days, to automatically rotate secrets.`
|
||||
};
|
||||
},
|
||||
UPDATE: (type: SecretRotation) => {
|
||||
const typeName = SECRET_ROTATION_NAME_MAP[type];
|
||||
return {
|
||||
rotationId: `The ID of the ${typeName} Rotation to be updated.`,
|
||||
name: `The updated name of the ${typeName} Rotation. Must be slug-friendly.`,
|
||||
environment: `The updated slug of the project environment to move the rotation to.`,
|
||||
secretPath: `The updated folder path to move the rotation to.`,
|
||||
description: `The updated description of the ${typeName} Rotation.`,
|
||||
isAutoRotationEnabled: `Whether secrets should be automatically rotated when the specified interval has elapsed.`,
|
||||
interval: `The interval, in days, to automatically rotate secrets.`
|
||||
};
|
||||
},
|
||||
DELETE: (type: SecretRotation) => ({
|
||||
rotationId: `The ID of the ${SECRET_ROTATION_NAME_MAP[type]} Rotation to be deleted.`,
|
||||
removeSecrets: `Whether the secrets belonging to this rotation should be deleted.`
|
||||
}),
|
||||
ROTATE: (type: SecretRotation) => ({
|
||||
rotationId: `The ID of the ${SECRET_ROTATION_NAME_MAP[type]} Rotation to rotate credentials for.`
|
||||
}),
|
||||
PARAMETERS: {
|
||||
POSTGRES_CREDENTIALS: {
|
||||
usernameSecretKey: "The secret key that the generated username will be mapped to.",
|
||||
passwordSecretKey: "The secret key that the generated password will be mapped to.",
|
||||
issueStatement: "The SQL statement to generate the credentials on rotation.",
|
||||
revokeStatement: "The SQL statement to revoke expired credentials on rotation."
|
||||
},
|
||||
MSSQL_CREDENTIALS: {
|
||||
usernameSecretKey: "The secret key that the generated username will be mapped to.",
|
||||
passwordSecretKey: "The secret key that the generated password will be mapped to.",
|
||||
issueStatement: "The SQL statement to generate the credentials on rotation.",
|
||||
revokeStatement: "The SQL statement to revoke expired credentials on rotation."
|
||||
}
|
||||
}
|
||||
};
|
||||
|
@ -28,8 +28,8 @@ export const createDigestAuthRequestInterceptor = (
|
||||
nc += 1;
|
||||
const nonceCount = nc.toString(16).padStart(8, "0");
|
||||
const cnonce = crypto.randomBytes(24).toString("hex");
|
||||
const realm = authDetails.find((el) => el[0].toLowerCase().indexOf("realm") > -1)?.[1]?.replaceAll('"', "") || "";
|
||||
const nonce = authDetails.find((el) => el[0].toLowerCase().indexOf("nonce") > -1)?.[1]?.replaceAll('"', "") || "";
|
||||
const realm = authDetails.find((el) => el[0].toLowerCase().indexOf("realm") > -1)?.[1].replace(/"/g, "");
|
||||
const nonce = authDetails.find((el) => el[0].toLowerCase().indexOf("nonce") > -1)?.[1].replace(/"/g, "");
|
||||
const ha1 = crypto.createHash("md5").update(`${username}:${realm}:${password}`).digest("hex");
|
||||
const path = opts.url;
|
||||
|
||||
|
@ -1,35 +1,26 @@
|
||||
type Base64Options = {
|
||||
urlSafe?: boolean;
|
||||
padding?: boolean;
|
||||
};
|
||||
|
||||
const base64WithPadding = /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=|[A-Za-z0-9+/]{4})$/;
|
||||
const base64WithoutPadding = /^[A-Za-z0-9+/]+$/;
|
||||
const base64UrlWithPadding = /^(?:[A-Za-z0-9_-]{4})*(?:[A-Za-z0-9_-]{2}==|[A-Za-z0-9_-]{3}=|[A-Za-z0-9_-]{4})$/;
|
||||
const base64UrlWithoutPadding = /^[A-Za-z0-9_-]+$/;
|
||||
|
||||
export const isBase64 = (str: string, options: Base64Options = {}): boolean => {
|
||||
if (typeof str !== "string") {
|
||||
throw new TypeError("Expected a string");
|
||||
// Credit: https://github.com/miguelmota/is-base64
|
||||
export const isBase64 = (
|
||||
v: string,
|
||||
opts = { allowEmpty: false, mimeRequired: false, allowMime: true, paddingRequired: false }
|
||||
) => {
|
||||
if (opts.allowEmpty === false && v === "") {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Default padding to true unless urlSafe is true
|
||||
const opts: Base64Options = {
|
||||
urlSafe: false,
|
||||
padding: options.urlSafe === undefined ? true : !options.urlSafe,
|
||||
...options
|
||||
};
|
||||
let regex = "(?:[A-Za-z0-9+\\/]{4})*(?:[A-Za-z0-9+\\/]{2}==|[A-Za-z0-9+/]{3}=)?";
|
||||
const mimeRegex = "(data:\\w+\\/[a-zA-Z\\+\\-\\.]+;base64,)";
|
||||
|
||||
if (str === "") return true;
|
||||
|
||||
let regex;
|
||||
if (opts.urlSafe) {
|
||||
regex = opts.padding ? base64UrlWithPadding : base64UrlWithoutPadding;
|
||||
} else {
|
||||
regex = opts.padding ? base64WithPadding : base64WithoutPadding;
|
||||
if (opts.mimeRequired === true) {
|
||||
regex = mimeRegex + regex;
|
||||
} else if (opts.allowMime === true) {
|
||||
regex = `${mimeRegex}?${regex}`;
|
||||
}
|
||||
|
||||
return (!opts.padding || str.length % 4 === 0) && regex.test(str);
|
||||
if (opts.paddingRequired === false) {
|
||||
regex = "(?:[A-Za-z0-9+\\/]{4})*(?:[A-Za-z0-9+\\/]{2}(==)?|[A-Za-z0-9+\\/]{3}=?)?";
|
||||
}
|
||||
|
||||
return new RegExp(`^${regex}$`, "gi").test(v);
|
||||
};
|
||||
|
||||
export const getBase64SizeInBytes = (base64String: string) => {
|
||||
|
@ -1,42 +0,0 @@
|
||||
import { extractX509CertFromChain } from "./extract-certificate";
|
||||
|
||||
describe("Extract Certificate Payload", () => {
|
||||
test("Single chain", () => {
|
||||
const payload = `-----BEGIN CERTIFICATE-----
|
||||
MIIEZzCCA0+gAwIBAgIUDk9+HZcMHppiNy0TvoBg8/aMEqIwDQYJKoZIhvcNAQEL
|
||||
BQAwDTELMAkGA1UEChMCUEgwHhcNMjQxMDI1MTU0MjAzWhcNMjUxMDI1MjE0MjAz
|
||||
-----END CERTIFICATE-----`;
|
||||
const result = extractX509CertFromChain(payload);
|
||||
expect(result).toBeDefined();
|
||||
expect(result?.length).toBe(1);
|
||||
expect(result?.[0]).toEqual(payload);
|
||||
});
|
||||
|
||||
test("Multiple chain", () => {
|
||||
const payload = `-----BEGIN CERTIFICATE-----
|
||||
MIIEZzCCA0+gAwIBAgIUDk9+HZcMHppiNy0TvoBg8/aMEqIwDQYJKoZIhvcNAQEL
|
||||
BQAwDTELMAkGA1UEChMCUEgwHhcNMjQxMDI1MTU0MjAzWhcNMjUxMDI1MjE0MjAz
|
||||
-----END CERTIFICATE-----
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIEZzCCA0+gAwIBAgIUDk9+HZcMHppiNy0TvoBg8/aMEqIwDQYJKoZIhvcNAQEL
|
||||
-----END CERTIFICATE-----
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIEZzCCA0+gAwIBAgIUDk9+HZcMHppiNy0TvoBg8/aMEqIwDQYJKoZIhvcNAQEL
|
||||
-----END CERTIFICATE-----`;
|
||||
const result = extractX509CertFromChain(payload);
|
||||
expect(result).toBeDefined();
|
||||
expect(result?.length).toBe(3);
|
||||
expect(result).toEqual([
|
||||
`-----BEGIN CERTIFICATE-----
|
||||
MIIEZzCCA0+gAwIBAgIUDk9+HZcMHppiNy0TvoBg8/aMEqIwDQYJKoZIhvcNAQEL
|
||||
BQAwDTELMAkGA1UEChMCUEgwHhcNMjQxMDI1MTU0MjAzWhcNMjUxMDI1MjE0MjAz
|
||||
-----END CERTIFICATE-----`,
|
||||
`-----BEGIN CERTIFICATE-----
|
||||
MIIEZzCCA0+gAwIBAgIUDk9+HZcMHppiNy0TvoBg8/aMEqIwDQYJKoZIhvcNAQEL
|
||||
-----END CERTIFICATE-----`,
|
||||
`-----BEGIN CERTIFICATE-----
|
||||
MIIEZzCCA0+gAwIBAgIUDk9+HZcMHppiNy0TvoBg8/aMEqIwDQYJKoZIhvcNAQEL
|
||||
-----END CERTIFICATE-----`
|
||||
]);
|
||||
});
|
||||
});
|
@ -1,51 +0,0 @@
|
||||
import { BadRequestError } from "../errors";
|
||||
|
||||
export const extractX509CertFromChain = (certificateChain: string): string[] => {
|
||||
if (!certificateChain) {
|
||||
throw new BadRequestError({
|
||||
message: "Certificate chain is empty or undefined"
|
||||
});
|
||||
}
|
||||
|
||||
const certificates: string[] = [];
|
||||
let currentPosition = 0;
|
||||
const chainLength = certificateChain.length;
|
||||
|
||||
while (currentPosition < chainLength) {
|
||||
// Find the start of a certificate
|
||||
const beginMarker = "-----BEGIN CERTIFICATE-----";
|
||||
const startIndex = certificateChain.indexOf(beginMarker, currentPosition);
|
||||
|
||||
if (startIndex === -1) {
|
||||
break; // No more certificates found
|
||||
}
|
||||
|
||||
// Find the end of the certificate
|
||||
const endMarker = "-----END CERTIFICATE-----";
|
||||
const endIndex = certificateChain.indexOf(endMarker, startIndex);
|
||||
|
||||
if (endIndex === -1) {
|
||||
throw new BadRequestError({
|
||||
message: "Malformed certificate chain: Found BEGIN marker without matching END marker"
|
||||
});
|
||||
}
|
||||
|
||||
// Extract the complete certificate including markers
|
||||
const completeEndIndex = endIndex + endMarker.length;
|
||||
const certificate = certificateChain.substring(startIndex, completeEndIndex);
|
||||
|
||||
// Add the extracted certificate to our results
|
||||
certificates.push(certificate);
|
||||
|
||||
// Move position to after this certificate
|
||||
currentPosition = completeEndIndex;
|
||||
}
|
||||
|
||||
if (certificates.length === 0) {
|
||||
throw new BadRequestError({
|
||||
message: "No valid certificates found in the chain"
|
||||
});
|
||||
}
|
||||
|
||||
return certificates;
|
||||
};
|
@ -56,7 +56,6 @@ const envSchema = z
|
||||
// TODO(akhilmhdh): will be changed to one
|
||||
ENCRYPTION_KEY: zpStr(z.string().optional()),
|
||||
ROOT_ENCRYPTION_KEY: zpStr(z.string().optional()),
|
||||
QUEUE_WORKERS_ENABLED: zodStrBool.default("true"),
|
||||
HTTPS_ENABLED: zodStrBool,
|
||||
// smtp options
|
||||
SMTP_HOST: zpStr(z.string().optional()),
|
||||
|
@ -68,23 +68,6 @@ export class ForbiddenRequestError extends Error {
|
||||
}
|
||||
}
|
||||
|
||||
export class PermissionBoundaryError extends ForbiddenRequestError {
|
||||
constructor({
|
||||
message,
|
||||
name,
|
||||
error,
|
||||
details
|
||||
}: {
|
||||
message?: string;
|
||||
name?: string;
|
||||
error?: unknown;
|
||||
details?: unknown;
|
||||
}) {
|
||||
super({ message, name, error, details });
|
||||
this.name = "PermissionBoundaryError";
|
||||
}
|
||||
}
|
||||
|
||||
export class BadRequestError extends Error {
|
||||
name: string;
|
||||
|
||||
|
@ -93,7 +93,6 @@ export const pingGatewayAndVerify = async ({
|
||||
let lastError: Error | null = null;
|
||||
const quicClient = await createQuicConnection(relayHost, relayPort, tlsOptions, identityId, orgId).catch((err) => {
|
||||
throw new BadRequestError({
|
||||
message: (err as Error)?.message,
|
||||
error: err as Error
|
||||
});
|
||||
});
|
||||
|
@ -107,6 +107,12 @@ export const isValidIp = (ip: string) => {
|
||||
return net.isIPv4(ip) || net.isIPv6(ip);
|
||||
};
|
||||
|
||||
export const isValidHostname = (name: string) => {
|
||||
const hostnameRegex = /^(?!:\/\/)(\*\.)?([a-zA-Z0-9-_]{1,63}\.?)+(?!:\/\/)([a-zA-Z]{2,63})$/;
|
||||
|
||||
return hostnameRegex.test(name);
|
||||
};
|
||||
|
||||
export type TIp = {
|
||||
ipAddress: string;
|
||||
type: IPType;
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user