Merge branch 'Infisical:main' into main

This commit is contained in:
Thalles Passos
2025-09-30 15:11:08 -03:00
committed by GitHub
408 changed files with 21032 additions and 2382 deletions
+5 -1
View File
@@ -122,7 +122,7 @@ INF_APP_CONNECTION_GITHUB_RADAR_APP_WEBHOOK_SECRET=
#gcp app connection #gcp app connection
INF_APP_CONNECTION_GCP_SERVICE_ACCOUNT_CREDENTIAL= INF_APP_CONNECTION_GCP_SERVICE_ACCOUNT_CREDENTIAL=
# azure app connection # azure app connections
INF_APP_CONNECTION_AZURE_APP_CONFIGURATION_CLIENT_ID= INF_APP_CONNECTION_AZURE_APP_CONFIGURATION_CLIENT_ID=
INF_APP_CONNECTION_AZURE_APP_CONFIGURATION_CLIENT_SECRET= INF_APP_CONNECTION_AZURE_APP_CONFIGURATION_CLIENT_SECRET=
@@ -135,6 +135,10 @@ INF_APP_CONNECTION_AZURE_CLIENT_SECRETS_CLIENT_SECRET=
INF_APP_CONNECTION_AZURE_DEVOPS_CLIENT_ID= INF_APP_CONNECTION_AZURE_DEVOPS_CLIENT_ID=
INF_APP_CONNECTION_AZURE_DEVOPS_CLIENT_SECRET= INF_APP_CONNECTION_AZURE_DEVOPS_CLIENT_SECRET=
# heroku app connection
INF_APP_CONNECTION_HEROKU_OAUTH_CLIENT_ID=
INF_APP_CONNECTION_HEROKU_OAUTH_CLIENT_SECRET=
# datadog # datadog
SHOULD_USE_DATADOG_TRACER= SHOULD_USE_DATADOG_TRACER=
DATADOG_PROFILING_ENABLED= DATADOG_PROFILING_ENABLED=
@@ -63,6 +63,8 @@ jobs:
build-args: | build-args: |
POSTHOG_API_KEY=${{ secrets.PUBLIC_POSTHOG_API_KEY }} POSTHOG_API_KEY=${{ secrets.PUBLIC_POSTHOG_API_KEY }}
INFISICAL_PLATFORM_VERSION=${{ steps.extract_version.outputs.version }} INFISICAL_PLATFORM_VERSION=${{ steps.extract_version.outputs.version }}
DD_GIT_REPOSITORY_URL=${{ github.server_url }}/${{ github.repository }}
DD_GIT_COMMIT_SHA=${{ github.sha }}
infisical-fips-standalone: infisical-fips-standalone:
name: Build infisical standalone image postgres name: Build infisical standalone image postgres
@@ -115,3 +117,28 @@ jobs:
build-args: | build-args: |
POSTHOG_API_KEY=${{ secrets.PUBLIC_POSTHOG_API_KEY }} POSTHOG_API_KEY=${{ secrets.PUBLIC_POSTHOG_API_KEY }}
INFISICAL_PLATFORM_VERSION=${{ steps.extract_version.outputs.version }} INFISICAL_PLATFORM_VERSION=${{ steps.extract_version.outputs.version }}
trigger-binary-release:
runs-on: ubuntu-latest
needs: [infisical-standalone, infisical-fips-standalone]
steps:
- name: Create tag if it doesn't exist
run: |
TAG_NAME="${{ github.ref_name }}"
echo "Checking for tag: $TAG_NAME"
if gh api repos/Infisical/infisical-omnibus/git/refs/tags/$TAG_NAME --silent 2>/dev/null; then
echo "Tag $TAG_NAME already exists, skipping..."
else
echo "Creating tag in Infisical/infisical-omnibus: $TAG_NAME"
LATEST_SHA=$(gh api repos/Infisical/infisical-omnibus/git/refs/heads/main --jq '.object.sha')
echo "Latest SHA: $LATEST_SHA"
gh api repos/Infisical/infisical-omnibus/git/refs \
--method POST \
--field ref="refs/tags/$TAG_NAME" \
--field sha="$LATEST_SHA"
echo "Successfully created tag $TAG_NAME"
fi
env:
GH_TOKEN: ${{ secrets.OMNIBUS_RELEASE_TOKEN }}
+1 -1
View File
@@ -35,7 +35,7 @@ jobs:
run: kubectl create namespace infisical-gateway run: kubectl create namespace infisical-gateway
- name: Create gateway secret - name: Create gateway secret
run: kubectl create secret generic infisical-gateway-environment --from-literal=TOKEN=my-test-token -n infisical-gateway run: kubectl create secret generic infisical-gateway-environment --from-literal=TOKEN=my-test-token --from-literal=INFISICAL_RELAY_NAME=my-test-relay -n infisical-gateway
- name: Run chart-testing (install) - name: Run chart-testing (install)
run: | run: |
@@ -0,0 +1,39 @@
name: "Validate Upgrade Path Configuration"
on:
pull_request:
types: [opened, synchronize]
paths:
- "backend/upgrade-path.yaml"
- "backend/scripts/validate-upgrade-path-file.ts"
- "backend/src/services/upgrade-path/upgrade-path-schemas.ts"
workflow_call:
jobs:
validate-upgrade-path:
name: Validate upgrade-path.yaml
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- name: Checkout source
uses: actions/checkout@v4
with:
fetch-depth: 1
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
cache-dependency-path: 'backend/package-lock.json'
- name: Install minimal dependencies
working-directory: backend
run: |
npm install --no-package-lock js-yaml@^4.1.0 zod@^3.22.0 tsx@^4.0.0 @types/js-yaml@^4.0.0 re2@^1.20.0
- name: Validate upgrade-path.yaml format
working-directory: backend
run: npx tsx ./scripts/validate-upgrade-path-file.ts
+1
View File
@@ -51,3 +51,4 @@ docs/integrations/app-connections/bitbucket.mdx:generic-api-key:123
docs/integrations/app-connections/railway.mdx:generic-api-key:156 docs/integrations/app-connections/railway.mdx:generic-api-key:156
.github/workflows/validate-db-schemas.yml:generic-api-key:21 .github/workflows/validate-db-schemas.yml:generic-api-key:21
k8-operator/config/samples/universalAuthIdentitySecret.yaml:generic-api-key:8 k8-operator/config/samples/universalAuthIdentitySecret.yaml:generic-api-key:8
docs/integrations/app-connections/redis.mdx:generic-api-key:80
+6
View File
@@ -173,6 +173,12 @@ COPY --from=frontend-runner /app ./backend/frontend-build
ARG INFISICAL_PLATFORM_VERSION ARG INFISICAL_PLATFORM_VERSION
ENV INFISICAL_PLATFORM_VERSION $INFISICAL_PLATFORM_VERSION ENV INFISICAL_PLATFORM_VERSION $INFISICAL_PLATFORM_VERSION
ARG DD_GIT_REPOSITORY_URL
ENV DD_GIT_REPOSITORY_URL $DD_GIT_REPOSITORY_URL
ARG DD_GIT_COMMIT_SHA
ENV DD_GIT_COMMIT_SHA $DD_GIT_COMMIT_SHA
ENV PORT 8080 ENV PORT 8080
ENV HOST=0.0.0.0 ENV HOST=0.0.0.0
ENV HTTPS_ENABLED false ENV HTTPS_ENABLED false
+10
View File
@@ -83,6 +83,7 @@
"ioredis": "^5.3.2", "ioredis": "^5.3.2",
"isomorphic-dompurify": "^2.22.0", "isomorphic-dompurify": "^2.22.0",
"jmespath": "^0.16.0", "jmespath": "^0.16.0",
"js-yaml": "^4.1.0",
"jsonwebtoken": "^9.0.2", "jsonwebtoken": "^9.0.2",
"jsrp": "^0.2.4", "jsrp": "^0.2.4",
"jwks-rsa": "^3.1.0", "jwks-rsa": "^3.1.0",
@@ -143,6 +144,7 @@
"@smithy/types": "^4.3.1", "@smithy/types": "^4.3.1",
"@types/bcrypt": "^5.0.2", "@types/bcrypt": "^5.0.2",
"@types/jmespath": "^0.15.2", "@types/jmespath": "^0.15.2",
"@types/js-yaml": "^4.0.9",
"@types/jsonwebtoken": "^9.0.5", "@types/jsonwebtoken": "^9.0.5",
"@types/jsrp": "^0.2.6", "@types/jsrp": "^0.2.6",
"@types/libsodium-wrappers": "^0.7.13", "@types/libsodium-wrappers": "^0.7.13",
@@ -13160,6 +13162,13 @@
"integrity": "sha512-pegh49FtNsC389Flyo9y8AfkVIZn9MMPE9yJrO9svhq6Fks2MwymULWjZqySuxmctd3ZH4/n7Mr98D+1Qo5vGA==", "integrity": "sha512-pegh49FtNsC389Flyo9y8AfkVIZn9MMPE9yJrO9svhq6Fks2MwymULWjZqySuxmctd3ZH4/n7Mr98D+1Qo5vGA==",
"dev": true "dev": true
}, },
"node_modules/@types/js-yaml": {
"version": "4.0.9",
"resolved": "https://registry.npmjs.org/@types/js-yaml/-/js-yaml-4.0.9.tgz",
"integrity": "sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/json-schema": { "node_modules/@types/json-schema": {
"version": "7.0.15", "version": "7.0.15",
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
@@ -20452,6 +20461,7 @@
"version": "4.1.0", "version": "4.1.0",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
"integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
"license": "MIT",
"dependencies": { "dependencies": {
"argparse": "^2.0.1" "argparse": "^2.0.1"
}, },
+4 -1
View File
@@ -73,7 +73,8 @@
"seed": "knex --knexfile ./dist/db/knexfile.ts --client pg seed:run", "seed": "knex --knexfile ./dist/db/knexfile.ts --client pg seed:run",
"seed-dev": "knex --knexfile ./src/db/knexfile.ts --client pg seed:run", "seed-dev": "knex --knexfile ./src/db/knexfile.ts --client pg seed:run",
"db:reset": "npm run migration:rollback -- --all && npm run migration:latest", "db:reset": "npm run migration:rollback -- --all && npm run migration:latest",
"email:dev": "email dev --dir src/services/smtp/emails" "email:dev": "email dev --dir src/services/smtp/emails",
"validate-upgrade-path": "tsx ./scripts/validate-upgrade-path-file.ts"
}, },
"keywords": [], "keywords": [],
"author": "", "author": "",
@@ -87,6 +88,7 @@
"@smithy/types": "^4.3.1", "@smithy/types": "^4.3.1",
"@types/bcrypt": "^5.0.2", "@types/bcrypt": "^5.0.2",
"@types/jmespath": "^0.15.2", "@types/jmespath": "^0.15.2",
"@types/js-yaml": "^4.0.9",
"@types/jsonwebtoken": "^9.0.5", "@types/jsonwebtoken": "^9.0.5",
"@types/jsrp": "^0.2.6", "@types/jsrp": "^0.2.6",
"@types/libsodium-wrappers": "^0.7.13", "@types/libsodium-wrappers": "^0.7.13",
@@ -203,6 +205,7 @@
"ioredis": "^5.3.2", "ioredis": "^5.3.2",
"isomorphic-dompurify": "^2.22.0", "isomorphic-dompurify": "^2.22.0",
"jmespath": "^0.16.0", "jmespath": "^0.16.0",
"js-yaml": "^4.1.0",
"jsonwebtoken": "^9.0.2", "jsonwebtoken": "^9.0.2",
"jsrp": "^0.2.4", "jsrp": "^0.2.4",
"jwks-rsa": "^3.1.0", "jwks-rsa": "^3.1.0",
@@ -0,0 +1,107 @@
/* eslint-disable no-console */
import { readFile } from "fs/promises";
import * as yaml from "js-yaml";
import * as path from "path";
import { z } from "zod";
import { upgradePathConfigSchema } from "../src/services/upgrade-path/upgrade-path-schemas";
async function validateUpgradePathConfig(): Promise<void> {
try {
const yamlPath = path.join(__dirname, "..", "upgrade-path.yaml");
const resolvedPath = path.resolve(yamlPath);
const expectedBaseDir = path.resolve(__dirname, "..");
if (!resolvedPath.startsWith(expectedBaseDir)) {
throw new Error("Invalid configuration file path");
}
try {
await readFile(yamlPath, "utf8");
} catch (error) {
if (error instanceof Error && "code" in error && error.code === "ENOENT") {
console.log("Warning: No upgrade-path.yaml file found");
return;
}
throw error;
}
const yamlContent = await readFile(yamlPath, "utf8");
if (yamlContent.length > 1024 * 1024) {
throw new Error("Config file too large (>1MB)");
}
let config: unknown;
try {
config = yaml.load(yamlContent, {
schema: yaml.FAILSAFE_SCHEMA,
filename: yamlPath,
onWarning: (warning) => {
console.log(`YAML Warning: ${warning.message}`);
}
});
} catch (yamlError) {
if (yamlError instanceof yaml.YAMLException) {
throw new Error(
`YAML parsing failed: ${yamlError.message} at line ${yamlError.mark?.line}, column ${yamlError.mark?.column}`
);
}
throw new Error(`YAML parsing failed: ${yamlError instanceof Error ? yamlError.message : "Unknown YAML error"}`);
}
if (!config) {
console.log("Warning: Empty configuration file");
return;
}
if (typeof config !== "object" || config === null) {
throw new Error("Configuration must be a valid YAML object");
}
const result = upgradePathConfigSchema.safeParse(config);
if (!result.success) {
console.log("Validation failed with the following errors:");
result.error.issues.forEach((issue: z.ZodIssue) => {
const issuePath = issue.path.length > 0 ? `[${issue.path.join(".")}]` : "";
console.log(` - ${issuePath}: ${issue.message}`);
});
throw new Error("Schema validation failed");
}
const validatedConfig = result.data;
const versions = validatedConfig?.versions || {};
const versionCount = Object.keys(versions).length;
if (versionCount === 0) {
console.log("Warning: No versions found in the configuration");
} else {
console.log(`Validated ${versionCount} version configuration(s)`);
const commonPatterns = [
/^v?\d+\.\d+\.\d+$/,
/^v?\d+\.\d+\.\d+\.\d+$/,
/^infisical\/v?\d+\.\d+\.\d+$/,
/^infisical\/v?\d+\.\d+\.\d+-\w+$/
];
for (const versionKey of Object.keys(versions)) {
const isCommonPattern = commonPatterns.some((pattern) => pattern.test(versionKey));
if (!isCommonPattern) {
console.log(`Warning: Version key '${versionKey}' doesn't match common patterns. This may be intentional.`);
}
}
}
console.log("upgrade-path.yaml format is valid");
} catch (error) {
console.error(`Validation failed: ${error instanceof Error ? error.message : "Unknown error"}`);
process.exit(1);
}
}
validateUpgradePathConfig().catch((error) => {
console.error("Unexpected error:", error);
process.exit(1);
});
+4
View File
@@ -93,6 +93,7 @@ import { TOrgAdminServiceFactory } from "@app/services/org-admin/org-admin-servi
import { TPkiAlertServiceFactory } from "@app/services/pki-alert/pki-alert-service"; import { TPkiAlertServiceFactory } from "@app/services/pki-alert/pki-alert-service";
import { TPkiCollectionServiceFactory } from "@app/services/pki-collection/pki-collection-service"; import { TPkiCollectionServiceFactory } from "@app/services/pki-collection/pki-collection-service";
import { TPkiSubscriberServiceFactory } from "@app/services/pki-subscriber/pki-subscriber-service"; import { TPkiSubscriberServiceFactory } from "@app/services/pki-subscriber/pki-subscriber-service";
import { TPkiSyncServiceFactory } from "@app/services/pki-sync/pki-sync-service";
import { TPkiTemplatesServiceFactory } from "@app/services/pki-templates/pki-templates-service"; import { TPkiTemplatesServiceFactory } from "@app/services/pki-templates/pki-templates-service";
import { TProjectServiceFactory } from "@app/services/project/project-service"; import { TProjectServiceFactory } from "@app/services/project/project-service";
import { TProjectBotServiceFactory } from "@app/services/project-bot/project-bot-service"; import { TProjectBotServiceFactory } from "@app/services/project-bot/project-bot-service";
@@ -114,6 +115,7 @@ import { TSlackServiceFactory } from "@app/services/slack/slack-service";
import { TSuperAdminServiceFactory } from "@app/services/super-admin/super-admin-service"; import { TSuperAdminServiceFactory } from "@app/services/super-admin/super-admin-service";
import { TTelemetryServiceFactory } from "@app/services/telemetry/telemetry-service"; import { TTelemetryServiceFactory } from "@app/services/telemetry/telemetry-service";
import { TTotpServiceFactory } from "@app/services/totp/totp-service"; import { TTotpServiceFactory } from "@app/services/totp/totp-service";
import { TUpgradePathService } from "@app/services/upgrade-path/upgrade-path-service";
import { TUserDALFactory } from "@app/services/user/user-dal"; import { TUserDALFactory } from "@app/services/user/user-dal";
import { TUserServiceFactory } from "@app/services/user/user-service"; import { TUserServiceFactory } from "@app/services/user/user-service";
import { TUserEngagementServiceFactory } from "@app/services/user-engagement/user-engagement-service"; import { TUserEngagementServiceFactory } from "@app/services/user-engagement/user-engagement-service";
@@ -267,6 +269,7 @@ declare module "fastify" {
certificateEst: TCertificateEstServiceFactory; certificateEst: TCertificateEstServiceFactory;
pkiCollection: TPkiCollectionServiceFactory; pkiCollection: TPkiCollectionServiceFactory;
pkiSubscriber: TPkiSubscriberServiceFactory; pkiSubscriber: TPkiSubscriberServiceFactory;
pkiSync: TPkiSyncServiceFactory;
secretScanning: TSecretScanningServiceFactory; secretScanning: TSecretScanningServiceFactory;
license: TLicenseServiceFactory; license: TLicenseServiceFactory;
trustedIp: TTrustedIpServiceFactory; trustedIp: TTrustedIpServiceFactory;
@@ -312,6 +315,7 @@ declare module "fastify" {
identityAuthTemplate: TIdentityAuthTemplateServiceFactory; identityAuthTemplate: TIdentityAuthTemplateServiceFactory;
notification: TNotificationServiceFactory; notification: TNotificationServiceFactory;
offlineUsageReport: TOfflineUsageReportServiceFactory; offlineUsageReport: TOfflineUsageReportServiceFactory;
upgradePath: TUpgradePathService;
}; };
// this is exclusive use for middlewares in which we need to inject data // this is exclusive use for middlewares in which we need to inject data
// everywhere else access using service layer // everywhere else access using service layer
+4
View File
@@ -263,6 +263,9 @@ import {
TPkiSubscribers, TPkiSubscribers,
TPkiSubscribersInsert, TPkiSubscribersInsert,
TPkiSubscribersUpdate, TPkiSubscribersUpdate,
TPkiSyncs,
TPkiSyncsInsert,
TPkiSyncsUpdate,
TProjectBots, TProjectBots,
TProjectBotsInsert, TProjectBotsInsert,
TProjectBotsUpdate, TProjectBotsUpdate,
@@ -680,6 +683,7 @@ declare module "knex/types/tables" {
TPkiSubscribersInsert, TPkiSubscribersInsert,
TPkiSubscribersUpdate TPkiSubscribersUpdate
>; >;
[TableName.PkiSync]: KnexOriginal.CompositeTableType<TPkiSyncs, TPkiSyncsInsert, TPkiSyncsUpdate>;
[TableName.UserGroupMembership]: KnexOriginal.CompositeTableType< [TableName.UserGroupMembership]: KnexOriginal.CompositeTableType<
TUserGroupMembership, TUserGroupMembership,
TUserGroupMembershipInsert, TUserGroupMembershipInsert,
@@ -0,0 +1,47 @@
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.PkiSync))) {
await knex.schema.createTable(TableName.PkiSync, (t) => {
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
t.string("name", 32).notNullable();
t.string("description");
t.string("destination").notNullable();
t.boolean("isAutoSyncEnabled").notNullable().defaultTo(true);
t.integer("version").defaultTo(1).notNullable();
t.jsonb("destinationConfig").notNullable();
t.jsonb("syncOptions").notNullable();
t.string("projectId").notNullable();
t.foreign("projectId").references("id").inTable(TableName.Project).onDelete("CASCADE");
t.uuid("subscriberId");
t.foreign("subscriberId").references("id").inTable(TableName.PkiSubscriber).onDelete("SET NULL");
t.uuid("connectionId").notNullable();
t.foreign("connectionId").references("id").inTable(TableName.AppConnection);
t.timestamps(true, true, true);
t.string("syncStatus");
t.string("lastSyncJobId");
t.string("lastSyncMessage");
t.datetime("lastSyncedAt");
t.string("importStatus");
t.string("lastImportJobId");
t.string("lastImportMessage");
t.datetime("lastImportedAt");
t.string("removeStatus");
t.string("lastRemoveJobId");
t.string("lastRemoveMessage");
t.datetime("lastRemovedAt");
t.unique(["name", "projectId"], { indexName: "pki_syncs_name_project_id_unique" });
});
await createOnUpdateTrigger(knex, TableName.PkiSync);
}
}
export async function down(knex: Knex): Promise<void> {
await knex.schema.dropTableIfExists(TableName.PkiSync);
await dropOnUpdateTrigger(knex, TableName.PkiSync);
}
@@ -0,0 +1,31 @@
import { Knex } from "knex";
import { TableName } from "../schemas";
export async function up(knex: Knex): Promise<void> {
const hasAllowedNamespaces = await knex.schema.hasColumn(TableName.IdentityKubernetesAuth, "allowedNamespaces");
const hasAllowedNames = await knex.schema.hasColumn(TableName.IdentityKubernetesAuth, "allowedNames");
const hasAllowedAudience = await knex.schema.hasColumn(TableName.IdentityKubernetesAuth, "allowedAudience");
if (hasAllowedNamespaces || hasAllowedNames || hasAllowedAudience) {
await knex.schema.alterTable(TableName.IdentityKubernetesAuth, (t) => {
if (hasAllowedNames) t.string("allowedNames", 1000).notNullable().alter();
if (hasAllowedNamespaces) t.string("allowedNamespaces", 1000).notNullable().alter();
if (hasAllowedAudience) t.string("allowedAudience", 1000).notNullable().alter();
});
}
}
export async function down(knex: Knex): Promise<void> {
const hasAllowedNamespaces = await knex.schema.hasColumn(TableName.IdentityKubernetesAuth, "allowedNamespaces");
const hasAllowedNames = await knex.schema.hasColumn(TableName.IdentityKubernetesAuth, "allowedNames");
const hasAllowedAudience = await knex.schema.hasColumn(TableName.IdentityKubernetesAuth, "allowedAudience");
if (hasAllowedNamespaces || hasAllowedNames || hasAllowedAudience) {
await knex.schema.alterTable(TableName.IdentityKubernetesAuth, (t) => {
if (hasAllowedNames) t.string("allowedNames", 255).notNullable().alter();
if (hasAllowedNamespaces) t.string("allowedNamespaces", 255).notNullable().alter();
if (hasAllowedAudience) t.string("allowedAudience", 255).notNullable().alter();
});
}
}
@@ -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.hasTable(TableName.SecretApprovalRequestSecretV2)) {
await knex.schema.alterTable(TableName.SecretApprovalRequestSecretV2, (t) => {
t.boolean("skipMultilineEncoding").alter();
});
}
}
export async function down(knex: Knex): Promise<void> {
if (await knex.schema.hasTable(TableName.SecretApprovalRequestSecretV2)) {
await knex.schema.alterTable(TableName.SecretApprovalRequestSecretV2, (t) => {
t.boolean("skipMultilineEncoding").defaultTo(false).alter();
});
}
}
+1
View File
@@ -87,6 +87,7 @@ export * from "./pki-alerts";
export * from "./pki-collection-items"; export * from "./pki-collection-items";
export * from "./pki-collections"; export * from "./pki-collections";
export * from "./pki-subscribers"; export * from "./pki-subscribers";
export * from "./pki-syncs";
export * from "./project-bots"; export * from "./project-bots";
export * from "./project-environments"; export * from "./project-environments";
export * from "./project-gateways"; export * from "./project-gateways";
+1
View File
@@ -156,6 +156,7 @@ export enum TableName {
ProjectSlackConfigs = "project_slack_configs", ProjectSlackConfigs = "project_slack_configs",
AppConnection = "app_connections", AppConnection = "app_connections",
SecretSync = "secret_syncs", SecretSync = "secret_syncs",
PkiSync = "pki_syncs",
KmipClient = "kmip_clients", KmipClient = "kmip_clients",
KmipOrgConfig = "kmip_org_configs", KmipOrgConfig = "kmip_org_configs",
KmipOrgServerCertificates = "kmip_org_server_certificates", KmipOrgServerCertificates = "kmip_org_server_certificates",
+40
View File
@@ -0,0 +1,40 @@
// 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 { TImmutableDBKeys } from "./models";
export const PkiSyncsSchema = z.object({
id: z.string().uuid(),
name: z.string(),
description: z.string().nullable().optional(),
destination: z.string(),
isAutoSyncEnabled: z.boolean().default(true),
version: z.number().default(1),
destinationConfig: z.unknown(),
syncOptions: z.unknown(),
projectId: z.string(),
subscriberId: z.string().uuid().nullable().optional(),
connectionId: z.string().uuid(),
createdAt: z.date(),
updatedAt: z.date(),
syncStatus: z.string().nullable().optional(),
lastSyncJobId: z.string().nullable().optional(),
lastSyncMessage: z.string().nullable().optional(),
lastSyncedAt: z.date().nullable().optional(),
importStatus: z.string().nullable().optional(),
lastImportJobId: z.string().nullable().optional(),
lastImportMessage: z.string().nullable().optional(),
lastImportedAt: z.date().nullable().optional(),
removeStatus: z.string().nullable().optional(),
lastRemoveJobId: z.string().nullable().optional(),
lastRemoveMessage: z.string().nullable().optional(),
lastRemovedAt: z.date().nullable().optional()
});
export type TPkiSyncs = z.infer<typeof PkiSyncsSchema>;
export type TPkiSyncsInsert = Omit<z.input<typeof PkiSyncsSchema>, TImmutableDBKeys>;
export type TPkiSyncsUpdate = Partial<Omit<z.input<typeof PkiSyncsSchema>, TImmutableDBKeys>>;
@@ -17,7 +17,7 @@ export const SecretApprovalRequestsSecretsV2Schema = z.object({
encryptedComment: zodBuffer.nullable().optional(), encryptedComment: zodBuffer.nullable().optional(),
reminderNote: z.string().nullable().optional(), reminderNote: z.string().nullable().optional(),
reminderRepeatDays: z.number().nullable().optional(), reminderRepeatDays: z.number().nullable().optional(),
skipMultilineEncoding: z.boolean().default(false).nullable().optional(), skipMultilineEncoding: z.boolean().nullable().optional(),
metadata: z.unknown().nullable().optional(), metadata: z.unknown().nullable().optional(),
createdAt: z.date(), createdAt: z.date(),
updatedAt: z.date(), updatedAt: z.date(),
@@ -2,6 +2,7 @@ import { packRules } from "@casl/ability/extra";
import { z } from "zod"; import { z } from "zod";
import { ProjectMembershipRole, ProjectRolesSchema } from "@app/db/schemas"; import { ProjectMembershipRole, ProjectRolesSchema } from "@app/db/schemas";
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
import { import {
backfillPermissionV1SchemaToV2Schema, backfillPermissionV1SchemaToV2Schema,
ProjectPermissionV1Schema ProjectPermissionV1Schema
@@ -50,6 +51,10 @@ export const registerDeprecatedProjectRoleRouter = async (server: FastifyZodProv
}, },
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => { handler: async (req) => {
const stringifiedPermissions = JSON.stringify(
packRules(backfillPermissionV1SchemaToV2Schema(req.body.permissions, true))
);
const role = await server.services.projectRole.createRole({ const role = await server.services.projectRole.createRole({
actorAuthMethod: req.permission.authMethod, actorAuthMethod: req.permission.authMethod,
actorId: req.permission.id, actorId: req.permission.id,
@@ -61,7 +66,23 @@ export const registerDeprecatedProjectRoleRouter = async (server: FastifyZodProv
}, },
data: { data: {
...req.body, ...req.body,
permissions: JSON.stringify(packRules(backfillPermissionV1SchemaToV2Schema(req.body.permissions, true))) permissions: stringifiedPermissions
}
});
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
orgId: req.permission.orgId,
projectId: role.projectId,
event: {
type: EventType.CREATE_PROJECT_ROLE,
metadata: {
roleId: role.id,
slug: req.body.slug,
name: req.body.name,
description: req.body.description,
permissions: stringifiedPermissions
}
} }
}); });
@@ -106,6 +127,10 @@ export const registerDeprecatedProjectRoleRouter = async (server: FastifyZodProv
}, },
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => { handler: async (req) => {
const stringifiedPermissions = req.body.permissions
? JSON.stringify(packRules(backfillPermissionV1SchemaToV2Schema(req.body.permissions, true)))
: undefined;
const role = await server.services.projectRole.updateRole({ const role = await server.services.projectRole.updateRole({
actorAuthMethod: req.permission.authMethod, actorAuthMethod: req.permission.authMethod,
actorId: req.permission.id, actorId: req.permission.id,
@@ -114,11 +139,26 @@ export const registerDeprecatedProjectRoleRouter = async (server: FastifyZodProv
roleId: req.params.roleId, roleId: req.params.roleId,
data: { data: {
...req.body, ...req.body,
permissions: req.body.permissions permissions: stringifiedPermissions
? JSON.stringify(packRules(backfillPermissionV1SchemaToV2Schema(req.body.permissions, true)))
: undefined
} }
}); });
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
orgId: req.permission.orgId,
projectId: role.projectId,
event: {
type: EventType.UPDATE_PROJECT_ROLE,
metadata: {
roleId: role.id,
slug: req.body.slug,
name: req.body.name,
description: req.body.description,
permissions: stringifiedPermissions
}
}
});
return { role }; return { role };
} }
}); });
@@ -155,6 +195,21 @@ export const registerDeprecatedProjectRoleRouter = async (server: FastifyZodProv
actor: req.permission.type, actor: req.permission.type,
roleId: req.params.roleId roleId: req.params.roleId
}); });
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
orgId: req.permission.orgId,
projectId: role.projectId,
event: {
type: EventType.DELETE_PROJECT_ROLE,
metadata: {
roleId: role.id,
slug: role.slug,
name: role.name
}
}
});
return { role }; return { role };
} }
}); });
@@ -1,6 +1,7 @@
import { z } from "zod"; import { z } from "zod";
import { OrgMembershipRole, OrgMembershipsSchema, OrgRolesSchema } from "@app/db/schemas"; import { OrgMembershipRole, OrgMembershipsSchema, OrgRolesSchema } from "@app/db/schemas";
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { slugSchema } from "@app/server/lib/schemas"; import { slugSchema } from "@app/server/lib/schemas";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
@@ -42,6 +43,22 @@ export const registerOrgRoleRouter = async (server: FastifyZodProvider) => {
req.permission.authMethod, req.permission.authMethod,
req.permission.orgId req.permission.orgId
); );
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
orgId: req.permission.orgId,
event: {
type: EventType.CREATE_ORG_ROLE,
metadata: {
roleId: role.id,
slug: req.body.slug,
name: req.body.name,
description: req.body.description,
permissions: JSON.stringify(req.body.permissions)
}
}
});
return { role }; return { role };
} }
}); });
@@ -116,6 +133,22 @@ export const registerOrgRoleRouter = async (server: FastifyZodProvider) => {
req.permission.authMethod, req.permission.authMethod,
req.permission.orgId req.permission.orgId
); );
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
orgId: req.permission.orgId,
event: {
type: EventType.UPDATE_ORG_ROLE,
metadata: {
roleId: role.id,
slug: req.body.slug,
name: req.body.name,
description: req.body.description,
permissions: req.body.permissions ? JSON.stringify(req.body.permissions) : undefined
}
}
});
return { role }; return { role };
} }
}); });
@@ -146,6 +179,16 @@ export const registerOrgRoleRouter = async (server: FastifyZodProvider) => {
req.permission.authMethod, req.permission.authMethod,
req.permission.orgId req.permission.orgId
); );
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
orgId: req.permission.orgId,
event: {
type: EventType.DELETE_ORG_ROLE,
metadata: { roleId: role.id, slug: role.slug, name: role.name }
}
});
return { role }; return { role };
} }
}); });
@@ -2,6 +2,7 @@ import { packRules } from "@casl/ability/extra";
import { z } from "zod"; import { z } from "zod";
import { ProjectMembershipRole, ProjectRolesSchema } from "@app/db/schemas"; import { ProjectMembershipRole, ProjectRolesSchema } from "@app/db/schemas";
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
import { checkForInvalidPermissionCombination } from "@app/ee/services/permission/permission-fns"; import { checkForInvalidPermissionCombination } from "@app/ee/services/permission/permission-fns";
import { ProjectPermissionV2Schema } from "@app/ee/services/permission/project-permission"; import { ProjectPermissionV2Schema } from "@app/ee/services/permission/project-permission";
import { ApiDocsTags, PROJECT_ROLE } from "@app/lib/api-docs"; import { ApiDocsTags, PROJECT_ROLE } from "@app/lib/api-docs";
@@ -52,6 +53,8 @@ export const registerProjectRoleRouter = async (server: FastifyZodProvider) => {
}, },
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => { handler: async (req) => {
const stringifiedPermissions = JSON.stringify(packRules(req.body.permissions));
const role = await server.services.projectRole.createRole({ const role = await server.services.projectRole.createRole({
actorAuthMethod: req.permission.authMethod, actorAuthMethod: req.permission.authMethod,
actorId: req.permission.id, actorId: req.permission.id,
@@ -63,9 +66,26 @@ export const registerProjectRoleRouter = async (server: FastifyZodProvider) => {
}, },
data: { data: {
...req.body, ...req.body,
permissions: JSON.stringify(packRules(req.body.permissions)) permissions: stringifiedPermissions
} }
}); });
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
orgId: req.permission.orgId,
projectId: role.projectId,
event: {
type: EventType.CREATE_PROJECT_ROLE,
metadata: {
roleId: role.id,
slug: req.body.slug,
name: req.body.name,
description: req.body.description,
permissions: stringifiedPermissions
}
}
});
return { role }; return { role };
} }
}); });
@@ -112,6 +132,7 @@ export const registerProjectRoleRouter = async (server: FastifyZodProvider) => {
}, },
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => { handler: async (req) => {
const stringifiedPermissions = req.body.permissions ? JSON.stringify(packRules(req.body.permissions)) : undefined;
const role = await server.services.projectRole.updateRole({ const role = await server.services.projectRole.updateRole({
actorAuthMethod: req.permission.authMethod, actorAuthMethod: req.permission.authMethod,
actorId: req.permission.id, actorId: req.permission.id,
@@ -120,9 +141,26 @@ export const registerProjectRoleRouter = async (server: FastifyZodProvider) => {
roleId: req.params.roleId, roleId: req.params.roleId,
data: { data: {
...req.body, ...req.body,
permissions: req.body.permissions ? JSON.stringify(packRules(req.body.permissions)) : undefined permissions: stringifiedPermissions
} }
}); });
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
orgId: req.permission.orgId,
projectId: role.projectId,
event: {
type: EventType.UPDATE_PROJECT_ROLE,
metadata: {
roleId: role.id,
slug: req.body.slug,
name: req.body.name,
description: req.body.description,
permissions: stringifiedPermissions
}
}
});
return { role }; return { role };
} }
}); });
@@ -161,6 +199,21 @@ export const registerProjectRoleRouter = async (server: FastifyZodProvider) => {
actor: req.permission.type, actor: req.permission.type,
roleId: req.params.roleId roleId: req.params.roleId
}); });
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
orgId: req.permission.orgId,
projectId: role.projectId,
event: {
type: EventType.DELETE_PROJECT_ROLE,
metadata: {
roleId: role.id,
slug: role.slug,
name: role.name
}
}
});
return { role }; return { role };
} }
}); });
+53 -7
View File
@@ -1,9 +1,10 @@
import { z } from "zod"; import { z } from "zod";
import { RelaysSchema } from "@app/db/schemas";
import { getConfig } from "@app/lib/config/env"; import { getConfig } from "@app/lib/config/env";
import { crypto } from "@app/lib/crypto/cryptography"; import { crypto } from "@app/lib/crypto/cryptography";
import { BadRequestError, UnauthorizedError } from "@app/lib/errors"; import { UnauthorizedError } from "@app/lib/errors";
import { writeLimit } from "@app/server/config/rateLimiter"; import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { slugSchema } from "@app/server/lib/schemas"; import { slugSchema } from "@app/server/lib/schemas";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type"; import { AuthMode } from "@app/services/auth/auth-type";
@@ -89,14 +90,59 @@ export const registerRelayRouter = async (server: FastifyZodProvider) => {
}, },
onRequest: verifyAuth([AuthMode.IDENTITY_ACCESS_TOKEN]), onRequest: verifyAuth([AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => { handler: async (req) => {
throw new BadRequestError({
message: "Org relay registration is not yet supported"
});
return server.services.relay.registerRelay({ return server.services.relay.registerRelay({
...req.body, ...req.body,
identityId: req.permission.id, identityId: req.permission.id,
orgId: req.permission.orgId orgId: req.permission.orgId,
actorAuthMethod: req.permission.authMethod
});
}
});
server.route({
method: "GET",
url: "/",
schema: {
response: {
200: RelaysSchema.array()
}
},
config: {
rateLimit: readLimit
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
return server.services.relay.getRelays({
actorId: req.permission.id,
actor: req.permission.type,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId
});
}
});
server.route({
method: "DELETE",
url: "/:id",
config: {
rateLimit: writeLimit
},
schema: {
params: z.object({
id: z.string()
}),
response: {
200: RelaysSchema
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
return server.services.relay.deleteRelay({
id: req.params.id,
actorId: req.permission.id,
actor: req.permission.type,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId
}); });
} }
}); });
@@ -320,10 +320,20 @@ export const registerSecretApprovalRequestRouter = async (server: FastifyZodProv
.array(), .array(),
secretPath: z.string(), secretPath: z.string(),
commits: secretRawSchema commits: secretRawSchema
.omit({ _id: true, environment: true, workspace: true, type: true, version: true, secretValue: true }) .omit({
_id: true,
environment: true,
workspace: true,
type: true,
version: true,
secretValue: true,
secretComment: true
})
.extend({ .extend({
secretValueHidden: z.boolean(), secretValueHidden: z.boolean(),
secretValue: z.string().optional(), secretValue: z.string().optional(),
secretComment: z.string().optional(),
skipMultilineEncoding: z.boolean().nullish(),
isRotatedSecret: z.boolean().optional(), isRotatedSecret: z.boolean().optional(),
op: z.string(), op: z.string(),
tags: SanitizedTagSchema.array().optional(), tags: SanitizedTagSchema.array().optional(),
@@ -348,7 +358,8 @@ export const registerSecretApprovalRequestRouter = async (server: FastifyZodProv
secretValueHidden: z.boolean(), secretValueHidden: z.boolean(),
secretComment: z.string().optional(), secretComment: z.string().optional(),
tags: SanitizedTagSchema.array().optional(), tags: SanitizedTagSchema.array().optional(),
secretMetadata: ResourceMetadataSchema.nullish() secretMetadata: ResourceMetadataSchema.nullish(),
skipMultilineEncoding: z.boolean().nullish()
}) })
.optional() .optional()
}) })
+2 -2
View File
@@ -34,7 +34,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
secretName: z.string().trim().describe(RAW_SECRETS.GET_ACCESS_LIST.secretName) secretName: z.string().trim().describe(RAW_SECRETS.GET_ACCESS_LIST.secretName)
}), }),
querystring: z.object({ querystring: z.object({
workspaceId: z.string().trim().describe(RAW_SECRETS.GET_ACCESS_LIST.workspaceId), projectId: z.string().trim().describe(RAW_SECRETS.GET_ACCESS_LIST.projectId),
environment: z.string().trim().describe(RAW_SECRETS.GET_ACCESS_LIST.environment), environment: z.string().trim().describe(RAW_SECRETS.GET_ACCESS_LIST.environment),
secretPath: z secretPath: z
.string() .string()
@@ -54,7 +54,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
onRequest: verifyAuth([AuthMode.JWT]), onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => { handler: async (req) => {
const { secretName } = req.params; const { secretName } = req.params;
const { secretPath, environment, workspaceId: projectId } = req.query; const { secretPath, environment, projectId } = req.query;
return server.services.secret.getSecretAccessList({ return server.services.secret.getSecretAccessList({
actorId: req.permission.id, actorId: req.permission.id,
@@ -2,6 +2,7 @@ import { packRules } from "@casl/ability/extra";
import { z } from "zod"; import { z } from "zod";
import { ProjectMembershipRole, ProjectRolesSchema } from "@app/db/schemas"; import { ProjectMembershipRole, ProjectRolesSchema } from "@app/db/schemas";
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
import { checkForInvalidPermissionCombination } from "@app/ee/services/permission/permission-fns"; import { checkForInvalidPermissionCombination } from "@app/ee/services/permission/permission-fns";
import { ProjectPermissionV2Schema } from "@app/ee/services/permission/project-permission"; import { ProjectPermissionV2Schema } from "@app/ee/services/permission/project-permission";
import { ApiDocsTags, PROJECT_ROLE } from "@app/lib/api-docs"; import { ApiDocsTags, PROJECT_ROLE } from "@app/lib/api-docs";
@@ -52,6 +53,8 @@ export const registerDeprecatedProjectRoleRouter = async (server: FastifyZodProv
}, },
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => { handler: async (req) => {
const stringifiedPermissions = JSON.stringify(packRules(req.body.permissions));
const role = await server.services.projectRole.createRole({ const role = await server.services.projectRole.createRole({
actorAuthMethod: req.permission.authMethod, actorAuthMethod: req.permission.authMethod,
actorId: req.permission.id, actorId: req.permission.id,
@@ -63,9 +66,26 @@ export const registerDeprecatedProjectRoleRouter = async (server: FastifyZodProv
}, },
data: { data: {
...req.body, ...req.body,
permissions: JSON.stringify(packRules(req.body.permissions)) permissions: stringifiedPermissions
} }
}); });
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
orgId: req.permission.orgId,
projectId: role.projectId,
event: {
type: EventType.CREATE_PROJECT_ROLE,
metadata: {
roleId: role.id,
slug: req.body.slug,
name: req.body.name,
description: req.body.description,
permissions: stringifiedPermissions
}
}
});
return { role }; return { role };
} }
}); });
@@ -112,6 +132,7 @@ export const registerDeprecatedProjectRoleRouter = async (server: FastifyZodProv
}, },
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => { handler: async (req) => {
const stringifiedPermissions = req.body.permissions ? JSON.stringify(packRules(req.body.permissions)) : undefined;
const role = await server.services.projectRole.updateRole({ const role = await server.services.projectRole.updateRole({
actorAuthMethod: req.permission.authMethod, actorAuthMethod: req.permission.authMethod,
actorId: req.permission.id, actorId: req.permission.id,
@@ -120,9 +141,26 @@ export const registerDeprecatedProjectRoleRouter = async (server: FastifyZodProv
roleId: req.params.roleId, roleId: req.params.roleId,
data: { data: {
...req.body, ...req.body,
permissions: req.body.permissions ? JSON.stringify(packRules(req.body.permissions)) : undefined permissions: stringifiedPermissions
} }
}); });
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
orgId: req.permission.orgId,
projectId: role.projectId,
event: {
type: EventType.UPDATE_PROJECT_ROLE,
metadata: {
roleId: role.id,
slug: req.body.slug,
name: req.body.name,
description: req.body.description,
permissions: stringifiedPermissions
}
}
});
return { role }; return { role };
} }
}); });
@@ -161,6 +199,21 @@ export const registerDeprecatedProjectRoleRouter = async (server: FastifyZodProv
actor: req.permission.type, actor: req.permission.type,
roleId: req.params.roleId roleId: req.params.roleId
}); });
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
orgId: req.permission.orgId,
projectId: role.projectId,
event: {
type: EventType.DELETE_PROJECT_ROLE,
metadata: {
roleId: role.id,
slug: role.slug,
name: role.name
}
}
});
return { role }; return { role };
} }
}); });
@@ -9,6 +9,7 @@ import { registerMySqlCredentialsRotationRouter } from "./mysql-credentials-rota
import { registerOktaClientSecretRotationRouter } from "./okta-client-secret-rotation-router"; import { registerOktaClientSecretRotationRouter } from "./okta-client-secret-rotation-router";
import { registerOracleDBCredentialsRotationRouter } from "./oracledb-credentials-rotation-router"; import { registerOracleDBCredentialsRotationRouter } from "./oracledb-credentials-rotation-router";
import { registerPostgresCredentialsRotationRouter } from "./postgres-credentials-rotation-router"; import { registerPostgresCredentialsRotationRouter } from "./postgres-credentials-rotation-router";
import { registerRedisCredentialsRotationRouter } from "./redis-credentials-rotation-router";
export * from "./secret-rotation-v2-router"; export * from "./secret-rotation-v2-router";
@@ -24,5 +25,6 @@ export const SECRET_ROTATION_REGISTER_ROUTER_MAP: Record<
[SecretRotation.AzureClientSecret]: registerAzureClientSecretRotationRouter, [SecretRotation.AzureClientSecret]: registerAzureClientSecretRotationRouter,
[SecretRotation.AwsIamUserSecret]: registerAwsIamUserSecretRotationRouter, [SecretRotation.AwsIamUserSecret]: registerAwsIamUserSecretRotationRouter,
[SecretRotation.LdapPassword]: registerLdapPasswordRotationRouter, [SecretRotation.LdapPassword]: registerLdapPasswordRotationRouter,
[SecretRotation.OktaClientSecret]: registerOktaClientSecretRotationRouter [SecretRotation.OktaClientSecret]: registerOktaClientSecretRotationRouter,
[SecretRotation.RedisCredentials]: registerRedisCredentialsRotationRouter
}; };
@@ -0,0 +1,19 @@
import {
CreateRedisCredentialsRotationSchema,
RedisCredentialsRotationGeneratedCredentialsSchema,
RedisCredentialsRotationSchema,
UpdateRedisCredentialsRotationSchema
} from "@app/ee/services/secret-rotation-v2/redis-credentials";
import { SecretRotation } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-enums";
import { registerSecretRotationEndpoints } from "./secret-rotation-v2-endpoints";
export const registerRedisCredentialsRotationRouter = async (server: FastifyZodProvider) =>
registerSecretRotationEndpoints({
type: SecretRotation.RedisCredentials,
server,
responseSchema: RedisCredentialsRotationSchema,
createSchema: CreateRedisCredentialsRotationSchema,
updateSchema: UpdateRedisCredentialsRotationSchema,
generatedCredentialsSchema: RedisCredentialsRotationGeneratedCredentialsSchema
});
@@ -10,6 +10,7 @@ import { MySqlCredentialsRotationListItemSchema } from "@app/ee/services/secret-
import { OktaClientSecretRotationListItemSchema } from "@app/ee/services/secret-rotation-v2/okta-client-secret"; import { OktaClientSecretRotationListItemSchema } from "@app/ee/services/secret-rotation-v2/okta-client-secret";
import { OracleDBCredentialsRotationListItemSchema } from "@app/ee/services/secret-rotation-v2/oracledb-credentials"; import { OracleDBCredentialsRotationListItemSchema } from "@app/ee/services/secret-rotation-v2/oracledb-credentials";
import { PostgresCredentialsRotationListItemSchema } from "@app/ee/services/secret-rotation-v2/postgres-credentials"; import { PostgresCredentialsRotationListItemSchema } from "@app/ee/services/secret-rotation-v2/postgres-credentials";
import { RedisCredentialsRotationListItemSchema } from "@app/ee/services/secret-rotation-v2/redis-credentials";
import { SecretRotationV2Schema } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-union-schema"; import { SecretRotationV2Schema } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-union-schema";
import { ApiDocsTags, SecretRotations } from "@app/lib/api-docs"; import { ApiDocsTags, SecretRotations } from "@app/lib/api-docs";
import { readLimit } from "@app/server/config/rateLimiter"; import { readLimit } from "@app/server/config/rateLimiter";
@@ -25,7 +26,8 @@ const SecretRotationV2OptionsSchema = z.discriminatedUnion("type", [
AzureClientSecretRotationListItemSchema, AzureClientSecretRotationListItemSchema,
AwsIamUserSecretRotationListItemSchema, AwsIamUserSecretRotationListItemSchema,
LdapPasswordRotationListItemSchema, LdapPasswordRotationListItemSchema,
OktaClientSecretRotationListItemSchema OktaClientSecretRotationListItemSchema,
RedisCredentialsRotationListItemSchema
]); ]);
export const registerSecretRotationV2Router = async (server: FastifyZodProvider) => { export const registerSecretRotationV2Router = async (server: FastifyZodProvider) => {
@@ -35,7 +35,7 @@ import { ApprovalStatus, TAccessApprovalRequestServiceFactory } from "./access-a
type TSecretApprovalRequestServiceFactoryDep = { type TSecretApprovalRequestServiceFactoryDep = {
additionalPrivilegeDAL: Pick<TProjectUserAdditionalPrivilegeDALFactory, "create" | "findById">; additionalPrivilegeDAL: Pick<TProjectUserAdditionalPrivilegeDALFactory, "create" | "findById">;
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">; permissionService: Pick<TPermissionServiceFactory, "getProjectPermission" | "invalidateProjectPermissionCache">;
accessApprovalPolicyApproverDAL: Pick<TAccessApprovalPolicyApproverDALFactory, "find">; accessApprovalPolicyApproverDAL: Pick<TAccessApprovalPolicyApproverDALFactory, "find">;
projectEnvDAL: Pick<TProjectEnvDALFactory, "findOne">; projectEnvDAL: Pick<TProjectEnvDALFactory, "findOne">;
projectDAL: Pick< projectDAL: Pick<
@@ -758,6 +758,8 @@ export const accessApprovalRequestServiceFactory = ({
{ privilegeId: privilegeIdToSet, status: ApprovalStatus.APPROVED }, { privilegeId: privilegeIdToSet, status: ApprovalStatus.APPROVED },
tx tx
); );
await permissionService.invalidateProjectPermissionCache(accessApprovalRequest.projectId, tx);
} }
} }
@@ -407,6 +407,14 @@ export enum EventType {
SECRET_SYNC_SYNC_SECRETS = "secret-sync-sync-secrets", SECRET_SYNC_SYNC_SECRETS = "secret-sync-sync-secrets",
SECRET_SYNC_IMPORT_SECRETS = "secret-sync-import-secrets", SECRET_SYNC_IMPORT_SECRETS = "secret-sync-import-secrets",
SECRET_SYNC_REMOVE_SECRETS = "secret-sync-remove-secrets", SECRET_SYNC_REMOVE_SECRETS = "secret-sync-remove-secrets",
GET_PKI_SYNCS = "get-pki-syncs",
GET_PKI_SYNC = "get-pki-sync",
CREATE_PKI_SYNC = "create-pki-sync",
UPDATE_PKI_SYNC = "update-pki-sync",
DELETE_PKI_SYNC = "delete-pki-sync",
PKI_SYNC_SYNC_CERTIFICATES = "pki-sync-sync-certificates",
PKI_SYNC_IMPORT_CERTIFICATES = "pki-sync-import-certificates",
PKI_SYNC_REMOVE_CERTIFICATES = "pki-sync-remove-certificates",
OIDC_GROUP_MEMBERSHIP_MAPPING_ASSIGN_USER = "oidc-group-membership-mapping-assign-user", OIDC_GROUP_MEMBERSHIP_MAPPING_ASSIGN_USER = "oidc-group-membership-mapping-assign-user",
OIDC_GROUP_MEMBERSHIP_MAPPING_REMOVE_USER = "oidc-group-membership-mapping-remove-user", OIDC_GROUP_MEMBERSHIP_MAPPING_REMOVE_USER = "oidc-group-membership-mapping-remove-user",
CREATE_KMIP_CLIENT = "create-kmip-client", CREATE_KMIP_CLIENT = "create-kmip-client",
@@ -478,9 +486,21 @@ export enum EventType {
UPDATE_PROJECT = "update-project", UPDATE_PROJECT = "update-project",
DELETE_PROJECT = "delete-project", DELETE_PROJECT = "delete-project",
CREATE_PROJECT_ROLE = "create-project-role",
UPDATE_PROJECT_ROLE = "update-project-role",
DELETE_PROJECT_ROLE = "delete-project-role",
CREATE_ORG_ROLE = "create-org-role",
UPDATE_ORG_ROLE = "update-org-role",
DELETE_ORG_ROLE = "delete-org-role",
CREATE_SECRET_REMINDER = "create-secret-reminder", CREATE_SECRET_REMINDER = "create-secret-reminder",
GET_SECRET_REMINDER = "get-secret-reminder", GET_SECRET_REMINDER = "get-secret-reminder",
DELETE_SECRET_REMINDER = "delete-secret-reminder" DELETE_SECRET_REMINDER = "delete-secret-reminder",
DASHBOARD_LIST_SECRETS = "dashboard-list-secrets",
DASHBOARD_GET_SECRET_VALUE = "dashboard-get-secret-value",
DASHBOARD_GET_SECRET_VERSION_VALUE = "dashboard-get-secret-version-value"
} }
export const filterableSecretEvents: EventType[] = [ export const filterableSecretEvents: EventType[] = [
@@ -591,6 +611,7 @@ interface CreateSecretEvent {
secretKey: string; secretKey: string;
secretVersion: number; secretVersion: number;
secretMetadata?: TSecretMetadata; secretMetadata?: TSecretMetadata;
secretTags?: string[];
}; };
} }
@@ -605,6 +626,7 @@ interface CreateSecretBatchEvent {
secretPath?: string; secretPath?: string;
secretVersion: number; secretVersion: number;
secretMetadata?: TSecretMetadata; secretMetadata?: TSecretMetadata;
secretTags?: string[];
}>; }>;
}; };
} }
@@ -618,6 +640,7 @@ interface UpdateSecretEvent {
secretKey: string; secretKey: string;
secretVersion: number; secretVersion: number;
secretMetadata?: TSecretMetadata; secretMetadata?: TSecretMetadata;
secretTags?: string[];
}; };
} }
@@ -632,6 +655,7 @@ interface UpdateSecretBatchEvent {
secretVersion: number; secretVersion: number;
secretMetadata?: TSecretMetadata; secretMetadata?: TSecretMetadata;
secretPath?: string; secretPath?: string;
secretTags?: string[];
}>; }>;
}; };
} }
@@ -2943,6 +2967,77 @@ interface SecretSyncRemoveSecretsEvent {
}; };
} }
interface GetPkiSyncsEvent {
type: EventType.GET_PKI_SYNCS;
metadata: {
projectId: string;
};
}
interface GetPkiSyncEvent {
type: EventType.GET_PKI_SYNC;
metadata: {
destination: string;
syncId: string;
};
}
interface CreatePkiSyncEvent {
type: EventType.CREATE_PKI_SYNC;
metadata: {
pkiSyncId: string;
name: string;
destination: string;
};
}
interface UpdatePkiSyncEvent {
type: EventType.UPDATE_PKI_SYNC;
metadata: {
pkiSyncId: string;
name: string;
};
}
interface DeletePkiSyncEvent {
type: EventType.DELETE_PKI_SYNC;
metadata: {
pkiSyncId: string;
name: string;
destination: string;
};
}
interface PkiSyncSyncCertificatesEvent {
type: EventType.PKI_SYNC_SYNC_CERTIFICATES;
metadata: {
syncId: string;
syncMessage: string | null;
jobId: string;
jobRanAt: Date;
};
}
interface PkiSyncImportCertificatesEvent {
type: EventType.PKI_SYNC_IMPORT_CERTIFICATES;
metadata: {
syncId: string;
importMessage: string | null;
jobId: string;
jobRanAt: Date;
};
}
interface PkiSyncRemoveCertificatesEvent {
type: EventType.PKI_SYNC_REMOVE_CERTIFICATES;
metadata: {
syncId: string;
removeMessage: string | null;
jobId: string;
jobRanAt: Date;
};
}
interface OidcGroupMembershipMappingAssignUserEvent { interface OidcGroupMembershipMappingAssignUserEvent {
type: EventType.OIDC_GROUP_MEMBERSHIP_MAPPING_ASSIGN_USER; type: EventType.OIDC_GROUP_MEMBERSHIP_MAPPING_ASSIGN_USER;
metadata: { metadata: {
@@ -3502,6 +3597,96 @@ interface ProjectDeleteEvent {
}; };
} }
interface DashboardListSecretsEvent {
type: EventType.DASHBOARD_LIST_SECRETS;
metadata: {
environment: string;
secretPath: string;
numberOfSecrets: number;
secretIds: string[];
};
}
interface DashboardGetSecretValueEvent {
type: EventType.DASHBOARD_GET_SECRET_VALUE;
metadata: {
secretId: string;
secretKey: string;
environment: string;
secretPath: string;
};
}
interface DashboardGetSecretVersionValueEvent {
type: EventType.DASHBOARD_GET_SECRET_VERSION_VALUE;
metadata: {
secretId: string;
version: string;
};
}
interface ProjectRoleCreateEvent {
type: EventType.CREATE_PROJECT_ROLE;
metadata: {
roleId: string;
slug: string;
name: string;
description?: string | null;
permissions: string;
};
}
interface ProjectRoleUpdateEvent {
type: EventType.UPDATE_PROJECT_ROLE;
metadata: {
roleId: string;
slug?: string;
name?: string;
description?: string | null;
permissions?: string;
};
}
interface ProjectRoleDeleteEvent {
type: EventType.DELETE_PROJECT_ROLE;
metadata: {
roleId: string;
slug: string;
name: string;
};
}
interface OrgRoleCreateEvent {
type: EventType.CREATE_ORG_ROLE;
metadata: {
roleId: string;
slug: string;
name: string;
description?: string | null;
permissions: string;
};
}
interface OrgRoleUpdateEvent {
type: EventType.UPDATE_ORG_ROLE;
metadata: {
roleId: string;
slug?: string;
name?: string;
description?: string | null;
permissions?: string;
};
}
interface OrgRoleDeleteEvent {
type: EventType.DELETE_ORG_ROLE;
metadata: {
roleId: string;
slug: string;
name: string;
};
}
export type Event = export type Event =
| GetSecretsEvent | GetSecretsEvent
| GetSecretEvent | GetSecretEvent
@@ -3753,6 +3938,14 @@ export type Event =
| SecretSyncSyncSecretsEvent | SecretSyncSyncSecretsEvent
| SecretSyncImportSecretsEvent | SecretSyncImportSecretsEvent
| SecretSyncRemoveSecretsEvent | SecretSyncRemoveSecretsEvent
| GetPkiSyncsEvent
| GetPkiSyncEvent
| CreatePkiSyncEvent
| UpdatePkiSyncEvent
| DeletePkiSyncEvent
| PkiSyncSyncCertificatesEvent
| PkiSyncImportCertificatesEvent
| PkiSyncRemoveCertificatesEvent
| OidcGroupMembershipMappingAssignUserEvent | OidcGroupMembershipMappingAssignUserEvent
| OidcGroupMembershipMappingRemoveUserEvent | OidcGroupMembershipMappingRemoveUserEvent
| CreateKmipClientEvent | CreateKmipClientEvent
@@ -3818,4 +4011,13 @@ export type Event =
| ProjectDeleteEvent | ProjectDeleteEvent
| SecretReminderCreateEvent | SecretReminderCreateEvent
| SecretReminderGetEvent | SecretReminderGetEvent
| SecretReminderDeleteEvent; | SecretReminderDeleteEvent
| DashboardListSecretsEvent
| DashboardGetSecretValueEvent
| DashboardGetSecretVersionValueEvent
| ProjectRoleCreateEvent
| ProjectRoleUpdateEvent
| ProjectRoleDeleteEvent
| OrgRoleCreateEvent
| OrgRoleUpdateEvent
| OrgRoleDeleteEvent;
@@ -0,0 +1,541 @@
import handlebars from "handlebars";
import knex from "knex";
import RE2 from "re2";
import { z } from "zod";
import { crypto } from "@app/lib/crypto/cryptography";
import { BadRequestError } from "@app/lib/errors";
import { sanitizeString } from "@app/lib/fn";
import { GatewayProxyProtocol, withGatewayProxy } from "@app/lib/gateway";
import { withGatewayV2Proxy } from "@app/lib/gateway-v2/gateway-v2";
import { alphaNumericNanoId } from "@app/lib/nanoid";
import { validateHandlebarTemplate } from "@app/lib/template/validate-handlebars";
import { TGatewayServiceFactory } from "../../gateway/gateway-service";
import { TGatewayV2ServiceFactory } from "../../gateway-v2/gateway-v2-service";
import { verifyHostInputValidity } from "../dynamic-secret-fns";
import { DynamicSecretAzureSqlDBSchema, PasswordRequirements, SqlProviders, TDynamicProviderFns } from "./models";
import { compileUsernameTemplate } from "./templateUtils";
const EXTERNAL_REQUEST_TIMEOUT = 10 * 1000;
const DEFAULT_PASSWORD_REQUIREMENTS = {
length: 48,
required: {
lowercase: 1,
uppercase: 1,
digits: 1,
symbols: 0
},
allowedSymbols: "-_.~!*"
};
const generatePassword = (requirements?: PasswordRequirements) => {
const finalReqs = requirements || DEFAULT_PASSWORD_REQUIREMENTS;
try {
const { length, required, allowedSymbols } = finalReqs;
const chars = {
lowercase: "abcdefghijklmnopqrstuvwxyz",
uppercase: "ABCDEFGHIJKLMNOPQRSTUVWXYZ",
digits: "0123456789",
symbols: allowedSymbols || "-_.~!*"
};
const parts: string[] = [];
if (required.lowercase > 0) {
parts.push(
...Array(required.lowercase)
.fill(0)
.map(() => chars.lowercase[crypto.randomInt(chars.lowercase.length)])
);
}
if (required.uppercase > 0) {
parts.push(
...Array(required.uppercase)
.fill(0)
.map(() => chars.uppercase[crypto.randomInt(chars.uppercase.length)])
);
}
if (required.digits > 0) {
parts.push(
...Array(required.digits)
.fill(0)
.map(() => chars.digits[crypto.randomInt(chars.digits.length)])
);
}
if (required.symbols > 0) {
parts.push(
...Array(required.symbols)
.fill(0)
.map(() => chars.symbols[crypto.randomInt(chars.symbols.length)])
);
}
const requiredTotal = Object.values(required).reduce<number>((a, b) => a + b, 0);
const remainingLength = Math.max(length - requiredTotal, 0);
const allowedChars = Object.entries(chars)
.filter(([key]) => required[key as keyof typeof required] > 0)
.map(([, value]) => value)
.join("");
parts.push(
...Array(remainingLength)
.fill(0)
.map(() => allowedChars[crypto.randomInt(allowedChars.length)])
);
// shuffle the array to mix up the characters
for (let i = parts.length - 1; i > 0; i -= 1) {
const j = crypto.randomInt(i + 1);
[parts[i], parts[j]] = [parts[j], parts[i]];
}
return parts.join("");
} catch (error: unknown) {
const message = error instanceof Error ? error.message : "Unknown error";
throw new Error(`Failed to generate password: ${message}`);
}
};
const generateUsername = (usernameTemplate?: string | null, identity?: { name: string }) => {
const randomUsername = alphaNumericNanoId(32);
if (!usernameTemplate) return randomUsername;
return compileUsernameTemplate({
usernameTemplate,
randomUsername,
identity
});
};
type TAzureSqlDatabaseProviderDTO = {
gatewayService: Pick<TGatewayServiceFactory, "fnGetGatewayClientTlsByGatewayId">;
gatewayV2Service: Pick<TGatewayV2ServiceFactory, "getPlatformConnectionDetailsByGatewayId">;
};
export const AzureSqlDatabaseProvider = ({
gatewayService,
gatewayV2Service
}: TAzureSqlDatabaseProviderDTO): TDynamicProviderFns => {
const validateProviderInputs = async (inputs: unknown) => {
const providerInputs = await DynamicSecretAzureSqlDBSchema.parseAsync(inputs);
const [hostIp] = await verifyHostInputValidity(providerInputs.host, Boolean(providerInputs.gatewayId));
validateHandlebarTemplate("Azure SQL master creation", providerInputs.masterCreationStatement, {
allowedExpressions: (val) => ["username", "password", "expiration", "database"].includes(val)
});
validateHandlebarTemplate("Azure SQL creation", providerInputs.creationStatement, {
allowedExpressions: (val) => ["username", "password", "expiration", "database"].includes(val)
});
if (providerInputs.renewStatement) {
validateHandlebarTemplate("Azure SQL renew", providerInputs.renewStatement, {
allowedExpressions: (val) => ["username", "expiration", "database"].includes(val)
});
}
validateHandlebarTemplate("Azure SQL revoke", providerInputs.revocationStatement, {
allowedExpressions: (val) => ["username", "database"].includes(val)
});
return { ...providerInputs, hostIp };
};
const $getClient = async (
providerInputs: z.infer<typeof DynamicSecretAzureSqlDBSchema> & { hostIp: string; originalHost: string },
targetDatabase?: string
) => {
const ssl = providerInputs.ca
? { rejectUnauthorized: false, ca: providerInputs.ca, servername: providerInputs.host }
: undefined;
/*
We route through the gateway by setting connection.host = "localhost".
Azure SQL identifies the logical server from the TDS login name when the host
isn't the Azure FQDN. Therefore, when using the gateway, ensure username is
"user@<azure-server-name>" so Azure opens the correct logical server.
Direct connections to the Azure FQDN usually don't require this suffix.
*/
const isAzureSql = new RE2(/\.database\.windows\.net$/i).test(providerInputs.originalHost);
const azureServerLabel =
isAzureSql && providerInputs.gatewayId ? providerInputs.originalHost?.split(".")[0] : undefined;
const effectiveUser =
isAzureSql && !providerInputs.username.includes("@") && azureServerLabel
? `${providerInputs.username}@${azureServerLabel}`
: providerInputs.username;
const db = knex({
client: SqlProviders.MsSQL,
connection: {
database: targetDatabase || providerInputs.database,
port: providerInputs.port,
host: providerInputs.host,
user: effectiveUser,
password: providerInputs.password,
ssl,
// @ts-expect-error this is because of knexjs type signature issue. This is directly passed to driver
// https://github.com/knex/knex/blob/b6507a7129d2b9fafebf5f831494431e64c6a8a0/lib/dialects/mssql/index.js#L66
// https://github.com/tediousjs/tedious/blob/ebb023ed90969a7ec0e4b036533ad52739d921f7/test/config.ci.ts#L19
options: {
...(providerInputs.sslEnabled !== undefined ? { encrypt: providerInputs.sslEnabled } : {}),
trustServerCertificate: !providerInputs.ca,
cryptoCredentialsDetails: providerInputs.ca ? { ca: providerInputs.ca } : {}
}
},
acquireConnectionTimeout: EXTERNAL_REQUEST_TIMEOUT,
pool: { min: 0, max: 7 }
});
return db;
};
const gatewayProxyWrapper = async (
providerInputs: z.infer<typeof DynamicSecretAzureSqlDBSchema>,
gatewayCallback: (host: string, port: number) => Promise<void>
) => {
const gatewayV2ConnectionDetails = await gatewayV2Service.getPlatformConnectionDetailsByGatewayId({
gatewayId: providerInputs.gatewayId as string,
targetHost: providerInputs.host,
targetPort: providerInputs.port
});
if (gatewayV2ConnectionDetails) {
return withGatewayV2Proxy(
async (port) => {
await gatewayCallback("localhost", port);
},
{
relayHost: gatewayV2ConnectionDetails.relayHost,
gateway: gatewayV2ConnectionDetails.gateway,
relay: gatewayV2ConnectionDetails.relay,
protocol: GatewayProxyProtocol.Tcp
}
);
}
const relayDetails = await gatewayService.fnGetGatewayClientTlsByGatewayId(providerInputs.gatewayId as string);
const [relayHost, relayPort] = relayDetails.relayAddress.split(":");
await withGatewayProxy(
async (port) => {
await gatewayCallback("localhost", port);
},
{
protocol: GatewayProxyProtocol.Tcp,
targetHost: providerInputs.host,
targetPort: providerInputs.port,
relayHost,
relayPort: Number(relayPort),
identityId: relayDetails.identityId,
orgId: relayDetails.orgId,
tlsOptions: {
ca: relayDetails.certChain,
cert: relayDetails.certificate,
key: relayDetails.privateKey.toString()
}
}
);
};
const validateConnection = async (inputs: unknown) => {
const providerInputs = await validateProviderInputs(inputs);
let isConnected = false;
const gatewayCallback = async (host = providerInputs.host, port = providerInputs.port) => {
const db = await $getClient({
...providerInputs,
port,
host,
hostIp: providerInputs.hostIp,
originalHost: providerInputs.host
});
try {
isConnected = await db.raw("SELECT 1").then(() => true);
} catch (err) {
const sanitizedErrorMessage = sanitizeString({
unsanitizedString: (err as Error)?.message,
tokens: [providerInputs.username]
});
throw new BadRequestError({
message: `Failed to connect with provider: ${sanitizedErrorMessage}`
});
} finally {
await db.destroy();
}
};
if (providerInputs.gatewayId) {
await gatewayProxyWrapper(providerInputs, gatewayCallback);
} else {
await gatewayCallback();
}
return isConnected;
};
const create = async (data: {
inputs: unknown;
expireAt: number;
usernameTemplate?: string | null;
identity?: { name: string };
}) => {
const { inputs, expireAt, usernameTemplate, identity } = data;
const providerInputs = await validateProviderInputs(inputs);
const { database, masterDatabase } = providerInputs;
const username = generateUsername(usernameTemplate, identity);
const password = generatePassword(providerInputs.passwordRequirements);
const gatewayCallback = async (host = providerInputs.host, port = providerInputs.port) => {
const expiration = new Date(expireAt).toISOString();
const masterDb = await $getClient(
{
...providerInputs,
port,
host,
originalHost: providerInputs.host
},
masterDatabase
);
try {
const masterCreationStatement = handlebars.compile(providerInputs.masterCreationStatement, { noEscape: true })({
username,
password,
expiration,
database
});
const masterQueries = masterCreationStatement.toString().split(";").filter(Boolean);
await masterDb.transaction(async (tx) => {
for (const query of masterQueries) {
// eslint-disable-next-line
await tx.raw(query);
}
});
} catch (err) {
const sanitizedErrorMessage = sanitizeString({
unsanitizedString: (err as Error)?.message,
tokens: [username, password, database]
});
throw new BadRequestError({
message: `Failed to create login in master database: ${sanitizedErrorMessage}`
});
} finally {
await masterDb.destroy();
}
const targetDb = await $getClient({
...providerInputs,
port,
host,
originalHost: providerInputs.host
});
try {
const creationStatement = handlebars.compile(providerInputs.creationStatement, { noEscape: true })({
username,
password,
expiration,
database
});
const queries = creationStatement.toString().split(";").filter(Boolean);
await targetDb.transaction(async (tx) => {
for (const query of queries) {
// eslint-disable-next-line
await tx.raw(query);
}
});
} catch (err) {
const sanitizedErrorMessage = sanitizeString({
unsanitizedString: (err as Error)?.message,
tokens: [username, password, database]
});
throw new BadRequestError({
message: `Failed to create user in target database: ${sanitizedErrorMessage}`
});
} finally {
await targetDb.destroy();
}
};
if (providerInputs.gatewayId) {
await gatewayProxyWrapper(providerInputs, gatewayCallback);
} else {
await gatewayCallback();
}
return { entityId: username, data: { DB_USERNAME: username, DB_PASSWORD: password } };
};
const revoke = async (inputs: unknown, entityId: string) => {
const providerInputs = await validateProviderInputs(inputs);
const username = entityId;
const { database, masterDatabase } = providerInputs;
const gatewayCallback = async (host = providerInputs.host, port = providerInputs.port) => {
const revokeStatement = handlebars.compile(providerInputs.revocationStatement)({ username, database });
const queries = revokeStatement.toString().split(";").filter(Boolean);
const userDropQueries = queries.filter((query) => query.toLowerCase().includes("drop user"));
const loginDropQueries = queries.filter((query) => query.toLowerCase().includes("drop login"));
if (userDropQueries.length > 0) {
const targetDb = await $getClient({
...providerInputs,
port,
host,
originalHost: providerInputs.host
});
try {
await targetDb.transaction(async (tx) => {
for (const query of userDropQueries) {
// eslint-disable-next-line
await tx.raw(query.trim());
}
});
} catch (err) {
const sanitizedErrorMessage = sanitizeString({
unsanitizedString: (err as Error)?.message,
tokens: [username, database]
});
throw new BadRequestError({
message: `Failed to drop user from target database: ${sanitizedErrorMessage}`
});
} finally {
await targetDb.destroy();
}
}
if (loginDropQueries.length > 0) {
const masterDb = await $getClient(
{
...providerInputs,
port,
host,
originalHost: providerInputs.host
},
masterDatabase
);
try {
await masterDb.transaction(async (tx) => {
for (const query of loginDropQueries) {
// eslint-disable-next-line
await tx.raw(query.trim());
}
});
} catch (err) {
const sanitizedErrorMessage = sanitizeString({
unsanitizedString: (err as Error)?.message,
tokens: [username, database]
});
throw new BadRequestError({
message: `Failed to drop login from master database: ${sanitizedErrorMessage}`
});
} finally {
await masterDb.destroy();
}
}
const otherQueries = queries.filter(
(query) => !query.toLowerCase().includes("drop user") && !query.toLowerCase().includes("drop login")
);
if (otherQueries.length > 0) {
const targetDb = await $getClient({
...providerInputs,
port,
host,
originalHost: providerInputs.host
});
try {
await targetDb.transaction(async (tx) => {
for (const query of otherQueries) {
// eslint-disable-next-line
await tx.raw(query.trim());
}
});
} catch (err) {
const sanitizedErrorMessage = sanitizeString({
unsanitizedString: (err as Error)?.message,
tokens: [username, database]
});
throw new BadRequestError({
message: `Failed to execute revocation statement: ${sanitizedErrorMessage}`
});
} finally {
await targetDb.destroy();
}
}
};
if (providerInputs.gatewayId) {
await gatewayProxyWrapper(providerInputs, gatewayCallback);
} else {
await gatewayCallback();
}
return { entityId: username };
};
const renew = async (inputs: unknown, entityId: string, expireAt: number) => {
const providerInputs = await validateProviderInputs(inputs);
if (!providerInputs.renewStatement) return { entityId };
const gatewayCallback = async (host = providerInputs.host, port = providerInputs.port) => {
const db = await $getClient({
...providerInputs,
port,
host,
originalHost: providerInputs.host
});
const expiration = new Date(expireAt).toISOString();
const { database } = providerInputs;
const renewStatement = handlebars.compile(providerInputs.renewStatement)({
username: entityId,
expiration,
database
});
try {
if (renewStatement) {
const queries = renewStatement.toString().split(";").filter(Boolean);
await db.transaction(async (tx) => {
for (const query of queries) {
// eslint-disable-next-line
await tx.raw(query);
}
});
}
} catch (err) {
const sanitizedErrorMessage = sanitizeString({
unsanitizedString: (err as Error)?.message,
tokens: [database]
});
throw new BadRequestError({
message: `Failed to renew lease from provider: ${sanitizedErrorMessage}`
});
} finally {
await db.destroy();
}
};
if (providerInputs.gatewayId) {
await gatewayProxyWrapper(providerInputs, gatewayCallback);
} else {
await gatewayCallback();
}
return { entityId };
};
return {
validateProviderInputs,
validateConnection,
create,
revoke,
renew
};
};
@@ -5,6 +5,7 @@ import { TGatewayV2ServiceFactory } from "../../gateway-v2/gateway-v2-service";
import { AwsElastiCacheDatabaseProvider } from "./aws-elasticache"; import { AwsElastiCacheDatabaseProvider } from "./aws-elasticache";
import { AwsIamProvider } from "./aws-iam"; import { AwsIamProvider } from "./aws-iam";
import { AzureEntraIDProvider } from "./azure-entra-id"; import { AzureEntraIDProvider } from "./azure-entra-id";
import { AzureSqlDatabaseProvider } from "./azure-sql-database";
import { CassandraProvider } from "./cassandra"; import { CassandraProvider } from "./cassandra";
import { CouchbaseProvider } from "./couchbase"; import { CouchbaseProvider } from "./couchbase";
import { ElasticSearchProvider } from "./elastic-search"; import { ElasticSearchProvider } from "./elastic-search";
@@ -42,6 +43,7 @@ export const buildDynamicSecretProviders = ({
[DynamicSecretProviders.ElasticSearch]: ElasticSearchProvider(), [DynamicSecretProviders.ElasticSearch]: ElasticSearchProvider(),
[DynamicSecretProviders.RabbitMq]: RabbitMqProvider(), [DynamicSecretProviders.RabbitMq]: RabbitMqProvider(),
[DynamicSecretProviders.AzureEntraID]: AzureEntraIDProvider(), [DynamicSecretProviders.AzureEntraID]: AzureEntraIDProvider(),
[DynamicSecretProviders.AzureSqlDatabase]: AzureSqlDatabaseProvider({ gatewayService, gatewayV2Service }),
[DynamicSecretProviders.Ldap]: LdapProvider(), [DynamicSecretProviders.Ldap]: LdapProvider(),
[DynamicSecretProviders.SapHana]: SapHanaProvider(), [DynamicSecretProviders.SapHana]: SapHanaProvider(),
[DynamicSecretProviders.Snowflake]: SnowflakeProvider(), [DynamicSecretProviders.Snowflake]: SnowflakeProvider(),
@@ -327,6 +327,44 @@ export const AzureEntraIDSchema = z.object({
clientSecret: z.string().trim().min(1) clientSecret: z.string().trim().min(1)
}); });
export const DynamicSecretAzureSqlDBSchema = z.object({
host: z.string().trim().toLowerCase(),
port: z.number(),
database: z.string().trim(),
masterDatabase: z.string().trim().optional().default("master"),
username: z.string().trim(),
password: z.string().trim(),
passwordRequirements: z
.object({
length: z.number().min(1).max(250),
required: z
.object({
lowercase: z.number().min(0),
uppercase: z.number().min(0),
digits: z.number().min(0),
symbols: z.number().min(0)
})
.refine((data) => {
const total = Object.values(data).reduce((sum, count) => sum + count, 0);
return total <= 250;
}, "Sum of required characters cannot exceed 250"),
allowedSymbols: z.string().optional()
})
.refine((data) => {
const total = Object.values(data.required).reduce((sum, count) => sum + count, 0);
return total <= data.length;
}, "Sum of required characters cannot exceed the total length")
.optional()
.describe("Password generation requirements"),
masterCreationStatement: z.string().trim(),
creationStatement: z.string().trim(),
revocationStatement: z.string().trim(),
renewStatement: z.string().trim().optional(),
ca: z.string().optional(),
sslEnabled: z.boolean().optional(),
gatewayId: z.string().nullable().optional()
});
export const LdapSchema = z.union([ export const LdapSchema = z.union([
z.object({ z.object({
url: z.string().trim().min(1), url: z.string().trim().min(1),
@@ -610,6 +648,7 @@ export enum DynamicSecretProviders {
MongoDB = "mongo-db", MongoDB = "mongo-db",
RabbitMq = "rabbit-mq", RabbitMq = "rabbit-mq",
AzureEntraID = "azure-entra-id", AzureEntraID = "azure-entra-id",
AzureSqlDatabase = "azure-sql-database",
Ldap = "ldap", Ldap = "ldap",
SapHana = "sap-hana", SapHana = "sap-hana",
Snowflake = "snowflake", Snowflake = "snowflake",
@@ -635,6 +674,7 @@ export const DynamicSecretProviderSchema = z.discriminatedUnion("type", [
z.object({ type: z.literal(DynamicSecretProviders.MongoDB), inputs: DynamicSecretMongoDBSchema }), z.object({ type: z.literal(DynamicSecretProviders.MongoDB), inputs: DynamicSecretMongoDBSchema }),
z.object({ type: z.literal(DynamicSecretProviders.RabbitMq), inputs: DynamicSecretRabbitMqSchema }), z.object({ type: z.literal(DynamicSecretProviders.RabbitMq), inputs: DynamicSecretRabbitMqSchema }),
z.object({ type: z.literal(DynamicSecretProviders.AzureEntraID), inputs: AzureEntraIDSchema }), z.object({ type: z.literal(DynamicSecretProviders.AzureEntraID), inputs: AzureEntraIDSchema }),
z.object({ type: z.literal(DynamicSecretProviders.AzureSqlDatabase), inputs: DynamicSecretAzureSqlDBSchema }),
z.object({ type: z.literal(DynamicSecretProviders.Ldap), inputs: LdapSchema }), z.object({ type: z.literal(DynamicSecretProviders.Ldap), inputs: LdapSchema }),
z.object({ type: z.literal(DynamicSecretProviders.Snowflake), inputs: DynamicSecretSnowflakeSchema }), z.object({ type: z.literal(DynamicSecretProviders.Snowflake), inputs: DynamicSecretSnowflakeSchema }),
z.object({ type: z.literal(DynamicSecretProviders.Totp), inputs: DynamicSecretTotpSchema }), z.object({ type: z.literal(DynamicSecretProviders.Totp), inputs: DynamicSecretTotpSchema }),
@@ -395,7 +395,8 @@ export const gatewayV2ServiceFactory = ({
relayId: gateway.relayId, relayId: gateway.relayId,
orgId: gateway.orgId, orgId: gateway.orgId,
orgName: gateway.orgName, orgName: gateway.orgName,
gatewayId gatewayId,
gatewayName: gateway.name
}); });
return { return {
@@ -508,7 +509,8 @@ export const gatewayV2ServiceFactory = ({
const relayCredentials = await relayService.getCredentialsForGateway({ const relayCredentials = await relayService.getCredentialsForGateway({
relayName, relayName,
orgId, orgId,
gatewayId: gateway.id gatewayId: gateway.id,
gatewayName: gateway.name
}); });
return { return {
+29 -1
View File
@@ -44,7 +44,10 @@ type TGroupServiceFactoryDep = {
projectDAL: Pick<TProjectDALFactory, "findProjectGhostUser" | "findById">; projectDAL: Pick<TProjectDALFactory, "findProjectGhostUser" | "findById">;
projectBotDAL: Pick<TProjectBotDALFactory, "findOne">; projectBotDAL: Pick<TProjectBotDALFactory, "findOne">;
projectKeyDAL: Pick<TProjectKeyDALFactory, "find" | "delete" | "findLatestProjectKey" | "insertMany">; projectKeyDAL: Pick<TProjectKeyDALFactory, "find" | "delete" | "findLatestProjectKey" | "insertMany">;
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission" | "getOrgPermissionByRole">; permissionService: Pick<
TPermissionServiceFactory,
"getOrgPermission" | "getOrgPermissionByRole" | "invalidateProjectPermissionCache"
>;
licenseService: Pick<TLicenseServiceFactory, "getPlan">; licenseService: Pick<TLicenseServiceFactory, "getPlan">;
oidcConfigDAL: Pick<TOidcConfigDALFactory, "findOne">; oidcConfigDAL: Pick<TOidcConfigDALFactory, "findOne">;
}; };
@@ -225,6 +228,15 @@ export const groupServiceFactory = ({
return updated; return updated;
}); });
if (role) {
const groupProjects = await groupProjectDAL.find({ groupId: group.id });
await Promise.allSettled([
...groupProjects.map((groupProject) =>
permissionService.invalidateProjectPermissionCache(groupProject.projectId)
)
]);
}
return updatedGroup; return updatedGroup;
}; };
@@ -247,11 +259,17 @@ export const groupServiceFactory = ({
message: "Failed to delete group due to plan restriction. Upgrade plan to delete group." message: "Failed to delete group due to plan restriction. Upgrade plan to delete group."
}); });
const groupProjects = await groupProjectDAL.find({ groupId: id });
const [group] = await groupDAL.delete({ const [group] = await groupDAL.delete({
id, id,
orgId: actorOrgId orgId: actorOrgId
}); });
await Promise.allSettled([
...groupProjects.map((groupProject) => permissionService.invalidateProjectPermissionCache(groupProject.projectId))
]);
return group; return group;
}; };
@@ -398,6 +416,11 @@ export const groupServiceFactory = ({
projectBotDAL projectBotDAL
}); });
const groupProjects = await groupProjectDAL.find({ groupId: group.id });
await Promise.allSettled([
...groupProjects.map((groupProject) => permissionService.invalidateProjectPermissionCache(groupProject.projectId))
]);
return users[0]; return users[0];
}; };
@@ -479,6 +502,11 @@ export const groupServiceFactory = ({
projectKeyDAL projectKeyDAL
}); });
const groupProjects = await groupProjectDAL.find({ groupId: group.id });
await Promise.allSettled([
...groupProjects.map((groupProject) => permissionService.invalidateProjectPermissionCache(groupProject.projectId))
]);
return users[0]; return users[0];
}; };
@@ -28,7 +28,7 @@ type TIdentityProjectAdditionalPrivilegeV2ServiceFactoryDep = {
identityProjectAdditionalPrivilegeDAL: TIdentityProjectAdditionalPrivilegeV2DALFactory; identityProjectAdditionalPrivilegeDAL: TIdentityProjectAdditionalPrivilegeV2DALFactory;
identityProjectDAL: Pick<TIdentityProjectDALFactory, "findOne" | "findById">; identityProjectDAL: Pick<TIdentityProjectDALFactory, "findOne" | "findById">;
projectDAL: Pick<TProjectDALFactory, "findProjectBySlug">; projectDAL: Pick<TProjectDALFactory, "findProjectBySlug">;
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">; permissionService: Pick<TPermissionServiceFactory, "getProjectPermission" | "invalidateProjectPermissionCache">;
}; };
export type TIdentityProjectAdditionalPrivilegeV2ServiceFactory = ReturnType< export type TIdentityProjectAdditionalPrivilegeV2ServiceFactory = ReturnType<
@@ -115,6 +115,8 @@ export const identityProjectAdditionalPrivilegeV2ServiceFactory = ({
permissions: packedPermission permissions: packedPermission
}); });
await permissionService.invalidateProjectPermissionCache(identityProjectMembership.projectId);
return { return {
...additionalPrivilege, ...additionalPrivilege,
permissions: unpackPermissions(additionalPrivilege.permissions) permissions: unpackPermissions(additionalPrivilege.permissions)
@@ -132,6 +134,9 @@ export const identityProjectAdditionalPrivilegeV2ServiceFactory = ({
temporaryAccessStartTime: new Date(dto.temporaryAccessStartTime), temporaryAccessStartTime: new Date(dto.temporaryAccessStartTime),
temporaryAccessEndTime: new Date(new Date(dto.temporaryAccessStartTime).getTime() + relativeTempAllocatedTimeInMs) temporaryAccessEndTime: new Date(new Date(dto.temporaryAccessStartTime).getTime() + relativeTempAllocatedTimeInMs)
}); });
await permissionService.invalidateProjectPermissionCache(identityProjectMembership.projectId);
return { return {
...additionalPrivilege, ...additionalPrivilege,
permissions: unpackPermissions(additionalPrivilege.permissions) permissions: unpackPermissions(additionalPrivilege.permissions)
@@ -224,6 +229,9 @@ export const identityProjectAdditionalPrivilegeV2ServiceFactory = ({
temporaryAccessStartTime: new Date(temporaryAccessStartTime || ""), temporaryAccessStartTime: new Date(temporaryAccessStartTime || ""),
temporaryAccessEndTime: new Date(new Date(temporaryAccessStartTime || "").getTime() + ms(temporaryRange || "")) temporaryAccessEndTime: new Date(new Date(temporaryAccessStartTime || "").getTime() + ms(temporaryRange || ""))
}); });
await permissionService.invalidateProjectPermissionCache(identityProjectMembership.projectId);
return { return {
...additionalPrivilege, ...additionalPrivilege,
permissions: unpackPermissions(additionalPrivilege.permissions) permissions: unpackPermissions(additionalPrivilege.permissions)
@@ -239,6 +247,9 @@ export const identityProjectAdditionalPrivilegeV2ServiceFactory = ({
temporaryRange: null, temporaryRange: null,
temporaryMode: null temporaryMode: null
}); });
await permissionService.invalidateProjectPermissionCache(identityProjectMembership.projectId);
return { return {
...additionalPrivilege, ...additionalPrivilege,
permissions: unpackPermissions(additionalPrivilege.permissions) permissions: unpackPermissions(additionalPrivilege.permissions)
@@ -294,6 +305,9 @@ export const identityProjectAdditionalPrivilegeV2ServiceFactory = ({
}); });
const deletedPrivilege = await identityProjectAdditionalPrivilegeDAL.deleteById(identityPrivilege.id); const deletedPrivilege = await identityProjectAdditionalPrivilegeDAL.deleteById(identityPrivilege.id);
await permissionService.invalidateProjectPermissionCache(identityProjectMembership.projectId);
return { return {
...deletedPrivilege, ...deletedPrivilege,
permissions: unpackPermissions(deletedPrivilege.permissions) permissions: unpackPermissions(deletedPrivilege.permissions)
@@ -31,7 +31,7 @@ type TIdentityProjectAdditionalPrivilegeServiceFactoryDep = {
identityProjectAdditionalPrivilegeDAL: TIdentityProjectAdditionalPrivilegeDALFactory; identityProjectAdditionalPrivilegeDAL: TIdentityProjectAdditionalPrivilegeDALFactory;
identityProjectDAL: Pick<TIdentityProjectDALFactory, "findOne" | "findById">; identityProjectDAL: Pick<TIdentityProjectDALFactory, "findOne" | "findById">;
projectDAL: Pick<TProjectDALFactory, "findProjectBySlug">; projectDAL: Pick<TProjectDALFactory, "findProjectBySlug">;
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">; permissionService: Pick<TPermissionServiceFactory, "getProjectPermission" | "invalidateProjectPermissionCache">;
}; };
export type TIdentityProjectAdditionalPrivilegeServiceFactory = ReturnType< export type TIdentityProjectAdditionalPrivilegeServiceFactory = ReturnType<
@@ -129,6 +129,9 @@ export const identityProjectAdditionalPrivilegeServiceFactory = ({
slug, slug,
permissions: packedPermission permissions: packedPermission
}); });
await permissionService.invalidateProjectPermissionCache(identityProjectMembership.projectId);
return { return {
...additionalPrivilege, ...additionalPrivilege,
permissions: unpackPermissions(additionalPrivilege.permissions) permissions: unpackPermissions(additionalPrivilege.permissions)
@@ -146,6 +149,9 @@ export const identityProjectAdditionalPrivilegeServiceFactory = ({
temporaryAccessStartTime: new Date(dto.temporaryAccessStartTime), temporaryAccessStartTime: new Date(dto.temporaryAccessStartTime),
temporaryAccessEndTime: new Date(new Date(dto.temporaryAccessStartTime).getTime() + relativeTempAllocatedTimeInMs) temporaryAccessEndTime: new Date(new Date(dto.temporaryAccessStartTime).getTime() + relativeTempAllocatedTimeInMs)
}); });
await permissionService.invalidateProjectPermissionCache(identityProjectMembership.projectId);
return { return {
...additionalPrivilege, ...additionalPrivilege,
permissions: unpackPermissions(additionalPrivilege.permissions) permissions: unpackPermissions(additionalPrivilege.permissions)
@@ -250,6 +256,9 @@ export const identityProjectAdditionalPrivilegeServiceFactory = ({
temporaryAccessStartTime: new Date(temporaryAccessStartTime || ""), temporaryAccessStartTime: new Date(temporaryAccessStartTime || ""),
temporaryAccessEndTime: new Date(new Date(temporaryAccessStartTime || "").getTime() + ms(temporaryRange || "")) temporaryAccessEndTime: new Date(new Date(temporaryAccessStartTime || "").getTime() + ms(temporaryRange || ""))
}); });
await permissionService.invalidateProjectPermissionCache(identityProjectMembership.projectId);
return { return {
...additionalPrivilege, ...additionalPrivilege,
permissions: unpackPermissions(additionalPrivilege.permissions) permissions: unpackPermissions(additionalPrivilege.permissions)
@@ -265,6 +274,9 @@ export const identityProjectAdditionalPrivilegeServiceFactory = ({
temporaryRange: null, temporaryRange: null,
temporaryMode: null temporaryMode: null
}); });
await permissionService.invalidateProjectPermissionCache(identityProjectMembership.projectId);
return { return {
...additionalPrivilege, ...additionalPrivilege,
permissions: unpackPermissions(additionalPrivilege.permissions) permissions: unpackPermissions(additionalPrivilege.permissions)
@@ -338,9 +350,11 @@ export const identityProjectAdditionalPrivilegeServiceFactory = ({
} }
const deletedPrivilege = await identityProjectAdditionalPrivilegeDAL.deleteById(identityPrivilege.id); const deletedPrivilege = await identityProjectAdditionalPrivilegeDAL.deleteById(identityPrivilege.id);
await permissionService.invalidateProjectPermissionCache(identityProjectMembership.projectId);
return { return {
...deletedPrivilege, ...deletedPrivilege,
permissions: unpackPermissions(deletedPrivilege.permissions) permissions: unpackPermissions(deletedPrivilege.permissions)
}; };
}; };
@@ -31,6 +31,7 @@ export const getDefaultOnPremFeatures = () => {
caCrl: false, caCrl: false,
sshHostGroups: false, sshHostGroups: false,
enterpriseSecretSyncs: false, enterpriseSecretSyncs: false,
enterpriseCertificateSyncs: false,
enterpriseAppConnections: true, enterpriseAppConnections: true,
machineIdentityAuthTemplates: false machineIdentityAuthTemplates: false
}; };
@@ -62,6 +62,7 @@ export const getDefaultOnPremFeatures = (): TFeatureSet => ({
sshHostGroups: false, sshHostGroups: false,
secretScanning: false, secretScanning: false,
enterpriseSecretSyncs: false, enterpriseSecretSyncs: false,
enterpriseCertificateSyncs: false,
enterpriseAppConnections: false, enterpriseAppConnections: false,
fips: false, fips: false,
eventSubscriptions: false, eventSubscriptions: false,
@@ -75,6 +75,7 @@ export type TFeatureSet = {
sshHostGroups: false; sshHostGroups: false;
secretScanning: false; secretScanning: false;
enterpriseSecretSyncs: false; enterpriseSecretSyncs: false;
enterpriseCertificateSyncs: false;
enterpriseAppConnections: false; enterpriseAppConnections: false;
machineIdentityAuthTemplates: false; machineIdentityAuthTemplates: false;
fips: false; fips: false;
@@ -13,6 +13,7 @@ import {
ProjectPermissionKmipActions, ProjectPermissionKmipActions,
ProjectPermissionMemberActions, ProjectPermissionMemberActions,
ProjectPermissionPkiSubscriberActions, ProjectPermissionPkiSubscriberActions,
ProjectPermissionPkiSyncActions,
ProjectPermissionPkiTemplateActions, ProjectPermissionPkiTemplateActions,
ProjectPermissionSecretActions, ProjectPermissionSecretActions,
ProjectPermissionSecretEventActions, ProjectPermissionSecretEventActions,
@@ -209,6 +210,19 @@ const buildAdminPermissionRules = () => {
ProjectPermissionSub.SecretSyncs ProjectPermissionSub.SecretSyncs
); );
can(
[
ProjectPermissionPkiSyncActions.Create,
ProjectPermissionPkiSyncActions.Edit,
ProjectPermissionPkiSyncActions.Delete,
ProjectPermissionPkiSyncActions.Read,
ProjectPermissionPkiSyncActions.SyncCertificates,
ProjectPermissionPkiSyncActions.ImportCertificates,
ProjectPermissionPkiSyncActions.RemoveCertificates
],
ProjectPermissionSub.PkiSyncs
);
can( can(
[ [
ProjectPermissionKmipActions.CreateClients, ProjectPermissionKmipActions.CreateClients,
@@ -462,6 +476,19 @@ const buildMemberPermissionRules = () => {
ProjectPermissionSub.SecretSyncs ProjectPermissionSub.SecretSyncs
); );
can(
[
ProjectPermissionPkiSyncActions.Create,
ProjectPermissionPkiSyncActions.Edit,
ProjectPermissionPkiSyncActions.Delete,
ProjectPermissionPkiSyncActions.Read,
ProjectPermissionPkiSyncActions.SyncCertificates,
ProjectPermissionPkiSyncActions.ImportCertificates,
ProjectPermissionPkiSyncActions.RemoveCertificates
],
ProjectPermissionSub.PkiSyncs
);
can( can(
[ [
ProjectPermissionSecretScanningDataSourceActions.Read, ProjectPermissionSecretScanningDataSourceActions.Read,
@@ -526,6 +553,7 @@ const buildViewerPermissionRules = () => {
can(ProjectPermissionActions.Read, ProjectPermissionSub.SshCertificates); can(ProjectPermissionActions.Read, ProjectPermissionSub.SshCertificates);
can(ProjectPermissionActions.Read, ProjectPermissionSub.SshCertificateTemplates); can(ProjectPermissionActions.Read, ProjectPermissionSub.SshCertificateTemplates);
can(ProjectPermissionSecretSyncActions.Read, ProjectPermissionSub.SecretSyncs); can(ProjectPermissionSecretSyncActions.Read, ProjectPermissionSub.SecretSyncs);
can(ProjectPermissionPkiSyncActions.Read, ProjectPermissionSub.PkiSyncs);
can(ProjectPermissionCommitsActions.Read, ProjectPermissionSub.Commits); can(ProjectPermissionCommitsActions.Read, ProjectPermissionSub.Commits);
can( can(
@@ -58,6 +58,13 @@ export enum OrgPermissionGatewayActions {
AttachGateways = "attach-gateways" AttachGateways = "attach-gateways"
} }
export enum OrgPermissionRelayActions {
CreateRelays = "create-relays",
ListRelays = "list-relays",
EditRelays = "edit-relays",
DeleteRelays = "delete-relays"
}
export enum OrgPermissionIdentityActions { export enum OrgPermissionIdentityActions {
Read = "read", Read = "read",
Create = "create", Create = "create",
@@ -109,6 +116,7 @@ export enum OrgPermissionSubjects {
AppConnections = "app-connections", AppConnections = "app-connections",
Kmip = "kmip", Kmip = "kmip",
Gateway = "gateway", Gateway = "gateway",
Relay = "relay",
SecretShare = "secret-share" SecretShare = "secret-share"
} }
@@ -136,6 +144,7 @@ export type OrgPermissionSet =
| [OrgPermissionAuditLogsActions, OrgPermissionSubjects.AuditLogs] | [OrgPermissionAuditLogsActions, OrgPermissionSubjects.AuditLogs]
| [OrgPermissionActions, OrgPermissionSubjects.ProjectTemplates] | [OrgPermissionActions, OrgPermissionSubjects.ProjectTemplates]
| [OrgPermissionGatewayActions, OrgPermissionSubjects.Gateway] | [OrgPermissionGatewayActions, OrgPermissionSubjects.Gateway]
| [OrgPermissionRelayActions, OrgPermissionSubjects.Relay]
| [ | [
OrgPermissionAppConnectionActions, OrgPermissionAppConnectionActions,
( (
@@ -279,6 +288,12 @@ export const OrgPermissionSchema = z.discriminatedUnion("subject", [
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(OrgPermissionGatewayActions).describe( action: CASL_ACTION_SCHEMA_NATIVE_ENUM(OrgPermissionGatewayActions).describe(
"Describe what action an entity can take." "Describe what action an entity can take."
) )
}),
z.object({
subject: z.literal(OrgPermissionSubjects.Relay).describe("The entity this permission pertains to."),
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(OrgPermissionRelayActions).describe(
"Describe what action an entity can take."
)
}) })
]); ]);
@@ -383,6 +398,11 @@ const buildAdminPermission = () => {
can(OrgPermissionGatewayActions.DeleteGateways, OrgPermissionSubjects.Gateway); can(OrgPermissionGatewayActions.DeleteGateways, OrgPermissionSubjects.Gateway);
can(OrgPermissionGatewayActions.AttachGateways, OrgPermissionSubjects.Gateway); can(OrgPermissionGatewayActions.AttachGateways, OrgPermissionSubjects.Gateway);
can(OrgPermissionRelayActions.ListRelays, OrgPermissionSubjects.Relay);
can(OrgPermissionRelayActions.CreateRelays, OrgPermissionSubjects.Relay);
can(OrgPermissionRelayActions.EditRelays, OrgPermissionSubjects.Relay);
can(OrgPermissionRelayActions.DeleteRelays, OrgPermissionSubjects.Relay);
can(OrgPermissionAdminConsoleAction.AccessAllProjects, OrgPermissionSubjects.AdminConsole); can(OrgPermissionAdminConsoleAction.AccessAllProjects, OrgPermissionSubjects.AdminConsole);
can(OrgPermissionKmipActions.Setup, OrgPermissionSubjects.Kmip); can(OrgPermissionKmipActions.Setup, OrgPermissionSubjects.Kmip);
@@ -445,6 +465,10 @@ const buildMemberPermission = () => {
can(OrgPermissionGatewayActions.CreateGateways, OrgPermissionSubjects.Gateway); can(OrgPermissionGatewayActions.CreateGateways, OrgPermissionSubjects.Gateway);
can(OrgPermissionGatewayActions.AttachGateways, OrgPermissionSubjects.Gateway); can(OrgPermissionGatewayActions.AttachGateways, OrgPermissionSubjects.Gateway);
can(OrgPermissionRelayActions.ListRelays, OrgPermissionSubjects.Relay);
can(OrgPermissionRelayActions.CreateRelays, OrgPermissionSubjects.Relay);
can(OrgPermissionRelayActions.EditRelays, OrgPermissionSubjects.Relay);
can(OrgPermissionMachineIdentityAuthTemplateActions.ListTemplates, OrgPermissionSubjects.MachineIdentityAuthTemplate); can(OrgPermissionMachineIdentityAuthTemplateActions.ListTemplates, OrgPermissionSubjects.MachineIdentityAuthTemplate);
can( can(
OrgPermissionMachineIdentityAuthTemplateActions.UnlinkTemplates, OrgPermissionMachineIdentityAuthTemplateActions.UnlinkTemplates,
@@ -1,5 +1,6 @@
import { MongoAbility, RawRuleOf } from "@casl/ability"; import { MongoAbility, RawRuleOf } from "@casl/ability";
import { MongoQuery } from "@ucast/mongo2js"; import { MongoQuery } from "@ucast/mongo2js";
import { Knex } from "knex";
import { ActionProjectType } from "@app/db/schemas"; import { ActionProjectType } from "@app/db/schemas";
import { ActorAuthMethod, ActorType } from "@app/services/auth/auth-type"; import { ActorAuthMethod, ActorType } from "@app/services/auth/auth-type";
@@ -283,4 +284,5 @@ export type TPermissionServiceFactory = {
projectId: string; projectId: string;
checkPermissions: ProjectPermissionSet; checkPermissions: ProjectPermissionSet;
}) => Promise<boolean>; }) => Promise<boolean>;
invalidateProjectPermissionCache: (projectId: string, tx?: Knex) => Promise<void>;
}; };
@@ -3,6 +3,7 @@ import { PackRule, unpackRules } from "@casl/ability/extra";
import { requestContext } from "@fastify/request-context"; import { requestContext } from "@fastify/request-context";
import { MongoQuery } from "@ucast/mongo2js"; import { MongoQuery } from "@ucast/mongo2js";
import handlebars from "handlebars"; import handlebars from "handlebars";
import { Knex } from "knex";
import { import {
ActionProjectType, ActionProjectType,
@@ -20,9 +21,11 @@ import {
projectViewerPermission, projectViewerPermission,
sshHostBootstrapPermissions sshHostBootstrapPermissions
} from "@app/ee/services/permission/default-roles"; } from "@app/ee/services/permission/default-roles";
import { KeyStorePrefixes, KeyStoreTtls, TKeyStoreFactory } from "@app/keystore/keystore";
import { conditionsMatcher } from "@app/lib/casl"; import { conditionsMatcher } from "@app/lib/casl";
import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors"; import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
import { objectify } from "@app/lib/fn"; import { objectify } from "@app/lib/fn";
import { logger } from "@app/lib/logger";
import { ActorType } from "@app/services/auth/auth-type"; import { ActorType } from "@app/services/auth/auth-type";
import { TOrgRoleDALFactory } from "@app/services/org/org-role-dal"; import { TOrgRoleDALFactory } from "@app/services/org/org-role-dal";
import { TProjectDALFactory } from "@app/services/project/project-dal"; import { TProjectDALFactory } from "@app/services/project/project-dal";
@@ -49,6 +52,7 @@ type TPermissionServiceFactoryDep = {
serviceTokenDAL: Pick<TServiceTokenDALFactory, "findById">; serviceTokenDAL: Pick<TServiceTokenDALFactory, "findById">;
projectDAL: Pick<TProjectDALFactory, "findById">; projectDAL: Pick<TProjectDALFactory, "findById">;
permissionDAL: TPermissionDALFactory; permissionDAL: TPermissionDALFactory;
keyStore: TKeyStoreFactory;
}; };
export const permissionServiceFactory = ({ export const permissionServiceFactory = ({
@@ -56,7 +60,8 @@ export const permissionServiceFactory = ({
orgRoleDAL, orgRoleDAL,
projectRoleDAL, projectRoleDAL,
serviceTokenDAL, serviceTokenDAL,
projectDAL projectDAL,
keyStore
}: TPermissionServiceFactoryDep): TPermissionServiceFactory => { }: TPermissionServiceFactoryDep): TPermissionServiceFactory => {
const buildOrgPermission = (orgUserRoles: TBuildOrgPermissionDTO) => { const buildOrgPermission = (orgUserRoles: TBuildOrgPermissionDTO) => {
const rules = orgUserRoles const rules = orgUserRoles
@@ -83,6 +88,68 @@ export const permissionServiceFactory = ({
}); });
}; };
const invalidateProjectPermissionCache = async (projectId: string, tx?: Knex) => {
const projectPermissionDalVersionKey = KeyStorePrefixes.ProjectPermissionDalVersion(projectId);
await keyStore.pgIncrementBy(projectPermissionDalVersionKey, {
incr: 1,
tx,
expiry: KeyStoreTtls.ProjectPermissionDalVersionTtl
});
};
const calculateProjectPermissionTtl = (membership: unknown): number => {
const now = new Date();
let minTtl = KeyStoreTtls.ProjectPermissionCacheInSeconds;
const getMinEndTime = (items: Array<{ temporaryAccessEndTime?: Date | null; isTemporary?: boolean }>) => {
return items
.filter((item) => item.isTemporary && item.temporaryAccessEndTime)
.map((item) => item.temporaryAccessEndTime!)
.filter((endTime) => endTime > now)
.reduce((min, endTime) => (!min || endTime < min ? endTime : min), null as Date | null);
};
const roleTimes: Date[] = [];
const additionalPrivilegeTimes: Date[] = [];
if (
membership &&
typeof membership === "object" &&
"roles" in membership &&
Array.isArray((membership as Record<string, unknown>).roles)
) {
const roles = (membership as Record<string, unknown>).roles as Array<{
temporaryAccessEndTime?: Date | null;
isTemporary?: boolean;
}>;
const minRoleEndTime = getMinEndTime(roles);
if (minRoleEndTime) roleTimes.push(minRoleEndTime);
}
if (
membership &&
typeof membership === "object" &&
"additionalPrivileges" in membership &&
Array.isArray((membership as Record<string, unknown>).additionalPrivileges)
) {
const additionalPrivileges = (membership as Record<string, unknown>).additionalPrivileges as Array<{
temporaryAccessEndTime?: Date | null;
isTemporary?: boolean;
}>;
const minAdditionalEndTime = getMinEndTime(additionalPrivileges);
if (minAdditionalEndTime) additionalPrivilegeTimes.push(minAdditionalEndTime);
}
const allEndTimes = [...roleTimes, ...additionalPrivilegeTimes];
if (allEndTimes.length > 0) {
const nearestEndTime = allEndTimes.reduce((min, endTime) => (!min || endTime < min ? endTime : min));
const timeUntilExpiry = Math.floor((nearestEndTime.getTime() - now.getTime()) / 1000);
minTtl = Math.min(minTtl, Math.max(1, timeUntilExpiry));
}
return minTtl;
};
const buildProjectPermissionRules = (projectUserRoles: TBuildProjectPermissionDTO) => { const buildProjectPermissionRules = (projectUserRoles: TBuildProjectPermissionDTO) => {
const rules = projectUserRoles const rules = projectUserRoles
.map(({ role, permissions }) => { .map(({ role, permissions }) => {
@@ -577,35 +644,94 @@ export const permissionServiceFactory = ({
actorId = assumedPrivilegeDetailsCtx.actorId; actorId = assumedPrivilegeDetailsCtx.actorId;
} }
if (actor === ActorType.SERVICE) {
return getServiceTokenProjectPermission({
serviceTokenId: actorId,
projectId,
actorOrgId,
actionProjectType
}) as Promise<TProjectPermissionRT<T>>;
}
const cachedProjectPermissionVersion = await keyStore.pgGetIntItem(
KeyStorePrefixes.ProjectPermissionDalVersion(projectId)
);
const projectPermissionVersion = Number(cachedProjectPermissionVersion || 0);
const cacheKey = KeyStorePrefixes.ProjectPermission(
projectId,
projectPermissionVersion,
actor,
actorId,
actionProjectType || ActionProjectType.Any
);
try {
const cachedData = await keyStore.getItem(cacheKey);
if (cachedData) {
const parsed = JSON.parse(cachedData) as {
rules: RawRuleOf<MongoAbility<ProjectPermissionSet>>[];
membership: {
roles?: Array<{ role: string; customRoleSlug?: string }>;
[key: string]: unknown;
};
};
const permission = createMongoAbility<ProjectPermissionSet>(parsed.rules, {
conditionsMatcher
});
return {
permission,
membership: parsed.membership,
hasRole: (role: string) =>
parsed.membership.roles?.findIndex(
({ role: slug, customRoleSlug }) => role === slug || slug === customRoleSlug
) !== -1
} as TProjectPermissionRT<T>;
}
} catch (error) {
logger.error(error, "Failed to get project permission");
}
let result: TProjectPermissionRT<T>;
switch (actor) { switch (actor) {
case ActorType.USER: case ActorType.USER:
return getUserProjectPermission({ result = (await getUserProjectPermission({
userId: actorId, userId: actorId,
projectId, projectId,
authMethod: actorAuthMethod, authMethod: actorAuthMethod,
userOrgId: actorOrgId, userOrgId: actorOrgId,
actionProjectType actionProjectType
}) as Promise<TProjectPermissionRT<T>>; })) as TProjectPermissionRT<T>;
case ActorType.SERVICE: break;
return getServiceTokenProjectPermission({
serviceTokenId: actorId,
projectId,
actorOrgId,
actionProjectType
}) as Promise<TProjectPermissionRT<T>>;
case ActorType.IDENTITY: case ActorType.IDENTITY:
return getIdentityProjectPermission({ result = (await getIdentityProjectPermission({
identityId: actorId, identityId: actorId,
projectId, projectId,
identityOrgId: actorOrgId, identityOrgId: actorOrgId,
actionProjectType actionProjectType
}) as Promise<TProjectPermissionRT<T>>; })) as TProjectPermissionRT<T>;
break;
default: default:
throw new BadRequestError({ throw new BadRequestError({
message: "Invalid actor provided", message: "Invalid actor provided",
name: "Get project permission" name: "Get project permission"
}); });
} }
try {
const cacheData = {
rules: result.permission.rules,
membership: result.membership
};
const ttl = calculateProjectPermissionTtl(result.membership);
await keyStore.setItemWithExpiry(cacheKey, ttl, JSON.stringify(cacheData));
} catch (error) {
logger.error(error, "Failed to cache project permission");
}
return result;
}; };
const getProjectPermissionByRole: TPermissionServiceFactory["getProjectPermissionByRole"] = async ( const getProjectPermissionByRole: TPermissionServiceFactory["getProjectPermissionByRole"] = async (
@@ -668,6 +794,7 @@ export const permissionServiceFactory = ({
getProjectPermissionByRole, getProjectPermissionByRole,
buildOrgPermission, buildOrgPermission,
buildProjectPermissionRules, buildProjectPermissionRules,
checkGroupProjectPermission checkGroupProjectPermission,
invalidateProjectPermissionCache
}; };
}; };
@@ -120,6 +120,16 @@ export enum ProjectPermissionSecretSyncActions {
RemoveSecrets = "remove-secrets" RemoveSecrets = "remove-secrets"
} }
export enum ProjectPermissionPkiSyncActions {
Read = "read",
Create = "create",
Edit = "edit",
Delete = "delete",
SyncCertificates = "sync-certificates",
ImportCertificates = "import-certificates",
RemoveCertificates = "remove-certificates"
}
export enum ProjectPermissionSecretRotationActions { export enum ProjectPermissionSecretRotationActions {
Read = "read", Read = "read",
ReadGeneratedCredentials = "read-generated-credentials", ReadGeneratedCredentials = "read-generated-credentials",
@@ -212,6 +222,7 @@ export enum ProjectPermissionSub {
Kms = "kms", Kms = "kms",
Cmek = "cmek", Cmek = "cmek",
SecretSyncs = "secret-syncs", SecretSyncs = "secret-syncs",
PkiSyncs = "pki-syncs",
Kmip = "kmip", Kmip = "kmip",
SecretScanningDataSources = "secret-scanning-data-sources", SecretScanningDataSources = "secret-scanning-data-sources",
SecretScanningFindings = "secret-scanning-findings", SecretScanningFindings = "secret-scanning-findings",
@@ -244,6 +255,10 @@ export type SecretSyncSubjectFields = {
secretPath: string; secretPath: string;
}; };
export type PkiSyncSubjectFields = {
subscriberName: string;
};
export type DynamicSecretSubjectFields = { export type DynamicSecretSubjectFields = {
environment: string; environment: string;
secretPath: string; secretPath: string;
@@ -308,6 +323,10 @@ export type ProjectPermissionSet =
ProjectPermissionSecretSyncActions, ProjectPermissionSecretSyncActions,
ProjectPermissionSub.SecretSyncs | (ForcedSubject<ProjectPermissionSub.SecretSyncs> & SecretSyncSubjectFields) ProjectPermissionSub.SecretSyncs | (ForcedSubject<ProjectPermissionSub.SecretSyncs> & SecretSyncSubjectFields)
] ]
| [
ProjectPermissionPkiSyncActions,
ProjectPermissionSub.PkiSyncs | (ForcedSubject<ProjectPermissionSub.PkiSyncs> & PkiSyncSubjectFields)
]
| [ | [
ProjectPermissionActions, ProjectPermissionActions,
( (
@@ -480,6 +499,22 @@ const SecretSyncConditionV2Schema = z
}) })
.partial(); .partial();
const PkiSyncConditionSchema = z
.object({
subscriberName: z.union([
z.string(),
z
.object({
[PermissionConditionOperators.$EQ]: PermissionConditionSchema[PermissionConditionOperators.$EQ],
[PermissionConditionOperators.$NEQ]: PermissionConditionSchema[PermissionConditionOperators.$NEQ],
[PermissionConditionOperators.$IN]: PermissionConditionSchema[PermissionConditionOperators.$IN],
[PermissionConditionOperators.$GLOB]: PermissionConditionSchema[PermissionConditionOperators.$GLOB]
})
.partial()
])
})
.partial();
const SecretImportConditionSchema = z const SecretImportConditionSchema = z
.object({ .object({
environment: z.union([ environment: z.union([
@@ -943,6 +978,16 @@ export const ProjectPermissionV2Schema = z.discriminatedUnion("subject", [
"When specified, only matching conditions will be allowed to access given resource." "When specified, only matching conditions will be allowed to access given resource."
).optional() ).optional()
}), }),
z.object({
subject: z.literal(ProjectPermissionSub.PkiSyncs).describe("The entity this permission pertains to."),
inverted: z.boolean().optional().describe("Whether rule allows or forbids."),
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionPkiSyncActions).describe(
"Describe what action an entity can take."
),
conditions: PkiSyncConditionSchema.describe(
"When specified, only matching conditions will be allowed to access given resource."
).optional()
}),
z.object({ z.object({
subject: z.literal(ProjectPermissionSub.SecretEvents).describe("The entity this permission pertains to."), subject: z.literal(ProjectPermissionSub.SecretEvents).describe("The entity this permission pertains to."),
inverted: z.boolean().optional().describe("Whether rule allows or forbids."), inverted: z.boolean().optional().describe("Whether rule allows or forbids."),
+4 -2
View File
@@ -754,7 +754,8 @@ export const pitServiceFactory = ({
secrets: newSecrets.map((secret) => ({ secrets: newSecrets.map((secret) => ({
secretId: secret.id, secretId: secret.id,
secretKey: secret.secretKey, secretKey: secret.secretKey,
secretVersion: secret.version secretVersion: secret.version,
secretTags: secret.tags?.map((tag) => tag.name)
})) }))
} }
}); });
@@ -781,7 +782,8 @@ export const pitServiceFactory = ({
secrets: updatedSecrets.map((secret) => ({ secrets: updatedSecrets.map((secret) => ({
secretId: secret.id, secretId: secret.id,
secretKey: secret.secretKey, secretKey: secret.secretKey,
secretVersion: secret.version secretVersion: secret.version,
secretTags: secret.tags?.map((tag) => tag.name)
})) }))
} }
}); });
@@ -27,7 +27,7 @@ import {
type TProjectUserAdditionalPrivilegeServiceFactoryDep = { type TProjectUserAdditionalPrivilegeServiceFactoryDep = {
projectUserAdditionalPrivilegeDAL: TProjectUserAdditionalPrivilegeDALFactory; projectUserAdditionalPrivilegeDAL: TProjectUserAdditionalPrivilegeDALFactory;
projectMembershipDAL: Pick<TProjectMembershipDALFactory, "findById" | "findOne">; projectMembershipDAL: Pick<TProjectMembershipDALFactory, "findById" | "findOne">;
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">; permissionService: Pick<TPermissionServiceFactory, "getProjectPermission" | "invalidateProjectPermissionCache">;
accessApprovalRequestDAL: Pick<TAccessApprovalRequestDALFactory, "update">; accessApprovalRequestDAL: Pick<TAccessApprovalRequestDALFactory, "update">;
}; };
@@ -115,6 +115,9 @@ export const projectUserAdditionalPrivilegeServiceFactory = ({
slug, slug,
permissions: packedPermission permissions: packedPermission
}); });
await permissionService.invalidateProjectPermissionCache(projectMembership.projectId);
return { return {
...additionalPrivilege, ...additionalPrivilege,
permissions: unpackPermissions(additionalPrivilege.permissions) permissions: unpackPermissions(additionalPrivilege.permissions)
@@ -133,6 +136,9 @@ export const projectUserAdditionalPrivilegeServiceFactory = ({
temporaryAccessStartTime: new Date(dto.temporaryAccessStartTime), temporaryAccessStartTime: new Date(dto.temporaryAccessStartTime),
temporaryAccessEndTime: new Date(new Date(dto.temporaryAccessStartTime).getTime() + relativeTempAllocatedTimeInMs) temporaryAccessEndTime: new Date(new Date(dto.temporaryAccessStartTime).getTime() + relativeTempAllocatedTimeInMs)
}); });
await permissionService.invalidateProjectPermissionCache(projectMembership.projectId);
return { return {
...additionalPrivilege, ...additionalPrivilege,
permissions: unpackPermissions(additionalPrivilege.permissions) permissions: unpackPermissions(additionalPrivilege.permissions)
@@ -230,6 +236,8 @@ export const projectUserAdditionalPrivilegeServiceFactory = ({
temporaryAccessEndTime: new Date(new Date(temporaryAccessStartTime || "").getTime() + ms(temporaryRange || "")) temporaryAccessEndTime: new Date(new Date(temporaryAccessStartTime || "").getTime() + ms(temporaryRange || ""))
}); });
await permissionService.invalidateProjectPermissionCache(projectMembership.projectId);
return { return {
...additionalPrivilege, ...additionalPrivilege,
permissions: unpackPermissions(additionalPrivilege.permissions) permissions: unpackPermissions(additionalPrivilege.permissions)
@@ -245,6 +253,9 @@ export const projectUserAdditionalPrivilegeServiceFactory = ({
temporaryRange: null, temporaryRange: null,
temporaryMode: null temporaryMode: null
}); });
await permissionService.invalidateProjectPermissionCache(projectMembership.projectId);
return { return {
...additionalPrivilege, ...additionalPrivilege,
permissions: unpackPermissions(additionalPrivilege.permissions) permissions: unpackPermissions(additionalPrivilege.permissions)
@@ -291,6 +302,9 @@ export const projectUserAdditionalPrivilegeServiceFactory = ({
} }
); );
const deletedPrivilege = await projectUserAdditionalPrivilegeDAL.deleteById(userPrivilege.id); const deletedPrivilege = await projectUserAdditionalPrivilegeDAL.deleteById(userPrivilege.id);
await permissionService.invalidateProjectPermissionCache(projectMembership.projectId);
return { return {
...deletedPrivilege, ...deletedPrivilege,
permissions: unpackPermissions(deletedPrivilege.permissions) permissions: unpackPermissions(deletedPrivilege.permissions)
@@ -0,0 +1 @@
export const RELAY_CONNECTING_GATEWAY_INFO = "1.3.6.1.4.1.12345.100.3";
+130 -9
View File
@@ -1,9 +1,13 @@
import { isIP } from "node:net";
import { ForbiddenError } from "@casl/ability";
import * as x509 from "@peculiar/x509"; import * as x509 from "@peculiar/x509";
import { TRelays } from "@app/db/schemas"; import { TRelays } from "@app/db/schemas";
import { PgSqlLock } from "@app/keystore/keystore"; import { PgSqlLock } from "@app/keystore/keystore";
import { crypto } from "@app/lib/crypto"; import { crypto } from "@app/lib/crypto";
import { BadRequestError, NotFoundError } from "@app/lib/errors"; import { BadRequestError, NotFoundError } from "@app/lib/errors";
import { ActorAuthMethod, ActorType } from "@app/services/auth/auth-type";
import { constructPemChainFromCerts, prependCertToPemChain } from "@app/services/certificate/certificate-fns"; import { constructPemChainFromCerts, prependCertToPemChain } from "@app/services/certificate/certificate-fns";
import { CertExtendedKeyUsage, CertKeyAlgorithm, CertKeyUsage } from "@app/services/certificate/certificate-types"; import { CertExtendedKeyUsage, CertKeyAlgorithm, CertKeyUsage } from "@app/services/certificate/certificate-types";
import { import {
@@ -14,11 +18,15 @@ import { TKmsServiceFactory } from "@app/services/kms/kms-service";
import { KmsDataKey } from "@app/services/kms/kms-types"; import { KmsDataKey } from "@app/services/kms/kms-types";
import { verifyHostInputValidity } from "../dynamic-secret/dynamic-secret-fns"; import { verifyHostInputValidity } from "../dynamic-secret/dynamic-secret-fns";
import { TLicenseServiceFactory } from "../license/license-service";
import { OrgPermissionRelayActions, OrgPermissionSubjects } from "../permission/org-permission";
import { TPermissionServiceFactory } from "../permission/permission-service-types";
import { createSshCert, createSshKeyPair } from "../ssh/ssh-certificate-authority-fns"; import { createSshCert, createSshKeyPair } from "../ssh/ssh-certificate-authority-fns";
import { SshCertType } from "../ssh/ssh-certificate-authority-types"; import { SshCertType } from "../ssh/ssh-certificate-authority-types";
import { SshCertKeyAlgorithm } from "../ssh-certificate/ssh-certificate-types"; import { SshCertKeyAlgorithm } from "../ssh-certificate/ssh-certificate-types";
import { TInstanceRelayConfigDALFactory } from "./instance-relay-config-dal"; import { TInstanceRelayConfigDALFactory } from "./instance-relay-config-dal";
import { TOrgRelayConfigDALFactory } from "./org-relay-config-dal"; import { TOrgRelayConfigDALFactory } from "./org-relay-config-dal";
import { RELAY_CONNECTING_GATEWAY_INFO } from "./relay-constants";
import { TRelayDALFactory } from "./relay-dal"; import { TRelayDALFactory } from "./relay-dal";
export type TRelayServiceFactory = ReturnType<typeof relayServiceFactory>; export type TRelayServiceFactory = ReturnType<typeof relayServiceFactory>;
@@ -29,12 +37,16 @@ export const relayServiceFactory = ({
instanceRelayConfigDAL, instanceRelayConfigDAL,
orgRelayConfigDAL, orgRelayConfigDAL,
relayDAL, relayDAL,
kmsService kmsService,
licenseService,
permissionService
}: { }: {
instanceRelayConfigDAL: TInstanceRelayConfigDALFactory; instanceRelayConfigDAL: TInstanceRelayConfigDALFactory;
orgRelayConfigDAL: TOrgRelayConfigDALFactory; orgRelayConfigDAL: TOrgRelayConfigDALFactory;
relayDAL: TRelayDALFactory; relayDAL: TRelayDALFactory;
kmsService: TKmsServiceFactory; kmsService: TKmsServiceFactory;
licenseService: TLicenseServiceFactory;
permissionService: TPermissionServiceFactory;
}) => { }) => {
const $getInstanceCAs = async () => { const $getInstanceCAs = async () => {
const instanceConfig = await instanceRelayConfigDAL.transaction(async (tx) => { const instanceConfig = await instanceRelayConfigDAL.transaction(async (tx) => {
@@ -639,8 +651,9 @@ export const relayServiceFactory = ({
true true
), ),
new x509.ExtendedKeyUsageExtension([x509.ExtendedKeyUsage[CertExtendedKeyUsage.SERVER_AUTH]], true), new x509.ExtendedKeyUsageExtension([x509.ExtendedKeyUsage[CertExtendedKeyUsage.SERVER_AUTH]], true),
// san // san
new x509.SubjectAlternativeNameExtension([{ type: "ip", value: host }], false) new x509.SubjectAlternativeNameExtension([{ type: isIP(host) ? "ip" : "dns", value: host }], false)
]; ];
const relayServerSerialNumber = createSerialNumber(); const relayServerSerialNumber = createSerialNumber();
@@ -689,6 +702,7 @@ export const relayServiceFactory = ({
const $generateRelayClientCredentials = async ({ const $generateRelayClientCredentials = async ({
gatewayId, gatewayId,
gatewayName,
orgId, orgId,
orgName, orgName,
relayPkiClientCaCertificate, relayPkiClientCaCertificate,
@@ -697,6 +711,7 @@ export const relayServiceFactory = ({
relayPkiServerCaCertificateChain relayPkiServerCaCertificateChain
}: { }: {
gatewayId: string; gatewayId: string;
gatewayName: string;
orgId: string; orgId: string;
orgName: string; orgName: string;
relayPkiClientCaCertificate: Buffer; relayPkiClientCaCertificate: Buffer;
@@ -727,6 +742,16 @@ export const relayServiceFactory = ({
const clientCertPrivateKey = crypto.nativeCrypto.KeyObject.from(clientKeys.privateKey); const clientCertPrivateKey = crypto.nativeCrypto.KeyObject.from(clientKeys.privateKey);
const clientCertSerialNumber = createSerialNumber(); const clientCertSerialNumber = createSerialNumber();
const connectingGatewayInfoExtension = new x509.Extension(
RELAY_CONNECTING_GATEWAY_INFO,
false,
Buffer.from(
JSON.stringify({
name: gatewayName
})
)
);
// Build standard extensions // Build standard extensions
const extensions: x509.Extension[] = [ const extensions: x509.Extension[] = [
new x509.BasicConstraintsExtension(false), new x509.BasicConstraintsExtension(false),
@@ -740,7 +765,8 @@ export const relayServiceFactory = ({
x509.KeyUsageFlags[CertKeyUsage.KEY_AGREEMENT], x509.KeyUsageFlags[CertKeyUsage.KEY_AGREEMENT],
true true
), ),
new x509.ExtendedKeyUsageExtension([x509.ExtendedKeyUsage[CertExtendedKeyUsage.CLIENT_AUTH]], true) new x509.ExtendedKeyUsageExtension([x509.ExtendedKeyUsage[CertExtendedKeyUsage.CLIENT_AUTH]], true),
connectingGatewayInfoExtension
]; ];
const clientCert = await x509.X509CertificateGenerator.create({ const clientCert = await x509.X509CertificateGenerator.create({
@@ -768,11 +794,13 @@ export const relayServiceFactory = ({
const getCredentialsForGateway = async ({ const getCredentialsForGateway = async ({
relayName, relayName,
orgId, orgId,
gatewayId gatewayId,
gatewayName
}: { }: {
relayName: string; relayName: string;
orgId: string; orgId: string;
gatewayId: string; gatewayId: string;
gatewayName: string;
}) => { }) => {
let relay: TRelays | null = await relayDAL.findOne({ let relay: TRelays | null = await relayDAL.findOne({
orgId, orgId,
@@ -819,10 +847,10 @@ export const relayServiceFactory = ({
const relayClientSshCert = await createSshCert({ const relayClientSshCert = await createSshCert({
caPrivateKey: orgCAs.relaySshClientCaPrivateKey.toString("utf8"), caPrivateKey: orgCAs.relaySshClientCaPrivateKey.toString("utf8"),
clientPublicKey: relayClientSshPublicKey, clientPublicKey: relayClientSshPublicKey,
keyId: `relay-client-${relay.id}`, keyId: `client-${relayName}`,
principals: [gatewayId], principals: [gatewayId, gatewayName],
certType: SshCertType.USER, certType: SshCertType.USER,
requestedTtl: "30d" requestedTtl: "1d"
}); });
return { return {
@@ -837,12 +865,14 @@ export const relayServiceFactory = ({
relayId, relayId,
orgId, orgId,
orgName, orgName,
gatewayId gatewayId,
gatewayName
}: { }: {
relayId: string; relayId: string;
orgId: string; orgId: string;
orgName: string; orgName: string;
gatewayId: string; gatewayId: string;
gatewayName: string;
}) => { }) => {
const relay = await relayDAL.findOne({ const relay = await relayDAL.findOne({
id: relayId id: relayId
@@ -860,6 +890,7 @@ export const relayServiceFactory = ({
const instanceCAs = await $getInstanceCAs(); const instanceCAs = await $getInstanceCAs();
const relayCertificateCredentials = await $generateRelayClientCredentials({ const relayCertificateCredentials = await $generateRelayClientCredentials({
gatewayId, gatewayId,
gatewayName,
orgId, orgId,
orgName, orgName,
relayPkiClientCaCertificate: instanceCAs.instanceRelayPkiClientCaCertificate, relayPkiClientCaCertificate: instanceCAs.instanceRelayPkiClientCaCertificate,
@@ -877,6 +908,7 @@ export const relayServiceFactory = ({
const orgCAs = await $getOrgCAs(orgId); const orgCAs = await $getOrgCAs(orgId);
const relayCertificateCredentials = await $generateRelayClientCredentials({ const relayCertificateCredentials = await $generateRelayClientCredentials({
gatewayId, gatewayId,
gatewayName,
orgId, orgId,
orgName, orgName,
relayPkiClientCaCertificate: orgCAs.relayPkiClientCaCertificate, relayPkiClientCaCertificate: orgCAs.relayPkiClientCaCertificate,
@@ -895,11 +927,13 @@ export const relayServiceFactory = ({
host, host,
name, name,
identityId, identityId,
actorAuthMethod,
orgId orgId
}: { }: {
host: string; host: string;
name: string; name: string;
identityId?: string; identityId?: string;
actorAuthMethod?: ActorAuthMethod;
orgId?: string; orgId?: string;
}) => { }) => {
let relay: TRelays; let relay: TRelays;
@@ -908,6 +942,27 @@ export const relayServiceFactory = ({
await verifyHostInputValidity(host); await verifyHostInputValidity(host);
if (isOrgRelay) { if (isOrgRelay) {
const orgLicensePlan = await licenseService.getPlan(orgId);
if (!orgLicensePlan.gateway) {
throw new BadRequestError({
message:
"Relay registration failed due to organization plan restrictions. Please upgrade your instance to Infisical's Enterprise plan."
});
}
const { permission } = await permissionService.getOrgPermission(
ActorType.IDENTITY,
identityId,
orgId,
actorAuthMethod!,
orgId
);
ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionRelayActions.CreateRelays,
OrgPermissionSubjects.Relay
);
relay = await relayDAL.transaction(async (tx) => { relay = await relayDAL.transaction(async (tx) => {
const existingRelay = await relayDAL.findOne( const existingRelay = await relayDAL.findOne(
{ {
@@ -995,9 +1050,75 @@ export const relayServiceFactory = ({
}); });
}; };
const getRelays = async ({
actorId,
actor,
actorAuthMethod,
actorOrgId
}: {
actorId: string;
actor: ActorType;
actorAuthMethod: ActorAuthMethod;
actorOrgId: string;
}) => {
const { permission } = await permissionService.getOrgPermission(
actor,
actorId,
actorOrgId,
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionRelayActions.ListRelays, OrgPermissionSubjects.Relay);
const instanceRelays = await relayDAL.find({
orgId: null
});
const orgRelays = await relayDAL.find({
orgId: actorOrgId
});
return [...instanceRelays, ...orgRelays];
};
const deleteRelay = async ({
id,
actorId,
actor,
actorAuthMethod,
actorOrgId
}: {
id: string;
actorId: string;
actor: ActorType;
actorAuthMethod: ActorAuthMethod;
actorOrgId: string;
}) => {
const { permission } = await permissionService.getOrgPermission(
actor,
actorId,
actorOrgId,
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionRelayActions.DeleteRelays, OrgPermissionSubjects.Relay);
const relay = await relayDAL.findById(id);
if (!relay || relay.orgId !== actorOrgId || relay.orgId === null) {
throw new NotFoundError({ message: "Relay not found" });
}
const deletedRelay = await relayDAL.deleteById(id);
return deletedRelay;
};
return { return {
registerRelay, registerRelay,
getCredentialsForGateway, getCredentialsForGateway,
getCredentialsForClient getCredentialsForClient,
getRelays,
deleteRelay
}; };
}; };
@@ -284,7 +284,8 @@ export const secretApprovalRequestSecretDALFactory = (db: TDbClient) => {
db.ref("version").withSchema(TableName.SecretVersionV2).as("secVerVersion"), db.ref("version").withSchema(TableName.SecretVersionV2).as("secVerVersion"),
db.ref("key").withSchema(TableName.SecretVersionV2).as("secVerKey"), db.ref("key").withSchema(TableName.SecretVersionV2).as("secVerKey"),
db.ref("encryptedValue").withSchema(TableName.SecretVersionV2).as("secVerValue"), db.ref("encryptedValue").withSchema(TableName.SecretVersionV2).as("secVerValue"),
db.ref("encryptedComment").withSchema(TableName.SecretVersionV2).as("secVerComment") db.ref("encryptedComment").withSchema(TableName.SecretVersionV2).as("secVerComment"),
db.ref("skipMultilineEncoding").withSchema(TableName.SecretVersionV2).as("secVerSkipMultilineEncoding")
) )
.select( .select(
db.ref("id").withSchema(TableName.ResourceMetadata).as("metadataId"), db.ref("id").withSchema(TableName.ResourceMetadata).as("metadataId"),
@@ -326,14 +327,22 @@ export const secretApprovalRequestSecretDALFactory = (db: TDbClient) => {
{ {
key: "secretVersion", key: "secretVersion",
label: "secretVersion" as const, label: "secretVersion" as const,
mapper: ({ secretVersion, secVerVersion, secVerKey, secVerValue, secVerComment }) => mapper: ({
secretVersion,
secVerVersion,
secVerKey,
secVerValue,
secVerComment,
secVerSkipMultilineEncoding
}) =>
secretVersion secretVersion
? { ? {
version: secVerVersion, version: secVerVersion,
id: secretVersion, id: secretVersion,
key: secVerKey, key: secVerKey,
encryptedValue: secVerValue, encryptedValue: secVerValue,
encryptedComment: secVerComment encryptedComment: secVerComment,
skipMultilineEncoding: secVerSkipMultilineEncoding
} }
: undefined, : undefined,
childrenMapper: [ childrenMapper: [
@@ -285,13 +285,10 @@ export const secretApprovalRequestServiceFactory = ({
) { ) {
throw new ForbiddenRequestError({ message: "User has insufficient privileges" }); throw new ForbiddenRequestError({ message: "User has insufficient privileges" });
} }
const getHasSecretReadAccess = ( const getHasSecretReadAccess = (environment: string, tags: { slug: string }[], secretPath?: string) => {
shouldCheckSecretPermission: boolean | null | undefined, const isReviewer = policy.approvers.some(({ userId }) => userId === actorId);
environment: string,
tags: { slug: string }[], if (!isReviewer) {
secretPath?: string
) => {
if (shouldCheckSecretPermission) {
const canRead = hasSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.ReadValue, { const canRead = hasSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.ReadValue, {
environment, environment,
secretPath: secretPath || "/", secretPath: secretPath || "/",
@@ -322,44 +319,33 @@ export const secretApprovalRequestServiceFactory = ({
version: el.version, version: el.version,
secretMetadata: el.secretMetadata as ResourceMetadataDTO, secretMetadata: el.secretMetadata as ResourceMetadataDTO,
isRotatedSecret: el.secret?.isRotatedSecret ?? false, isRotatedSecret: el.secret?.isRotatedSecret ?? false,
secretValueHidden: !getHasSecretReadAccess( secretValueHidden: !getHasSecretReadAccess(secretApprovalRequest.environment, el.tags, secretPath?.[0]?.path),
secretApprovalRequest.policy.shouldCheckSecretPermission, secretValue: !getHasSecretReadAccess(secretApprovalRequest.environment, el.tags, secretPath?.[0]?.path)
secretApprovalRequest.environment,
el.tags,
secretPath?.[0]?.path
),
secretValue: !getHasSecretReadAccess(
secretApprovalRequest.policy.shouldCheckSecretPermission,
secretApprovalRequest.environment,
el.tags,
secretPath?.[0]?.path
)
? INFISICAL_SECRET_VALUE_HIDDEN_MASK ? INFISICAL_SECRET_VALUE_HIDDEN_MASK
: el.secret && el.secret.isRotatedSecret : el.secret && el.secret.isRotatedSecret
? undefined ? undefined
: el.encryptedValue : el.encryptedValue !== undefined && el.encryptedValue !== null
? secretManagerDecryptor({ cipherTextBlob: el.encryptedValue }).toString() ? secretManagerDecryptor({ cipherTextBlob: el.encryptedValue }).toString()
: "", : undefined,
secretComment: el.encryptedComment secretComment:
? secretManagerDecryptor({ cipherTextBlob: el.encryptedComment }).toString() el.encryptedComment !== undefined && el.encryptedComment !== null
: "", ? secretManagerDecryptor({ cipherTextBlob: el.encryptedComment }).toString()
: undefined,
skipMultilineEncoding:
el.skipMultilineEncoding !== undefined && el.skipMultilineEncoding !== null
? el.skipMultilineEncoding
: undefined,
secret: el.secret secret: el.secret
? { ? {
secretKey: el.secret.key, secretKey: el.secret.key,
id: el.secret.id, id: el.secret.id,
version: el.secret.version, version: el.secret.version,
secretValueHidden: !getHasSecretReadAccess( secretValueHidden: !getHasSecretReadAccess(
secretApprovalRequest.policy.shouldCheckSecretPermission,
secretApprovalRequest.environment, secretApprovalRequest.environment,
el.tags, el.tags,
secretPath?.[0]?.path secretPath?.[0]?.path
), ),
secretValue: !getHasSecretReadAccess( secretValue: !getHasSecretReadAccess(secretApprovalRequest.environment, el.tags, secretPath?.[0]?.path)
secretApprovalRequest.policy.shouldCheckSecretPermission,
secretApprovalRequest.environment,
el.tags,
secretPath?.[0]?.path
)
? INFISICAL_SECRET_VALUE_HIDDEN_MASK ? INFISICAL_SECRET_VALUE_HIDDEN_MASK
: el.secret.encryptedValue : el.secret.encryptedValue
? secretManagerDecryptor({ cipherTextBlob: el.secret.encryptedValue }).toString() ? secretManagerDecryptor({ cipherTextBlob: el.secret.encryptedValue }).toString()
@@ -375,17 +361,11 @@ export const secretApprovalRequestServiceFactory = ({
id: el.secretVersion.id, id: el.secretVersion.id,
version: el.secretVersion.version, version: el.secretVersion.version,
secretValueHidden: !getHasSecretReadAccess( secretValueHidden: !getHasSecretReadAccess(
secretApprovalRequest.policy.shouldCheckSecretPermission,
secretApprovalRequest.environment, secretApprovalRequest.environment,
el.tags, el.tags,
secretPath?.[0]?.path secretPath?.[0]?.path
), ),
secretValue: !getHasSecretReadAccess( secretValue: !getHasSecretReadAccess(secretApprovalRequest.environment, el.tags, secretPath?.[0]?.path)
secretApprovalRequest.policy.shouldCheckSecretPermission,
secretApprovalRequest.environment,
el.tags,
secretPath?.[0]?.path
)
? INFISICAL_SECRET_VALUE_HIDDEN_MASK ? INFISICAL_SECRET_VALUE_HIDDEN_MASK
: el.secretVersion.encryptedValue : el.secretVersion.encryptedValue
? secretManagerDecryptor({ cipherTextBlob: el.secretVersion.encryptedValue }).toString() ? secretManagerDecryptor({ cipherTextBlob: el.secretVersion.encryptedValue }).toString()
@@ -394,7 +374,8 @@ export const secretApprovalRequestServiceFactory = ({
? secretManagerDecryptor({ cipherTextBlob: el.secretVersion.encryptedComment }).toString() ? secretManagerDecryptor({ cipherTextBlob: el.secretVersion.encryptedComment }).toString()
: "", : "",
tags: el.secretVersion.tags, tags: el.secretVersion.tags,
secretMetadata: el.oldSecretMetadata as ResourceMetadataDTO secretMetadata: el.oldSecretMetadata as ResourceMetadataDTO,
skipMultilineEncoding: el.secretVersion.skipMultilineEncoding
} }
: undefined : undefined
})); }));
@@ -403,12 +384,7 @@ export const secretApprovalRequestServiceFactory = ({
const encryptedSecrets = await secretApprovalRequestSecretDAL.findByRequestId(secretApprovalRequest.id); const encryptedSecrets = await secretApprovalRequestSecretDAL.findByRequestId(secretApprovalRequest.id);
secrets = encryptedSecrets.map((el) => ({ secrets = encryptedSecrets.map((el) => ({
...el, ...el,
secretValueHidden: !getHasSecretReadAccess( secretValueHidden: !getHasSecretReadAccess(secretApprovalRequest.environment, el.tags, secretPath?.[0]?.path),
secretApprovalRequest.policy.shouldCheckSecretPermission,
secretApprovalRequest.environment,
el.tags,
secretPath?.[0]?.path
),
...decryptSecretWithBot(el, botKey), ...decryptSecretWithBot(el, botKey),
secret: el.secret secret: el.secret
? { ? {
@@ -733,9 +709,9 @@ export const secretApprovalRequestServiceFactory = ({
tx, tx,
inputSecrets: secretUpdationCommits.map((el) => { inputSecrets: secretUpdationCommits.map((el) => {
const encryptedValue = const encryptedValue =
!el.secret?.isRotatedSecret && typeof el.encryptedValue !== "undefined" !el.secret?.isRotatedSecret && el.encryptedValue !== null && el.encryptedValue !== undefined
? { ? {
encryptedValue: el.encryptedValue as Buffer, encryptedValue: el.encryptedValue,
references: el.encryptedValue references: el.encryptedValue
? getAllSecretReferencesV2Bridge( ? getAllSecretReferencesV2Bridge(
secretManagerDecryptor({ secretManagerDecryptor({
@@ -749,9 +725,9 @@ export const secretApprovalRequestServiceFactory = ({
filter: { id: el.secretId as string, type: SecretType.Shared }, filter: { id: el.secretId as string, type: SecretType.Shared },
data: { data: {
reminderRepeatDays: el.reminderRepeatDays, reminderRepeatDays: el.reminderRepeatDays,
encryptedComment: el.encryptedComment, encryptedComment: el.encryptedComment !== null ? el.encryptedComment : undefined,
reminderNote: el.reminderNote, reminderNote: el.reminderNote,
skipMultilineEncoding: el.skipMultilineEncoding, skipMultilineEncoding: el.skipMultilineEncoding !== null ? el.skipMultilineEncoding : undefined,
key: el.key, key: el.key,
tags: el?.tags.map(({ id }) => id), tags: el?.tags.map(({ id }) => id),
secretMetadata: el.secretMetadata as ResourceMetadataDTO, secretMetadata: el.secretMetadata as ResourceMetadataDTO,
@@ -1084,7 +1060,9 @@ export const secretApprovalRequestServiceFactory = ({
// @ts-expect-error not present on v1 secrets // @ts-expect-error not present on v1 secrets
secretKey: secret.key as string, secretKey: secret.key as string,
// @ts-expect-error not present on v1 secrets // @ts-expect-error not present on v1 secrets
secretMetadata: secret.secretMetadata as ResourceMetadataDTO secretMetadata: secret.secretMetadata as ResourceMetadataDTO,
// @ts-expect-error not present on v1 secrets
secretTags: (secret.tags as { name: string }[])?.map((tag) => tag.name)
})) }))
} }
}); });
@@ -1100,7 +1078,9 @@ export const secretApprovalRequestServiceFactory = ({
// @ts-expect-error not present on v1 secrets // @ts-expect-error not present on v1 secrets
secretKey: secret.key as string, secretKey: secret.key as string,
// @ts-expect-error not present on v1 secrets // @ts-expect-error not present on v1 secrets
secretMetadata: secret.secretMetadata as ResourceMetadataDTO secretMetadata: secret.secretMetadata as ResourceMetadataDTO,
// @ts-expect-error not present on v1 secrets
secretTags: (secret.tags as { name: string }[])?.map((tag) => tag.name)
} }
}); });
} }
@@ -1119,7 +1099,9 @@ export const secretApprovalRequestServiceFactory = ({
// @ts-expect-error not present on v1 secrets // @ts-expect-error not present on v1 secrets
secretKey: secret.key as string, secretKey: secret.key as string,
// @ts-expect-error not present on v1 secrets // @ts-expect-error not present on v1 secrets
secretMetadata: secret.secretMetadata as ResourceMetadataDTO secretMetadata: secret.secretMetadata as ResourceMetadataDTO,
// @ts-expect-error not present on v1 secrets
secretTags: (secret.tags as { name: string }[])?.map((tag) => tag.name)
})) }))
} }
}); });
@@ -1135,7 +1117,9 @@ export const secretApprovalRequestServiceFactory = ({
// @ts-expect-error not present on v1 secrets // @ts-expect-error not present on v1 secrets
secretKey: secret.key as string, secretKey: secret.key as string,
// @ts-expect-error not present on v1 secrets // @ts-expect-error not present on v1 secrets
secretMetadata: secret.secretMetadata as ResourceMetadataDTO secretMetadata: secret.secretMetadata as ResourceMetadataDTO,
// @ts-expect-error not present on v1 secrets
secretTags: (secret.tags as { name: string }[])?.map((tag) => tag.name)
} }
}); });
} }
@@ -1625,11 +1609,13 @@ export const secretApprovalRequestServiceFactory = ({
key: newSecretName || secretKey, key: newSecretName || secretKey,
encryptedComment: setKnexStringValue( encryptedComment: setKnexStringValue(
secretComment, secretComment,
(value) => secretManagerEncryptor({ plainText: Buffer.from(value) }).cipherTextBlob (value) => secretManagerEncryptor({ plainText: Buffer.from(value) }).cipherTextBlob,
true // scott: we need to encrypt empty string on update to differentiate not updating comment vs clearing comment
), ),
encryptedValue: setKnexStringValue( encryptedValue: setKnexStringValue(
secretValue, secretValue,
(value) => secretManagerEncryptor({ plainText: Buffer.from(value) }).cipherTextBlob (value) => secretManagerEncryptor({ plainText: Buffer.from(value) }).cipherTextBlob,
true // scott: we need to encrypt empty string on update to differentiate not updating value vs clearing value
), ),
reminderRepeatDays, reminderRepeatDays,
reminderNote, reminderNote,
@@ -0,0 +1,4 @@
export * from "./redis-credentials-rotation-constants";
export * from "./redis-credentials-rotation-fns";
export * from "./redis-credentials-rotation-schemas";
export * from "./redis-credentials-rotation-types";
@@ -0,0 +1,15 @@
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 REDIS_CREDENTIALS_ROTATION_LIST_OPTION: TSecretRotationV2ListItem = {
name: "Redis Credentials",
type: SecretRotation.RedisCredentials,
connection: AppConnection.Redis,
template: {
secretsMapping: {
username: "REDIS_USERNAME",
password: "REDIS_PASSWORD"
}
}
};
@@ -0,0 +1,194 @@
/* eslint-disable no-await-in-loop */
import Redis from "ioredis";
import {
TRotationFactory,
TRotationFactoryGetSecretsPayload,
TRotationFactoryIssueCredentials,
TRotationFactoryRevokeCredentials,
TRotationFactoryRotateCredentials
} from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-types";
import { BadRequestError } from "@app/lib/errors";
import { verifyHostInputValidity } from "../../dynamic-secret/dynamic-secret-fns";
import { DEFAULT_PASSWORD_REQUIREMENTS, generatePassword } from "../shared/utils";
import {
TRedisCredentialsRotationGeneratedCredentials,
TRedisCredentialsRotationWithConnection
} from "./redis-credentials-rotation-types";
const redactPasswords = (e: unknown, credentials: TRedisCredentialsRotationGeneratedCredentials) => {
const error = e as Error;
if (!error?.message) return "Unknown error";
let redactedMessage = error.message;
credentials.forEach(({ password }) => {
redactedMessage = redactedMessage.replaceAll(password, "*******************");
});
return redactedMessage;
};
export const redisCredentialsRotationFactory: TRotationFactory<
TRedisCredentialsRotationWithConnection,
TRedisCredentialsRotationGeneratedCredentials
> = (secretRotation) => {
const { connection, secretsMapping, parameters } = secretRotation;
const $getClient = async () => {
const [hostIp] = await verifyHostInputValidity(connection.credentials.host);
let conn: Redis | null = null;
try {
conn = new Redis({
username: connection.credentials.username,
host: hostIp,
port: connection.credentials.port,
password: connection.credentials.password,
...(connection.credentials.sslEnabled && {
tls: {
rejectUnauthorized: connection.credentials.sslRejectUnauthorized,
ca: connection.credentials.sslCertificate
}
})
});
let result: string;
if (connection.credentials.password) {
result = await conn.auth(connection.credentials.username, connection.credentials.password, () => {});
} else {
result = await conn.auth(connection.credentials.username, () => {});
}
if (result !== "OK") {
throw new BadRequestError({ message: `Invalid credentials, Redis returned ${result} status` });
}
return conn;
} catch (err) {
if (conn) await conn.quit();
throw err;
}
};
/**
* Creates a new user and password for the redis user using ACL
*/
const $rotateAclUser = async () => {
let client: Redis | null = null;
const username = generatePassword({
length: 32,
required: {
symbols: 0,
digits: 5,
uppercase: 5,
lowercase: 5
}
});
const password = generatePassword(parameters.passwordRequirements || DEFAULT_PASSWORD_REQUIREMENTS);
try {
client = await $getClient();
// important: permissionScope is user input so we need to sanitize it, which we do by splitting the permission scope into parts and then passing them to the ACL command as separate arguments
const permissionParts = parameters.permissionScope.split(" ");
await client.call("ACL", "SETUSER", username, `>${password}`, "on", ...permissionParts);
return {
username,
password
};
} catch (error: unknown) {
throw new BadRequestError({
message: `Unable to rotate credentials: ${redactPasswords(error, [{ username, password }])}`
});
} finally {
if (client) await client.quit();
}
};
/**
* Revokes a ACL password from the Redis server using its username and password.
*/
const revokeCredential = async (username: string) => {
let client: Redis | null = null;
try {
client = await $getClient();
await client.call("ACL", "DELUSER", username);
} catch (error: unknown) {
throw new BadRequestError({
message: `Unable to revoke credential: ${redactPasswords(error, [{ username, password: username }])}`
});
} finally {
if (client) await client.quit();
}
};
/**
* Issues a new set of credentials.
*/
const issueCredentials: TRotationFactoryIssueCredentials<TRedisCredentialsRotationGeneratedCredentials> = async (
callback
) => {
const credentials = await $rotateAclUser();
return callback(credentials);
};
/**
* Revokes a list of credentials.
*/
const revokeCredentials: TRotationFactoryRevokeCredentials<TRedisCredentialsRotationGeneratedCredentials> = async (
credentials,
callback
) => {
if (!credentials?.length) return callback();
for (const { username } of credentials) {
await revokeCredential(username);
// eslint-disable-next-line no-promise-executor-return
await new Promise((resolve) => setTimeout(resolve, 1000));
}
return callback();
};
/**
* Rotates credentials by issuing new ones and revoking the old.
*/
const rotateCredentials: TRotationFactoryRotateCredentials<TRedisCredentialsRotationGeneratedCredentials> = async (
oldCredentials,
callback
) => {
const newCredentials = await $rotateAclUser();
if (oldCredentials?.username) {
await revokeCredential(oldCredentials.username);
}
return callback(newCredentials);
};
/**
* Maps the generated credentials into the secret payload format.
*/
const getSecretsPayload: TRotationFactoryGetSecretsPayload<TRedisCredentialsRotationGeneratedCredentials> = ({
username,
password
}) => [
{ key: secretsMapping.username, value: username },
{ key: secretsMapping.password, value: password }
];
return {
issueCredentials,
revokeCredentials,
rotateCredentials,
getSecretsPayload
};
};
@@ -0,0 +1,70 @@
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 { SecretNameSchema } from "@app/server/lib/schemas";
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
import { PasswordRequirementsSchema } from "../shared/general";
export const RedisCredentialsRotationGeneratedCredentialsSchema = z
.object({
username: z.string(),
password: z.string()
})
.array()
.min(1)
.max(2);
const RedisCredentialsRotationSecretsMappingSchema = z.object({
username: SecretNameSchema.describe(SecretRotations.SECRETS_MAPPING.REDIS_CREDENTIALS.username),
password: SecretNameSchema.describe(SecretRotations.SECRETS_MAPPING.REDIS_CREDENTIALS.password)
});
export const RedisCredentialsRotationParametersSchema = z.object({
passwordRequirements: PasswordRequirementsSchema.optional(),
permissionScope: z
.string()
.trim()
.min(1, "Permission scope is required")
.describe(SecretRotations.PARAMETERS.REDIS_CREDENTIALS.permissionScope)
});
export const RedisCredentialsRotationTemplateSchema = z.object({
secretsMapping: z.object({
username: z.string(),
password: z.string()
})
});
export const RedisCredentialsRotationSchema = BaseSecretRotationSchema(SecretRotation.RedisCredentials).extend({
type: z.literal(SecretRotation.RedisCredentials),
parameters: RedisCredentialsRotationParametersSchema,
secretsMapping: RedisCredentialsRotationSecretsMappingSchema
});
export const CreateRedisCredentialsRotationSchema = BaseCreateSecretRotationSchema(
SecretRotation.RedisCredentials
).extend({
parameters: RedisCredentialsRotationParametersSchema,
secretsMapping: RedisCredentialsRotationSecretsMappingSchema
});
export const UpdateRedisCredentialsRotationSchema = BaseUpdateSecretRotationSchema(
SecretRotation.RedisCredentials
).extend({
parameters: RedisCredentialsRotationParametersSchema.optional(),
secretsMapping: RedisCredentialsRotationSecretsMappingSchema.optional()
});
export const RedisCredentialsRotationListItemSchema = z.object({
name: z.literal("Redis Credentials"),
connection: z.literal(AppConnection.Redis),
type: z.literal(SecretRotation.RedisCredentials),
template: RedisCredentialsRotationTemplateSchema
});
@@ -0,0 +1,24 @@
import { z } from "zod";
import { TRedisConnection } from "@app/services/app-connection/redis";
import {
CreateRedisCredentialsRotationSchema,
RedisCredentialsRotationGeneratedCredentialsSchema,
RedisCredentialsRotationListItemSchema,
RedisCredentialsRotationSchema
} from "./redis-credentials-rotation-schemas";
export type TRedisCredentialsRotation = z.infer<typeof RedisCredentialsRotationSchema>;
export type TRedisCredentialsRotationInput = z.infer<typeof CreateRedisCredentialsRotationSchema>;
export type TRedisCredentialsRotationListItem = z.infer<typeof RedisCredentialsRotationListItemSchema>;
export type TRedisCredentialsRotationWithConnection = TRedisCredentialsRotation & {
connection: TRedisConnection;
};
export type TRedisCredentialsRotationGeneratedCredentials = z.infer<
typeof RedisCredentialsRotationGeneratedCredentialsSchema
>;
@@ -7,7 +7,8 @@ export enum SecretRotation {
AzureClientSecret = "azure-client-secret", AzureClientSecret = "azure-client-secret",
AwsIamUserSecret = "aws-iam-user-secret", AwsIamUserSecret = "aws-iam-user-secret",
LdapPassword = "ldap-password", LdapPassword = "ldap-password",
OktaClientSecret = "okta-client-secret" OktaClientSecret = "okta-client-secret",
RedisCredentials = "redis-credentials"
} }
export enum SecretRotationStatus { export enum SecretRotationStatus {
@@ -14,6 +14,7 @@ import { MYSQL_CREDENTIALS_ROTATION_LIST_OPTION } from "./mysql-credentials";
import { OKTA_CLIENT_SECRET_ROTATION_LIST_OPTION } from "./okta-client-secret"; import { OKTA_CLIENT_SECRET_ROTATION_LIST_OPTION } from "./okta-client-secret";
import { ORACLEDB_CREDENTIALS_ROTATION_LIST_OPTION } from "./oracledb-credentials"; import { ORACLEDB_CREDENTIALS_ROTATION_LIST_OPTION } from "./oracledb-credentials";
import { POSTGRES_CREDENTIALS_ROTATION_LIST_OPTION } from "./postgres-credentials"; import { POSTGRES_CREDENTIALS_ROTATION_LIST_OPTION } from "./postgres-credentials";
import { REDIS_CREDENTIALS_ROTATION_LIST_OPTION } from "./redis-credentials";
import { TSecretRotationV2DALFactory } from "./secret-rotation-v2-dal"; import { TSecretRotationV2DALFactory } from "./secret-rotation-v2-dal";
import { SecretRotation, SecretRotationStatus } from "./secret-rotation-v2-enums"; import { SecretRotation, SecretRotationStatus } from "./secret-rotation-v2-enums";
import { TSecretRotationV2ServiceFactory, TSecretRotationV2ServiceFactoryDep } from "./secret-rotation-v2-service"; import { TSecretRotationV2ServiceFactory, TSecretRotationV2ServiceFactoryDep } from "./secret-rotation-v2-service";
@@ -35,7 +36,8 @@ const SECRET_ROTATION_LIST_OPTIONS: Record<SecretRotation, TSecretRotationV2List
[SecretRotation.AzureClientSecret]: AZURE_CLIENT_SECRET_ROTATION_LIST_OPTION, [SecretRotation.AzureClientSecret]: AZURE_CLIENT_SECRET_ROTATION_LIST_OPTION,
[SecretRotation.AwsIamUserSecret]: AWS_IAM_USER_SECRET_ROTATION_LIST_OPTION, [SecretRotation.AwsIamUserSecret]: AWS_IAM_USER_SECRET_ROTATION_LIST_OPTION,
[SecretRotation.LdapPassword]: LDAP_PASSWORD_ROTATION_LIST_OPTION, [SecretRotation.LdapPassword]: LDAP_PASSWORD_ROTATION_LIST_OPTION,
[SecretRotation.OktaClientSecret]: OKTA_CLIENT_SECRET_ROTATION_LIST_OPTION [SecretRotation.OktaClientSecret]: OKTA_CLIENT_SECRET_ROTATION_LIST_OPTION,
[SecretRotation.RedisCredentials]: REDIS_CREDENTIALS_ROTATION_LIST_OPTION
}; };
export const listSecretRotationOptions = () => { export const listSecretRotationOptions = () => {
@@ -10,7 +10,8 @@ export const SECRET_ROTATION_NAME_MAP: Record<SecretRotation, string> = {
[SecretRotation.AzureClientSecret]: "Azure Client Secret", [SecretRotation.AzureClientSecret]: "Azure Client Secret",
[SecretRotation.AwsIamUserSecret]: "AWS IAM User Secret", [SecretRotation.AwsIamUserSecret]: "AWS IAM User Secret",
[SecretRotation.LdapPassword]: "LDAP Password", [SecretRotation.LdapPassword]: "LDAP Password",
[SecretRotation.OktaClientSecret]: "Okta Client Secret" [SecretRotation.OktaClientSecret]: "Okta Client Secret",
[SecretRotation.RedisCredentials]: "Redis Credentials"
}; };
export const SECRET_ROTATION_CONNECTION_MAP: Record<SecretRotation, AppConnection> = { export const SECRET_ROTATION_CONNECTION_MAP: Record<SecretRotation, AppConnection> = {
@@ -22,5 +23,6 @@ export const SECRET_ROTATION_CONNECTION_MAP: Record<SecretRotation, AppConnectio
[SecretRotation.AzureClientSecret]: AppConnection.AzureClientSecrets, [SecretRotation.AzureClientSecret]: AppConnection.AzureClientSecrets,
[SecretRotation.AwsIamUserSecret]: AppConnection.AWS, [SecretRotation.AwsIamUserSecret]: AppConnection.AWS,
[SecretRotation.LdapPassword]: AppConnection.LDAP, [SecretRotation.LdapPassword]: AppConnection.LDAP,
[SecretRotation.OktaClientSecret]: AppConnection.Okta [SecretRotation.OktaClientSecret]: AppConnection.Okta,
[SecretRotation.RedisCredentials]: AppConnection.Redis
}; };
@@ -85,6 +85,7 @@ import { TSecretVersionV2TagDALFactory } from "@app/services/secret-v2-bridge/se
import { TGatewayV2ServiceFactory } from "../gateway-v2/gateway-v2-service"; import { TGatewayV2ServiceFactory } from "../gateway-v2/gateway-v2-service";
import { awsIamUserSecretRotationFactory } from "./aws-iam-user-secret/aws-iam-user-secret-rotation-fns"; import { awsIamUserSecretRotationFactory } from "./aws-iam-user-secret/aws-iam-user-secret-rotation-fns";
import { oktaClientSecretRotationFactory } from "./okta-client-secret/okta-client-secret-rotation-fns"; import { oktaClientSecretRotationFactory } from "./okta-client-secret/okta-client-secret-rotation-fns";
import { redisCredentialsRotationFactory } from "./redis-credentials/redis-credentials-rotation-fns";
import { TSecretRotationV2DALFactory } from "./secret-rotation-v2-dal"; import { TSecretRotationV2DALFactory } from "./secret-rotation-v2-dal";
export type TSecretRotationV2ServiceFactoryDep = { export type TSecretRotationV2ServiceFactoryDep = {
@@ -132,7 +133,8 @@ const SECRET_ROTATION_FACTORY_MAP: Record<SecretRotation, TRotationFactoryImplem
[SecretRotation.AzureClientSecret]: azureClientSecretRotationFactory as TRotationFactoryImplementation, [SecretRotation.AzureClientSecret]: azureClientSecretRotationFactory as TRotationFactoryImplementation,
[SecretRotation.AwsIamUserSecret]: awsIamUserSecretRotationFactory as TRotationFactoryImplementation, [SecretRotation.AwsIamUserSecret]: awsIamUserSecretRotationFactory as TRotationFactoryImplementation,
[SecretRotation.LdapPassword]: ldapPasswordRotationFactory as TRotationFactoryImplementation, [SecretRotation.LdapPassword]: ldapPasswordRotationFactory as TRotationFactoryImplementation,
[SecretRotation.OktaClientSecret]: oktaClientSecretRotationFactory as TRotationFactoryImplementation [SecretRotation.OktaClientSecret]: oktaClientSecretRotationFactory as TRotationFactoryImplementation,
[SecretRotation.RedisCredentials]: redisCredentialsRotationFactory as TRotationFactoryImplementation
}; };
export const secretRotationV2ServiceFactory = ({ export const secretRotationV2ServiceFactory = ({
@@ -66,6 +66,13 @@ import {
TPostgresCredentialsRotationListItem, TPostgresCredentialsRotationListItem,
TPostgresCredentialsRotationWithConnection TPostgresCredentialsRotationWithConnection
} from "./postgres-credentials"; } from "./postgres-credentials";
import {
TRedisCredentialsRotation,
TRedisCredentialsRotationGeneratedCredentials,
TRedisCredentialsRotationInput,
TRedisCredentialsRotationListItem,
TRedisCredentialsRotationWithConnection
} from "./redis-credentials/redis-credentials-rotation-types";
import { TSecretRotationV2DALFactory } from "./secret-rotation-v2-dal"; import { TSecretRotationV2DALFactory } from "./secret-rotation-v2-dal";
import { SecretRotation } from "./secret-rotation-v2-enums"; import { SecretRotation } from "./secret-rotation-v2-enums";
@@ -78,7 +85,8 @@ export type TSecretRotationV2 =
| TAzureClientSecretRotation | TAzureClientSecretRotation
| TLdapPasswordRotation | TLdapPasswordRotation
| TAwsIamUserSecretRotation | TAwsIamUserSecretRotation
| TOktaClientSecretRotation; | TOktaClientSecretRotation
| TRedisCredentialsRotation;
export type TSecretRotationV2WithConnection = export type TSecretRotationV2WithConnection =
| TPostgresCredentialsRotationWithConnection | TPostgresCredentialsRotationWithConnection
@@ -89,7 +97,8 @@ export type TSecretRotationV2WithConnection =
| TAzureClientSecretRotationWithConnection | TAzureClientSecretRotationWithConnection
| TLdapPasswordRotationWithConnection | TLdapPasswordRotationWithConnection
| TAwsIamUserSecretRotationWithConnection | TAwsIamUserSecretRotationWithConnection
| TOktaClientSecretRotationWithConnection; | TOktaClientSecretRotationWithConnection
| TRedisCredentialsRotationWithConnection;
export type TSecretRotationV2GeneratedCredentials = export type TSecretRotationV2GeneratedCredentials =
| TSqlCredentialsRotationGeneratedCredentials | TSqlCredentialsRotationGeneratedCredentials
@@ -97,7 +106,8 @@ export type TSecretRotationV2GeneratedCredentials =
| TAzureClientSecretRotationGeneratedCredentials | TAzureClientSecretRotationGeneratedCredentials
| TLdapPasswordRotationGeneratedCredentials | TLdapPasswordRotationGeneratedCredentials
| TAwsIamUserSecretRotationGeneratedCredentials | TAwsIamUserSecretRotationGeneratedCredentials
| TOktaClientSecretRotationGeneratedCredentials; | TOktaClientSecretRotationGeneratedCredentials
| TRedisCredentialsRotationGeneratedCredentials;
export type TSecretRotationV2Input = export type TSecretRotationV2Input =
| TPostgresCredentialsRotationInput | TPostgresCredentialsRotationInput
@@ -108,7 +118,8 @@ export type TSecretRotationV2Input =
| TAzureClientSecretRotationInput | TAzureClientSecretRotationInput
| TLdapPasswordRotationInput | TLdapPasswordRotationInput
| TAwsIamUserSecretRotationInput | TAwsIamUserSecretRotationInput
| TOktaClientSecretRotationInput; | TOktaClientSecretRotationInput
| TRedisCredentialsRotationInput;
export type TSecretRotationV2ListItem = export type TSecretRotationV2ListItem =
| TPostgresCredentialsRotationListItem | TPostgresCredentialsRotationListItem
@@ -119,7 +130,8 @@ export type TSecretRotationV2ListItem =
| TAzureClientSecretRotationListItem | TAzureClientSecretRotationListItem
| TLdapPasswordRotationListItem | TLdapPasswordRotationListItem
| TAwsIamUserSecretRotationListItem | TAwsIamUserSecretRotationListItem
| TOktaClientSecretRotationListItem; | TOktaClientSecretRotationListItem
| TRedisCredentialsRotationListItem;
export type TSecretRotationV2TemporaryParameters = TLdapPasswordRotationInput["temporaryParameters"] | undefined; export type TSecretRotationV2TemporaryParameters = TLdapPasswordRotationInput["temporaryParameters"] | undefined;
@@ -9,6 +9,7 @@ import { MySqlCredentialsRotationSchema } from "@app/ee/services/secret-rotation
import { OktaClientSecretRotationSchema } from "@app/ee/services/secret-rotation-v2/okta-client-secret"; import { OktaClientSecretRotationSchema } from "@app/ee/services/secret-rotation-v2/okta-client-secret";
import { OracleDBCredentialsRotationSchema } from "@app/ee/services/secret-rotation-v2/oracledb-credentials"; import { OracleDBCredentialsRotationSchema } from "@app/ee/services/secret-rotation-v2/oracledb-credentials";
import { PostgresCredentialsRotationSchema } from "@app/ee/services/secret-rotation-v2/postgres-credentials"; import { PostgresCredentialsRotationSchema } from "@app/ee/services/secret-rotation-v2/postgres-credentials";
import { RedisCredentialsRotationSchema } from "@app/ee/services/secret-rotation-v2/redis-credentials";
export const SecretRotationV2Schema = z.discriminatedUnion("type", [ export const SecretRotationV2Schema = z.discriminatedUnion("type", [
PostgresCredentialsRotationSchema, PostgresCredentialsRotationSchema,
@@ -19,5 +20,6 @@ export const SecretRotationV2Schema = z.discriminatedUnion("type", [
AzureClientSecretRotationSchema, AzureClientSecretRotationSchema,
LdapPasswordRotationSchema, LdapPasswordRotationSchema,
AwsIamUserSecretRotationSchema, AwsIamUserSecretRotationSchema,
OktaClientSecretRotationSchema OktaClientSecretRotationSchema,
RedisCredentialsRotationSchema
]); ]);
+18 -2
View File
@@ -47,6 +47,7 @@ export const KeyStorePrefixes = {
SyncSecretIntegrationLastRunTimestamp: (projectId: string, environmentSlug: string, secretPath: string) => SyncSecretIntegrationLastRunTimestamp: (projectId: string, environmentSlug: string, secretPath: string) =>
`sync-integration-last-run-${projectId}-${environmentSlug}-${secretPath}` as const, `sync-integration-last-run-${projectId}-${environmentSlug}-${secretPath}` as const,
SecretSyncLock: (syncId: string) => `secret-sync-mutex-${syncId}` as const, SecretSyncLock: (syncId: string) => `secret-sync-mutex-${syncId}` as const,
PkiSyncLock: (syncId: string) => `pki-sync-mutex-${syncId}` as const,
AppConnectionConcurrentJobs: (connectionId: string) => `app-connection-concurrency-${connectionId}` as const, AppConnectionConcurrentJobs: (connectionId: string) => `app-connection-concurrency-${connectionId}` as const,
SecretRotationLock: (rotationId: string) => `secret-rotation-v2-mutex-${rotationId}` as const, SecretRotationLock: (rotationId: string) => `secret-rotation-v2-mutex-${rotationId}` as const,
SecretScanningLock: (dataSourceId: string, resourceExternalId: string) => SecretScanningLock: (dataSourceId: string, resourceExternalId: string) =>
@@ -62,13 +63,28 @@ export const KeyStorePrefixes = {
ActiveSSEConnectionsSet: (projectId: string, identityId: string) => ActiveSSEConnectionsSet: (projectId: string, identityId: string) =>
`sse-connections:${projectId}:${identityId}` as const, `sse-connections:${projectId}:${identityId}` as const,
ActiveSSEConnections: (projectId: string, identityId: string, connectionId: string) => ActiveSSEConnections: (projectId: string, identityId: string, connectionId: string) =>
`sse-connections:${projectId}:${identityId}:${connectionId}` as const `sse-connections:${projectId}:${identityId}:${connectionId}` as const,
ProjectPermission: (
projectId: string,
version: number,
actorType: string,
actorId: string,
actionProjectType: string
) => `project-permission:${projectId}:${version}:${actorType}:${actorId}:${actionProjectType}` as const,
ProjectPermissionDalVersion: (projectId: string) => `project-permission:${projectId}:dal-version` as const,
UserProjectPermissionPattern: (userId: string) => `project-permission:*:*:USER:${userId}:*` as const,
IdentityProjectPermissionPattern: (identityId: string) => `project-permission:*:*:IDENTITY:${identityId}:*` as const,
GroupMemberProjectPermissionPattern: (projectId: string, groupId: string) =>
`group-member-project-permission:${projectId}:${groupId}:*` as const
}; };
export const KeyStoreTtls = { export const KeyStoreTtls = {
SetSyncSecretIntegrationLastRunTimestampInSeconds: 60, SetSyncSecretIntegrationLastRunTimestampInSeconds: 60,
SetSecretSyncLastRunTimestampInSeconds: 60, SetSecretSyncLastRunTimestampInSeconds: 60,
AccessTokenStatusUpdateInSeconds: 120 AccessTokenStatusUpdateInSeconds: 120,
ProjectPermissionCacheInSeconds: 300, // 5 minutes
ProjectPermissionDalVersionTtl: "15m" // Project permission DAL version TTL
}; };
type TDeleteItems = { type TDeleteItems = {
+9 -1
View File
@@ -50,6 +50,7 @@ export enum ApiDocsTags {
IdentitySpecificPrivilegesV2 = "Identity Specific Privileges V2", IdentitySpecificPrivilegesV2 = "Identity Specific Privileges V2",
AppConnections = "App Connections", AppConnections = "App Connections",
SecretSyncs = "Secret Syncs", SecretSyncs = "Secret Syncs",
PkiSyncs = "PKI Syncs",
Integrations = "Integrations", Integrations = "Integrations",
ServiceTokens = "Service Tokens", ServiceTokens = "Service Tokens",
AuditLogs = "Audit Logs", AuditLogs = "Audit Logs",
@@ -1048,7 +1049,7 @@ export const RAW_SECRETS = {
}, },
GET_ACCESS_LIST: { GET_ACCESS_LIST: {
secretName: "The name of the secret to get the access list for.", secretName: "The name of the secret to get the access list for.",
workspaceId: "The ID of the project where the secret is located.", projectId: "The ID of the project where the secret is located.",
environment: "The slug of the environment where the the secret is located.", environment: "The slug of the environment where the the secret is located.",
secretPath: "The folder path where the secret is located." secretPath: "The folder path where the secret is located."
} }
@@ -2685,9 +2686,16 @@ export const SecretRotations = {
}, },
OKTA_CLIENT_SECRET: { OKTA_CLIENT_SECRET: {
clientId: "The ID of the Okta Application to rotate the client secret for." clientId: "The ID of the Okta Application to rotate the client secret for."
},
REDIS_CREDENTIALS: {
permissionScope: "The ACL permission scope to assign to the issued Redis users."
} }
}, },
SECRETS_MAPPING: { SECRETS_MAPPING: {
REDIS_CREDENTIALS: {
username: "The name of the secret that the username will be mapped to.",
password: "The name of the secret that the rotated password will be mapped to."
},
SQL_CREDENTIALS: { SQL_CREDENTIALS: {
username: "The name of the secret that the active username will be mapped to.", username: "The name of the secret that the active username will be mapped to.",
password: "The name of the secret that the generated password will be mapped to." password: "The name of the secret that the generated password will be mapped to."
+23 -1
View File
@@ -129,6 +129,8 @@ const envSchema = z
POSTHOG_HOST: zpStr(z.string().optional().default("https://app.posthog.com")), POSTHOG_HOST: zpStr(z.string().optional().default("https://app.posthog.com")),
POSTHOG_PROJECT_API_KEY: zpStr(z.string().optional().default("phc_nSin8j5q2zdhpFDI1ETmFNUIuTG4DwKVyIigrY10XiE")), POSTHOG_PROJECT_API_KEY: zpStr(z.string().optional().default("phc_nSin8j5q2zdhpFDI1ETmFNUIuTG4DwKVyIigrY10XiE")),
LOOPS_API_KEY: zpStr(z.string().optional()), LOOPS_API_KEY: zpStr(z.string().optional()),
// GitHub API token for upgrade path tool
GITHUB_API_TOKEN: zpStr(z.string().optional()),
// jwt options // jwt options
AUTH_SECRET: zpStr(z.string()).default(process.env.JWT_AUTH_SECRET), // for those still using old JWT_AUTH_SECRET AUTH_SECRET: zpStr(z.string()).default(process.env.JWT_AUTH_SECRET), // for those still using old JWT_AUTH_SECRET
JWT_AUTH_LIFETIME: zpStr(z.string().default("10d")), JWT_AUTH_LIFETIME: zpStr(z.string().default("10d")),
@@ -323,6 +325,10 @@ const envSchema = z
INF_APP_CONNECTION_AZURE_DEVOPS_CLIENT_ID: zpStr(z.string().optional()), INF_APP_CONNECTION_AZURE_DEVOPS_CLIENT_ID: zpStr(z.string().optional()),
INF_APP_CONNECTION_AZURE_DEVOPS_CLIENT_SECRET: zpStr(z.string().optional()), INF_APP_CONNECTION_AZURE_DEVOPS_CLIENT_SECRET: zpStr(z.string().optional()),
// Heroku App Connection
INF_APP_CONNECTION_HEROKU_OAUTH_CLIENT_ID: zpStr(z.string().optional()),
INF_APP_CONNECTION_HEROKU_OAUTH_CLIENT_SECRET: zpStr(z.string().optional()),
// datadog // datadog
SHOULD_USE_DATADOG_TRACER: zodStrBool.default("false"), SHOULD_USE_DATADOG_TRACER: zodStrBool.default("false"),
DATADOG_PROFILING_ENABLED: zodStrBool.default("false"), DATADOG_PROFILING_ENABLED: zodStrBool.default("false"),
@@ -433,7 +439,10 @@ const envSchema = z
INF_APP_CONNECTION_AZURE_APP_CONFIGURATION_CLIENT_ID: INF_APP_CONNECTION_AZURE_APP_CONFIGURATION_CLIENT_ID:
data.INF_APP_CONNECTION_AZURE_APP_CONFIGURATION_CLIENT_ID || data.INF_APP_CONNECTION_AZURE_CLIENT_ID, data.INF_APP_CONNECTION_AZURE_APP_CONFIGURATION_CLIENT_ID || data.INF_APP_CONNECTION_AZURE_CLIENT_ID,
INF_APP_CONNECTION_AZURE_APP_CONFIGURATION_CLIENT_SECRET: INF_APP_CONNECTION_AZURE_APP_CONFIGURATION_CLIENT_SECRET:
data.INF_APP_CONNECTION_AZURE_APP_CONFIGURATION_CLIENT_SECRET || data.INF_APP_CONNECTION_AZURE_CLIENT_SECRET data.INF_APP_CONNECTION_AZURE_APP_CONFIGURATION_CLIENT_SECRET || data.INF_APP_CONNECTION_AZURE_CLIENT_SECRET,
INF_APP_CONNECTION_HEROKU_OAUTH_CLIENT_ID: data.INF_APP_CONNECTION_HEROKU_OAUTH_CLIENT_ID || data.CLIENT_ID_HEROKU,
INF_APP_CONNECTION_HEROKU_OAUTH_CLIENT_SECRET:
data.INF_APP_CONNECTION_HEROKU_OAUTH_CLIENT_SECRET || data.CLIENT_SECRET_HEROKU
})); }));
export type TEnvConfig = Readonly<z.infer<typeof envSchema>>; export type TEnvConfig = Readonly<z.infer<typeof envSchema>>;
@@ -736,6 +745,19 @@ export const overwriteSchema: {
description: "The Client Secret of your GCP OAuth2 application." description: "The Client Secret of your GCP OAuth2 application."
} }
] ]
},
heroku: {
name: "Heroku",
fields: [
{
key: "INF_APP_CONNECTION_HEROKU_OAUTH_CLIENT_ID",
description: "The Client ID of your Heroku application."
},
{
key: "INF_APP_CONNECTION_HEROKU_OAUTH_CLIENT_SECRET",
description: "The Client Secret of your Heroku application."
}
]
} }
}; };
+6 -2
View File
@@ -16,8 +16,12 @@ export const stripUndefinedInWhere = <T extends object>(val: T): Exclude<T, unde
// if its undefined its skipped in knex // if its undefined its skipped in knex
// if its empty string its set as null // if its empty string its set as null
// else pass to the required one // else pass to the required one
export const setKnexStringValue = <T>(value: string | null | undefined, cb: (arg: string) => T) => { export const setKnexStringValue = <T>(
value: string | null | undefined,
cb: (arg: string) => T,
allowEmptyString?: boolean
) => {
if (typeof value === "undefined") return; if (typeof value === "undefined") return;
if (value === "" || value === null) return null; if ((value === "" && !allowEmptyString) || value === null) return null;
return cb(value); return cb(value);
}; };
+29
View File
@@ -24,6 +24,11 @@ import { QueueWorkerProfile } from "@app/lib/types";
import { CaType } from "@app/services/certificate-authority/certificate-authority-enums"; import { CaType } from "@app/services/certificate-authority/certificate-authority-enums";
import { ExternalPlatforms } from "@app/services/external-migration/external-migration-types"; import { ExternalPlatforms } from "@app/services/external-migration/external-migration-types";
import { TCreateUserNotificationDTO } from "@app/services/notification/notification-types"; import { TCreateUserNotificationDTO } from "@app/services/notification/notification-types";
import {
TQueuePkiSyncImportCertificatesByIdDTO,
TQueuePkiSyncRemoveCertificatesByIdDTO,
TQueuePkiSyncSyncCertificatesByIdDTO
} from "@app/services/pki-sync/pki-sync-types";
import { import {
TFailedIntegrationSyncEmailsPayload, TFailedIntegrationSyncEmailsPayload,
TIntegrationSyncPayload, TIntegrationSyncPayload,
@@ -46,6 +51,7 @@ export enum QueueName {
AuditLogPrune = "audit-log-prune", AuditLogPrune = "audit-log-prune",
DailyResourceCleanUp = "daily-resource-cleanup", DailyResourceCleanUp = "daily-resource-cleanup",
DailyExpiringPkiItemAlert = "daily-expiring-pki-item-alert", DailyExpiringPkiItemAlert = "daily-expiring-pki-item-alert",
PkiSyncCleanup = "pki-sync-cleanup",
PkiSubscriber = "pki-subscriber", PkiSubscriber = "pki-subscriber",
TelemetryInstanceStats = "telemtry-self-hosted-stats", TelemetryInstanceStats = "telemtry-self-hosted-stats",
IntegrationSync = "sync-integrations", IntegrationSync = "sync-integrations",
@@ -58,6 +64,7 @@ export enum QueueName {
CaLifecycle = "ca-lifecycle", // parent queue to ca-order-certificate-for-subscriber CaLifecycle = "ca-lifecycle", // parent queue to ca-order-certificate-for-subscriber
SecretReplication = "secret-replication", SecretReplication = "secret-replication",
SecretSync = "secret-sync", // parent queue to push integration sync, webhook, and secret replication SecretSync = "secret-sync", // parent queue to push integration sync, webhook, and secret replication
PkiSync = "pki-sync",
ProjectV3Migration = "project-v3-migration", ProjectV3Migration = "project-v3-migration",
AccessTokenStatusUpdate = "access-token-status-update", AccessTokenStatusUpdate = "access-token-status-update",
ImportSecretsFromExternalSource = "import-secrets-from-external-source", ImportSecretsFromExternalSource = "import-secrets-from-external-source",
@@ -80,6 +87,7 @@ export enum QueueJobs {
AuditLogPrune = "audit-log-prune-job", AuditLogPrune = "audit-log-prune-job",
DailyResourceCleanUp = "daily-resource-cleanup-job", DailyResourceCleanUp = "daily-resource-cleanup-job",
DailyExpiringPkiItemAlert = "daily-expiring-pki-item-alert", DailyExpiringPkiItemAlert = "daily-expiring-pki-item-alert",
PkiSyncCleanup = "pki-sync-cleanup-job",
SecWebhook = "secret-webhook-trigger", SecWebhook = "secret-webhook-trigger",
TelemetryInstanceStats = "telemetry-self-hosted-stats", TelemetryInstanceStats = "telemetry-self-hosted-stats",
IntegrationSync = "secret-integration-pull", IntegrationSync = "secret-integration-pull",
@@ -91,6 +99,7 @@ export enum QueueJobs {
CaCrlRotation = "ca-crl-rotation-job", CaCrlRotation = "ca-crl-rotation-job",
SecretReplication = "secret-replication", SecretReplication = "secret-replication",
SecretSync = "secret-sync", // parent queue to push integration sync, webhook, and secret replication SecretSync = "secret-sync", // parent queue to push integration sync, webhook, and secret replication
PkiSync = "pki-sync",
ProjectV3Migration = "project-v3-migration", ProjectV3Migration = "project-v3-migration",
IdentityAccessTokenStatusUpdate = "identity-access-token-status-update", IdentityAccessTokenStatusUpdate = "identity-access-token-status-update",
ServiceTokenStatusUpdate = "service-token-status-update", ServiceTokenStatusUpdate = "service-token-status-update",
@@ -99,6 +108,9 @@ export enum QueueJobs {
SecretSyncImportSecrets = "secret-sync-import-secrets", SecretSyncImportSecrets = "secret-sync-import-secrets",
SecretSyncRemoveSecrets = "secret-sync-remove-secrets", SecretSyncRemoveSecrets = "secret-sync-remove-secrets",
SecretSyncSendActionFailedNotifications = "secret-sync-send-action-failed-notifications", SecretSyncSendActionFailedNotifications = "secret-sync-send-action-failed-notifications",
PkiSyncSyncCertificates = "pki-sync-sync-certificates",
PkiSyncImportCertificates = "pki-sync-import-certificates",
PkiSyncRemoveCertificates = "pki-sync-remove-certificates",
SecretRotationV2QueueRotations = "secret-rotation-v2-queue-rotations", SecretRotationV2QueueRotations = "secret-rotation-v2-queue-rotations",
SecretRotationV2RotateSecrets = "secret-rotation-v2-rotate-secrets", SecretRotationV2RotateSecrets = "secret-rotation-v2-rotate-secrets",
SecretRotationV2SendNotification = "secret-rotation-v2-send-notification", SecretRotationV2SendNotification = "secret-rotation-v2-send-notification",
@@ -141,6 +153,10 @@ export type TQueueJobTypes = {
name: QueueJobs.DailyExpiringPkiItemAlert; name: QueueJobs.DailyExpiringPkiItemAlert;
payload: undefined; payload: undefined;
}; };
[QueueName.PkiSyncCleanup]: {
name: QueueJobs.PkiSyncCleanup;
payload: undefined;
};
[QueueName.AuditLogPrune]: { [QueueName.AuditLogPrune]: {
name: QueueJobs.AuditLogPrune; name: QueueJobs.AuditLogPrune;
payload: undefined; payload: undefined;
@@ -218,6 +234,19 @@ export type TQueueJobTypes = {
name: QueueJobs.SecretSync; name: QueueJobs.SecretSync;
payload: TSyncSecretsDTO; payload: TSyncSecretsDTO;
}; };
[QueueName.PkiSync]:
| {
name: QueueJobs.PkiSyncSyncCertificates;
payload: TQueuePkiSyncSyncCertificatesByIdDTO;
}
| {
name: QueueJobs.PkiSyncImportCertificates;
payload: TQueuePkiSyncImportCertificatesByIdDTO;
}
| {
name: QueueJobs.PkiSyncRemoveCertificates;
payload: TQueuePkiSyncRemoveCertificatesByIdDTO;
};
[QueueName.ProjectV3Migration]: { [QueueName.ProjectV3Migration]: {
name: QueueJobs.ProjectV3Migration; name: QueueJobs.ProjectV3Migration;
payload: { projectId: string }; payload: { projectId: string };
+3 -5
View File
@@ -43,8 +43,6 @@ export const GenericResourceNameSchema = z
export const BaseSecretNameSchema = z.string().trim().min(1); export const BaseSecretNameSchema = z.string().trim().min(1);
export const SecretNameSchema = BaseSecretNameSchema.refine( export const SecretNameSchema = BaseSecretNameSchema.refine(
(el) => !el.includes(" "), (el) => !el.includes(":"),
"Secret name cannot contain spaces." "Secret name cannot contain colon."
) ).refine((el) => !el.includes("/"), "Secret name cannot contain forward slash.");
.refine((el) => !el.includes(":"), "Secret name cannot contain colon.")
.refine((el) => !el.includes("/"), "Secret name cannot contain forward slash.");
+130 -74
View File
@@ -248,6 +248,10 @@ import { pkiCollectionServiceFactory } from "@app/services/pki-collection/pki-co
import { pkiSubscriberDALFactory } from "@app/services/pki-subscriber/pki-subscriber-dal"; import { pkiSubscriberDALFactory } from "@app/services/pki-subscriber/pki-subscriber-dal";
import { pkiSubscriberQueueServiceFactory } from "@app/services/pki-subscriber/pki-subscriber-queue"; import { pkiSubscriberQueueServiceFactory } from "@app/services/pki-subscriber/pki-subscriber-queue";
import { pkiSubscriberServiceFactory } from "@app/services/pki-subscriber/pki-subscriber-service"; import { pkiSubscriberServiceFactory } from "@app/services/pki-subscriber/pki-subscriber-service";
import { pkiSyncCleanupQueueServiceFactory } from "@app/services/pki-sync/pki-sync-cleanup-queue";
import { pkiSyncDALFactory } from "@app/services/pki-sync/pki-sync-dal";
import { pkiSyncQueueFactory } from "@app/services/pki-sync/pki-sync-queue";
import { pkiSyncServiceFactory } from "@app/services/pki-sync/pki-sync-service";
import { pkiTemplatesDALFactory } from "@app/services/pki-templates/pki-templates-dal"; import { pkiTemplatesDALFactory } from "@app/services/pki-templates/pki-templates-dal";
import { pkiTemplatesServiceFactory } from "@app/services/pki-templates/pki-templates-service"; import { pkiTemplatesServiceFactory } from "@app/services/pki-templates/pki-templates-service";
import { projectDALFactory } from "@app/services/project/project-dal"; import { projectDALFactory } from "@app/services/project/project-dal";
@@ -309,6 +313,7 @@ import { telemetryQueueServiceFactory } from "@app/services/telemetry/telemetry-
import { telemetryServiceFactory } from "@app/services/telemetry/telemetry-service"; import { telemetryServiceFactory } from "@app/services/telemetry/telemetry-service";
import { totpConfigDALFactory } from "@app/services/totp/totp-config-dal"; import { totpConfigDALFactory } from "@app/services/totp/totp-config-dal";
import { totpServiceFactory } from "@app/services/totp/totp-service"; import { totpServiceFactory } from "@app/services/totp/totp-service";
import { upgradePathServiceFactory } from "@app/services/upgrade-path/upgrade-path-service";
import { userDALFactory } from "@app/services/user/user-dal"; import { userDALFactory } from "@app/services/user/user-dal";
import { userServiceFactory } from "@app/services/user/user-service"; import { userServiceFactory } from "@app/services/user/user-service";
import { userAliasDALFactory } from "@app/services/user-alias/user-alias-dal"; import { userAliasDALFactory } from "@app/services/user-alias/user-alias-dal";
@@ -526,7 +531,8 @@ export const registerRoutes = async (
orgRoleDAL, orgRoleDAL,
projectRoleDAL, projectRoleDAL,
serviceTokenDAL, serviceTokenDAL,
projectDAL projectDAL,
keyStore
}); });
const assumePrivilegeService = assumePrivilegeServiceFactory({ const assumePrivilegeService = assumePrivilegeServiceFactory({
projectDAL, projectDAL,
@@ -760,6 +766,8 @@ export const registerRoutes = async (
userAliasDAL userAliasDAL
}); });
const upgradePathService = upgradePathServiceFactory({ keyStore });
const totpService = totpServiceFactory({ const totpService = totpServiceFactory({
totpConfigDAL, totpConfigDAL,
userDAL, userDAL,
@@ -781,6 +789,7 @@ export const registerRoutes = async (
smtpService, smtpService,
authDAL, authDAL,
userDAL, userDAL,
orgMembershipDAL,
totpConfigDAL totpConfigDAL
}); });
@@ -979,6 +988,7 @@ export const registerRoutes = async (
const pkiCollectionDAL = pkiCollectionDALFactory(db); const pkiCollectionDAL = pkiCollectionDALFactory(db);
const pkiCollectionItemDAL = pkiCollectionItemDALFactory(db); const pkiCollectionItemDAL = pkiCollectionItemDALFactory(db);
const pkiSubscriberDAL = pkiSubscriberDALFactory(db); const pkiSubscriberDAL = pkiSubscriberDALFactory(db);
const pkiSyncDAL = pkiSyncDALFactory(db);
const pkiTemplatesDAL = pkiTemplatesDALFactory(db); const pkiTemplatesDAL = pkiTemplatesDALFactory(db);
const instanceRelayConfigDAL = instanceRelayConfigDalFactory(db); const instanceRelayConfigDAL = instanceRelayConfigDalFactory(db);
@@ -988,21 +998,6 @@ export const registerRoutes = async (
const orgGatewayConfigV2DAL = orgGatewayConfigV2DalFactory(db); const orgGatewayConfigV2DAL = orgGatewayConfigV2DalFactory(db);
const certificateService = certificateServiceFactory({
certificateDAL,
certificateBodyDAL,
certificateSecretDAL,
certificateAuthorityDAL,
certificateAuthorityCertDAL,
certificateAuthorityCrlDAL,
certificateAuthoritySecretDAL,
projectDAL,
kmsService,
permissionService,
pkiCollectionDAL,
pkiCollectionItemDAL
});
const sshCertificateAuthorityService = sshCertificateAuthorityServiceFactory({ const sshCertificateAuthorityService = sshCertificateAuthorityServiceFactory({
sshCertificateAuthorityDAL, sshCertificateAuthorityDAL,
sshCertificateAuthoritySecretDAL, sshCertificateAuthoritySecretDAL,
@@ -1110,7 +1105,9 @@ export const registerRoutes = async (
instanceRelayConfigDAL, instanceRelayConfigDAL,
orgRelayConfigDAL, orgRelayConfigDAL,
relayDAL, relayDAL,
kmsService kmsService,
licenseService,
permissionService
}); });
const gatewayV2Service = gatewayV2ServiceFactory({ const gatewayV2Service = gatewayV2ServiceFactory({
@@ -1862,52 +1859,6 @@ export const registerRoutes = async (
licenseService licenseService
}); });
const certificateAuthorityQueue = certificateAuthorityQueueFactory({
certificateAuthorityCrlDAL,
certificateAuthorityDAL,
certificateAuthoritySecretDAL,
certificateDAL,
projectDAL,
kmsService,
queueService,
pkiSubscriberDAL,
certificateBodyDAL,
certificateSecretDAL,
externalCertificateAuthorityDAL,
keyStore,
appConnectionDAL,
appConnectionService
});
const internalCertificateAuthorityService = internalCertificateAuthorityServiceFactory({
certificateAuthorityDAL,
certificateAuthorityCertDAL,
certificateAuthoritySecretDAL,
certificateAuthorityCrlDAL,
certificateTemplateDAL,
certificateAuthorityQueue,
certificateDAL,
certificateBodyDAL,
certificateSecretDAL,
pkiCollectionDAL,
pkiCollectionItemDAL,
projectDAL,
internalCertificateAuthorityDAL,
kmsService,
permissionService
});
const certificateEstService = certificateEstServiceFactory({
internalCertificateAuthorityService,
certificateTemplateService,
certificateTemplateDAL,
certificateAuthorityCertDAL,
certificateAuthorityDAL,
projectDAL,
kmsService,
licenseService
});
const kmipService = kmipServiceFactory({ const kmipService = kmipServiceFactory({
kmipClientDAL, kmipClientDAL,
permissionService, permissionService,
@@ -1950,6 +1901,79 @@ export const registerRoutes = async (
gatewayV2Service gatewayV2Service
}); });
const pkiSyncQueue = pkiSyncQueueFactory({
queueService,
kmsService,
appConnectionDAL,
keyStore,
pkiSyncDAL,
auditLogService,
projectDAL,
licenseService,
certificateDAL,
certificateBodyDAL,
certificateSecretDAL,
certificateAuthorityDAL,
certificateAuthorityCertDAL
});
const pkiSyncCleanup = pkiSyncCleanupQueueServiceFactory({
queueService,
pkiSyncDAL,
pkiSyncQueue
});
const internalCaFns = InternalCertificateAuthorityFns({
certificateAuthorityDAL,
certificateAuthorityCertDAL,
certificateAuthoritySecretDAL,
certificateAuthorityCrlDAL,
certificateDAL,
certificateBodyDAL,
certificateSecretDAL,
projectDAL,
kmsService,
pkiSyncDAL,
pkiSyncQueue
});
const certificateAuthorityQueue = certificateAuthorityQueueFactory({
certificateAuthorityCrlDAL,
certificateAuthorityDAL,
certificateAuthoritySecretDAL,
certificateDAL,
projectDAL,
kmsService,
queueService,
pkiSubscriberDAL,
certificateBodyDAL,
certificateSecretDAL,
externalCertificateAuthorityDAL,
keyStore,
appConnectionDAL,
appConnectionService,
pkiSyncDAL,
pkiSyncQueue
});
const internalCertificateAuthorityService = internalCertificateAuthorityServiceFactory({
certificateAuthorityDAL,
certificateAuthorityCertDAL,
certificateAuthoritySecretDAL,
certificateAuthorityCrlDAL,
certificateTemplateDAL,
certificateAuthorityQueue,
certificateDAL,
certificateBodyDAL,
certificateSecretDAL,
pkiCollectionDAL,
pkiCollectionItemDAL,
projectDAL,
internalCertificateAuthorityDAL,
kmsService,
permissionService
});
const certificateAuthorityService = certificateAuthorityServiceFactory({ const certificateAuthorityService = certificateAuthorityServiceFactory({
certificateAuthorityDAL, certificateAuthorityDAL,
permissionService, permissionService,
@@ -1962,19 +1986,20 @@ export const registerRoutes = async (
certificateSecretDAL, certificateSecretDAL,
kmsService, kmsService,
pkiSubscriberDAL, pkiSubscriberDAL,
projectDAL projectDAL,
pkiSyncDAL,
pkiSyncQueue
}); });
const internalCaFns = InternalCertificateAuthorityFns({ const certificateEstService = certificateEstServiceFactory({
certificateAuthorityDAL, internalCertificateAuthorityService,
certificateTemplateService,
certificateTemplateDAL,
certificateAuthorityCertDAL, certificateAuthorityCertDAL,
certificateAuthoritySecretDAL, certificateAuthorityDAL,
certificateAuthorityCrlDAL,
certificateDAL,
certificateBodyDAL,
certificateSecretDAL,
projectDAL, projectDAL,
kmsService kmsService,
licenseService
}); });
const pkiSubscriberQueue = pkiSubscriberQueueServiceFactory({ const pkiSubscriberQueue = pkiSubscriberQueueServiceFactory({
@@ -1987,6 +2012,23 @@ export const registerRoutes = async (
internalCaFns internalCaFns
}); });
const certificateService = certificateServiceFactory({
certificateDAL,
certificateBodyDAL,
certificateSecretDAL,
certificateAuthorityDAL,
certificateAuthorityCertDAL,
certificateAuthorityCrlDAL,
certificateAuthoritySecretDAL,
projectDAL,
kmsService,
permissionService,
pkiCollectionDAL,
pkiCollectionItemDAL,
pkiSyncDAL,
pkiSyncQueue
});
const pkiSubscriberService = pkiSubscriberServiceFactory({ const pkiSubscriberService = pkiSubscriberServiceFactory({
pkiSubscriberDAL, pkiSubscriberDAL,
certificateAuthorityDAL, certificateAuthorityDAL,
@@ -2000,7 +2042,18 @@ export const registerRoutes = async (
kmsService, kmsService,
permissionService, permissionService,
certificateAuthorityQueue, certificateAuthorityQueue,
internalCaFns internalCaFns,
pkiSyncDAL,
pkiSyncQueue
});
const pkiSyncService = pkiSyncServiceFactory({
pkiSyncDAL,
pkiSubscriberDAL,
appConnectionService,
permissionService,
licenseService,
pkiSyncQueue
}); });
const pkiTemplateService = pkiTemplatesServiceFactory({ const pkiTemplateService = pkiTemplatesServiceFactory({
@@ -2065,6 +2118,7 @@ export const registerRoutes = async (
await telemetryQueue.startTelemetryCheck(); await telemetryQueue.startTelemetryCheck();
await telemetryQueue.startAggregatedEventsJob(); await telemetryQueue.startAggregatedEventsJob();
await dailyResourceCleanUp.init(); await dailyResourceCleanUp.init();
await pkiSyncCleanup.init();
await dailyReminderQueueService.startDailyRemindersJob(); await dailyReminderQueueService.startDailyRemindersJob();
await dailyReminderQueueService.startSecretReminderMigrationJob(); await dailyReminderQueueService.startSecretReminderMigrationJob();
await dailyExpiringPkiItemAlert.startSendingAlerts(); await dailyExpiringPkiItemAlert.startSendingAlerts();
@@ -2148,6 +2202,7 @@ export const registerRoutes = async (
pkiAlert: pkiAlertService, pkiAlert: pkiAlertService,
pkiCollection: pkiCollectionService, pkiCollection: pkiCollectionService,
pkiSubscriber: pkiSubscriberService, pkiSubscriber: pkiSubscriberService,
pkiSync: pkiSyncService,
pkiTemplate: pkiTemplateService, pkiTemplate: pkiTemplateService,
secretScanning: secretScanningService, secretScanning: secretScanningService,
license: licenseService, license: licenseService,
@@ -2186,7 +2241,8 @@ export const registerRoutes = async (
reminder: reminderService, reminder: reminderService,
bus: eventBusService, bus: eventBusService,
sse: sseService, sse: sseService,
notification: notificationService notification: notificationService,
upgradePath: upgradePathService
}); });
const cronJobs: CronJob[] = []; const cronJobs: CronJob[] = [];
@@ -93,6 +93,7 @@ import {
RailwayConnectionListItemSchema, RailwayConnectionListItemSchema,
SanitizedRailwayConnectionSchema SanitizedRailwayConnectionSchema
} from "@app/services/app-connection/railway"; } from "@app/services/app-connection/railway";
import { RedisConnectionListItemSchema, SanitizedRedisConnectionSchema } from "@app/services/app-connection/redis";
import { import {
RenderConnectionListItemSchema, RenderConnectionListItemSchema,
SanitizedRenderConnectionSchema SanitizedRenderConnectionSchema
@@ -156,7 +157,8 @@ const SanitizedAppConnectionSchema = z.union([
...SanitizedDigitalOceanConnectionSchema.options, ...SanitizedDigitalOceanConnectionSchema.options,
...SanitizedNetlifyConnectionSchema.options, ...SanitizedNetlifyConnectionSchema.options,
...SanitizedOktaConnectionSchema.options, ...SanitizedOktaConnectionSchema.options,
...SanitizedAzureADCSConnectionSchema.options ...SanitizedAzureADCSConnectionSchema.options,
...SanitizedRedisConnectionSchema.options
]); ]);
const AppConnectionOptionsSchema = z.discriminatedUnion("app", [ const AppConnectionOptionsSchema = z.discriminatedUnion("app", [
@@ -197,7 +199,8 @@ const AppConnectionOptionsSchema = z.discriminatedUnion("app", [
DigitalOceanConnectionListItemSchema, DigitalOceanConnectionListItemSchema,
NetlifyConnectionListItemSchema, NetlifyConnectionListItemSchema,
OktaConnectionListItemSchema, OktaConnectionListItemSchema,
AzureADCSConnectionListItemSchema AzureADCSConnectionListItemSchema,
RedisConnectionListItemSchema
]); ]);
export const registerAppConnectionRouter = async (server: FastifyZodProvider) => { export const registerAppConnectionRouter = async (server: FastifyZodProvider) => {
@@ -31,6 +31,7 @@ import { registerNetlifyConnectionRouter } from "./netlify-connection-router";
import { registerOktaConnectionRouter } from "./okta-connection-router"; import { registerOktaConnectionRouter } from "./okta-connection-router";
import { registerPostgresConnectionRouter } from "./postgres-connection-router"; import { registerPostgresConnectionRouter } from "./postgres-connection-router";
import { registerRailwayConnectionRouter } from "./railway-connection-router"; import { registerRailwayConnectionRouter } from "./railway-connection-router";
import { registerRedisConnectionRouter } from "./redis-connection-router";
import { registerRenderConnectionRouter } from "./render-connection-router"; import { registerRenderConnectionRouter } from "./render-connection-router";
import { registerSupabaseConnectionRouter } from "./supabase-connection-router"; import { registerSupabaseConnectionRouter } from "./supabase-connection-router";
import { registerTeamCityConnectionRouter } from "./teamcity-connection-router"; import { registerTeamCityConnectionRouter } from "./teamcity-connection-router";
@@ -80,5 +81,6 @@ export const APP_CONNECTION_REGISTER_ROUTER_MAP: Record<AppConnection, (server:
[AppConnection.Supabase]: registerSupabaseConnectionRouter, [AppConnection.Supabase]: registerSupabaseConnectionRouter,
[AppConnection.DigitalOcean]: registerDigitalOceanConnectionRouter, [AppConnection.DigitalOcean]: registerDigitalOceanConnectionRouter,
[AppConnection.Netlify]: registerNetlifyConnectionRouter, [AppConnection.Netlify]: registerNetlifyConnectionRouter,
[AppConnection.Okta]: registerOktaConnectionRouter [AppConnection.Okta]: registerOktaConnectionRouter,
[AppConnection.Redis]: registerRedisConnectionRouter
}; };
@@ -0,0 +1,18 @@
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
import {
CreateRedisConnectionSchema,
SanitizedRedisConnectionSchema,
UpdateRedisConnectionSchema
} from "@app/services/app-connection/redis";
import { registerAppConnectionEndpoints } from "./app-connection-endpoints";
export const registerRedisConnectionRouter = async (server: FastifyZodProvider) => {
registerAppConnectionEndpoints({
app: AppConnection.Redis,
server,
sanitizedResponseSchema: SanitizedRedisConnectionSchema,
createSchema: CreateRedisConnectionSchema,
updateSchema: UpdateRedisConnectionSchema
});
};
+358 -56
View File
@@ -1,16 +1,16 @@
import { ForbiddenError } from "@casl/ability"; import { ForbiddenError } from "@casl/ability";
import { z } from "zod"; import { z } from "zod";
import { SecretFoldersSchema, SecretImportsSchema, UsersSchema } from "@app/db/schemas"; import { SecretFoldersSchema, SecretImportsSchema, SecretType, UsersSchema } from "@app/db/schemas";
import { RemindersSchema } from "@app/db/schemas/reminders"; import { RemindersSchema } from "@app/db/schemas/reminders";
import { EventType, UserAgentType } from "@app/ee/services/audit-log/audit-log-types"; import { EventType, UserAgentType } from "@app/ee/services/audit-log/audit-log-types";
import { ProjectPermissionSecretActions } from "@app/ee/services/permission/project-permission"; import { ProjectPermissionSecretActions } from "@app/ee/services/permission/project-permission";
import { SecretRotationV2Schema } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-union-schema"; import { SecretRotationV2Schema } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-union-schema";
import { DASHBOARD } from "@app/lib/api-docs"; import { DASHBOARD } from "@app/lib/api-docs";
import { BadRequestError } from "@app/lib/errors"; import { BadRequestError, NotFoundError } from "@app/lib/errors";
import { removeTrailingSlash } from "@app/lib/fn"; import { removeTrailingSlash } from "@app/lib/fn";
import { OrderByDirection } from "@app/lib/types"; import { OrderByDirection } from "@app/lib/types";
import { secretsLimit } from "@app/server/config/rateLimiter"; import { readLimit, secretsLimit } from "@app/server/config/rateLimiter";
import { getTelemetryDistinctId } from "@app/server/lib/telemetry"; import { getTelemetryDistinctId } from "@app/server/lib/telemetry";
import { getUserAgentType } from "@app/server/plugins/audit-log"; import { getUserAgentType } from "@app/server/plugins/audit-log";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
@@ -111,6 +111,7 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
SecretRotationV2Schema, SecretRotationV2Schema,
z.object({ z.object({
secrets: secretRawSchema secrets: secretRawSchema
.omit({ secretValue: true })
.extend({ .extend({
secretValueHidden: z.boolean(), secretValueHidden: z.boolean(),
secretPath: z.string().optional(), secretPath: z.string().optional(),
@@ -124,7 +125,9 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
.array() .array()
.optional(), .optional(),
secrets: secretRawSchema secrets: secretRawSchema
.omit({ secretValue: true })
.extend({ .extend({
isEmpty: z.boolean(),
secretValueHidden: z.boolean(), secretValueHidden: z.boolean(),
secretPath: z.string().optional(), secretPath: z.string().optional(),
secretMetadata: ResourceMetadataSchema.optional(), secretMetadata: ResourceMetadataSchema.optional(),
@@ -219,7 +222,9 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
let imports: Awaited<ReturnType<typeof server.services.secretImport.getImportsMultiEnv>> | undefined; let imports: Awaited<ReturnType<typeof server.services.secretImport.getImportsMultiEnv>> | undefined;
let folders: Awaited<ReturnType<typeof server.services.folder.getFoldersMultiEnv>> | undefined; let folders: Awaited<ReturnType<typeof server.services.folder.getFoldersMultiEnv>> | undefined;
let secrets: Awaited<ReturnType<typeof server.services.secret.getSecretsRawMultiEnv>> | undefined; let secrets:
| (Awaited<ReturnType<typeof server.services.secret.getSecretsRawMultiEnv>>[number] & { isEmpty: boolean })[]
| undefined;
let dynamicSecrets: let dynamicSecrets:
| Awaited<ReturnType<typeof server.services.dynamicSecret.listDynamicSecretsByEnvs>> | Awaited<ReturnType<typeof server.services.dynamicSecret.listDynamicSecretsByEnvs>>
| undefined; | undefined;
@@ -426,43 +431,51 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
}); });
if (remainingLimit > 0 && totalSecretCount > adjustedOffset) { if (remainingLimit > 0 && totalSecretCount > adjustedOffset) {
secrets = await server.services.secret.getSecretsRawMultiEnv({ secrets = (
viewSecretValue: true, await server.services.secret.getSecretsRawMultiEnv({
actorId: req.permission.id, viewSecretValue: true,
actor: req.permission.type, actorId: req.permission.id,
actorOrgId: req.permission.orgId, actor: req.permission.type,
environments, actorOrgId: req.permission.orgId,
actorAuthMethod: req.permission.authMethod, environments,
projectId, actorAuthMethod: req.permission.authMethod,
path: secretPath, projectId,
orderBy, path: secretPath,
orderDirection, orderBy,
search, orderDirection,
limit: remainingLimit, search,
offset: adjustedOffset, limit: remainingLimit,
isInternal: true offset: adjustedOffset,
}); isInternal: true
})
).map((secret) => ({ ...secret, isEmpty: !secret.secretValue }));
} }
} }
if (secrets?.length || secretRotations?.length) { if (secrets?.length || secretRotations?.length) {
for await (const environment of environments) { for await (const environment of environments) {
const secretCountFromEnv = const secretIds = [
(secrets?.filter((secret) => secret.environment === environment).length ?? 0) + ...new Set(
(secretRotations [
?.filter((rotation) => rotation.environment.slug === environment) ...(secrets?.filter((secret) => secret.environment === environment) ?? []),
.flatMap((rotation) => rotation.secrets.filter((secret) => Boolean(secret))).length ?? 0); ...(secretRotations
?.filter((rotation) => rotation.environment.slug === environment)
.flatMap((rotation) => rotation.secrets.filter((secret) => Boolean(secret))) ?? [])
].map((secret) => secret.id)
)
];
if (secretCountFromEnv) { if (secretIds) {
await server.services.auditLog.createAuditLog({ await server.services.auditLog.createAuditLog({
projectId, projectId,
...req.auditLogInfo, ...req.auditLogInfo,
event: { event: {
type: EventType.GET_SECRETS, type: EventType.DASHBOARD_LIST_SECRETS,
metadata: { metadata: {
environment, environment,
secretPath, secretPath,
numberOfSecrets: secretCountFromEnv numberOfSecrets: secretIds.length,
secretIds
} }
} }
}); });
@@ -473,7 +486,7 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
distinctId: getTelemetryDistinctId(req), distinctId: getTelemetryDistinctId(req),
organizationId: req.permission.orgId, organizationId: req.permission.orgId,
properties: { properties: {
numberOfSecrets: secretCountFromEnv, numberOfSecrets: secretIds.length,
projectId, projectId,
environment, environment,
secretPath, secretPath,
@@ -584,7 +597,6 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
.optional(), .optional(),
search: z.string().trim().describe(DASHBOARD.SECRET_DETAILS_LIST.search).optional(), search: z.string().trim().describe(DASHBOARD.SECRET_DETAILS_LIST.search).optional(),
tags: z.string().trim().transform(decodeURIComponent).describe(DASHBOARD.SECRET_DETAILS_LIST.tags).optional(), tags: z.string().trim().transform(decodeURIComponent).describe(DASHBOARD.SECRET_DETAILS_LIST.tags).optional(),
viewSecretValue: booleanSchema.default(true),
includeSecrets: booleanSchema.describe(DASHBOARD.SECRET_DETAILS_LIST.includeSecrets), includeSecrets: booleanSchema.describe(DASHBOARD.SECRET_DETAILS_LIST.includeSecrets),
includeFolders: booleanSchema.describe(DASHBOARD.SECRET_DETAILS_LIST.includeFolders), includeFolders: booleanSchema.describe(DASHBOARD.SECRET_DETAILS_LIST.includeFolders),
includeDynamicSecrets: booleanSchema.describe(DASHBOARD.SECRET_DETAILS_LIST.includeDynamicSecrets), includeDynamicSecrets: booleanSchema.describe(DASHBOARD.SECRET_DETAILS_LIST.includeDynamicSecrets),
@@ -606,7 +618,9 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
SecretRotationV2Schema, SecretRotationV2Schema,
z.object({ z.object({
secrets: secretRawSchema secrets: secretRawSchema
.omit({ secretValue: true })
.extend({ .extend({
isEmpty: z.boolean(),
secretValueHidden: z.boolean(), secretValueHidden: z.boolean(),
secretPath: z.string().optional(), secretPath: z.string().optional(),
secretMetadata: ResourceMetadataSchema.optional(), secretMetadata: ResourceMetadataSchema.optional(),
@@ -619,7 +633,9 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
.array() .array()
.optional(), .optional(),
secrets: secretRawSchema secrets: secretRawSchema
.omit({ secretValue: true })
.extend({ .extend({
isEmpty: z.boolean(),
secretReminderRecipients: z secretReminderRecipients: z
.object({ .object({
user: UsersSchema.pick({ id: true, email: true, username: true }), user: UsersSchema.pick({ id: true, email: true, username: true }),
@@ -715,12 +731,21 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
let folders: Awaited<ReturnType<typeof server.services.folder.getFolders>> | undefined; let folders: Awaited<ReturnType<typeof server.services.folder.getFolders>> | undefined;
let secrets: let secrets:
| (Awaited<ReturnType<typeof server.services.secret.getSecretsRaw>>["secrets"][number] & { | (Awaited<ReturnType<typeof server.services.secret.getSecretsRaw>>["secrets"][number] & {
isEmpty: boolean;
reminder: Awaited<ReturnType<typeof server.services.reminder.getRemindersForDashboard>>[string] | null; reminder: Awaited<ReturnType<typeof server.services.reminder.getRemindersForDashboard>>[string] | null;
})[] })[]
| undefined; | undefined;
let dynamicSecrets: Awaited<ReturnType<typeof server.services.dynamicSecret.listDynamicSecretsByEnv>> | undefined; let dynamicSecrets: Awaited<ReturnType<typeof server.services.dynamicSecret.listDynamicSecretsByEnv>> | undefined;
let secretRotations: let secretRotations:
| Awaited<ReturnType<typeof server.services.secretRotationV2.getDashboardSecretRotations>> | (Awaited<ReturnType<typeof server.services.secretRotationV2.getDashboardSecretRotations>>[number] & {
secrets: (NonNullable<
Awaited<
ReturnType<typeof server.services.secretRotationV2.getDashboardSecretRotations>
>[number]["secrets"][number] & {
isEmpty: boolean;
}
> | null)[];
})[]
| undefined; | undefined;
let totalImportCount: number | undefined; let totalImportCount: number | undefined;
@@ -822,19 +847,31 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
); );
if (remainingLimit > 0 && totalSecretRotationCount > adjustedOffset) { if (remainingLimit > 0 && totalSecretRotationCount > adjustedOffset) {
secretRotations = await server.services.secretRotationV2.getDashboardSecretRotations( secretRotations = (
{ await server.services.secretRotationV2.getDashboardSecretRotations(
projectId, {
search, projectId,
orderBy, search,
orderDirection, orderBy,
environments: [environment], orderDirection,
secretPath, environments: [environment],
limit: remainingLimit, secretPath,
offset: adjustedOffset limit: remainingLimit,
}, offset: adjustedOffset
req.permission },
); req.permission
)
).map((rotation) => ({
...rotation,
secrets: rotation.secrets.map((secret) =>
secret
? {
...secret,
isEmpty: !secret.secretValue
}
: secret
)
}));
await server.services.auditLog.createAuditLog({ await server.services.auditLog.createAuditLog({
projectId, projectId,
@@ -919,7 +956,7 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
await server.services.secret.getSecretsRaw({ await server.services.secret.getSecretsRaw({
actorId: req.permission.id, actorId: req.permission.id,
actor: req.permission.type, actor: req.permission.type,
viewSecretValue: req.query.viewSecretValue, viewSecretValue: true,
throwOnMissingReadValuePermission: false, throwOnMissingReadValuePermission: false,
actorOrgId: req.permission.orgId, actorOrgId: req.permission.orgId,
environment, environment,
@@ -943,6 +980,7 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
secrets = rawSecrets.map((secret) => ({ secrets = rawSecrets.map((secret) => ({
...secret, ...secret,
isEmpty: !secret.secretValue,
reminder: reminders[secret.id] ?? null reminder: reminders[secret.id] ?? null
})); }));
} }
@@ -977,19 +1015,25 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
})); }));
if (secrets?.length || secretRotations?.length) { if (secrets?.length || secretRotations?.length) {
const secretCount = const secretIds = [
(secrets?.length ?? 0) + ...new Set(
(secretRotations?.flatMap((rotation) => rotation.secrets.filter((secret) => Boolean(secret))).length ?? 0); [
...(secrets ?? []),
...(secretRotations?.flatMap((rotation) => rotation.secrets.filter((secret) => Boolean(secret))) ?? [])
].map((secret) => secret.id)
)
];
await server.services.auditLog.createAuditLog({ await server.services.auditLog.createAuditLog({
projectId, projectId,
...req.auditLogInfo, ...req.auditLogInfo,
event: { event: {
type: EventType.GET_SECRETS, type: EventType.DASHBOARD_LIST_SECRETS,
metadata: { metadata: {
environment, environment,
secretPath, secretPath,
numberOfSecrets: secretCount numberOfSecrets: secretIds.length,
secretIds
} }
} }
}); });
@@ -1000,7 +1044,7 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
distinctId: getTelemetryDistinctId(req), distinctId: getTelemetryDistinctId(req),
organizationId: req.permission.orgId, organizationId: req.permission.orgId,
properties: { properties: {
numberOfSecrets: secretCount, numberOfSecrets: secretIds.length,
projectId, projectId,
environment, environment,
secretPath, secretPath,
@@ -1060,6 +1104,7 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
.array() .array()
.optional(), .optional(),
secrets: secretRawSchema secrets: secretRawSchema
.omit({ secretValue: true })
.extend({ .extend({
secretValueHidden: z.boolean(), secretValueHidden: z.boolean(),
secretPath: z.string().optional(), secretPath: z.string().optional(),
@@ -1145,18 +1190,20 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
); );
for await (const environment of environments) { for await (const environment of environments) {
const secretCountForEnv = secrets.filter((secret) => secret.environment === environment).length; const envSecrets = secrets.filter((secret) => secret.environment === environment);
const secretCountForEnv = envSecrets.length;
if (secretCountForEnv) { if (secretCountForEnv) {
await server.services.auditLog.createAuditLog({ await server.services.auditLog.createAuditLog({
projectId, projectId,
...req.auditLogInfo, ...req.auditLogInfo,
event: { event: {
type: EventType.GET_SECRETS, type: EventType.DASHBOARD_LIST_SECRETS,
metadata: { metadata: {
environment, environment,
secretPath, secretPath,
numberOfSecrets: secretCountForEnv numberOfSecrets: secretCountForEnv,
secretIds: envSecrets.map((secret) => secret.id)
} }
} }
}); });
@@ -1259,6 +1306,7 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
}), }),
response: { response: {
200: z.object({ 200: z.object({
// TODO(scott): omit secretValue here, but requires refactor of uploading env/copy from board
secrets: secretRawSchema secrets: secretRawSchema
.extend({ .extend({
secretPath: z.string().optional(), secretPath: z.string().optional(),
@@ -1310,6 +1358,7 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
}), }),
response: { response: {
200: z.object({ 200: z.object({
// TODO(scott): omit secretValue here, but requires refactor of uploading env/copy from board
secrets: secretRawSchema secrets: secretRawSchema
.extend({ .extend({
secretValueHidden: z.boolean(), secretValueHidden: z.boolean(),
@@ -1345,11 +1394,12 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
projectId, projectId,
...req.auditLogInfo, ...req.auditLogInfo,
event: { event: {
type: EventType.GET_SECRETS, type: EventType.DASHBOARD_LIST_SECRETS,
metadata: { metadata: {
environment, environment,
secretPath, secretPath,
numberOfSecrets: secrets.length numberOfSecrets: secrets.length,
secretIds: secrets.map((secret) => secret.id)
} }
} }
}); });
@@ -1373,4 +1423,256 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
return { secrets }; return { secrets };
} }
}); });
server.route({
method: "GET",
url: "/secret-value",
config: {
rateLimit: secretsLimit
},
schema: {
security: [
{
bearerAuth: []
}
],
querystring: z.object({
projectId: z.string().trim(),
environment: z.string().trim(),
secretPath: z.string().trim().default("/").transform(removeTrailingSlash),
secretKey: z.string().trim(),
isOverride: z
.enum(["true", "false"])
.transform((value) => value === "true")
.optional()
}),
response: {
200: z.object({
valueOverride: z.string().optional(),
value: z.string().optional()
})
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
const { secretPath, projectId, environment, secretKey, isOverride } = req.query;
// TODO (scott): just get the secret instead of searching for it in list
const { secrets } = await server.services.secret.getSecretsRaw({
actorId: req.permission.id,
actor: req.permission.type,
viewSecretValue: true,
throwOnMissingReadValuePermission: false,
actorOrgId: req.permission.orgId,
environment,
actorAuthMethod: req.permission.authMethod,
projectId,
path: secretPath,
search: secretKey,
includeTagsInSearch: true,
includeMetadataInSearch: true
});
if (isOverride) {
const personalSecret = secrets.find(
(secret) => secret.type === SecretType.Personal && secret.secretKey === secretKey
);
if (!personalSecret)
throw new BadRequestError({
message: `Could not find personal secret with key "${secretKey}" at secret path "${secretPath}" in environment "${environment}" for project with ID "${projectId}"`
});
if (personalSecret)
return {
valueOverride: personalSecret.secretValue
};
}
const sharedSecret = secrets.find(
(secret) => secret.type === SecretType.Shared && secret.secretKey === secretKey
);
if (!sharedSecret)
throw new BadRequestError({
message: `Could not find secret with key "${secretKey}" at secret path "${secretPath}" in environment "${environment}" for project with ID "${projectId}"`
});
// only audit if not personal
await server.services.auditLog.createAuditLog({
projectId,
...req.auditLogInfo,
event: {
type: EventType.DASHBOARD_GET_SECRET_VALUE,
metadata: {
environment: req.query.environment,
secretPath: req.query.secretPath,
secretKey,
secretId: sharedSecret.id
}
}
});
return { value: sharedSecret.secretValue };
}
});
server.route({
url: "/secret-imports",
method: "GET",
config: {
rateLimit: secretsLimit
},
schema: {
querystring: z.object({
projectId: z.string().trim(),
environment: z.string().trim(),
path: z.string().trim().default("/").transform(removeTrailingSlash)
}),
response: {
200: z.object({
secrets: z
.object({
secretPath: z.string(),
environment: z.string(),
environmentInfo: z.object({
id: z.string(),
name: z.string(),
slug: z.string()
}),
folderId: z.string().optional(),
secrets: secretRawSchema.omit({ secretValue: true }).extend({ isEmpty: z.boolean() }).array()
})
.array()
})
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
const importedSecrets = await server.services.secretImport.getRawSecretsFromImports({
actorId: req.permission.id,
actor: req.permission.type,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
...req.query
});
await server.services.auditLog.createAuditLog({
projectId: req.query.projectId,
...req.auditLogInfo,
event: {
type: EventType.DASHBOARD_LIST_SECRETS,
metadata: {
environment: req.query.environment,
secretPath: req.query.path,
numberOfSecrets: importedSecrets.length,
secretIds: importedSecrets.map((secret) => secret.id)
}
}
});
return {
secrets: importedSecrets.map((importData) => ({
...importData,
secrets: importData.secrets.map((secret) => ({
...secret,
isEmpty: !secret.secretValue
}))
}))
};
}
});
server.route({
method: "GET",
url: "/secret-versions/:secretId",
config: {
rateLimit: readLimit
},
schema: {
params: z.object({
secretId: z.string()
}),
querystring: z.object({
offset: z.coerce.number(),
limit: z.coerce.number()
}),
response: {
200: z.object({
secretVersions: secretRawSchema
.omit({ secretValue: true })
.extend({
secretValueHidden: z.boolean()
})
.array()
})
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
const secretVersions = await server.services.secret.getSecretVersions({
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
limit: req.query.limit,
offset: req.query.offset,
secretId: req.params.secretId
});
return { secretVersions };
}
});
server.route({
method: "GET",
url: "/secret-versions/:secretId/value/:version",
config: {
rateLimit: readLimit
},
schema: {
params: z.object({
secretId: z.string(),
version: z.string()
}),
response: {
200: z.object({
value: z.string()
})
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
const { version, secretId } = req.params;
const [secretVersion] = await server.services.secret.getSecretVersions({
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
secretId,
secretVersions: [version]
});
if (!secretVersion)
throw new NotFoundError({
message: `Could not find secret version "${version}" for secret with ID "${secretId}`
});
await server.services.auditLog.createAuditLog({
projectId: secretVersion.workspace,
...req.auditLogInfo,
event: {
type: EventType.DASHBOARD_GET_SECRET_VERSION_VALUE,
metadata: {
secretId,
version
}
}
});
return { value: secretVersion.secretValue };
}
});
}; };
+12
View File
@@ -48,6 +48,7 @@ import { registerPasswordRouter } from "./password-router";
import { registerPkiAlertRouter } from "./pki-alert-router"; import { registerPkiAlertRouter } from "./pki-alert-router";
import { registerPkiCollectionRouter } from "./pki-collection-router"; import { registerPkiCollectionRouter } from "./pki-collection-router";
import { registerPkiSubscriberRouter } from "./pki-subscriber-router"; import { registerPkiSubscriberRouter } from "./pki-subscriber-router";
import { PKI_SYNC_REGISTER_ROUTER_MAP, registerPkiSyncRouter } from "./pki-sync-routers";
import { registerProjectEnvRouter } from "./project-env-router"; import { registerProjectEnvRouter } from "./project-env-router";
import { registerProjectKeyRouter } from "./project-key-router"; import { registerProjectKeyRouter } from "./project-key-router";
import { registerProjectMembershipRouter } from "./project-membership-router"; import { registerProjectMembershipRouter } from "./project-membership-router";
@@ -57,6 +58,7 @@ import { registerSecretRequestsRouter } from "./secret-requests-router";
import { registerSecretSharingRouter } from "./secret-sharing-router"; import { registerSecretSharingRouter } from "./secret-sharing-router";
import { registerSecretTagRouter } from "./secret-tag-router"; import { registerSecretTagRouter } from "./secret-tag-router";
import { registerSlackRouter } from "./slack-router"; import { registerSlackRouter } from "./slack-router";
import { registerUpgradePathRouter } from "./upgrade-path-router";
import { registerSsoRouter } from "./sso-router"; import { registerSsoRouter } from "./sso-router";
import { registerUserActionRouter } from "./user-action-router"; import { registerUserActionRouter } from "./user-action-router";
import { registerUserEngagementRouter } from "./user-engagement-router"; import { registerUserEngagementRouter } from "./user-engagement-router";
@@ -147,6 +149,15 @@ export const registerV1Routes = async (server: FastifyZodProvider) => {
await pkiRouter.register(registerPkiAlertRouter, { prefix: "/alerts" }); await pkiRouter.register(registerPkiAlertRouter, { prefix: "/alerts" });
await pkiRouter.register(registerPkiCollectionRouter, { prefix: "/collections" }); await pkiRouter.register(registerPkiCollectionRouter, { prefix: "/collections" });
await pkiRouter.register(registerPkiSubscriberRouter, { prefix: "/subscribers" }); await pkiRouter.register(registerPkiSubscriberRouter, { prefix: "/subscribers" });
await pkiRouter.register(
async (pkiSyncRouter) => {
await pkiSyncRouter.register(registerPkiSyncRouter);
for await (const [destination, router] of Object.entries(PKI_SYNC_REGISTER_ROUTER_MAP)) {
await pkiSyncRouter.register(router, { prefix: `/${destination}` });
}
},
{ prefix: "/syncs" }
);
}, },
{ prefix: "/pki" } { prefix: "/pki" }
); );
@@ -207,4 +218,5 @@ export const registerV1Routes = async (server: FastifyZodProvider) => {
); );
await server.register(registerEventRouter, { prefix: "/events" }); await server.register(registerEventRouter, { prefix: "/events" });
await server.register(registerUpgradePathRouter, { prefix: "/upgrade-path" });
}; };
@@ -0,0 +1,22 @@
import {
AZURE_KEY_VAULT_PKI_SYNC_LIST_OPTION,
AzureKeyVaultPkiSyncSchema,
CreateAzureKeyVaultPkiSyncSchema,
UpdateAzureKeyVaultPkiSyncSchema
} from "@app/services/pki-sync/azure-key-vault";
import { PkiSync } from "@app/services/pki-sync/pki-sync-enums";
import { registerSyncPkiEndpoints } from "./pki-sync-endpoints";
export const registerAzureKeyVaultPkiSyncRouter = async (server: FastifyZodProvider) =>
registerSyncPkiEndpoints({
destination: PkiSync.AzureKeyVault,
server,
responseSchema: AzureKeyVaultPkiSyncSchema,
createSchema: CreateAzureKeyVaultPkiSyncSchema,
updateSchema: UpdateAzureKeyVaultPkiSyncSchema,
syncOptions: {
canImportCertificates: AZURE_KEY_VAULT_PKI_SYNC_LIST_OPTION.canImportCertificates,
canRemoveCertificates: AZURE_KEY_VAULT_PKI_SYNC_LIST_OPTION.canRemoveCertificates
}
});
@@ -0,0 +1,9 @@
import { PkiSync } from "@app/services/pki-sync/pki-sync-enums";
import { registerAzureKeyVaultPkiSyncRouter } from "./azure-key-vault-pki-sync-router";
export * from "./pki-sync-router";
export const PKI_SYNC_REGISTER_ROUTER_MAP: Record<PkiSync, (server: FastifyZodProvider) => Promise<void>> = {
[PkiSync.AzureKeyVault]: registerAzureKeyVaultPkiSyncRouter
};
@@ -0,0 +1,341 @@
import { z } from "zod";
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
import { ApiDocsTags } from "@app/lib/api-docs";
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";
import { PkiSync } from "@app/services/pki-sync/pki-sync-enums";
import { PKI_SYNC_NAME_MAP } from "@app/services/pki-sync/pki-sync-maps";
export const registerSyncPkiEndpoints = ({
server,
destination,
createSchema,
updateSchema,
responseSchema,
syncOptions
}: {
destination: PkiSync;
server: FastifyZodProvider;
createSchema: z.ZodType<{
name: string;
projectId: string;
connectionId: string;
destinationConfig: Record<string, unknown>;
syncOptions?: Record<string, unknown>;
description?: string;
isAutoSyncEnabled?: boolean;
subscriberId?: string;
}>;
updateSchema: z.ZodType<{
connectionId?: string;
name?: string;
destinationConfig?: Record<string, unknown>;
syncOptions?: Record<string, unknown>;
description?: string;
isAutoSyncEnabled?: boolean;
subscriberId?: string;
}>;
responseSchema: z.ZodTypeAny;
syncOptions: {
canImportCertificates: boolean;
canRemoveCertificates: boolean;
};
}) => {
const destinationName = PKI_SYNC_NAME_MAP[destination];
server.route({
method: "GET",
url: `/`,
config: {
rateLimit: readLimit
},
schema: {
hide: false,
tags: [ApiDocsTags.PkiSyncs],
description: `List the ${destinationName} PKI Syncs for the specified project.`,
querystring: z.object({
projectId: z.string().trim().min(1, "Project ID required")
}),
response: {
200: z.object({ pkiSyncs: responseSchema.array() })
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const {
query: { projectId }
} = req;
const pkiSyncs = await server.services.pkiSync.listPkiSyncsByProjectId({ projectId }, req.permission);
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
projectId,
event: {
type: EventType.GET_PKI_SYNCS,
metadata: {
projectId
}
}
});
return { pkiSyncs };
}
});
server.route({
method: "GET",
url: "/:pkiSyncId",
config: {
rateLimit: readLimit
},
schema: {
hide: false,
tags: [ApiDocsTags.PkiSyncs],
description: `Get the specified ${destinationName} PKI Sync by ID.`,
params: z.object({
pkiSyncId: z.string()
}),
response: {
200: responseSchema
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const { pkiSyncId } = req.params;
const pkiSync = await server.services.pkiSync.findPkiSyncById({ id: pkiSyncId }, req.permission);
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
projectId: pkiSync.projectId,
event: {
type: EventType.GET_PKI_SYNC,
metadata: {
syncId: pkiSyncId,
destination
}
}
});
return pkiSync;
}
});
server.route({
method: "POST",
url: "/",
config: {
rateLimit: writeLimit
},
schema: {
hide: false,
tags: [ApiDocsTags.PkiSyncs],
description: `Create a ${destinationName} PKI Sync for the specified project.`,
body: createSchema,
response: {
200: responseSchema
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const pkiSync = await server.services.pkiSync.createPkiSync({ ...req.body, destination }, req.permission);
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
projectId: pkiSync.projectId,
event: {
type: EventType.CREATE_PKI_SYNC,
metadata: {
pkiSyncId: pkiSync.id,
name: pkiSync.name,
destination
}
}
});
return pkiSync;
}
});
server.route({
method: "PATCH",
url: "/:pkiSyncId",
config: {
rateLimit: writeLimit
},
schema: {
hide: false,
tags: [ApiDocsTags.PkiSyncs],
description: `Update the specified ${destinationName} PKI Sync.`,
params: z.object({
pkiSyncId: z.string()
}),
body: updateSchema,
response: {
200: responseSchema
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const { pkiSyncId } = req.params;
const pkiSync = await server.services.pkiSync.updatePkiSync({ ...req.body, id: pkiSyncId }, req.permission);
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
projectId: pkiSync.projectId,
event: {
type: EventType.UPDATE_PKI_SYNC,
metadata: {
pkiSyncId,
name: pkiSync.name
}
}
});
return pkiSync;
}
});
server.route({
method: "DELETE",
url: `/:pkiSyncId`,
config: {
rateLimit: writeLimit
},
schema: {
hide: false,
tags: [ApiDocsTags.PkiSyncs],
description: `Delete the specified ${destinationName} PKI Sync.`,
params: z.object({
pkiSyncId: z.string()
}),
response: {
200: responseSchema
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const { pkiSyncId } = req.params;
const pkiSync = await server.services.pkiSync.deletePkiSync({ id: pkiSyncId }, req.permission);
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
projectId: pkiSync.projectId,
event: {
type: EventType.DELETE_PKI_SYNC,
metadata: {
pkiSyncId,
name: pkiSync.name,
destination: pkiSync.destination
}
}
});
return pkiSync;
}
});
server.route({
method: "POST",
url: "/:pkiSyncId/sync",
config: {
rateLimit: writeLimit
},
schema: {
hide: false,
tags: [ApiDocsTags.PkiSyncs],
description: `Trigger a sync for the specified ${destinationName} PKI Sync.`,
params: z.object({
pkiSyncId: z.string()
}),
response: {
200: z.object({ message: z.string() })
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const { pkiSyncId } = req.params;
const result = await server.services.pkiSync.triggerPkiSyncSyncCertificatesById(
{
id: pkiSyncId
},
req.permission
);
return result;
}
});
// Only register import route if the destination supports it
if (syncOptions.canImportCertificates) {
server.route({
method: "POST",
url: "/:pkiSyncId/import",
config: {
rateLimit: writeLimit
},
schema: {
hide: false,
tags: [ApiDocsTags.PkiSyncs],
description: `Import certificates from the specified ${destinationName} PKI Sync destination.`,
params: z.object({
pkiSyncId: z.string()
}),
response: {
200: z.object({ message: z.string() })
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const { pkiSyncId } = req.params;
const result = await server.services.pkiSync.triggerPkiSyncImportCertificatesById(
{
id: pkiSyncId
},
req.permission
);
return result;
}
});
}
server.route({
method: "POST",
url: "/:pkiSyncId/remove-certificates",
config: {
rateLimit: writeLimit
},
schema: {
hide: false,
tags: [ApiDocsTags.PkiSyncs],
description: `Remove certificates from the specified ${destinationName} PKI Sync destination.`,
params: z.object({
pkiSyncId: z.string()
}),
response: {
200: z.object({ message: z.string() })
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const { pkiSyncId } = req.params;
const result = await server.services.pkiSync.triggerPkiSyncRemoveCertificatesById(
{
id: pkiSyncId
},
req.permission
);
return result;
}
});
};
@@ -0,0 +1,182 @@
import { z } from "zod";
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
import { ApiDocsTags } from "@app/lib/api-docs";
import { readLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
import { AuthMode } from "@app/services/auth/auth-type";
import { PkiSync } from "@app/services/pki-sync/pki-sync-enums";
const PkiSyncSchema = z.object({
id: z.string().uuid(),
name: z.string(),
description: z.string().nullable().optional(),
destination: z.nativeEnum(PkiSync),
isAutoSyncEnabled: z.boolean(),
destinationConfig: z.record(z.unknown()),
syncOptions: z.record(z.unknown()),
projectId: z.string().uuid(),
subscriberId: z.string().uuid().nullable().optional(),
connectionId: z.string().uuid(),
createdAt: z.date(),
updatedAt: z.date(),
// Sync status fields
syncStatus: z.string().nullable().optional(),
lastSyncJobId: z.string().nullable().optional(),
lastSyncMessage: z.string().nullable().optional(),
lastSyncedAt: z.date().nullable().optional(),
// Import status fields
importStatus: z.string().nullable().optional(),
lastImportJobId: z.string().nullable().optional(),
lastImportMessage: z.string().nullable().optional(),
lastImportedAt: z.date().nullable().optional(),
// Remove status fields
removeStatus: z.string().nullable().optional(),
lastRemoveJobId: z.string().nullable().optional(),
lastRemoveMessage: z.string().nullable().optional(),
lastRemovedAt: z.date().nullable().optional(),
// App connection info
appConnectionName: z.string(),
appConnectionApp: z.string(),
connection: z.object({
id: z.string(),
name: z.string(),
app: z.string(),
encryptedCredentials: z.unknown().nullable(),
orgId: z.string().uuid(),
projectId: z.string().uuid().nullable().optional(),
method: z.string(),
description: z.string().nullable().optional(),
version: z.number(),
gatewayId: z.string().uuid().nullable().optional(),
createdAt: z.date(),
updatedAt: z.date(),
isPlatformManagedCredentials: z.boolean().nullable().optional()
}),
subscriber: z
.object({
id: z.string(),
name: z.string()
})
.nullable()
.optional()
});
const PkiSyncOptionsSchema = z.object({
name: z.string(),
connection: z.nativeEnum(AppConnection),
destination: z.nativeEnum(PkiSync),
canImportCertificates: z.boolean(),
canRemoveCertificates: z.boolean(),
defaultCertificateNameSchema: z.string().optional(),
forbiddenCharacters: z.string().optional(),
allowedCharacterPattern: z.string().optional(),
maxCertificateNameLength: z.number().optional(),
minCertificateNameLength: z.number().optional()
});
export const registerPkiSyncRouter = async (server: FastifyZodProvider) => {
server.route({
method: "GET",
url: "/options",
config: {
rateLimit: readLimit
},
schema: {
hide: false,
tags: [ApiDocsTags.PkiSyncs],
description: "List the available PKI Sync Options.",
response: {
200: z.object({
pkiSyncOptions: PkiSyncOptionsSchema.array()
})
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: () => {
const pkiSyncOptions = server.services.pkiSync.getPkiSyncOptions();
return { pkiSyncOptions };
}
});
server.route({
method: "GET",
url: "/",
config: {
rateLimit: readLimit
},
schema: {
hide: false,
tags: [ApiDocsTags.PkiSyncs],
description: "List all the PKI Syncs for the specified project.",
querystring: z.object({
projectId: z.string().trim().min(1)
}),
response: {
200: z.object({ pkiSyncs: PkiSyncSchema.array() })
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const {
query: { projectId },
permission
} = req;
const pkiSyncs = await server.services.pkiSync.listPkiSyncsByProjectId({ projectId }, permission);
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
projectId,
event: {
type: EventType.GET_PKI_SYNCS,
metadata: {
projectId
}
}
});
return { pkiSyncs };
}
});
server.route({
method: "GET",
url: "/:pkiSyncId",
config: {
rateLimit: readLimit
},
schema: {
hide: false,
tags: [ApiDocsTags.PkiSyncs],
description: "Get a PKI Sync by ID.",
params: z.object({
pkiSyncId: z.string()
}),
response: {
200: PkiSyncSchema
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const { pkiSyncId } = req.params;
const pkiSync = await server.services.pkiSync.findPkiSyncById({ id: pkiSyncId }, req.permission);
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
projectId: pkiSync.projectId,
event: {
type: EventType.GET_PKI_SYNC,
metadata: {
syncId: pkiSyncId,
destination: pkiSync.destination
}
}
});
return pkiSync;
}
});
};
@@ -316,7 +316,7 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
} }
], ],
params: z.object({ params: z.object({
slug: slugSchema({ max: 36 }).describe("The slug of the project to get.") slug: slugSchema({ max: 64 }).describe("The slug of the project to get.")
}), }),
response: { response: {
200: projectWithEnv 200: projectWithEnv
@@ -0,0 +1,117 @@
import { z } from "zod";
import { BadRequestError } from "@app/lib/errors";
import { logger } from "@app/lib/logger";
import { publicEndpointLimit } from "@app/server/config/rateLimiter";
import { versionSchema } from "@app/services/upgrade-path/upgrade-path-schemas";
export const registerUpgradePathRouter = async (server: FastifyZodProvider) => {
server.route({
method: "GET",
url: "/versions",
config: {
rateLimit: publicEndpointLimit
},
schema: {
response: {
200: z.object({
versions: z.array(
z.object({
tagName: z.string(),
name: z.string(),
publishedAt: z.string(),
prerelease: z.boolean(),
draft: z.boolean()
})
)
})
}
},
handler: async (req) => {
try {
const versions = await req.server.services.upgradePath.getGitHubReleases();
return {
versions
};
} catch (error) {
logger.error(error, "Failed to fetch versions");
if (error instanceof z.ZodError) {
throw new BadRequestError({ message: "Invalid query parameters" });
}
throw new BadRequestError({ message: "Failed to fetch GitHub releases" });
}
}
});
server.route({
method: "POST",
url: "/calculate",
config: {
rateLimit: publicEndpointLimit
},
schema: {
body: z.object({
fromVersion: versionSchema,
toVersion: versionSchema
}),
response: {
200: z.object({
path: z.array(
z.object({
version: z.string(),
name: z.string(),
publishedAt: z.string(),
prerelease: z.boolean()
})
),
breakingChanges: z.array(
z.object({
version: z.string(),
changes: z.array(
z.object({
title: z.string(),
description: z.string(),
action: z.string()
})
)
})
),
features: z.array(
z.object({
version: z.string(),
name: z.string(),
body: z.string(),
publishedAt: z.string()
})
),
hasDbMigration: z.boolean(),
config: z.record(z.unknown())
})
}
},
handler: async (req) => {
try {
const { fromVersion, toVersion } = req.body;
const result = await req.server.services.upgradePath.calculateUpgradePath(fromVersion, toVersion);
logger.info(
{ pathLength: result.path.length, hasBreaking: result.breakingChanges.length > 0 },
"Upgrade path calculated"
);
return result;
} catch (error) {
logger.error(error, "Failed to calculate upgrade path");
if (error instanceof z.ZodError) {
throw new BadRequestError({ message: `Invalid input: ${error.errors.map((e) => e.message).join(", ")}` });
}
if (error instanceof Error) {
throw new BadRequestError({ message: error.message });
}
throw new BadRequestError({ message: "Failed to calculate upgrade path" });
}
}
});
};
+3 -1
View File
@@ -255,7 +255,9 @@ export const registerUserRouter = async (server: FastifyZodProvider) => {
totp: z.string() totp: z.string()
}), }),
response: { response: {
200: z.object({}) 200: z.object({
recoveryCodes: z.string().array()
})
} }
}, },
onRequest: verifyAuth([AuthMode.JWT], { onRequest: verifyAuth([AuthMode.JWT], {
@@ -180,7 +180,7 @@ export const registerDeprecatedProjectRouter = async (server: FastifyZodProvider
} }
], ],
params: z.object({ params: z.object({
slug: slugSchema({ min: 5, max: 36 }).describe("The slug of the project to delete.") slug: slugSchema({ min: 5, max: 64 }).describe("The slug of the project to delete.")
}), }),
response: { response: {
200: SanitizedProjectSchema 200: SanitizedProjectSchema
@@ -233,7 +233,7 @@ export const registerDeprecatedProjectRouter = async (server: FastifyZodProvider
} }
], ],
params: z.object({ params: z.object({
slug: slugSchema({ max: 36 }).describe("The slug of the project to get.") slug: slugSchema({ max: 64 }).describe("The slug of the project to get.")
}), }),
response: { response: {
200: projectWithEnv 200: projectWithEnv
@@ -266,7 +266,7 @@ export const registerDeprecatedProjectRouter = async (server: FastifyZodProvider
}, },
schema: { schema: {
params: z.object({ params: z.object({
slug: slugSchema({ min: 5, max: 36 }).describe("The slug of the project to update.") slug: slugSchema({ min: 5, max: 64 }).describe("The slug of the project to update.")
}), }),
body: z.object({ body: z.object({
name: z.string().trim().optional().describe(PROJECTS.UPDATE.name), name: z.string().trim().optional().describe(PROJECTS.UPDATE.name),
@@ -322,7 +322,7 @@ export const registerDeprecatedProjectRouter = async (server: FastifyZodProvider
hide: false, hide: false,
tags: [ApiDocsTags.PkiCertificateAuthorities], tags: [ApiDocsTags.PkiCertificateAuthorities],
params: z.object({ params: z.object({
slug: slugSchema({ min: 5, max: 36 }).describe(PROJECTS.LIST_CAS.slug) slug: slugSchema({ min: 5, max: 64 }).describe(PROJECTS.LIST_CAS.slug)
}), }),
querystring: z.object({ querystring: z.object({
status: z.enum([CaStatus.ACTIVE, CaStatus.PENDING_CERTIFICATE]).optional().describe(PROJECTS.LIST_CAS.status), status: z.enum([CaStatus.ACTIVE, CaStatus.PENDING_CERTIFICATE]).optional().describe(PROJECTS.LIST_CAS.status),
@@ -365,7 +365,7 @@ export const registerDeprecatedProjectRouter = async (server: FastifyZodProvider
hide: false, hide: false,
tags: [ApiDocsTags.PkiCertificates], tags: [ApiDocsTags.PkiCertificates],
params: z.object({ params: z.object({
slug: slugSchema({ min: 5, max: 36 }).describe(PROJECTS.LIST_CERTIFICATES.slug) slug: slugSchema({ min: 5, max: 64 }).describe(PROJECTS.LIST_CERTIFICATES.slug)
}), }),
querystring: z.object({ querystring: z.object({
friendlyName: z.string().optional().describe(PROJECTS.LIST_CERTIFICATES.friendlyName), friendlyName: z.string().optional().describe(PROJECTS.LIST_CERTIFICATES.friendlyName),
+75 -32
View File
@@ -1,5 +1,7 @@
import { FastifyReply, FastifyRequest } from "fastify";
import { z } from "zod"; import { z } from "zod";
import { TUsers } from "@app/db/schemas";
import { getConfig } from "@app/lib/config/env"; import { getConfig } from "@app/lib/config/env";
import { crypto } from "@app/lib/crypto"; import { crypto } from "@app/lib/crypto";
import { BadRequestError, NotFoundError } from "@app/lib/errors"; import { BadRequestError, NotFoundError } from "@app/lib/errors";
@@ -7,11 +9,54 @@ import { mfaRateLimit } from "@app/server/config/rateLimiter";
import { addAuthOriginDomainCookie } from "@app/server/lib/cookie"; import { addAuthOriginDomainCookie } from "@app/server/lib/cookie";
import { AuthModeMfaJwtTokenPayload, AuthTokenType, MfaMethod } from "@app/services/auth/auth-type"; import { AuthModeMfaJwtTokenPayload, AuthTokenType, MfaMethod } from "@app/services/auth/auth-type";
const handleMfaVerification = async (
req: FastifyRequest & { mfa: { userId: string; orgId?: string; user: TUsers } },
res: FastifyReply,
server: FastifyZodProvider,
mfaToken: string,
mfaMethod: MfaMethod,
isRecoveryCode?: boolean
) => {
const userAgent = req.headers["user-agent"];
const mfaJwtToken = req.headers.authorization?.replace("Bearer ", "");
if (!userAgent) throw new Error("user agent header is required");
if (!mfaJwtToken) throw new Error("authorization header is required");
const appCfg = getConfig();
const { user, token } = await server.services.login.verifyMfaToken({
userAgent,
mfaJwtToken,
ip: req.realIp,
userId: req.mfa.userId,
orgId: req.mfa.orgId,
mfaToken,
mfaMethod,
isRecoveryCode
});
void res.setCookie("jid", token.refresh, {
httpOnly: true,
path: "/",
sameSite: "strict",
secure: appCfg.HTTPS_ENABLED
});
addAuthOriginDomainCookie(res);
return {
...user,
token: token.access,
protectedKey: user.protectedKey || null,
protectedKeyIV: user.protectedKeyIV || null,
protectedKeyTag: user.protectedKeyTag || null
};
};
export const registerMfaRouter = async (server: FastifyZodProvider) => { export const registerMfaRouter = async (server: FastifyZodProvider) => {
const cfg = getConfig(); const cfg = getConfig();
server.decorateRequest("mfa", null); server.decorateRequest("mfa", null);
server.addHook("preParsing", async (req, res) => { server.addHook("preValidation", async (req, res) => {
const authorizationHeader = req.headers.authorization; const authorizationHeader = req.headers.authorization;
if (!authorizationHeader || !authorizationHeader.startsWith("Bearer ")) { if (!authorizationHeader || !authorizationHeader.startsWith("Bearer ")) {
@@ -109,38 +154,36 @@ export const registerMfaRouter = async (server: FastifyZodProvider) => {
} }
}, },
handler: async (req, res) => { handler: async (req, res) => {
const userAgent = req.headers["user-agent"]; return handleMfaVerification(req, res, server, req.body.mfaToken, req.body.mfaMethod);
const mfaJwtToken = req.headers.authorization?.replace("Bearer ", ""); }
if (!userAgent) throw new Error("user agent header is required"); });
if (!mfaJwtToken) throw new Error("authorization header is required");
const appCfg = getConfig();
const { user, token } = await server.services.login.verifyMfaToken({ server.route({
userAgent, url: "/mfa/verify/recovery-code",
mfaJwtToken, method: "POST",
ip: req.realIp, config: {
userId: req.mfa.userId, rateLimit: mfaRateLimit
orgId: req.mfa.orgId, },
mfaToken: req.body.mfaToken, schema: {
mfaMethod: req.body.mfaMethod body: z.object({
}); recoveryCode: z.string().trim().length(8, "Recovery code must be 8 characters")
}),
void res.setCookie("jid", token.refresh, { response: {
httpOnly: true, 200: z.object({
path: "/", encryptionVersion: z.number().default(1).nullable().optional(),
sameSite: "strict", protectedKey: z.string().nullish(),
secure: appCfg.HTTPS_ENABLED protectedKeyIV: z.string().nullish(),
}); protectedKeyTag: z.string().nullish(),
publicKey: z.string().nullish(),
addAuthOriginDomainCookie(res); encryptedPrivateKey: z.string().nullish(),
iv: z.string().nullish(),
return { tag: z.string().nullish(),
...user, token: z.string()
token: token.access, })
protectedKey: user.protectedKey || null, }
protectedKeyIV: user.protectedKeyIV || null, },
protectedKeyTag: user.protectedKeyTag || null handler: async (req, res) => {
}; return handleMfaVerification(req, res, server, req.body.recoveryCode, MfaMethod.TOTP, true);
} }
}); });
}; };
@@ -627,7 +627,8 @@ export const registerDeprecatedSecretRouter = async (server: FastifyZodProvider)
secretId: secret.id, secretId: secret.id,
secretKey: req.params.secretName, secretKey: req.params.secretName,
secretVersion: secret.version, secretVersion: secret.version,
secretMetadata: req.body.secretMetadata secretMetadata: req.body.secretMetadata,
secretTags: secret.tags?.map((tag) => tag.name)
} }
} }
}); });
@@ -780,7 +781,8 @@ export const registerDeprecatedSecretRouter = async (server: FastifyZodProvider)
secretId: secret.id, secretId: secret.id,
secretKey: req.params.secretName, secretKey: req.params.secretName,
secretVersion: secret.version, secretVersion: secret.version,
secretMetadata: req.body.secretMetadata secretMetadata: req.body.secretMetadata,
secretTags: secret.tags?.map((tag) => tag.name)
} }
} }
}); });
@@ -2154,7 +2156,8 @@ export const registerDeprecatedSecretRouter = async (server: FastifyZodProvider)
secretId: secret.id, secretId: secret.id,
secretKey: secret.secretKey, secretKey: secret.secretKey,
secretVersion: secret.version, secretVersion: secret.version,
secretMetadata: secretMetadataMap.get(secret.secretKey) secretMetadata: secretMetadataMap.get(secret.secretKey),
secretTags: secret.tags?.map((tag) => tag.name)
})) }))
} }
} }
@@ -2288,7 +2291,6 @@ export const registerDeprecatedSecretRouter = async (server: FastifyZodProvider)
return { approval: secretOperation.approval }; return { approval: secretOperation.approval };
} }
const { secrets } = secretOperation; const { secrets } = secretOperation;
const secretMetadataMap = new Map( const secretMetadataMap = new Map(
inputSecrets.map(({ secretKey, secretMetadata }) => [secretKey, secretMetadata]) inputSecrets.map(({ secretKey, secretMetadata }) => [secretKey, secretMetadata])
); );
@@ -2308,7 +2310,8 @@ export const registerDeprecatedSecretRouter = async (server: FastifyZodProvider)
secretPath: secret.secretPath, secretPath: secret.secretPath,
secretKey: secret.secretKey, secretKey: secret.secretKey,
secretVersion: secret.version, secretVersion: secret.version,
secretMetadata: secretMetadataMap.get(secret.secretKey) secretMetadata: secretMetadataMap.get(secret.secretKey),
secretTags: secret.tags?.map((tag) => tag.name)
})) }))
} }
} }
@@ -2328,7 +2331,8 @@ export const registerDeprecatedSecretRouter = async (server: FastifyZodProvider)
secretPath: secret.secretPath, secretPath: secret.secretPath,
secretKey: secret.secretKey, secretKey: secret.secretKey,
secretVersion: secret.version, secretVersion: secret.version,
secretMetadata: secretMetadataMap.get(secret.secretKey) secretMetadata: secretMetadataMap.get(secret.secretKey),
secretTags: secret.tags?.map((tag) => tag.name)
})) }))
} }
} }
+25 -6
View File
@@ -478,7 +478,8 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
secretId: secret.id, secretId: secret.id,
secretKey: req.params.secretName, secretKey: req.params.secretName,
secretVersion: secret.version, secretVersion: secret.version,
secretMetadata: req.body.secretMetadata secretMetadata: req.body.secretMetadata,
secretTags: secret.tags?.map((tag) => tag.name)
} }
} }
}); });
@@ -621,7 +622,8 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
secretId: secret.id, secretId: secret.id,
secretKey: req.params.secretName, secretKey: req.params.secretName,
secretVersion: secret.version, secretVersion: secret.version,
secretMetadata: req.body.secretMetadata secretMetadata: req.body.secretMetadata,
secretTags: secret.tags?.map((tag) => tag.name)
} }
} }
}); });
@@ -762,7 +764,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
hide: false, hide: false,
tags: [ApiDocsTags.Secrets], tags: [ApiDocsTags.Secrets],
body: z.object({ body: z.object({
projectSlug: z.string().trim(), projectId: z.string().trim(),
sourceEnvironment: z.string().trim(), sourceEnvironment: z.string().trim(),
sourceSecretPath: z.string().trim().default("/").transform(removeTrailingSlash), sourceSecretPath: z.string().trim().default("/").transform(removeTrailingSlash),
destinationEnvironment: z.string().trim(), destinationEnvironment: z.string().trim(),
@@ -911,7 +913,8 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
secretId: secret.id, secretId: secret.id,
secretKey: secret.secretKey, secretKey: secret.secretKey,
secretVersion: secret.version, secretVersion: secret.version,
secretMetadata: secretMetadataMap.get(secret.secretKey) secretMetadata: secretMetadataMap.get(secret.secretKey),
secretTags: secret.tags?.map((tag) => tag.name)
})) }))
} }
} }
@@ -1063,7 +1066,8 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
secretPath: secret.secretPath, secretPath: secret.secretPath,
secretKey: secret.secretKey, secretKey: secret.secretKey,
secretVersion: secret.version, secretVersion: secret.version,
secretMetadata: secretMetadataMap.get(secret.secretKey) secretMetadata: secretMetadataMap.get(secret.secretKey),
secretTags: secret.tags?.map((tag) => tag.name)
})) }))
} }
} }
@@ -1262,7 +1266,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
handler: async (req) => { handler: async (req) => {
const { secretName } = req.params; const { secretName } = req.params;
const { secretPath, environment, projectId } = req.query; const { secretPath, environment, projectId } = req.query;
const { tree, value } = await server.services.secret.getSecretReferenceTree({ const { tree, value, secret } = await server.services.secret.getSecretReferenceTree({
actorId: req.permission.id, actorId: req.permission.id,
actor: req.permission.type, actor: req.permission.type,
actorAuthMethod: req.permission.authMethod, actorAuthMethod: req.permission.authMethod,
@@ -1273,6 +1277,21 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
environment environment
}); });
await server.services.auditLog.createAuditLog({
projectId,
...req.auditLogInfo,
event: {
type: EventType.GET_SECRET,
metadata: {
environment,
secretPath,
secretId: secret.id,
secretKey: secretName,
secretVersion: secret.version
}
}
});
return { tree, value }; return { tree, value };
} }
}); });
@@ -36,7 +36,8 @@ export enum AppConnection {
Supabase = "supabase", Supabase = "supabase",
DigitalOcean = "digital-ocean", DigitalOcean = "digital-ocean",
Netlify = "netlify", Netlify = "netlify",
Okta = "okta" Okta = "okta",
Redis = "redis"
} }
export enum AWSRegion { export enum AWSRegion {
@@ -111,6 +111,7 @@ import { getNetlifyConnectionListItem, validateNetlifyConnectionCredentials } fr
import { getOktaConnectionListItem, OktaConnectionMethod, validateOktaConnectionCredentials } from "./okta"; import { getOktaConnectionListItem, OktaConnectionMethod, validateOktaConnectionCredentials } from "./okta";
import { getPostgresConnectionListItem, PostgresConnectionMethod } from "./postgres"; import { getPostgresConnectionListItem, PostgresConnectionMethod } from "./postgres";
import { getRailwayConnectionListItem, validateRailwayConnectionCredentials } from "./railway"; import { getRailwayConnectionListItem, validateRailwayConnectionCredentials } from "./railway";
import { getRedisConnectionListItem, RedisConnectionMethod, validateRedisConnectionCredentials } from "./redis";
import { RenderConnectionMethod } from "./render/render-connection-enums"; import { RenderConnectionMethod } from "./render/render-connection-enums";
import { getRenderConnectionListItem, validateRenderConnectionCredentials } from "./render/render-connection-fns"; import { getRenderConnectionListItem, validateRenderConnectionCredentials } from "./render/render-connection-fns";
import { import {
@@ -150,7 +151,12 @@ const SECRET_SCANNING_APP_CONNECTION_MAP = Object.fromEntries(
); );
// scott: ideally this would be derived from a utilized map like the above // scott: ideally this would be derived from a utilized map like the above
const PKI_APP_CONNECTIONS = [AppConnection.AWS, AppConnection.Cloudflare, AppConnection.AzureADCS]; const PKI_APP_CONNECTIONS = [
AppConnection.AWS,
AppConnection.Cloudflare,
AppConnection.AzureADCS,
AppConnection.AzureKeyVault
];
export const listAppConnectionOptions = (projectType?: ProjectType) => { export const listAppConnectionOptions = (projectType?: ProjectType) => {
return [ return [
@@ -191,7 +197,8 @@ export const listAppConnectionOptions = (projectType?: ProjectType) => {
getSupabaseConnectionListItem(), getSupabaseConnectionListItem(),
getDigitalOceanConnectionListItem(), getDigitalOceanConnectionListItem(),
getNetlifyConnectionListItem(), getNetlifyConnectionListItem(),
getOktaConnectionListItem() getOktaConnectionListItem(),
getRedisConnectionListItem()
] ]
.filter((option) => { .filter((option) => {
switch (projectType) { switch (projectType) {
@@ -317,7 +324,8 @@ export const validateAppConnectionCredentials = async (
[AppConnection.Supabase]: validateSupabaseConnectionCredentials as TAppConnectionCredentialsValidator, [AppConnection.Supabase]: validateSupabaseConnectionCredentials as TAppConnectionCredentialsValidator,
[AppConnection.DigitalOcean]: validateDigitalOceanConnectionCredentials as TAppConnectionCredentialsValidator, [AppConnection.DigitalOcean]: validateDigitalOceanConnectionCredentials as TAppConnectionCredentialsValidator,
[AppConnection.Okta]: validateOktaConnectionCredentials as TAppConnectionCredentialsValidator, [AppConnection.Okta]: validateOktaConnectionCredentials as TAppConnectionCredentialsValidator,
[AppConnection.Netlify]: validateNetlifyConnectionCredentials as TAppConnectionCredentialsValidator [AppConnection.Netlify]: validateNetlifyConnectionCredentials as TAppConnectionCredentialsValidator,
[AppConnection.Redis]: validateRedisConnectionCredentials as TAppConnectionCredentialsValidator
}; };
return VALIDATE_APP_CONNECTION_CREDENTIALS_MAP[appConnection.app](appConnection, gatewayService, gatewayV2Service); return VALIDATE_APP_CONNECTION_CREDENTIALS_MAP[appConnection.app](appConnection, gatewayService, gatewayV2Service);
@@ -364,6 +372,7 @@ export const getAppConnectionMethodName = (method: TAppConnection["method"]) =>
case MySqlConnectionMethod.UsernameAndPassword: case MySqlConnectionMethod.UsernameAndPassword:
case OracleDBConnectionMethod.UsernameAndPassword: case OracleDBConnectionMethod.UsernameAndPassword:
case AzureADCSConnectionMethod.UsernamePassword: case AzureADCSConnectionMethod.UsernamePassword:
case RedisConnectionMethod.UsernameAndPassword:
return "Username & Password"; return "Username & Password";
case WindmillConnectionMethod.AccessToken: case WindmillConnectionMethod.AccessToken:
case HCVaultConnectionMethod.AccessToken: case HCVaultConnectionMethod.AccessToken:
@@ -451,7 +460,8 @@ export const TRANSITION_CONNECTION_CREDENTIALS_TO_PLATFORM: Record<
[AppConnection.Supabase]: platformManagedCredentialsNotSupported, [AppConnection.Supabase]: platformManagedCredentialsNotSupported,
[AppConnection.DigitalOcean]: platformManagedCredentialsNotSupported, [AppConnection.DigitalOcean]: platformManagedCredentialsNotSupported,
[AppConnection.Netlify]: platformManagedCredentialsNotSupported, [AppConnection.Netlify]: platformManagedCredentialsNotSupported,
[AppConnection.Okta]: platformManagedCredentialsNotSupported [AppConnection.Okta]: platformManagedCredentialsNotSupported,
[AppConnection.Redis]: platformManagedCredentialsNotSupported
}; };
export const enterpriseAppCheck = async ( export const enterpriseAppCheck = async (
@@ -38,7 +38,8 @@ export const APP_CONNECTION_NAME_MAP: Record<AppConnection, string> = {
[AppConnection.Supabase]: "Supabase", [AppConnection.Supabase]: "Supabase",
[AppConnection.DigitalOcean]: "DigitalOcean App Platform", [AppConnection.DigitalOcean]: "DigitalOcean App Platform",
[AppConnection.Netlify]: "Netlify", [AppConnection.Netlify]: "Netlify",
[AppConnection.Okta]: "Okta" [AppConnection.Okta]: "Okta",
[AppConnection.Redis]: "Redis"
}; };
export const APP_CONNECTION_PLAN_MAP: Record<AppConnection, AppConnectionPlanType> = { export const APP_CONNECTION_PLAN_MAP: Record<AppConnection, AppConnectionPlanType> = {
@@ -79,5 +80,6 @@ export const APP_CONNECTION_PLAN_MAP: Record<AppConnection, AppConnectionPlanTyp
[AppConnection.Supabase]: AppConnectionPlanType.Regular, [AppConnection.Supabase]: AppConnectionPlanType.Regular,
[AppConnection.DigitalOcean]: AppConnectionPlanType.Regular, [AppConnection.DigitalOcean]: AppConnectionPlanType.Regular,
[AppConnection.Netlify]: AppConnectionPlanType.Regular, [AppConnection.Netlify]: AppConnectionPlanType.Regular,
[AppConnection.Okta]: AppConnectionPlanType.Regular [AppConnection.Okta]: AppConnectionPlanType.Regular,
[AppConnection.Redis]: AppConnectionPlanType.Regular
}; };
@@ -99,6 +99,7 @@ import { oktaConnectionService } from "./okta/okta-connection-service";
import { ValidatePostgresConnectionCredentialsSchema } from "./postgres"; import { ValidatePostgresConnectionCredentialsSchema } from "./postgres";
import { ValidateRailwayConnectionCredentialsSchema } from "./railway"; import { ValidateRailwayConnectionCredentialsSchema } from "./railway";
import { railwayConnectionService } from "./railway/railway-connection-service"; import { railwayConnectionService } from "./railway/railway-connection-service";
import { ValidateRedisConnectionCredentialsSchema } from "./redis";
import { ValidateRenderConnectionCredentialsSchema } from "./render/render-connection-schema"; import { ValidateRenderConnectionCredentialsSchema } from "./render/render-connection-schema";
import { renderConnectionService } from "./render/render-connection-service"; import { renderConnectionService } from "./render/render-connection-service";
import { ValidateSupabaseConnectionCredentialsSchema } from "./supabase"; import { ValidateSupabaseConnectionCredentialsSchema } from "./supabase";
@@ -166,7 +167,8 @@ const VALIDATE_APP_CONNECTION_CREDENTIALS_MAP: Record<AppConnection, TValidateAp
[AppConnection.Supabase]: ValidateSupabaseConnectionCredentialsSchema, [AppConnection.Supabase]: ValidateSupabaseConnectionCredentialsSchema,
[AppConnection.DigitalOcean]: ValidateDigitalOceanConnectionCredentialsSchema, [AppConnection.DigitalOcean]: ValidateDigitalOceanConnectionCredentialsSchema,
[AppConnection.Netlify]: ValidateNetlifyConnectionCredentialsSchema, [AppConnection.Netlify]: ValidateNetlifyConnectionCredentialsSchema,
[AppConnection.Okta]: ValidateOktaConnectionCredentialsSchema [AppConnection.Okta]: ValidateOktaConnectionCredentialsSchema,
[AppConnection.Redis]: ValidateRedisConnectionCredentialsSchema
}; };
export const appConnectionServiceFactory = ({ export const appConnectionServiceFactory = ({
@@ -266,9 +268,9 @@ export const appConnectionServiceFactory = ({
const { permission } = await permissionService.getOrgPermission( const { permission } = await permissionService.getOrgPermission(
actor.type, actor.type,
actor.id, actor.id,
actor.orgId, appConnection.orgId,
actor.authMethod, actor.authMethod,
appConnection.orgId actor.orgId
); );
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
@@ -316,9 +318,9 @@ export const appConnectionServiceFactory = ({
const { permission } = await permissionService.getOrgPermission( const { permission } = await permissionService.getOrgPermission(
actor.type, actor.type,
actor.id, actor.id,
actor.orgId, appConnection.orgId,
actor.authMethod, actor.authMethod,
appConnection.orgId actor.orgId
); );
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
@@ -475,9 +477,9 @@ export const appConnectionServiceFactory = ({
const { permission: orgPermission } = await permissionService.getOrgPermission( const { permission: orgPermission } = await permissionService.getOrgPermission(
actor.type, actor.type,
actor.id, actor.id,
actor.orgId, appConnection.orgId,
actor.authMethod, actor.authMethod,
appConnection.orgId actor.orgId
); );
if (appConnection.projectId) { if (appConnection.projectId) {
@@ -633,9 +635,9 @@ export const appConnectionServiceFactory = ({
const { permission } = await permissionService.getOrgPermission( const { permission } = await permissionService.getOrgPermission(
actor.type, actor.type,
actor.id, actor.id,
actor.orgId, appConnection.orgId,
actor.authMethod, actor.authMethod,
appConnection.orgId actor.orgId
); );
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
@@ -803,9 +805,9 @@ export const appConnectionServiceFactory = ({
const { permission } = await permissionService.getOrgPermission( const { permission } = await permissionService.getOrgPermission(
actor.type, actor.type,
actor.id, actor.id,
actor.orgId, appConnection.orgId,
actor.authMethod, actor.authMethod,
appConnection.orgId actor.orgId
); );
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
@@ -179,6 +179,12 @@ import {
TRailwayConnectionInput, TRailwayConnectionInput,
TValidateRailwayConnectionCredentialsSchema TValidateRailwayConnectionCredentialsSchema
} from "./railway"; } from "./railway";
import {
TRedisConnection,
TRedisConnectionConfig,
TRedisConnectionInput,
TValidateRedisConnectionCredentialsSchema
} from "./redis";
import { import {
TRenderConnection, TRenderConnection,
TRenderConnectionConfig, TRenderConnectionConfig,
@@ -261,6 +267,7 @@ export type TAppConnection = { id: string } & (
| TDigitalOceanConnection | TDigitalOceanConnection
| TNetlifyConnection | TNetlifyConnection
| TOktaConnection | TOktaConnection
| TRedisConnection
); );
export type TAppConnectionRaw = NonNullable<Awaited<ReturnType<TAppConnectionDALFactory["findById"]>>>; export type TAppConnectionRaw = NonNullable<Awaited<ReturnType<TAppConnectionDALFactory["findById"]>>>;
@@ -306,6 +313,7 @@ export type TAppConnectionInput = { id: string } & (
| TDigitalOceanConnectionInput | TDigitalOceanConnectionInput
| TNetlifyConnectionInput | TNetlifyConnectionInput
| TOktaConnectionInput | TOktaConnectionInput
| TRedisConnectionInput
); );
export type TSqlConnectionInput = export type TSqlConnectionInput =
@@ -368,7 +376,8 @@ export type TAppConnectionConfig =
| TSupabaseConnectionConfig | TSupabaseConnectionConfig
| TDigitalOceanConnectionConfig | TDigitalOceanConnectionConfig
| TNetlifyConnectionConfig | TNetlifyConnectionConfig
| TOktaConnectionConfig; | TOktaConnectionConfig
| TRedisConnectionConfig;
export type TValidateAppConnectionCredentialsSchema = export type TValidateAppConnectionCredentialsSchema =
| TValidateAwsConnectionCredentialsSchema | TValidateAwsConnectionCredentialsSchema
@@ -408,7 +417,8 @@ export type TValidateAppConnectionCredentialsSchema =
| TValidateSupabaseConnectionCredentialsSchema | TValidateSupabaseConnectionCredentialsSchema
| TValidateDigitalOceanCredentialsSchema | TValidateDigitalOceanCredentialsSchema
| TValidateNetlifyConnectionCredentialsSchema | TValidateNetlifyConnectionCredentialsSchema
| TValidateOktaConnectionCredentialsSchema; | TValidateOktaConnectionCredentialsSchema
| TValidateRedisConnectionCredentialsSchema;
export type TListAwsConnectionKmsKeys = { export type TListAwsConnectionKmsKeys = {
connectionId: string; connectionId: string;
@@ -22,13 +22,13 @@ interface HerokuOAuthTokenResponse {
} }
export const getHerokuConnectionListItem = () => { export const getHerokuConnectionListItem = () => {
const { CLIENT_ID_HEROKU } = getConfig(); const { INF_APP_CONNECTION_HEROKU_OAUTH_CLIENT_ID } = getConfig();
return { return {
name: "Heroku" as const, name: "Heroku" as const,
app: AppConnection.Heroku as const, app: AppConnection.Heroku as const,
methods: Object.values(HerokuConnectionMethod) as [HerokuConnectionMethod.AuthToken, HerokuConnectionMethod.OAuth], methods: Object.values(HerokuConnectionMethod) as [HerokuConnectionMethod.AuthToken, HerokuConnectionMethod.OAuth],
oauthClientId: CLIENT_ID_HEROKU oauthClientId: INF_APP_CONNECTION_HEROKU_OAUTH_CLIENT_ID
}; };
}; };
@@ -40,12 +40,12 @@ export const refreshHerokuToken = async (
appConnectionDAL: Pick<TAppConnectionDALFactory, "updateById">, appConnectionDAL: Pick<TAppConnectionDALFactory, "updateById">,
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey"> kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">
): Promise<string> => { ): Promise<string> => {
const { CLIENT_SECRET_HEROKU } = getConfig(); const { INF_APP_CONNECTION_HEROKU_OAUTH_CLIENT_SECRET } = getConfig();
const payload = { const payload = {
grant_type: "refresh_token", grant_type: "refresh_token",
refresh_token: refreshToken, refresh_token: refreshToken,
client_secret: CLIENT_SECRET_HEROKU client_secret: INF_APP_CONNECTION_HEROKU_OAUTH_CLIENT_SECRET
}; };
const { data } = await request.post<{ access_token: string; expires_in: number }>( const { data } = await request.post<{ access_token: string; expires_in: number }>(
@@ -75,7 +75,7 @@ export const refreshHerokuToken = async (
}; };
export const exchangeHerokuOAuthCode = async (code: string): Promise<HerokuOAuthTokenResponse> => { export const exchangeHerokuOAuthCode = async (code: string): Promise<HerokuOAuthTokenResponse> => {
const { CLIENT_SECRET_HEROKU } = getConfig(); const { INF_APP_CONNECTION_HEROKU_OAUTH_CLIENT_SECRET } = getConfig();
try { try {
const response = await request.post<HerokuOAuthTokenResponse>( const response = await request.post<HerokuOAuthTokenResponse>(
@@ -83,7 +83,7 @@ export const exchangeHerokuOAuthCode = async (code: string): Promise<HerokuOAuth
{ {
grant_type: "authorization_code", grant_type: "authorization_code",
code, code,
client_secret: CLIENT_SECRET_HEROKU client_secret: INF_APP_CONNECTION_HEROKU_OAUTH_CLIENT_SECRET
}, },
{ {
headers: { headers: {
@@ -0,0 +1,4 @@
export * from "./redis-connection-enums";
export * from "./redis-connection-fns";
export * from "./redis-connection-schemas";
export * from "./redis-connection-types";
@@ -0,0 +1,3 @@
export enum RedisConnectionMethod {
UsernameAndPassword = "username-and-password"
}
@@ -0,0 +1,59 @@
import Redis from "ioredis";
import { verifyHostInputValidity } from "@app/ee/services/dynamic-secret/dynamic-secret-fns";
import { BadRequestError } from "@app/lib/errors";
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
import { RedisConnectionMethod } from "./redis-connection-enums";
import { TRedisConnectionConfig } from "./redis-connection-types";
export const getRedisConnectionListItem = () => {
return {
name: "Redis" as const,
app: AppConnection.Redis as const,
methods: Object.values(RedisConnectionMethod) as [RedisConnectionMethod.UsernameAndPassword],
supportsPlatformManagement: false as const
};
};
export const validateRedisConnectionCredentials = async (config: TRedisConnectionConfig) => {
const [hostIp] = await verifyHostInputValidity(config.credentials.host);
let connection: Redis | null = null;
try {
connection = new Redis({
username: config.credentials.username,
host: hostIp,
port: config.credentials.port,
password: config.credentials.password,
...(config.credentials.sslEnabled && {
tls: {
rejectUnauthorized: config.credentials.sslRejectUnauthorized,
ca: config.credentials.sslCertificate
}
})
});
let result: string;
if (config.credentials.password) {
result = await connection.auth(config.credentials.username, config.credentials.password, () => {});
} else {
result = await connection.auth(config.credentials.username, () => {});
}
if (result !== "OK") {
throw new BadRequestError({ message: `Invalid credentials, Redis returned ${result} status` });
}
return config.credentials;
} catch (err) {
if (err instanceof BadRequestError) {
throw err;
}
throw new BadRequestError({
message: `Unable to validate connection: ${(err as Error)?.message || "verify credentials"}`
});
} finally {
if (connection) await connection.quit();
}
};
@@ -0,0 +1,87 @@
import z from "zod";
import { AppConnections } from "@app/lib/api-docs";
import {
BaseAppConnectionSchema,
GenericCreateAppConnectionFieldsSchema,
GenericUpdateAppConnectionFieldsSchema
} from "@app/services/app-connection/app-connection-schemas";
import { AppConnection } from "../app-connection-enums";
import { RedisConnectionMethod } from "./redis-connection-enums";
export const BaseRedisUsernameAndPasswordConnectionSchema = z.object({
host: z.string().toLowerCase().min(1),
port: z.coerce.number(),
username: z.string().min(1),
password: z.string().min(1).optional(),
sslRejectUnauthorized: z.boolean(),
sslEnabled: z.boolean(),
sslCertificate: z
.string()
.trim()
.transform((value) => value || undefined)
.optional()
});
export const RedisConnectionAccessTokenCredentialsSchema = BaseRedisUsernameAndPasswordConnectionSchema;
const BaseRedisConnectionSchema = BaseAppConnectionSchema.extend({ app: z.literal(AppConnection.Redis) });
export const RedisConnectionSchema = BaseRedisConnectionSchema.extend({
method: z.literal(RedisConnectionMethod.UsernameAndPassword),
credentials: RedisConnectionAccessTokenCredentialsSchema
});
export const SanitizedRedisConnectionSchema = z.discriminatedUnion("method", [
BaseRedisConnectionSchema.extend({
method: z.literal(RedisConnectionMethod.UsernameAndPassword),
credentials: RedisConnectionAccessTokenCredentialsSchema.pick({
host: true,
port: true,
username: true,
sslEnabled: true,
sslRejectUnauthorized: true,
sslCertificate: true
})
})
]);
export const ValidateRedisConnectionCredentialsSchema = z.discriminatedUnion("method", [
z.object({
method: z
.literal(RedisConnectionMethod.UsernameAndPassword)
.describe(AppConnections.CREATE(AppConnection.Redis).method),
credentials: RedisConnectionAccessTokenCredentialsSchema.describe(
AppConnections.CREATE(AppConnection.Redis).credentials
)
})
]);
export const CreateRedisConnectionSchema = ValidateRedisConnectionCredentialsSchema.and(
GenericCreateAppConnectionFieldsSchema(AppConnection.Redis, {
supportsPlatformManagedCredentials: false,
supportsGateways: false
})
);
export const UpdateRedisConnectionSchema = z
.object({
credentials: RedisConnectionAccessTokenCredentialsSchema.optional().describe(
AppConnections.UPDATE(AppConnection.Redis).credentials
)
})
.and(
GenericUpdateAppConnectionFieldsSchema(AppConnection.Redis, {
supportsPlatformManagedCredentials: false,
supportsGateways: false
})
);
export const RedisConnectionListItemSchema = z.object({
name: z.literal("Redis"),
app: z.literal(AppConnection.Redis),
methods: z.nativeEnum(RedisConnectionMethod).array(),
supportsPlatformManagement: z.literal(false)
});
@@ -0,0 +1,22 @@
import z from "zod";
import { DiscriminativePick } from "@app/lib/types";
import { AppConnection } from "../app-connection-enums";
import {
CreateRedisConnectionSchema,
RedisConnectionSchema,
ValidateRedisConnectionCredentialsSchema
} from "./redis-connection-schemas";
export type TRedisConnection = z.infer<typeof RedisConnectionSchema>;
export type TRedisConnectionInput = z.infer<typeof CreateRedisConnectionSchema> & {
app: AppConnection.Redis;
};
export type TValidateRedisConnectionCredentialsSchema = typeof ValidateRedisConnectionCredentialsSchema;
export type TRedisConnectionConfig = DiscriminativePick<TRedisConnectionInput, "method" | "app" | "credentials"> & {
orgId: string;
};
@@ -684,7 +684,8 @@ export const authLoginServiceFactory = ({
mfaJwtToken, mfaJwtToken,
ip, ip,
userAgent, userAgent,
orgId orgId,
isRecoveryCode = false
}: TVerifyMfaTokenDTO) => { }: TVerifyMfaTokenDTO) => {
const appCfg = getConfig(); const appCfg = getConfig();
const user = await userDAL.findById(userId); const user = await userDAL.findById(userId);
@@ -698,16 +699,21 @@ export const authLoginServiceFactory = ({
code: mfaToken code: mfaToken
}); });
} else if (mfaMethod === MfaMethod.TOTP) { } else if (mfaMethod === MfaMethod.TOTP) {
if (mfaToken.length === 6) { if (isRecoveryCode) {
await totpService.verifyUserTotp({
userId,
totp: mfaToken
});
} else {
await totpService.verifyWithUserRecoveryCode({ await totpService.verifyWithUserRecoveryCode({
userId, userId,
recoveryCode: mfaToken recoveryCode: mfaToken
}); });
} else {
if (mfaToken.length !== 6) {
throw new BadRequestError({
message: "Please use a valid TOTP code."
});
}
await totpService.verifyUserTotp({
userId,
totp: mfaToken
});
} }
} }
} catch (err) { } catch (err) {
@@ -24,6 +24,7 @@ export type TVerifyMfaTokenDTO = {
ip: string; ip: string;
userAgent: string; userAgent: string;
orgId?: string; orgId?: string;
isRecoveryCode?: boolean;
}; };
export type TOauthLoginDTO = { export type TOauthLoginDTO = {
@@ -6,6 +6,7 @@ import { OrgServiceActor } from "@app/lib/types";
import { TAuthTokenServiceFactory } from "../auth-token/auth-token-service"; import { TAuthTokenServiceFactory } from "../auth-token/auth-token-service";
import { TokenType } from "../auth-token/auth-token-types"; import { TokenType } from "../auth-token/auth-token-types";
import { TOrgMembershipDALFactory } from "../org-membership/org-membership-dal";
import { SmtpTemplates, TSmtpService } from "../smtp/smtp-service"; import { SmtpTemplates, TSmtpService } from "../smtp/smtp-service";
import { TTotpConfigDALFactory } from "../totp/totp-config-dal"; import { TTotpConfigDALFactory } from "../totp/totp-config-dal";
import { TUserDALFactory } from "../user/user-dal"; import { TUserDALFactory } from "../user/user-dal";
@@ -22,6 +23,7 @@ import { ActorType, AuthMethod, AuthTokenType } from "./auth-type";
type TAuthPasswordServiceFactoryDep = { type TAuthPasswordServiceFactoryDep = {
authDAL: TAuthDALFactory; authDAL: TAuthDALFactory;
userDAL: TUserDALFactory; userDAL: TUserDALFactory;
orgMembershipDAL: Pick<TOrgMembershipDALFactory, "find">;
tokenService: TAuthTokenServiceFactory; tokenService: TAuthTokenServiceFactory;
smtpService: TSmtpService; smtpService: TSmtpService;
totpConfigDAL: Pick<TTotpConfigDALFactory, "delete">; totpConfigDAL: Pick<TTotpConfigDALFactory, "delete">;
@@ -31,6 +33,7 @@ export type TAuthPasswordFactory = ReturnType<typeof authPaswordServiceFactory>;
export const authPaswordServiceFactory = ({ export const authPaswordServiceFactory = ({
authDAL, authDAL,
userDAL, userDAL,
orgMembershipDAL,
tokenService, tokenService,
smtpService, smtpService,
totpConfigDAL totpConfigDAL
@@ -47,21 +50,46 @@ export const authPaswordServiceFactory = ({
if (user && user.isAccepted) { if (user && user.isAccepted) {
const cfg = getConfig(); const cfg = getConfig();
const token = await tokenService.createTokenForUser({
type: TokenType.TOKEN_EMAIL_PASSWORD_RESET,
userId: user.id
});
await smtpService.sendMail({ const hasEmailAuth = user.authMethods?.includes(AuthMethod.EMAIL);
template: SmtpTemplates.ResetPassword,
recipients: [email], if (!hasEmailAuth) {
subjectLine: "Infisical password reset", const orgMemberships = await orgMembershipDAL.find({ userId: user.id });
substitutions: { const lastLoginMethod =
orgMemberships
.filter((membership) => membership.lastLoginAuthMethod)
.sort((a, b) => (b.updatedAt || new Date(0)).getTime() - (a.updatedAt || new Date(0)).getTime())[0]
?.lastLoginAuthMethod || null;
const substitutions = {
email, email,
token, lastLoginMethod,
callback_url: cfg.SITE_URL ? `${cfg.SITE_URL}/password-reset` : "" isCloud: cfg.isCloud,
} siteUrl: cfg.SITE_URL || ""
}); };
await smtpService.sendMail({
template: SmtpTemplates.OAuthPasswordReset,
recipients: [email],
subjectLine: "Password reset not available",
substitutions
});
} else {
const token = await tokenService.createTokenForUser({
type: TokenType.TOKEN_EMAIL_PASSWORD_RESET,
userId: user.id
});
await smtpService.sendMail({
template: SmtpTemplates.ResetPassword,
recipients: [email],
subjectLine: "Infisical password reset",
substitutions: {
email,
token,
callback_url: cfg.SITE_URL ? `${cfg.SITE_URL}/password-reset` : ""
}
});
}
} }
}; };
@@ -23,6 +23,9 @@ import {
} from "@app/services/certificate/certificate-types"; } from "@app/services/certificate/certificate-types";
import { TKmsServiceFactory } from "@app/services/kms/kms-service"; import { TKmsServiceFactory } from "@app/services/kms/kms-service";
import { TPkiSubscriberDALFactory } from "@app/services/pki-subscriber/pki-subscriber-dal"; import { TPkiSubscriberDALFactory } from "@app/services/pki-subscriber/pki-subscriber-dal";
import { TPkiSyncDALFactory } from "@app/services/pki-sync/pki-sync-dal";
import { TPkiSyncQueueFactory } from "@app/services/pki-sync/pki-sync-queue";
import { triggerAutoSyncForSubscriber } from "@app/services/pki-sync/pki-sync-utils";
import { TProjectDALFactory } from "@app/services/project/project-dal"; import { TProjectDALFactory } from "@app/services/project/project-dal";
import { getProjectKmsCertificateKeyId } from "@app/services/project/project-fns"; import { getProjectKmsCertificateKeyId } from "@app/services/project/project-fns";
@@ -56,6 +59,8 @@ type TAcmeCertificateAuthorityFnsDeps = {
"encryptWithKmsKey" | "generateKmsKey" | "createCipherPairWithDataKey" | "decryptWithKmsKey" "encryptWithKmsKey" | "generateKmsKey" | "createCipherPairWithDataKey" | "decryptWithKmsKey"
>; >;
pkiSubscriberDAL: Pick<TPkiSubscriberDALFactory, "findById">; pkiSubscriberDAL: Pick<TPkiSubscriberDALFactory, "findById">;
pkiSyncDAL: Pick<TPkiSyncDALFactory, "find">;
pkiSyncQueue: Pick<TPkiSyncQueueFactory, "queuePkiSyncSyncCertificatesById">;
projectDAL: Pick<TProjectDALFactory, "findById" | "findOne" | "updateById" | "transaction">; projectDAL: Pick<TProjectDALFactory, "findById" | "findOne" | "updateById" | "transaction">;
}; };
@@ -109,7 +114,9 @@ export const AcmeCertificateAuthorityFns = ({
certificateSecretDAL, certificateSecretDAL,
kmsService, kmsService,
projectDAL, projectDAL,
pkiSubscriberDAL pkiSubscriberDAL,
pkiSyncDAL,
pkiSyncQueue
}: TAcmeCertificateAuthorityFnsDeps) => { }: TAcmeCertificateAuthorityFnsDeps) => {
const createCertificateAuthority = async ({ const createCertificateAuthority = async ({
name, name,
@@ -524,6 +531,8 @@ export const AcmeCertificateAuthorityFns = ({
tx tx
); );
}); });
await triggerAutoSyncForSubscriber(subscriber.id, { pkiSyncDAL, pkiSyncQueue });
}; };
return { return {

Some files were not shown because too many files have changed in this diff Show More