mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-10 17:29:14 +00:00
Merge branch 'main' into feature/slack-secret-sync-error-notification
This commit is contained in:
@@ -135,10 +135,10 @@ jobs:
|
||||
TAG_NAME="${{ github.ref_name }}"
|
||||
echo "Checking for tag: $TAG_NAME"
|
||||
|
||||
EXACT_MATCH=$(gh api repos/Infisical/infisical-omnibus/git/refs/tags/$TAG_NAME | jq -r 'if type == "array" then .[].ref else .ref end' | grep -x "refs/tags/$TAG_NAME")
|
||||
EXACT_MATCH=$(gh api repos/Infisical/infisical-omnibus/git/refs/tags/$TAG_NAME 2>/dev/null | jq -r 'if type == "array" then .[].ref else .ref end' | grep -x "refs/tags/$TAG_NAME" || true)
|
||||
|
||||
if [ "$EXACT_MATCH" == "refs/tags/$TAG_NAME" ]; then
|
||||
echo "Tag $TAG_NAME already exists, skipping..."
|
||||
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')
|
||||
|
||||
@@ -24,6 +24,8 @@ jobs:
|
||||
|
||||
- name: Set up chart-testing
|
||||
uses: helm/[email protected]
|
||||
with:
|
||||
yamale_version: "6.0.0"
|
||||
|
||||
- name: Run chart-testing (lint)
|
||||
run: ct lint --config ct.yaml --charts helm-charts/infisical-gateway
|
||||
|
||||
@@ -27,6 +27,8 @@ jobs:
|
||||
|
||||
- name: Set up chart-testing
|
||||
uses: helm/[email protected]
|
||||
with:
|
||||
yamale_version: "6.0.0"
|
||||
|
||||
- name: Run chart-testing (lint)
|
||||
run: ct lint --config ct.yaml --charts helm-charts/infisical-gateway
|
||||
|
||||
Generated
+1032
-2040
File diff suppressed because it is too large
Load Diff
@@ -40,10 +40,10 @@
|
||||
"type:check": "node --max-old-space-size=8192 ./node_modules/.bin/tsc --noEmit",
|
||||
"lint:fix": "node --max-old-space-size=8192 ./node_modules/.bin/eslint --fix --ext js,ts ./src",
|
||||
"lint": "node --max-old-space-size=8192 ./node_modules/.bin/eslint 'src/**/*.ts'",
|
||||
"test:unit": "vitest run -c vitest.unit.config.ts",
|
||||
"test:e2e": "vitest run -c vitest.e2e.config.ts --bail=1",
|
||||
"test:e2e-watch": "vitest -c vitest.e2e.config.ts --bail=1",
|
||||
"test:e2e-coverage": "vitest run --coverage -c vitest.e2e.config.ts",
|
||||
"test:unit": "vitest run -c vitest.unit.config.mts",
|
||||
"test:e2e": "vitest run -c vitest.e2e.config.mts --bail=1",
|
||||
"test:e2e-watch": "vitest -c vitest.e2e.config.mts --bail=1",
|
||||
"test:e2e-coverage": "vitest run --coverage -c vitest.e2e.config.mts",
|
||||
"generate:component": "tsx ./scripts/create-backend-file.ts",
|
||||
"generate:schema": "tsx ./scripts/generate-schema-types.ts && eslint --fix --ext ts ./src/db/schemas",
|
||||
"auditlog-migration:latest": "node ./dist/db/rename-migrations-to-mjs.mjs && knex --knexfile ./dist/db/auditlog-knexfile.mjs --client pg migrate:latest",
|
||||
@@ -98,7 +98,7 @@
|
||||
"@types/jsrp": "^0.2.6",
|
||||
"@types/libsodium-wrappers": "^0.7.13",
|
||||
"@types/lodash.isequal": "^4.5.8",
|
||||
"@types/node": "^20.17.30",
|
||||
"@types/node": "^20.19.0",
|
||||
"@types/nodemailer": "^6.4.14",
|
||||
"@types/passport-google-oauth20": "^2.0.14",
|
||||
"@types/pg": "^8.10.9",
|
||||
@@ -130,10 +130,10 @@
|
||||
"ts-node": "^10.9.2",
|
||||
"tsc-alias": "^1.8.8",
|
||||
"tsconfig-paths": "^4.2.0",
|
||||
"tsup": "^8.0.1",
|
||||
"tsup": "^8.5.0",
|
||||
"tsx": "^4.4.0",
|
||||
"typescript": "^5.3.2",
|
||||
"vitest": "^1.2.2"
|
||||
"vitest": "^3.0.6"
|
||||
},
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-elasticache": "^3.637.0",
|
||||
|
||||
Vendored
+14
@@ -135,9 +135,23 @@ import { TWorkflowIntegrationServiceFactory } from "@app/services/workflow-integ
|
||||
declare module "@fastify/request-context" {
|
||||
interface RequestContextData {
|
||||
reqId: string;
|
||||
ip?: string;
|
||||
userAgent?: string;
|
||||
orgId?: string;
|
||||
orgName?: string;
|
||||
userAuthInfo?: {
|
||||
userId: string;
|
||||
email: string;
|
||||
};
|
||||
projectDetails?: {
|
||||
id: string;
|
||||
name: string;
|
||||
slug: string;
|
||||
};
|
||||
identityAuthInfo?: {
|
||||
identityId: string;
|
||||
identityName: string;
|
||||
authMethod: string;
|
||||
oidc?: {
|
||||
claims: Record<string, string>;
|
||||
};
|
||||
|
||||
Vendored
+8
@@ -62,6 +62,9 @@ import {
|
||||
TCertificateSecretsUpdate,
|
||||
TCertificatesInsert,
|
||||
TCertificatesUpdate,
|
||||
TCertificateSyncs,
|
||||
TCertificateSyncsInsert,
|
||||
TCertificateSyncsUpdate,
|
||||
TCertificateTemplateEstConfigs,
|
||||
TCertificateTemplateEstConfigsInsert,
|
||||
TCertificateTemplateEstConfigsUpdate,
|
||||
@@ -738,6 +741,11 @@ declare module "knex/types/tables" {
|
||||
TPkiSubscribersUpdate
|
||||
>;
|
||||
[TableName.PkiSync]: KnexOriginal.CompositeTableType<TPkiSyncs, TPkiSyncsInsert, TPkiSyncsUpdate>;
|
||||
[TableName.CertificateSync]: KnexOriginal.CompositeTableType<
|
||||
TCertificateSyncs,
|
||||
TCertificateSyncsInsert,
|
||||
TCertificateSyncsUpdate
|
||||
>;
|
||||
[TableName.UserGroupMembership]: KnexOriginal.CompositeTableType<
|
||||
TUserGroupMembership,
|
||||
TUserGroupMembershipInsert,
|
||||
|
||||
@@ -2,7 +2,7 @@ import { Knex } from "knex";
|
||||
|
||||
import { dropConstraintIfExists } from "@app/db/migrations/utils/dropConstraintIfExists";
|
||||
|
||||
import { AccessScope, TableName } from "../schemas";
|
||||
import { TableName } from "../schemas";
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
const hasParentOrgId = await knex.schema.hasColumn(TableName.Organization, "parentOrgId");
|
||||
@@ -18,8 +18,6 @@ export async function up(knex: Knex): Promise<void> {
|
||||
await dropConstraintIfExists(TableName.Organization, "organizations_slug_unique", knex);
|
||||
t.unique(["rootOrgId", "parentOrgId", "slug"]);
|
||||
});
|
||||
|
||||
// had to switch to raw for null not distinct
|
||||
}
|
||||
|
||||
const hasIdentityOrgCol = await knex.schema.hasColumn(TableName.Identity, "orgId");
|
||||
@@ -28,24 +26,6 @@ export async function up(knex: Knex): Promise<void> {
|
||||
t.uuid("orgId");
|
||||
t.foreign("orgId").references("id").inTable(TableName.Organization).onDelete("CASCADE");
|
||||
});
|
||||
|
||||
await knex.raw(
|
||||
`
|
||||
UPDATE ?? AS identity
|
||||
SET "orgId" = membership."scopeOrgId"
|
||||
FROM ?? AS membership
|
||||
WHERE
|
||||
membership."actorIdentityId" = identity."id"
|
||||
AND membership."scope" = ?
|
||||
`,
|
||||
[TableName.Identity, TableName.Membership, AccessScope.Organization]
|
||||
);
|
||||
|
||||
await knex.raw(`DELETE FROM ?? WHERE "orgId" IS NULL`, [TableName.Identity]);
|
||||
|
||||
await knex.schema.alterTable(TableName.Identity, (t) => {
|
||||
t.uuid("orgId").notNullable().alter();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { chunkArray } from "@app/lib/fn";
|
||||
|
||||
import { AccessScope, TableName } from "../schemas";
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
await knex.transaction(async (tx) => {
|
||||
const hasIdentityOrgCol = await tx.schema.hasColumn(TableName.Identity, "orgId");
|
||||
if (hasIdentityOrgCol) {
|
||||
const identityMemberships = await tx(TableName.Membership)
|
||||
.where({
|
||||
scope: AccessScope.Organization
|
||||
})
|
||||
.whereNotNull("actorIdentityId")
|
||||
.select("actorIdentityId", "scopeOrgId");
|
||||
|
||||
const identityToOrgMapping: Record<string, string> = {};
|
||||
identityMemberships.forEach((el) => {
|
||||
if (el.actorIdentityId) {
|
||||
identityToOrgMapping[el.actorIdentityId] = el.scopeOrgId;
|
||||
}
|
||||
});
|
||||
|
||||
const batchMemberships = chunkArray(identityMemberships, 500);
|
||||
for await (const membership of batchMemberships) {
|
||||
const identityIds = membership.map((el) => el.actorIdentityId).filter(Boolean) as string[];
|
||||
if (identityIds.length) {
|
||||
const identities = await tx(TableName.Identity).whereIn("id", identityIds).select("*");
|
||||
await tx(TableName.Identity)
|
||||
.insert(
|
||||
identities.map((el) => ({
|
||||
...el,
|
||||
orgId: identityToOrgMapping[el.id]
|
||||
}))
|
||||
)
|
||||
.onConflict("id")
|
||||
.merge();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export async function down(): Promise<void> {}
|
||||
|
||||
const config = { transaction: false };
|
||||
export { config };
|
||||
@@ -0,0 +1,51 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TableName } from "../schemas";
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
if (await knex.schema.hasColumn(TableName.PkiApiEnrollmentConfig, "autoRenewDays")) {
|
||||
await knex.schema.alterTable(TableName.PkiApiEnrollmentConfig, (t) => {
|
||||
t.renameColumn("autoRenewDays", "renewBeforeDays");
|
||||
});
|
||||
}
|
||||
|
||||
if (!(await knex.schema.hasColumn(TableName.Certificate, "renewBeforeDays"))) {
|
||||
await knex.schema.alterTable(TableName.Certificate, (t) => {
|
||||
t.integer("renewBeforeDays").nullable();
|
||||
t.uuid("renewedFromCertificateId").nullable();
|
||||
t.uuid("renewedByCertificateId").nullable();
|
||||
t.text("renewalError").nullable();
|
||||
t.string("keyAlgorithm").nullable();
|
||||
t.string("signatureAlgorithm").nullable();
|
||||
t.foreign("renewedFromCertificateId").references("id").inTable(TableName.Certificate).onDelete("SET NULL");
|
||||
t.foreign("renewedByCertificateId").references("id").inTable(TableName.Certificate).onDelete("SET NULL");
|
||||
t.index("renewedFromCertificateId");
|
||||
t.index("renewedByCertificateId");
|
||||
t.index("renewBeforeDays");
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
if (await knex.schema.hasColumn(TableName.Certificate, "renewBeforeDays")) {
|
||||
await knex.schema.alterTable(TableName.Certificate, (t) => {
|
||||
t.dropForeign(["renewedFromCertificateId"]);
|
||||
t.dropForeign(["renewedByCertificateId"]);
|
||||
t.dropIndex("renewedFromCertificateId");
|
||||
t.dropIndex("renewedByCertificateId");
|
||||
t.dropIndex("renewBeforeDays");
|
||||
t.dropColumn("renewBeforeDays");
|
||||
t.dropColumn("renewedFromCertificateId");
|
||||
t.dropColumn("renewedByCertificateId");
|
||||
t.dropColumn("renewalError");
|
||||
t.dropColumn("keyAlgorithm");
|
||||
t.dropColumn("signatureAlgorithm");
|
||||
});
|
||||
}
|
||||
|
||||
if (await knex.schema.hasColumn(TableName.PkiApiEnrollmentConfig, "renewBeforeDays")) {
|
||||
await knex.schema.alterTable(TableName.PkiApiEnrollmentConfig, (t) => {
|
||||
t.renameColumn("renewBeforeDays", "autoRenewDays");
|
||||
});
|
||||
}
|
||||
}
|
||||
+68
@@ -0,0 +1,68 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TableName } from "../schemas";
|
||||
|
||||
// Fix for 20250722152841_add-policies-environments-table.ts migration.
|
||||
// 20250722152841_add-policies-environments-table.ts introduced a bug where you can no longer delete a project if it has any approval policy environments.
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
// Fix SecretApprovalPolicyEnvironment to cascade delete when environment is deleted
|
||||
// note: this won't actually happen, as we prevent deletion of environments with active approval policies
|
||||
|
||||
// in the old migration it was ON DELETE SET NULL, which doesn't work because envId is not a nullable col
|
||||
await knex.schema.alterTable(TableName.SecretApprovalPolicyEnvironment, (t) => {
|
||||
t.dropForeign(["envId"]);
|
||||
t.foreign("envId").references("id").inTable(TableName.Environment).onDelete("CASCADE");
|
||||
});
|
||||
|
||||
// Fix AccessApprovalPolicyEnvironment to cascade delete when environment is deleted
|
||||
// note: this won't actually happen, as we prevent deletion of environments with active approval policies
|
||||
|
||||
// in the old migration it was ON DELETE SET NULL, which doesn't work because envId is not a nullable col
|
||||
await knex.schema.alterTable(TableName.AccessApprovalPolicyEnvironment, (t) => {
|
||||
t.dropForeign(["envId"]);
|
||||
t.foreign("envId").references("id").inTable(TableName.Environment).onDelete("CASCADE");
|
||||
});
|
||||
|
||||
// Fix SecretApprovalPolicy to CASCADE instead of SET NULL
|
||||
|
||||
// in the old migration it was ON DELETE SET NULL, which doesn't work because envId is not a nullable col
|
||||
await knex.schema.alterTable(TableName.SecretApprovalPolicy, (t) => {
|
||||
t.dropForeign(["envId"]);
|
||||
t.foreign("envId").references("id").inTable(TableName.Environment).onDelete("CASCADE");
|
||||
});
|
||||
|
||||
// Fix AccessApprovalPolicy to CASCADE instead of SET NULL
|
||||
|
||||
// in the old migration it was ON DELETE SET NULL, which doesn't work because envId is not a nullable col
|
||||
await knex.schema.alterTable(TableName.AccessApprovalPolicy, (t) => {
|
||||
t.dropForeign(["envId"]);
|
||||
t.foreign("envId").references("id").inTable(TableName.Environment).onDelete("CASCADE");
|
||||
});
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
// Revert SecretApprovalPolicyEnvironment
|
||||
await knex.schema.alterTable(TableName.SecretApprovalPolicyEnvironment, (t) => {
|
||||
t.dropForeign(["envId"]);
|
||||
t.foreign("envId").references("id").inTable(TableName.Environment);
|
||||
});
|
||||
|
||||
// Revert AccessApprovalPolicyEnvironment
|
||||
await knex.schema.alterTable(TableName.AccessApprovalPolicyEnvironment, (t) => {
|
||||
t.dropForeign(["envId"]);
|
||||
t.foreign("envId").references("id").inTable(TableName.Environment);
|
||||
});
|
||||
|
||||
// Revert SecretApprovalPolicy back to SET NULL
|
||||
await knex.schema.alterTable(TableName.SecretApprovalPolicy, (t) => {
|
||||
t.dropForeign(["envId"]);
|
||||
t.foreign("envId").references("id").inTable(TableName.Environment).onDelete("SET NULL");
|
||||
});
|
||||
|
||||
// Revert AccessApprovalPolicy back to SET NULL
|
||||
await knex.schema.alterTable(TableName.AccessApprovalPolicy, (t) => {
|
||||
t.dropForeign(["envId"]);
|
||||
t.foreign("envId").references("id").inTable(TableName.Environment).onDelete("SET NULL");
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TableName } from "../schemas";
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
const hasOrgBlockDuplicateColumn = await knex.schema.hasColumn(
|
||||
TableName.Organization,
|
||||
"blockDuplicateSecretSyncDestinations"
|
||||
);
|
||||
if (!hasOrgBlockDuplicateColumn) {
|
||||
await knex.schema.table(TableName.Organization, (table) => {
|
||||
table.boolean("blockDuplicateSecretSyncDestinations").notNullable().defaultTo(false);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
const hasOrgBlockDuplicateColumn = await knex.schema.hasColumn(
|
||||
TableName.Organization,
|
||||
"blockDuplicateSecretSyncDestinations"
|
||||
);
|
||||
if (hasOrgBlockDuplicateColumn) {
|
||||
await knex.schema.table(TableName.Organization, (table) => {
|
||||
table.dropColumn("blockDuplicateSecretSyncDestinations");
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TableName } from "../schemas";
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
if (!(await knex.schema.hasColumn(TableName.PamAccount, "rotationStatus"))) {
|
||||
await knex.schema.alterTable(TableName.PamAccount, (t) => {
|
||||
t.string("rotationStatus").nullable();
|
||||
});
|
||||
}
|
||||
if (!(await knex.schema.hasColumn(TableName.PamAccount, "encryptedLastRotationMessage"))) {
|
||||
await knex.schema.alterTable(TableName.PamAccount, (t) => {
|
||||
t.binary("encryptedLastRotationMessage").nullable();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
if (await knex.schema.hasColumn(TableName.PamAccount, "rotationStatus")) {
|
||||
await knex.schema.alterTable(TableName.PamAccount, (t) => {
|
||||
t.dropColumn("rotationStatus");
|
||||
});
|
||||
}
|
||||
if (await knex.schema.hasColumn(TableName.PamAccount, "encryptedLastRotationMessage")) {
|
||||
await knex.schema.alterTable(TableName.PamAccount, (t) => {
|
||||
t.dropColumn("encryptedLastRotationMessage");
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TableName } from "@app/db/schemas";
|
||||
import { createOnUpdateTrigger, dropOnUpdateTrigger } from "@app/db/utils";
|
||||
import { CertificateSyncStatus } from "@app/services/certificate-sync/certificate-sync-enums";
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
if (!(await knex.schema.hasTable(TableName.CertificateSync))) {
|
||||
await knex.schema.createTable(TableName.CertificateSync, (t) => {
|
||||
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
|
||||
t.uuid("pkiSyncId").notNullable();
|
||||
t.foreign("pkiSyncId").references("id").inTable(TableName.PkiSync).onDelete("CASCADE");
|
||||
t.uuid("certificateId").notNullable();
|
||||
t.foreign("certificateId").references("id").inTable(TableName.Certificate).onDelete("CASCADE");
|
||||
t.string("syncStatus").defaultTo(CertificateSyncStatus.Pending);
|
||||
t.text("lastSyncMessage");
|
||||
t.datetime("lastSyncedAt");
|
||||
t.timestamps(true, true, true);
|
||||
|
||||
// Ensure unique combination of pki sync and certificate
|
||||
t.unique(["pkiSyncId", "certificateId"]);
|
||||
|
||||
t.index("pkiSyncId");
|
||||
t.index("certificateId");
|
||||
t.index("syncStatus");
|
||||
});
|
||||
|
||||
await createOnUpdateTrigger(knex, TableName.CertificateSync);
|
||||
}
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
await knex.schema.dropTableIfExists(TableName.CertificateSync);
|
||||
await dropOnUpdateTrigger(knex, TableName.CertificateSync);
|
||||
}
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TableName } from "../schemas";
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
await knex.transaction(async (tx) => {
|
||||
await tx.schema.alterTable(TableName.IdentityAccessToken, (table) => {
|
||||
table.dropForeign("identityId");
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
await knex.transaction(async (tx) => {
|
||||
await tx.schema.alterTable(TableName.IdentityAccessToken, (table) => {
|
||||
table.foreign("identityId").references("id").inTable(TableName.Identity);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
const config = { transaction: false };
|
||||
export { config };
|
||||
@@ -0,0 +1,30 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TableName } from "../schemas";
|
||||
|
||||
const MIGRATION_TIMEOUT = 30 * 60 * 1000; // 30 minutes
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
const result = await knex.raw("SHOW statement_timeout");
|
||||
const originalTimeout = result.rows[0].statement_timeout;
|
||||
|
||||
await knex.transaction(async (tx) => {
|
||||
try {
|
||||
await tx.raw(`SET statement_timeout = ${MIGRATION_TIMEOUT}`);
|
||||
const hasIdentityOrgCol = await tx.schema.hasColumn(TableName.Identity, "orgId");
|
||||
if (hasIdentityOrgCol) {
|
||||
await tx(TableName.Identity).whereNull("orgId").delete();
|
||||
await tx.schema.alterTable(TableName.Identity, (t) => {
|
||||
t.uuid("orgId").notNullable().alter();
|
||||
});
|
||||
}
|
||||
} finally {
|
||||
await tx.raw(`SET statement_timeout = '${originalTimeout}'`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export async function down(): Promise<void> {}
|
||||
|
||||
const config = { transaction: false };
|
||||
export { config };
|
||||
@@ -0,0 +1,21 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TableName } from "../schemas";
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
if (!(await knex.schema.hasColumn(TableName.CertificateSync, "externalIdentifier"))) {
|
||||
await knex.schema.alterTable(TableName.CertificateSync, (t) => {
|
||||
t.text("externalIdentifier").nullable();
|
||||
t.index("externalIdentifier");
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
if (await knex.schema.hasColumn(TableName.CertificateSync, "externalIdentifier")) {
|
||||
await knex.schema.alterTable(TableName.CertificateSync, (t) => {
|
||||
t.dropIndex("externalIdentifier");
|
||||
t.dropColumn("externalIdentifier");
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
// 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 CertificateSyncsSchema = z.object({
|
||||
id: z.string().uuid(),
|
||||
pkiSyncId: z.string().uuid(),
|
||||
certificateId: z.string().uuid(),
|
||||
syncStatus: z.string().default("pending").nullable().optional(),
|
||||
lastSyncMessage: z.string().nullable().optional(),
|
||||
lastSyncedAt: z.date().nullable().optional(),
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date(),
|
||||
externalIdentifier: z.string().nullable().optional()
|
||||
});
|
||||
|
||||
export type TCertificateSyncs = z.infer<typeof CertificateSyncsSchema>;
|
||||
export type TCertificateSyncsInsert = Omit<z.input<typeof CertificateSyncsSchema>, TImmutableDBKeys>;
|
||||
export type TCertificateSyncsUpdate = Partial<Omit<z.input<typeof CertificateSyncsSchema>, TImmutableDBKeys>>;
|
||||
@@ -27,7 +27,13 @@ export const CertificatesSchema = z.object({
|
||||
extendedKeyUsages: z.string().array().nullable().optional(),
|
||||
projectId: z.string(),
|
||||
pkiSubscriberId: z.string().uuid().nullable().optional(),
|
||||
profileId: z.string().uuid().nullable().optional()
|
||||
profileId: z.string().uuid().nullable().optional(),
|
||||
renewBeforeDays: z.number().nullable().optional(),
|
||||
renewedFromCertificateId: z.string().uuid().nullable().optional(),
|
||||
renewedByCertificateId: z.string().uuid().nullable().optional(),
|
||||
renewalError: z.string().nullable().optional(),
|
||||
keyAlgorithm: z.string().nullable().optional(),
|
||||
signatureAlgorithm: z.string().nullable().optional()
|
||||
});
|
||||
|
||||
export type TCertificates = z.infer<typeof CertificatesSchema>;
|
||||
|
||||
@@ -17,6 +17,7 @@ export * from "./certificate-authority-crl";
|
||||
export * from "./certificate-authority-secret";
|
||||
export * from "./certificate-bodies";
|
||||
export * from "./certificate-secrets";
|
||||
export * from "./certificate-syncs";
|
||||
export * from "./certificate-template-est-configs";
|
||||
export * from "./certificate-templates";
|
||||
export * from "./certificates";
|
||||
|
||||
@@ -161,6 +161,7 @@ export enum TableName {
|
||||
AppConnection = "app_connections",
|
||||
SecretSync = "secret_syncs",
|
||||
PkiSync = "pki_syncs",
|
||||
CertificateSync = "certificate_syncs",
|
||||
KmipClient = "kmip_clients",
|
||||
KmipOrgConfig = "kmip_org_configs",
|
||||
KmipOrgServerCertificates = "kmip_org_server_certificates",
|
||||
|
||||
@@ -40,7 +40,8 @@ export const OrganizationsSchema = z.object({
|
||||
googleSsoAuthEnforced: z.boolean().default(false),
|
||||
googleSsoAuthLastUsed: z.date().nullable().optional(),
|
||||
parentOrgId: z.string().uuid().nullable().optional(),
|
||||
rootOrgId: z.string().uuid().nullable().optional()
|
||||
rootOrgId: z.string().uuid().nullable().optional(),
|
||||
blockDuplicateSecretSyncDestinations: z.boolean().default(false)
|
||||
});
|
||||
|
||||
export type TOrganizations = z.infer<typeof OrganizationsSchema>;
|
||||
|
||||
@@ -21,7 +21,9 @@ export const PamAccountsSchema = z.object({
|
||||
updatedAt: z.date(),
|
||||
rotationEnabled: z.boolean().default(false),
|
||||
rotationIntervalSeconds: z.number().nullable().optional(),
|
||||
lastRotatedAt: z.date().nullable().optional()
|
||||
lastRotatedAt: z.date().nullable().optional(),
|
||||
rotationStatus: z.string().nullable().optional(),
|
||||
encryptedLastRotationMessage: zodBuffer.nullable().optional()
|
||||
});
|
||||
|
||||
export type TPamAccounts = z.infer<typeof PamAccountsSchema>;
|
||||
|
||||
@@ -10,7 +10,7 @@ import { TImmutableDBKeys } from "./models";
|
||||
export const PkiApiEnrollmentConfigsSchema = z.object({
|
||||
id: z.string().uuid(),
|
||||
autoRenew: z.boolean().default(false).nullable().optional(),
|
||||
autoRenewDays: z.number().nullable().optional(),
|
||||
renewBeforeDays: z.number().nullable().optional(),
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date()
|
||||
});
|
||||
|
||||
@@ -2,7 +2,6 @@ import { z } from "zod";
|
||||
|
||||
import { DynamicSecretLeasesSchema } from "@app/db/schemas";
|
||||
import { ApiDocsTags, DYNAMIC_SECRET_LEASES } from "@app/lib/api-docs";
|
||||
import { daysToMillisecond } from "@app/lib/dates";
|
||||
import { removeTrailingSlash } from "@app/lib/fn";
|
||||
import { ms } from "@app/lib/ms";
|
||||
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||
@@ -32,8 +31,8 @@ export const registerDynamicSecretLeaseRouter = async (server: FastifyZodProvide
|
||||
const valMs = ms(val);
|
||||
if (valMs < 60 * 1000)
|
||||
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be a greater than 1min" });
|
||||
if (valMs > daysToMillisecond(1))
|
||||
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than a day" });
|
||||
if (valMs > ms("10y"))
|
||||
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than 10 years" });
|
||||
}),
|
||||
path: z.string().trim().default("/").transform(removeTrailingSlash).describe(DYNAMIC_SECRET_LEASES.CREATE.path),
|
||||
environmentSlug: z.string().min(1).describe(DYNAMIC_SECRET_LEASES.CREATE.environmentSlug),
|
||||
@@ -127,8 +126,8 @@ export const registerDynamicSecretLeaseRouter = async (server: FastifyZodProvide
|
||||
const valMs = ms(val);
|
||||
if (valMs < 60 * 1000)
|
||||
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be a greater than 1min" });
|
||||
if (valMs > daysToMillisecond(1))
|
||||
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than a day" });
|
||||
if (valMs > ms("10y"))
|
||||
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than 10 years" });
|
||||
}),
|
||||
projectSlug: z.string().min(1).describe(DYNAMIC_SECRET_LEASES.RENEW.projectSlug),
|
||||
path: z
|
||||
|
||||
@@ -2,7 +2,6 @@ import { z } from "zod";
|
||||
|
||||
import { DynamicSecretLeasesSchema } from "@app/db/schemas";
|
||||
import { ApiDocsTags, DYNAMIC_SECRET_LEASES } from "@app/lib/api-docs";
|
||||
import { daysToMillisecond } from "@app/lib/dates";
|
||||
import { removeTrailingSlash } from "@app/lib/fn";
|
||||
import { ms } from "@app/lib/ms";
|
||||
import { writeLimit } from "@app/server/config/rateLimiter";
|
||||
@@ -32,8 +31,8 @@ export const registerKubernetesDynamicSecretLeaseRouter = async (server: Fastify
|
||||
const valMs = ms(val);
|
||||
if (valMs < 60 * 1000)
|
||||
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be greater than 1min" });
|
||||
if (valMs > daysToMillisecond(1))
|
||||
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than a day" });
|
||||
if (valMs > ms("10y"))
|
||||
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than 10 years" });
|
||||
}),
|
||||
path: z.string().trim().default("/").transform(removeTrailingSlash).describe(DYNAMIC_SECRET_LEASES.CREATE.path),
|
||||
environmentSlug: z.string().min(1).describe(DYNAMIC_SECRET_LEASES.CREATE.environmentSlug),
|
||||
|
||||
@@ -3,7 +3,6 @@ import { z } from "zod";
|
||||
import { DynamicSecretLeasesSchema } from "@app/db/schemas";
|
||||
import { DynamicSecretProviderSchema } from "@app/ee/services/dynamic-secret/providers/models";
|
||||
import { ApiDocsTags, DYNAMIC_SECRETS } from "@app/lib/api-docs";
|
||||
import { daysToMillisecond } from "@app/lib/dates";
|
||||
import { removeTrailingSlash } from "@app/lib/fn";
|
||||
import { ms } from "@app/lib/ms";
|
||||
import { isValidHandleBarTemplate } from "@app/lib/template/validate-handlebars";
|
||||
@@ -60,8 +59,8 @@ export const registerDynamicSecretRouter = async (server: FastifyZodProvider) =>
|
||||
const valMs = ms(val);
|
||||
if (valMs < 60 * 1000)
|
||||
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be a greater than 1min" });
|
||||
if (valMs > daysToMillisecond(1))
|
||||
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than a day" });
|
||||
if (valMs > ms("10y"))
|
||||
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than 10 years" });
|
||||
}),
|
||||
maxTTL: z
|
||||
.string()
|
||||
@@ -72,8 +71,8 @@ export const registerDynamicSecretRouter = async (server: FastifyZodProvider) =>
|
||||
const valMs = ms(val);
|
||||
if (valMs < 60 * 1000)
|
||||
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be a greater than 1min" });
|
||||
if (valMs > daysToMillisecond(1))
|
||||
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than a day" });
|
||||
if (valMs > ms("10y"))
|
||||
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than 10 years" });
|
||||
})
|
||||
.nullable(),
|
||||
path: z.string().describe(DYNAMIC_SECRETS.CREATE.path).trim().default("/").transform(removeTrailingSlash),
|
||||
@@ -130,8 +129,8 @@ export const registerDynamicSecretRouter = async (server: FastifyZodProvider) =>
|
||||
const valMs = ms(val);
|
||||
if (valMs < 60 * 1000)
|
||||
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be a greater than 1min" });
|
||||
if (valMs > daysToMillisecond(1))
|
||||
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than a day" });
|
||||
if (valMs > ms("10y"))
|
||||
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than 10 years" });
|
||||
}),
|
||||
maxTTL: z
|
||||
.string()
|
||||
@@ -142,8 +141,8 @@ export const registerDynamicSecretRouter = async (server: FastifyZodProvider) =>
|
||||
const valMs = ms(val);
|
||||
if (valMs < 60 * 1000)
|
||||
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be a greater than 1min" });
|
||||
if (valMs > daysToMillisecond(1))
|
||||
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than a day" });
|
||||
if (valMs > ms("10y"))
|
||||
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than 10 years" });
|
||||
})
|
||||
.nullable(),
|
||||
newName: z.string().describe(DYNAMIC_SECRETS.UPDATE.newName).optional(),
|
||||
|
||||
@@ -182,7 +182,8 @@ export const registerKmipSpecRouter = async (server: FastifyZodProvider) => {
|
||||
algorithm: z.string(),
|
||||
isActive: z.boolean(),
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date()
|
||||
updatedAt: z.date(),
|
||||
kmipMetadata: z.record(z.any()).nullish()
|
||||
})
|
||||
}
|
||||
},
|
||||
@@ -384,7 +385,8 @@ export const registerKmipSpecRouter = async (server: FastifyZodProvider) => {
|
||||
isActive: z.boolean(),
|
||||
algorithm: z.string(),
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date()
|
||||
updatedAt: z.date(),
|
||||
kmipMetadata: z.record(z.any()).nullish()
|
||||
})
|
||||
.array()
|
||||
})
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
import {
|
||||
CreateMySQLAccountSchema,
|
||||
SanitizedMySQLAccountWithResourceSchema,
|
||||
UpdateMySQLAccountSchema
|
||||
} from "@app/ee/services/pam-resource/mysql/mysql-resource-schemas";
|
||||
import { PamResource } from "@app/ee/services/pam-resource/pam-resource-enums";
|
||||
import {
|
||||
CreatePostgresAccountSchema,
|
||||
@@ -16,5 +21,14 @@ export const PAM_ACCOUNT_REGISTER_ROUTER_MAP: Record<PamResource, (server: Fasti
|
||||
createAccountSchema: CreatePostgresAccountSchema,
|
||||
updateAccountSchema: UpdatePostgresAccountSchema
|
||||
});
|
||||
},
|
||||
[PamResource.MySQL]: async (server: FastifyZodProvider) => {
|
||||
registerPamResourceEndpoints({
|
||||
server,
|
||||
resourceType: PamResource.MySQL,
|
||||
accountResponseSchema: SanitizedMySQLAccountWithResourceSchema,
|
||||
createAccountSchema: CreateMySQLAccountSchema,
|
||||
updateAccountSchema: UpdateMySQLAccountSchema
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@@ -2,6 +2,7 @@ import { z } from "zod";
|
||||
|
||||
import { PamFoldersSchema } from "@app/db/schemas";
|
||||
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
||||
import { SanitizedMySQLAccountWithResourceSchema } from "@app/ee/services/pam-resource/mysql/mysql-resource-schemas";
|
||||
import { PamResource } from "@app/ee/services/pam-resource/pam-resource-enums";
|
||||
import { SanitizedPostgresAccountWithResourceSchema } from "@app/ee/services/pam-resource/postgres/postgres-resource-schemas";
|
||||
import { BadRequestError } from "@app/lib/errors";
|
||||
@@ -10,8 +11,10 @@ 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";
|
||||
|
||||
// Use z.union([...]) when more resources are added
|
||||
const SanitizedAccountSchema = SanitizedPostgresAccountWithResourceSchema;
|
||||
const SanitizedAccountSchema = z.union([
|
||||
SanitizedPostgresAccountWithResourceSchema,
|
||||
SanitizedMySQLAccountWithResourceSchema
|
||||
]);
|
||||
|
||||
export const registerPamAccountRouter = async (server: FastifyZodProvider) => {
|
||||
server.route({
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
import {
|
||||
CreateMySQLResourceSchema,
|
||||
MySQLResourceSchema,
|
||||
UpdateMySQLResourceSchema
|
||||
} from "@app/ee/services/pam-resource/mysql/mysql-resource-schemas";
|
||||
import { PamResource } from "@app/ee/services/pam-resource/pam-resource-enums";
|
||||
import {
|
||||
CreatePostgresResourceSchema,
|
||||
@@ -16,5 +21,14 @@ export const PAM_RESOURCE_REGISTER_ROUTER_MAP: Record<PamResource, (server: Fast
|
||||
createResourceSchema: CreatePostgresResourceSchema,
|
||||
updateResourceSchema: UpdatePostgresResourceSchema
|
||||
});
|
||||
},
|
||||
[PamResource.MySQL]: async (server: FastifyZodProvider) => {
|
||||
registerPamResourceEndpoints({
|
||||
server,
|
||||
resourceType: PamResource.MySQL,
|
||||
resourceResponseSchema: MySQLResourceSchema,
|
||||
createResourceSchema: CreateMySQLResourceSchema,
|
||||
updateResourceSchema: UpdateMySQLResourceSchema
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
||||
import {
|
||||
MySQLResourceListItemSchema,
|
||||
SanitizedMySQLResourceSchema
|
||||
} from "@app/ee/services/pam-resource/mysql/mysql-resource-schemas";
|
||||
import {
|
||||
PostgresResourceListItemSchema,
|
||||
SanitizedPostgresResourceSchema
|
||||
@@ -9,10 +13,12 @@ import { readLimit } from "@app/server/config/rateLimiter";
|
||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||
import { AuthMode } from "@app/services/auth/auth-type";
|
||||
|
||||
// Use z.union([...]) when more resources are added
|
||||
const SanitizedResourceSchema = SanitizedPostgresResourceSchema;
|
||||
const SanitizedResourceSchema = z.union([SanitizedPostgresResourceSchema, SanitizedMySQLResourceSchema]);
|
||||
|
||||
const ResourceOptionsSchema = z.discriminatedUnion("resource", [PostgresResourceListItemSchema]);
|
||||
const ResourceOptionsSchema = z.discriminatedUnion("resource", [
|
||||
PostgresResourceListItemSchema,
|
||||
MySQLResourceListItemSchema
|
||||
]);
|
||||
|
||||
export const registerPamResourceRouter = async (server: FastifyZodProvider) => {
|
||||
server.route({
|
||||
|
||||
@@ -2,14 +2,14 @@ import { z } from "zod";
|
||||
|
||||
import { PamSessionsSchema } from "@app/db/schemas";
|
||||
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
||||
import { MySQLSessionCredentialsSchema } from "@app/ee/services/pam-resource/mysql/mysql-resource-schemas";
|
||||
import { PostgresSessionCredentialsSchema } from "@app/ee/services/pam-resource/postgres/postgres-resource-schemas";
|
||||
import { PamSessionCommandLogSchema, SanitizedSessionSchema } from "@app/ee/services/pam-session/pam-session-schemas";
|
||||
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";
|
||||
|
||||
// Use z.union([]) once there's multiple
|
||||
const SessionCredentialsSchema = PostgresSessionCredentialsSchema;
|
||||
const SessionCredentialsSchema = z.union([PostgresSessionCredentialsSchema, MySQLSessionCredentialsSchema]);
|
||||
|
||||
export const registerPamSessionRouter = async (server: FastifyZodProvider) => {
|
||||
// Meant to be hit solely by gateway identities
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
// All the any rules are disabled because passport typesense with fastify is really poor
|
||||
|
||||
import { Authenticator } from "@fastify/passport";
|
||||
import { requestContext } from "@fastify/request-context";
|
||||
import fastifySession from "@fastify/session";
|
||||
import { MultiSamlStrategy } from "@node-saml/passport-saml";
|
||||
import { FastifyRequest } from "fastify";
|
||||
@@ -17,6 +18,7 @@ import { ApiDocsTags, SamlSso } from "@app/lib/api-docs";
|
||||
import { getConfig } from "@app/lib/config/env";
|
||||
import { BadRequestError } from "@app/lib/errors";
|
||||
import { logger } from "@app/lib/logger";
|
||||
import { AuthAttemptAuthMethod, AuthAttemptAuthResult, authAttemptCounter } from "@app/lib/telemetry/metrics";
|
||||
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||
import { SanitizedSamlConfigSchema } from "@app/server/routes/sanitizedSchema/directory-config";
|
||||
@@ -102,15 +104,15 @@ export const registerSamlRouter = async (server: FastifyZodProvider) => {
|
||||
},
|
||||
// eslint-disable-next-line
|
||||
async (req, profile, cb) => {
|
||||
if (!profile) throw new BadRequestError({ message: "Missing profile" });
|
||||
|
||||
const email =
|
||||
profile?.email ??
|
||||
// entra sends data in this format
|
||||
(profile["http://schemas.xmlsoap.org/ws/2005/05/identity/claims/email"] as string) ??
|
||||
(profile?.emailAddress as string); // emailRippling is added because in Rippling the field `email` reserved\
|
||||
|
||||
try {
|
||||
if (!profile) throw new BadRequestError({ message: "Missing profile" });
|
||||
|
||||
const email =
|
||||
profile?.email ??
|
||||
// entra sends data in this format
|
||||
(profile["http://schemas.xmlsoap.org/ws/2005/05/identity/claims/email"] as string) ??
|
||||
(profile?.emailAddress as string); // emailRippling is added because in Rippling the field `email` reserved\
|
||||
|
||||
const firstName = (profile.firstName ??
|
||||
// entra sends data in this format
|
||||
profile["http://schemas.xmlsoap.org/ws/2005/05/identity/claims/firstName"]) as string;
|
||||
@@ -144,7 +146,7 @@ export const registerSamlRouter = async (server: FastifyZodProvider) => {
|
||||
})
|
||||
.filter((el) => el.key && !["email", "firstName", "lastName"].includes(el.key));
|
||||
|
||||
const { isUserCompleted, providerAuthToken } = await server.services.saml.samlLogin({
|
||||
const { isUserCompleted, providerAuthToken, user, organization } = await server.services.saml.samlLogin({
|
||||
externalId: profile.nameID,
|
||||
email: email.toLowerCase(),
|
||||
firstName,
|
||||
@@ -154,8 +156,32 @@ export const registerSamlRouter = async (server: FastifyZodProvider) => {
|
||||
orgId: (req as unknown as FastifyRequest).ssoConfig?.orgId,
|
||||
metadata: userMetadata
|
||||
});
|
||||
|
||||
if (appCfg.OTEL_TELEMETRY_COLLECTION_ENABLED) {
|
||||
authAttemptCounter.add(1, {
|
||||
"infisical.user.email": email.toLowerCase(),
|
||||
"infisical.user.id": user.id,
|
||||
"infisical.organization.id": organization.id,
|
||||
"infisical.organization.name": organization.name,
|
||||
"infisical.auth.method": AuthAttemptAuthMethod.SAML,
|
||||
"infisical.auth.result": AuthAttemptAuthResult.SUCCESS,
|
||||
"client.address": requestContext.get("ip"),
|
||||
"user_agent.original": requestContext.get("userAgent")
|
||||
});
|
||||
}
|
||||
|
||||
cb(null, { isUserCompleted, providerAuthToken });
|
||||
} catch (error) {
|
||||
if (appCfg.OTEL_TELEMETRY_COLLECTION_ENABLED) {
|
||||
authAttemptCounter.add(1, {
|
||||
"infisical.user.email": email.toLowerCase(),
|
||||
"infisical.auth.method": AuthAttemptAuthMethod.SAML,
|
||||
"infisical.auth.result": AuthAttemptAuthResult.FAILURE,
|
||||
"client.address": requestContext.get("ip"),
|
||||
"user_agent.original": requestContext.get("userAgent")
|
||||
});
|
||||
}
|
||||
|
||||
logger.error(error);
|
||||
cb(error as Error);
|
||||
}
|
||||
|
||||
@@ -340,6 +340,8 @@ export enum EventType {
|
||||
ISSUE_PKI_SUBSCRIBER_CERT = "issue-pki-subscriber-cert",
|
||||
SIGN_PKI_SUBSCRIBER_CERT = "sign-pki-subscriber-cert",
|
||||
AUTOMATED_RENEW_SUBSCRIBER_CERT = "automated-renew-subscriber-cert",
|
||||
AUTOMATED_RENEW_CERTIFICATE = "automated-renew-certificate",
|
||||
AUTOMATED_RENEW_CERTIFICATE_FAILED = "automated-renew-certificate-failed",
|
||||
LIST_PKI_SUBSCRIBER_CERTS = "list-pki-subscriber-certs",
|
||||
GET_SUBSCRIBER_ACTIVE_CERT_BUNDLE = "get-subscriber-active-cert-bundle",
|
||||
CREATE_KMS = "create-kms",
|
||||
@@ -367,6 +369,9 @@ export enum EventType {
|
||||
ISSUE_CERTIFICATE_FROM_PROFILE = "issue-certificate-from-profile",
|
||||
SIGN_CERTIFICATE_FROM_PROFILE = "sign-certificate-from-profile",
|
||||
ORDER_CERTIFICATE_FROM_PROFILE = "order-certificate-from-profile",
|
||||
RENEW_CERTIFICATE = "renew-certificate",
|
||||
UPDATE_CERTIFICATE_RENEWAL_CONFIG = "update-certificate-renewal-config",
|
||||
DISABLE_CERTIFICATE_RENEWAL_CONFIG = "disable-certificate-renewal-config",
|
||||
ATTEMPT_CREATE_SLACK_INTEGRATION = "attempt-create-slack-integration",
|
||||
ATTEMPT_REINSTALL_SLACK_INTEGRATION = "attempt-reinstall-slack-integration",
|
||||
GET_PROJECT_SLACK_CONFIG = "get-project-slack-config",
|
||||
@@ -421,6 +426,7 @@ export enum EventType {
|
||||
SECRET_SYNC_REMOVE_SECRETS = "secret-sync-remove-secrets",
|
||||
GET_PKI_SYNCS = "get-pki-syncs",
|
||||
GET_PKI_SYNC = "get-pki-sync",
|
||||
GET_PKI_SYNC_CERTIFICATES = "get-pki-sync-certificates",
|
||||
CREATE_PKI_SYNC = "create-pki-sync",
|
||||
UPDATE_PKI_SYNC = "update-pki-sync",
|
||||
DELETE_PKI_SYNC = "delete-pki-sync",
|
||||
@@ -2458,6 +2464,29 @@ interface AutomatedRenewPkiSubscriberCert {
|
||||
};
|
||||
}
|
||||
|
||||
interface AutomatedRenewCertificate {
|
||||
type: EventType.AUTOMATED_RENEW_CERTIFICATE;
|
||||
metadata: {
|
||||
certificateId: string;
|
||||
commonName: string;
|
||||
profileId: string;
|
||||
renewBeforeDays: string;
|
||||
profileName: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface AutomatedRenewCertificateFailed {
|
||||
type: EventType.AUTOMATED_RENEW_CERTIFICATE_FAILED;
|
||||
metadata: {
|
||||
certificateId: string;
|
||||
commonName: string;
|
||||
profileId: string;
|
||||
renewBeforeDays: string;
|
||||
profileName: string;
|
||||
error: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface SignPkiSubscriberCert {
|
||||
type: EventType.SIGN_PKI_SUBSCRIBER_CERT;
|
||||
metadata: {
|
||||
@@ -2720,6 +2749,16 @@ interface OrderCertificateFromProfile {
|
||||
};
|
||||
}
|
||||
|
||||
interface RenewCertificate {
|
||||
type: EventType.RENEW_CERTIFICATE;
|
||||
metadata: {
|
||||
originalCertificateId: string;
|
||||
newCertificateId: string;
|
||||
profileName: string;
|
||||
commonName: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface AttemptCreateSlackIntegration {
|
||||
type: EventType.ATTEMPT_CREATE_SLACK_INTEGRATION;
|
||||
metadata: {
|
||||
@@ -3123,6 +3162,16 @@ interface GetPkiSyncEvent {
|
||||
};
|
||||
}
|
||||
|
||||
interface GetPkiSyncCertificatesEvent {
|
||||
type: EventType.GET_PKI_SYNC_CERTIFICATES;
|
||||
metadata: {
|
||||
syncId: string;
|
||||
count: number;
|
||||
certificateIds: string[];
|
||||
destination: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface CreatePkiSyncEvent {
|
||||
type: EventType.CREATE_PKI_SYNC;
|
||||
metadata: {
|
||||
@@ -4009,6 +4058,23 @@ interface PamResourceDeleteEvent {
|
||||
};
|
||||
}
|
||||
|
||||
interface UpdateCertificateRenewalConfigEvent {
|
||||
type: EventType.UPDATE_CERTIFICATE_RENEWAL_CONFIG;
|
||||
metadata: {
|
||||
certificateId: string;
|
||||
renewBeforeDays: string;
|
||||
commonName: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface DisableCertificateRenewalConfigEvent {
|
||||
type: EventType.DISABLE_CERTIFICATE_RENEWAL_CONFIG;
|
||||
metadata: {
|
||||
certificateId: string;
|
||||
commonName: string;
|
||||
};
|
||||
}
|
||||
|
||||
export type Event =
|
||||
| CreateSubOrganizationEvent
|
||||
| UpdateSubOrganizationEvent
|
||||
@@ -4216,6 +4282,7 @@ export type Event =
|
||||
| IssueCertificateFromProfile
|
||||
| SignCertificateFromProfile
|
||||
| OrderCertificateFromProfile
|
||||
| RenewCertificate
|
||||
| GetAzureAdCsTemplatesEvent
|
||||
| AttemptCreateSlackIntegration
|
||||
| AttemptReinstallSlackIntegration
|
||||
@@ -4273,6 +4340,7 @@ export type Event =
|
||||
| SecretSyncRemoveSecretsEvent
|
||||
| GetPkiSyncsEvent
|
||||
| GetPkiSyncEvent
|
||||
| GetPkiSyncCertificatesEvent
|
||||
| CreatePkiSyncEvent
|
||||
| UpdatePkiSyncEvent
|
||||
| DeletePkiSyncEvent
|
||||
@@ -4373,4 +4441,8 @@ export type Event =
|
||||
| PamResourceGetEvent
|
||||
| PamResourceCreateEvent
|
||||
| PamResourceUpdateEvent
|
||||
| PamResourceDeleteEvent;
|
||||
| PamResourceDeleteEvent
|
||||
| UpdateCertificateRenewalConfigEvent
|
||||
| DisableCertificateRenewalConfigEvent
|
||||
| AutomatedRenewCertificate
|
||||
| AutomatedRenewCertificateFailed;
|
||||
|
||||
@@ -112,7 +112,7 @@ export const dynamicSecretServiceFactory = ({
|
||||
|
||||
const existingDynamicSecret = await dynamicSecretDAL.findOne({ name, folderId: folder.id });
|
||||
if (existingDynamicSecret)
|
||||
throw new BadRequestError({ message: "Provided dynamic secret already exist under the folder" });
|
||||
throw new BadRequestError({ message: "Provided dynamic secret already exists under the folder" });
|
||||
|
||||
const selectedProvider = dynamicSecretProviders[provider.type];
|
||||
const inputs = await selectedProvider.validateProviderInputs(provider.inputs, { projectId });
|
||||
@@ -265,7 +265,7 @@ export const dynamicSecretServiceFactory = ({
|
||||
if (newName) {
|
||||
const existingDynamicSecret = await dynamicSecretDAL.findOne({ name: newName, folderId: folder.id });
|
||||
if (existingDynamicSecret)
|
||||
throw new BadRequestError({ message: "Provided dynamic secret already exist under the folder" });
|
||||
throw new BadRequestError({ message: "Provided dynamic secret already exists under the folder" });
|
||||
}
|
||||
const { encryptor: secretManagerEncryptor, decryptor: secretManagerDecryptor } =
|
||||
await kmsService.createCipherPairWithDataKey({
|
||||
|
||||
@@ -25,7 +25,7 @@ import { KmsDataKey } from "@app/services/kms/kms-types";
|
||||
import { TNotificationServiceFactory } from "@app/services/notification/notification-service";
|
||||
import { NotificationType } from "@app/services/notification/notification-types";
|
||||
import { TOrgDALFactory } from "@app/services/org/org-dal";
|
||||
import { SmtpTemplates, TSmtpService } from "@app/services/smtp/smtp-service";
|
||||
import { TSmtpService } from "@app/services/smtp/smtp-service";
|
||||
|
||||
import { TLicenseServiceFactory } from "../license/license-service";
|
||||
import { PamResource } from "../pam-resource/pam-resource-enums";
|
||||
@@ -61,8 +61,7 @@ export const gatewayV2ServiceFactory = ({
|
||||
relayDAL,
|
||||
permissionService,
|
||||
orgDAL,
|
||||
notificationService,
|
||||
smtpService
|
||||
notificationService
|
||||
}: TGatewayV2ServiceFactoryDep) => {
|
||||
const $validateIdentityAccessToGateway = async (orgId: string, actorId: string, actorAuthMethod: ActorAuthMethod) => {
|
||||
const orgLicensePlan = await licenseService.getPlan(orgId);
|
||||
@@ -931,15 +930,17 @@ export const gatewayV2ServiceFactory = ({
|
||||
}))
|
||||
);
|
||||
|
||||
await smtpService.sendMail({
|
||||
recipients: admins.map((admin) => admin.user.email).filter((v): v is string => !!v),
|
||||
subjectLine: "Gateway Health Alert",
|
||||
substitutions: {
|
||||
type: "gateway",
|
||||
names: gatewayNames
|
||||
},
|
||||
template: SmtpTemplates.HealthAlert
|
||||
});
|
||||
// Temporarily disabled email notifications due to excessive noise. Will be revised later
|
||||
//
|
||||
// await smtpService.sendMail({
|
||||
// recipients: admins.map((admin) => admin.user.email).filter((v): v is string => !!v),
|
||||
// subjectLine: "Gateway Health Alert",
|
||||
// substitutions: {
|
||||
// type: "gateway",
|
||||
// names: gatewayNames
|
||||
// },
|
||||
// template: SmtpTemplates.HealthAlert
|
||||
// });
|
||||
|
||||
await Promise.all(gateways.map((gw) => gatewayV2DAL.updateById(gw.id, { healthAlertedAt: new Date() })));
|
||||
} catch (error) {
|
||||
|
||||
@@ -341,7 +341,8 @@ export const kmipOperationServiceFactory = ({
|
||||
algorithm: completeKeyDetails.internalKms.encryptionAlgorithm,
|
||||
isActive: !key.isDisabled,
|
||||
createdAt: key.createdAt,
|
||||
updatedAt: key.updatedAt
|
||||
updatedAt: key.updatedAt,
|
||||
kmipMetadata: key.kmipMetadata as Record<string, unknown>
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-call */
|
||||
import { ForbiddenError } from "@casl/ability";
|
||||
import { requestContext } from "@fastify/request-context";
|
||||
import { Issuer, Issuer as OpenIdIssuer, Strategy as OpenIdStrategy, TokenSet } from "openid-client";
|
||||
|
||||
import { AccessScope, OrganizationActionScope, OrgMembershipStatus, TableName, TUsers } from "@app/db/schemas";
|
||||
@@ -15,6 +16,7 @@ import { TPermissionServiceFactory } from "@app/ee/services/permission/permissio
|
||||
import { getConfig } from "@app/lib/config/env";
|
||||
import { crypto } from "@app/lib/crypto";
|
||||
import { BadRequestError, ForbiddenRequestError, NotFoundError, OidcAuthError } from "@app/lib/errors";
|
||||
import { AuthAttemptAuthMethod, AuthAttemptAuthResult, authAttemptCounter } from "@app/lib/telemetry/metrics";
|
||||
import { OrgServiceActor } from "@app/lib/types";
|
||||
import { ActorType, AuthMethod, AuthTokenType } from "@app/services/auth/auth-type";
|
||||
import { TAuthTokenServiceFactory } from "@app/services/auth-token/auth-token-service";
|
||||
@@ -471,7 +473,7 @@ export const oidcConfigServiceFactory = ({
|
||||
});
|
||||
}
|
||||
|
||||
return { isUserCompleted, providerAuthToken };
|
||||
return { isUserCompleted, providerAuthToken, user };
|
||||
};
|
||||
|
||||
const updateOidcCfg = async ({
|
||||
@@ -754,10 +756,35 @@ export const oidcConfigServiceFactory = ({
|
||||
callbackPort,
|
||||
manageGroupMemberships: oidcCfg.manageGroupMemberships
|
||||
})
|
||||
.then(({ isUserCompleted, providerAuthToken }) => {
|
||||
.then(({ isUserCompleted, providerAuthToken, user }) => {
|
||||
if (appCfg.OTEL_TELEMETRY_COLLECTION_ENABLED) {
|
||||
authAttemptCounter.add(1, {
|
||||
"infisical.user.email": claims?.email?.toLowerCase(),
|
||||
"infisical.user.id": user.id,
|
||||
"infisical.organization.id": org.id,
|
||||
"infisical.organization.name": org.name,
|
||||
"infisical.auth.method": AuthAttemptAuthMethod.OIDC,
|
||||
"infisical.auth.result": AuthAttemptAuthResult.SUCCESS,
|
||||
"client.address": requestContext.get("ip"),
|
||||
"user_agent.original": requestContext.get("userAgent")
|
||||
});
|
||||
}
|
||||
|
||||
cb(null, { isUserCompleted, providerAuthToken });
|
||||
})
|
||||
.catch((error) => {
|
||||
if (appCfg.OTEL_TELEMETRY_COLLECTION_ENABLED) {
|
||||
authAttemptCounter.add(1, {
|
||||
"infisical.user.email": claims?.email?.toLowerCase(),
|
||||
"infisical.organization.id": org.id,
|
||||
"infisical.organization.name": org.name,
|
||||
"infisical.auth.method": AuthAttemptAuthMethod.OIDC,
|
||||
"infisical.auth.result": AuthAttemptAuthResult.FAILURE,
|
||||
"client.address": requestContext.get("ip"),
|
||||
"user_agent.original": requestContext.get("userAgent")
|
||||
});
|
||||
}
|
||||
|
||||
cb(error);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -45,17 +45,47 @@ export const decryptAccountCredentials = async ({
|
||||
return JSON.parse(decryptedPlainTextBlob.toString()) as TPamAccountCredentials;
|
||||
};
|
||||
|
||||
export const decryptAccount = async <T extends { encryptedCredentials: Buffer }>(
|
||||
export const decryptAccountMessage = async ({
|
||||
projectId,
|
||||
encryptedMessage,
|
||||
kmsService
|
||||
}: {
|
||||
projectId: string;
|
||||
encryptedMessage: Buffer;
|
||||
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
|
||||
}) => {
|
||||
const { decryptor } = await kmsService.createCipherPairWithDataKey({
|
||||
type: KmsDataKey.SecretManager,
|
||||
projectId
|
||||
});
|
||||
|
||||
const decryptedPlainTextBlob = decryptor({
|
||||
cipherTextBlob: encryptedMessage
|
||||
});
|
||||
|
||||
return decryptedPlainTextBlob.toString();
|
||||
};
|
||||
|
||||
export const decryptAccount = async <
|
||||
T extends { encryptedCredentials: Buffer; encryptedLastRotationMessage?: Buffer | null }
|
||||
>(
|
||||
account: T,
|
||||
projectId: string,
|
||||
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">
|
||||
): Promise<T & { credentials: TPamAccountCredentials }> => {
|
||||
): Promise<T & { credentials: TPamAccountCredentials; lastRotationMessage: string | null }> => {
|
||||
return {
|
||||
...account,
|
||||
credentials: await decryptAccountCredentials({
|
||||
encryptedCredentials: account.encryptedCredentials,
|
||||
projectId,
|
||||
kmsService
|
||||
})
|
||||
} as T & { credentials: TPamAccountCredentials };
|
||||
}),
|
||||
lastRotationMessage: account.encryptedLastRotationMessage
|
||||
? await decryptAccountMessage({
|
||||
encryptedMessage: account.encryptedLastRotationMessage,
|
||||
projectId,
|
||||
kmsService
|
||||
})
|
||||
: null
|
||||
};
|
||||
};
|
||||
|
||||
@@ -15,6 +15,7 @@ import { logger } from "@app/lib/logger";
|
||||
import { OrgServiceActor } from "@app/lib/types";
|
||||
import { ActorType } from "@app/services/auth/auth-type";
|
||||
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
||||
import { KmsDataKey } from "@app/services/kms/kms-types";
|
||||
import { TProjectDALFactory } from "@app/services/project/project-dal";
|
||||
import { TUserDALFactory } from "@app/services/user/user-dal";
|
||||
|
||||
@@ -353,6 +354,7 @@ export const pamAccountServiceFactory = ({
|
||||
TPamAccounts & {
|
||||
resource: Pick<TPamResources, "id" | "name" | "resourceType"> & { rotationCredentialsConfigured: boolean };
|
||||
credentials: TPamAccountCredentials;
|
||||
lastRotationMessage: string | null;
|
||||
}
|
||||
> = [];
|
||||
|
||||
@@ -376,6 +378,7 @@ export const pamAccountServiceFactory = ({
|
||||
) {
|
||||
// Decrypt the account only if the user has permission to read it
|
||||
const decryptedAccount = await decryptAccount(account, account.projectId, kmsService);
|
||||
|
||||
decryptedAndPermittedAccounts.push({
|
||||
...decryptedAccount,
|
||||
resource: {
|
||||
@@ -575,10 +578,10 @@ export const pamAccountServiceFactory = ({
|
||||
for (let i = 0; i < accounts.length; i += ROTATION_CONCURRENCY_LIMIT) {
|
||||
const batch = accounts.slice(i, i + ROTATION_CONCURRENCY_LIMIT);
|
||||
|
||||
const rotationPromises = batch.map(async (account) =>
|
||||
pamAccountDAL.transaction(async (tx) => {
|
||||
let logResourceType = "unknown";
|
||||
try {
|
||||
const rotationPromises = batch.map(async (account) => {
|
||||
let logResourceType = "unknown";
|
||||
try {
|
||||
await pamAccountDAL.transaction(async (tx) => {
|
||||
const resource = await pamResourceDAL.findById(account.resourceId, tx);
|
||||
if (!resource || !resource.encryptedRotationAccountCredentials) return;
|
||||
logResourceType = resource.resourceType;
|
||||
@@ -619,7 +622,9 @@ export const pamAccountServiceFactory = ({
|
||||
account.id,
|
||||
{
|
||||
encryptedCredentials,
|
||||
lastRotatedAt: new Date()
|
||||
lastRotatedAt: new Date(),
|
||||
rotationStatus: "success",
|
||||
encryptedLastRotationMessage: null
|
||||
},
|
||||
tx
|
||||
);
|
||||
@@ -640,32 +645,45 @@ export const pamAccountServiceFactory = ({
|
||||
}
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error(error, `Failed to rotate credentials for account [accountId=${account.id}]`);
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error(error, `Failed to rotate credentials for account [accountId=${account.id}]`);
|
||||
|
||||
const errorMessage = error instanceof Error ? error.message : "An unknown error occurred";
|
||||
const errorMessage = error instanceof Error ? error.message : "An unknown error occurred";
|
||||
|
||||
await auditLogService.createAuditLog({
|
||||
projectId: account.projectId,
|
||||
actor: {
|
||||
type: ActorType.PLATFORM,
|
||||
metadata: {}
|
||||
},
|
||||
event: {
|
||||
type: EventType.PAM_ACCOUNT_CREDENTIAL_ROTATION_FAILED,
|
||||
metadata: {
|
||||
accountId: account.id,
|
||||
accountName: account.name,
|
||||
resourceId: account.resourceId,
|
||||
resourceType: logResourceType,
|
||||
errorMessage
|
||||
}
|
||||
const { encryptor } = await kmsService.createCipherPairWithDataKey({
|
||||
type: KmsDataKey.SecretManager,
|
||||
projectId: account.projectId
|
||||
});
|
||||
|
||||
const { cipherTextBlob: encryptedMessage } = encryptor({
|
||||
plainText: Buffer.from(errorMessage)
|
||||
});
|
||||
|
||||
await pamAccountDAL.updateById(account.id, {
|
||||
rotationStatus: "failed",
|
||||
encryptedLastRotationMessage: encryptedMessage
|
||||
});
|
||||
|
||||
await auditLogService.createAuditLog({
|
||||
projectId: account.projectId,
|
||||
actor: {
|
||||
type: ActorType.PLATFORM,
|
||||
metadata: {}
|
||||
},
|
||||
event: {
|
||||
type: EventType.PAM_ACCOUNT_CREDENTIAL_ROTATION_FAILED,
|
||||
metadata: {
|
||||
accountId: account.id,
|
||||
accountName: account.name,
|
||||
resourceId: account.resourceId,
|
||||
resourceType: logResourceType,
|
||||
errorMessage
|
||||
}
|
||||
});
|
||||
throw error; // Rollback transaction
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
await Promise.all(rotationPromises);
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
import { MySQLResourceListItemSchema } from "./mysql-resource-schemas";
|
||||
|
||||
export const getMySQLResourceListItem = () => {
|
||||
return {
|
||||
name: MySQLResourceListItemSchema.shape.name.value,
|
||||
resource: MySQLResourceListItemSchema.shape.resource.value
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,76 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { PamResource } from "../pam-resource-enums";
|
||||
import {
|
||||
BaseCreatePamAccountSchema,
|
||||
BaseCreatePamResourceSchema,
|
||||
BasePamAccountSchema,
|
||||
BasePamAccountSchemaWithResource,
|
||||
BasePamResourceSchema,
|
||||
BaseUpdatePamAccountSchema,
|
||||
BaseUpdatePamResourceSchema
|
||||
} from "../pam-resource-schemas";
|
||||
import {
|
||||
BaseSqlAccountCredentialsSchema,
|
||||
BaseSqlResourceConnectionDetailsSchema
|
||||
} from "../shared/sql/sql-resource-schemas";
|
||||
|
||||
// Resources
|
||||
export const MySQLResourceConnectionDetailsSchema = BaseSqlResourceConnectionDetailsSchema.extend({
|
||||
// MySQL db in many cases the db will not be provided when making connection
|
||||
database: z.string().trim()
|
||||
});
|
||||
export const MySQLAccountCredentialsSchema = BaseSqlAccountCredentialsSchema;
|
||||
|
||||
const BaseMySQLResourceSchema = BasePamResourceSchema.extend({ resourceType: z.literal(PamResource.MySQL) });
|
||||
|
||||
export const MySQLResourceSchema = BaseMySQLResourceSchema.extend({
|
||||
connectionDetails: MySQLResourceConnectionDetailsSchema,
|
||||
rotationAccountCredentials: MySQLAccountCredentialsSchema.nullable().optional()
|
||||
});
|
||||
|
||||
export const SanitizedMySQLResourceSchema = BaseMySQLResourceSchema.extend({
|
||||
connectionDetails: MySQLResourceConnectionDetailsSchema,
|
||||
rotationAccountCredentials: MySQLAccountCredentialsSchema.pick({
|
||||
username: true
|
||||
})
|
||||
.nullable()
|
||||
.optional()
|
||||
});
|
||||
|
||||
export const MySQLResourceListItemSchema = z.object({
|
||||
name: z.literal("MySQL"),
|
||||
resource: z.literal(PamResource.MySQL)
|
||||
});
|
||||
|
||||
export const CreateMySQLResourceSchema = BaseCreatePamResourceSchema.extend({
|
||||
connectionDetails: MySQLResourceConnectionDetailsSchema,
|
||||
rotationAccountCredentials: MySQLAccountCredentialsSchema.nullable().optional()
|
||||
});
|
||||
|
||||
export const UpdateMySQLResourceSchema = BaseUpdatePamResourceSchema.extend({
|
||||
connectionDetails: MySQLResourceConnectionDetailsSchema.optional(),
|
||||
rotationAccountCredentials: MySQLAccountCredentialsSchema.nullable().optional()
|
||||
});
|
||||
|
||||
// Accounts
|
||||
export const MySQLAccountSchema = BasePamAccountSchema.extend({
|
||||
credentials: MySQLAccountCredentialsSchema
|
||||
});
|
||||
|
||||
export const CreateMySQLAccountSchema = BaseCreatePamAccountSchema.extend({
|
||||
credentials: MySQLAccountCredentialsSchema
|
||||
});
|
||||
|
||||
export const UpdateMySQLAccountSchema = BaseUpdatePamAccountSchema.extend({
|
||||
credentials: MySQLAccountCredentialsSchema.optional()
|
||||
});
|
||||
|
||||
export const SanitizedMySQLAccountWithResourceSchema = BasePamAccountSchemaWithResource.extend({
|
||||
credentials: MySQLAccountCredentialsSchema.pick({
|
||||
username: true
|
||||
})
|
||||
});
|
||||
|
||||
// Sessions
|
||||
export const MySQLSessionCredentialsSchema = MySQLResourceConnectionDetailsSchema.and(MySQLAccountCredentialsSchema);
|
||||
@@ -0,0 +1,16 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import {
|
||||
MySQLAccountCredentialsSchema,
|
||||
MySQLAccountSchema,
|
||||
MySQLResourceConnectionDetailsSchema,
|
||||
MySQLResourceSchema
|
||||
} from "./mysql-resource-schemas";
|
||||
|
||||
// Resources
|
||||
export type TMySQLResource = z.infer<typeof MySQLResourceSchema>;
|
||||
export type TMySQLResourceConnectionDetails = z.infer<typeof MySQLResourceConnectionDetailsSchema>;
|
||||
|
||||
// Accounts
|
||||
export type TMySQLAccount = z.infer<typeof MySQLAccountSchema>;
|
||||
export type TMySQLAccountCredentials = z.infer<typeof MySQLAccountCredentialsSchema>;
|
||||
@@ -1,3 +1,4 @@
|
||||
export enum PamResource {
|
||||
Postgres = "postgres"
|
||||
Postgres = "postgres",
|
||||
MySQL = "mysql"
|
||||
}
|
||||
|
||||
@@ -5,5 +5,6 @@ import { sqlResourceFactory } from "./shared/sql/sql-resource-factory";
|
||||
type TPamResourceFactoryImplementation = TPamResourceFactory<TPamResourceConnectionDetails, TPamAccountCredentials>;
|
||||
|
||||
export const PAM_RESOURCE_FACTORY_MAP: Record<PamResource, TPamResourceFactoryImplementation> = {
|
||||
[PamResource.Postgres]: sqlResourceFactory as TPamResourceFactoryImplementation
|
||||
[PamResource.Postgres]: sqlResourceFactory as TPamResourceFactoryImplementation,
|
||||
[PamResource.MySQL]: sqlResourceFactory as TPamResourceFactoryImplementation
|
||||
};
|
||||
|
||||
@@ -3,11 +3,12 @@ import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
||||
import { KmsDataKey } from "@app/services/kms/kms-types";
|
||||
|
||||
import { decryptAccountCredentials } from "../pam-account/pam-account-fns";
|
||||
import { getMySQLResourceListItem } from "./mysql/mysql-resource-fns";
|
||||
import { TPamResource, TPamResourceConnectionDetails } from "./pam-resource-types";
|
||||
import { getPostgresResourceListItem } from "./postgres/postgres-resource-fns";
|
||||
|
||||
export const listResourceOptions = () => {
|
||||
return [getPostgresResourceListItem()].sort((a, b) => a.name.localeCompare(b.name));
|
||||
return [getPostgresResourceListItem(), getMySQLResourceListItem()].sort((a, b) => a.name.localeCompare(b.name));
|
||||
};
|
||||
|
||||
// Resource
|
||||
|
||||
@@ -33,7 +33,9 @@ export const BasePamAccountSchemaWithResource = BasePamAccountSchema.extend({
|
||||
resourceType: true
|
||||
}).extend({
|
||||
rotationCredentialsConfigured: z.boolean()
|
||||
})
|
||||
}),
|
||||
lastRotationMessage: z.string().nullable().optional(),
|
||||
rotationStatus: z.string().nullable().optional()
|
||||
});
|
||||
|
||||
export const BaseCreatePamAccountSchema = z.object({
|
||||
|
||||
@@ -1,4 +1,10 @@
|
||||
import { TGatewayV2ServiceFactory } from "../gateway-v2/gateway-v2-service";
|
||||
import {
|
||||
TMySQLAccount,
|
||||
TMySQLAccountCredentials,
|
||||
TMySQLResource,
|
||||
TMySQLResourceConnectionDetails
|
||||
} from "./mysql/mysql-resource-types";
|
||||
import { PamResource } from "./pam-resource-enums";
|
||||
import {
|
||||
TPostgresAccount,
|
||||
@@ -8,12 +14,13 @@ import {
|
||||
} from "./postgres/postgres-resource-types";
|
||||
|
||||
// Resource types
|
||||
export type TPamResource = TPostgresResource;
|
||||
export type TPamResourceConnectionDetails = TPostgresResourceConnectionDetails;
|
||||
export type TPamResource = TPostgresResource | TMySQLResource;
|
||||
export type TPamResourceConnectionDetails = TPostgresResourceConnectionDetails | TMySQLResourceConnectionDetails;
|
||||
|
||||
// Account types
|
||||
export type TPamAccount = TPostgresAccount;
|
||||
export type TPamAccountCredentials = TPostgresAccountCredentials;
|
||||
export type TPamAccount = TPostgresAccount | TMySQLAccount;
|
||||
// eslint-disable-next-line @typescript-eslint/no-duplicate-type-constituents
|
||||
export type TPamAccountCredentials = TPostgresAccountCredentials | TMySQLAccountCredentials;
|
||||
|
||||
// Resource DTOs
|
||||
export type TCreateResourceDTO = Pick<
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import knex, { Knex } from "knex";
|
||||
import knex from "knex";
|
||||
import mysql, { Connection } from "mysql2/promise";
|
||||
import * as pg from "pg";
|
||||
import tls, { PeerCertificate } from "tls";
|
||||
|
||||
import { verifyHostInputValidity } from "@app/ee/services/dynamic-secret/dynamic-secret-fns";
|
||||
@@ -20,30 +22,162 @@ const EXTERNAL_REQUEST_TIMEOUT = 10 * 1000;
|
||||
|
||||
const TEST_CONNECTION_USERNAME = "infisical-gateway-connection-test";
|
||||
const TEST_CONNECTION_PASSWORD = "infisical-gateway-connection-test-password";
|
||||
const SIMPLE_QUERY = "select 1";
|
||||
|
||||
const SQL_CONNECTION_CLIENT_MAP = {
|
||||
[PamResource.Postgres]: "pg"
|
||||
};
|
||||
export interface SqlResourceConnection {
|
||||
/**
|
||||
* Check and see if the connection is good or not.
|
||||
*
|
||||
* @param connectOnly when true, if we only want to know that making the connection is possible or not,
|
||||
* we don't care about authentication failures
|
||||
* @returns Promise to be resolved when the connection is good, otherwise an error will be errbacked
|
||||
*/
|
||||
validate: (connectOnly: boolean) => Promise<void>;
|
||||
|
||||
const getConnectionConfig = (
|
||||
resourceType: PamResource,
|
||||
{ host, sslEnabled, sslRejectUnauthorized, sslCertificate }: TSqlResourceConnectionDetails
|
||||
) => {
|
||||
switch (resourceType) {
|
||||
/**
|
||||
* Rotate password and return the new credentials.
|
||||
*
|
||||
* @param currentCredentials the current credentials to rotate
|
||||
*
|
||||
* @returns Promise to be resolved with the new credentials
|
||||
*/
|
||||
rotateCredentials: (
|
||||
currentCredentials: TSqlAccountCredentials,
|
||||
newPassword: string
|
||||
) => Promise<TSqlAccountCredentials>;
|
||||
|
||||
/**
|
||||
* Close the connection.
|
||||
*
|
||||
* @returns Promise for closing the connection
|
||||
*/
|
||||
close: () => Promise<void>;
|
||||
}
|
||||
|
||||
const makeSqlConnection = (
|
||||
proxyPort: number,
|
||||
config: {
|
||||
connectionDetails: TSqlResourceConnectionDetails;
|
||||
resourceType: PamResource;
|
||||
username?: string;
|
||||
password?: string;
|
||||
}
|
||||
): SqlResourceConnection => {
|
||||
const { connectionDetails, resourceType, username, password } = config;
|
||||
const { host, sslEnabled, sslRejectUnauthorized, sslCertificate } = connectionDetails;
|
||||
const actualUsername = username ?? TEST_CONNECTION_USERNAME; // Use provided username or fallback
|
||||
const actualPassword = password ?? TEST_CONNECTION_PASSWORD; // Use provided password or fallback
|
||||
switch (config.resourceType) {
|
||||
case PamResource.Postgres: {
|
||||
const client = knex({
|
||||
client: "pg",
|
||||
connection: {
|
||||
host: "localhost",
|
||||
port: proxyPort,
|
||||
user: actualUsername,
|
||||
password: actualPassword,
|
||||
database: connectionDetails.database,
|
||||
connectionTimeoutMillis: EXTERNAL_REQUEST_TIMEOUT,
|
||||
ssl: sslEnabled
|
||||
? {
|
||||
rejectUnauthorized: sslRejectUnauthorized,
|
||||
ca: sslCertificate,
|
||||
servername: host,
|
||||
// When using proxy, we need to bypass hostname validation since we connect to localhost
|
||||
// but validate the certificate against the actual hostname
|
||||
checkServerIdentity: (hostname: string, cert: PeerCertificate) => {
|
||||
return tls.checkServerIdentity(host, cert);
|
||||
}
|
||||
}
|
||||
: false
|
||||
}
|
||||
});
|
||||
return {
|
||||
ssl: sslEnabled
|
||||
? {
|
||||
rejectUnauthorized: sslRejectUnauthorized,
|
||||
ca: sslCertificate,
|
||||
servername: host,
|
||||
// When using proxy, we need to bypass hostname validation since we connect to localhost
|
||||
// but validate the certificate against the actual hostname
|
||||
checkServerIdentity: (hostname: string, cert: PeerCertificate) => {
|
||||
return tls.checkServerIdentity(host, cert);
|
||||
validate: async (connectOnly) => {
|
||||
try {
|
||||
await client.raw(SIMPLE_QUERY);
|
||||
} catch (error) {
|
||||
if (error instanceof pg.DatabaseError) {
|
||||
// Hacky way to know if we successfully hit the database.
|
||||
// TODO: potentially two approaches to solve the problem.
|
||||
// 1. change the work flow, add account first then resource
|
||||
// 2. modify relay to add a new endpoint for returning if the target host is healthy or not
|
||||
// (like being able to do an auth handshake regardless pass or not)
|
||||
if (
|
||||
connectOnly &&
|
||||
(error.message === `password authentication failed for user "${TEST_CONNECTION_USERNAME}"` ||
|
||||
error.message.includes("no pg_hba.conf entry for host"))
|
||||
) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
: false
|
||||
throw new BadRequestError({
|
||||
message: `Unable to validate connection to ${resourceType}: ${(error as Error).message || String(error)}`
|
||||
});
|
||||
}
|
||||
},
|
||||
rotateCredentials: async (currentCredentials, newPassword) => {
|
||||
// Note: The generated random password is not really going to make SQL Injection possible.
|
||||
// The reason we are not using parameters binding is that the "ALTER USER" syntax is DDL,
|
||||
// parameters binding is not supported. But just in case if the this code got copied
|
||||
// around and repurposed, let's just do some naive escaping regardless
|
||||
await client.raw(`ALTER USER :username: WITH PASSWORD '${newPassword.replace(/'/g, "''")}'`, {
|
||||
username: currentCredentials.username
|
||||
});
|
||||
return { username: currentCredentials.username, password: newPassword };
|
||||
},
|
||||
close: () => client.destroy()
|
||||
};
|
||||
}
|
||||
case PamResource.MySQL: {
|
||||
return {
|
||||
validate: async (connectOnly) => {
|
||||
let client: Connection | null = null;
|
||||
try {
|
||||
// Notice: the reason we are not using Knex for mysql2 is because we don't need any fancy feature from Knex.
|
||||
// mysql2 doesn't provide custom ssl verification function pass in.
|
||||
// ref: https://github.com/sidorares/node-mysql2/blob/2543272a2ada8d8a07f74582549d7dd3fe948e2d/lib/base/connection.js#L358-L362
|
||||
// and then even I tried to workaround it with Knex's pool afterCreate hook, but then encounter a bug:
|
||||
// ref: https://github.com/knex/knex/issues/5352
|
||||
// It appears that using Knex causing more troubles than not, we are just checking the connections,
|
||||
// so it's much easier to create raw connection with the driver lib directly
|
||||
client = await mysql.createConnection({
|
||||
host: "localhost",
|
||||
port: proxyPort,
|
||||
user: actualUsername, // Use provided username or fallback
|
||||
password: actualPassword, // Use provided password or fallback
|
||||
database: connectionDetails.database,
|
||||
ssl: sslEnabled
|
||||
? {
|
||||
rejectUnauthorized: sslRejectUnauthorized,
|
||||
ca: sslCertificate
|
||||
}
|
||||
: undefined
|
||||
});
|
||||
await client.query(SIMPLE_QUERY);
|
||||
} catch (error) {
|
||||
if (connectOnly) {
|
||||
// Hacky way to know if we successfully hit the database.
|
||||
if (
|
||||
error instanceof Error &&
|
||||
error.message.startsWith(`Access denied for user '${TEST_CONNECTION_USERNAME}'@`)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
// TODO: handle other errors, and throw standardlized errors providing user-friendly msg
|
||||
throw error;
|
||||
} finally {
|
||||
await client?.end();
|
||||
}
|
||||
},
|
||||
rotateCredentials: async () => {
|
||||
// TODO: the pwd rotation for MySQL is not supported yet
|
||||
throw new BadRequestError({
|
||||
message: "Unsupported operation"
|
||||
});
|
||||
},
|
||||
close: async () => {}
|
||||
};
|
||||
}
|
||||
default:
|
||||
@@ -62,10 +196,9 @@ export const executeWithGateway = async <T>(
|
||||
password?: string;
|
||||
},
|
||||
gatewayV2Service: Pick<TGatewayV2ServiceFactory, "getPlatformConnectionDetailsByGatewayId">,
|
||||
operation: (client: Knex) => Promise<T>
|
||||
operation: (connection: SqlResourceConnection) => Promise<T>
|
||||
): Promise<T> => {
|
||||
const { connectionDetails, resourceType, gatewayId, username, password } = config;
|
||||
|
||||
const { connectionDetails, gatewayId } = config;
|
||||
const [targetHost] = await verifyHostInputValidity(connectionDetails.host, true);
|
||||
const platformConnectionDetails = await gatewayV2Service.getPlatformConnectionDetailsByGatewayId({
|
||||
gatewayId,
|
||||
@@ -79,22 +212,11 @@ export const executeWithGateway = async <T>(
|
||||
|
||||
return withGatewayV2Proxy(
|
||||
async (proxyPort) => {
|
||||
const client = knex({
|
||||
client: SQL_CONNECTION_CLIENT_MAP[resourceType],
|
||||
connection: {
|
||||
database: connectionDetails.database,
|
||||
port: proxyPort,
|
||||
host: "localhost",
|
||||
user: username ?? TEST_CONNECTION_USERNAME, // Use provided username or fallback
|
||||
password: password ?? TEST_CONNECTION_PASSWORD, // Use provided password or fallback
|
||||
connectionTimeoutMillis: EXTERNAL_REQUEST_TIMEOUT,
|
||||
...getConnectionConfig(resourceType, connectionDetails)
|
||||
}
|
||||
});
|
||||
const connection = makeSqlConnection(proxyPort, config);
|
||||
try {
|
||||
return await operation(client);
|
||||
return await operation(connection);
|
||||
} finally {
|
||||
await client.destroy();
|
||||
await connection.close();
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -115,25 +237,14 @@ export const sqlResourceFactory: TPamResourceFactory<TSqlResourceConnectionDetai
|
||||
const validateConnection = async () => {
|
||||
try {
|
||||
await executeWithGateway({ connectionDetails, gatewayId, resourceType }, gatewayV2Service, async (client) => {
|
||||
await client.raw("Select 1");
|
||||
await client.validate(true);
|
||||
});
|
||||
return connectionDetails;
|
||||
} catch (error) {
|
||||
// Hacky way to know if we successfully hit the database
|
||||
if (error instanceof BadRequestError) {
|
||||
if (error.message === `password authentication failed for user "${TEST_CONNECTION_USERNAME}"`) {
|
||||
return connectionDetails;
|
||||
}
|
||||
|
||||
if (error.message.includes("no pg_hba.conf entry for host")) {
|
||||
return connectionDetails;
|
||||
}
|
||||
|
||||
if (error.message === "Connection terminated unexpectedly") {
|
||||
throw new BadRequestError({
|
||||
message: "Connection terminated unexpectedly. Verify that host and port are correct"
|
||||
});
|
||||
}
|
||||
if (error instanceof BadRequestError && error.message === "Connection terminated unexpectedly") {
|
||||
throw new BadRequestError({
|
||||
message: "Connection terminated unexpectedly. Verify that host and port are correct"
|
||||
});
|
||||
}
|
||||
|
||||
throw new BadRequestError({
|
||||
@@ -156,11 +267,12 @@ export const sqlResourceFactory: TPamResourceFactory<TSqlResourceConnectionDetai
|
||||
},
|
||||
gatewayV2Service,
|
||||
async (client) => {
|
||||
await client.raw("Select 1");
|
||||
await client.validate(false);
|
||||
}
|
||||
);
|
||||
return credentials;
|
||||
} catch (error) {
|
||||
// TODO: extract these logic into each SQL connection
|
||||
if (error instanceof BadRequestError) {
|
||||
if (error.message === `password authentication failed for user "${credentials.username}"`) {
|
||||
throw new BadRequestError({
|
||||
@@ -185,10 +297,9 @@ export const sqlResourceFactory: TPamResourceFactory<TSqlResourceConnectionDetai
|
||||
rotationAccountCredentials,
|
||||
currentCredentials
|
||||
) => {
|
||||
const newPassword = alphaNumericNanoId(32);
|
||||
try {
|
||||
const newPassword = alphaNumericNanoId(32);
|
||||
|
||||
await executeWithGateway(
|
||||
return await executeWithGateway(
|
||||
{
|
||||
connectionDetails,
|
||||
gatewayId,
|
||||
@@ -197,20 +308,8 @@ export const sqlResourceFactory: TPamResourceFactory<TSqlResourceConnectionDetai
|
||||
password: rotationAccountCredentials.password
|
||||
},
|
||||
gatewayV2Service,
|
||||
async (client) => {
|
||||
switch (resourceType) {
|
||||
case PamResource.Postgres:
|
||||
await client.raw(`ALTER USER ?? WITH PASSWORD '${newPassword}'`, [currentCredentials.username]);
|
||||
break;
|
||||
default:
|
||||
throw new BadRequestError({
|
||||
message: `Password rotation for ${resourceType as PamResource} is not supported.`
|
||||
});
|
||||
}
|
||||
}
|
||||
(client) => client.rotateCredentials(currentCredentials, newPassword)
|
||||
);
|
||||
|
||||
return { username: currentCredentials.username, password: newPassword };
|
||||
} catch (error) {
|
||||
if (error instanceof BadRequestError) {
|
||||
if (error.message === `password authentication failed for user "${rotationAccountCredentials.username}"`) {
|
||||
@@ -232,8 +331,10 @@ export const sqlResourceFactory: TPamResourceFactory<TSqlResourceConnectionDetai
|
||||
}
|
||||
}
|
||||
|
||||
const sanitizedErrorMessage = ((error as Error).message || String(error)).replaceAll(newPassword, "REDACTED");
|
||||
|
||||
throw new BadRequestError({
|
||||
message: `Unable to rotate account credentials for ${resourceType}: ${(error as Error).message || String(error)}`
|
||||
message: `Unable to rotate account credentials for ${resourceType}: ${sanitizedErrorMessage}`
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import { TMySQLAccountCredentials, TMySQLResourceConnectionDetails } from "../../mysql/mysql-resource-types";
|
||||
import {
|
||||
TPostgresAccountCredentials,
|
||||
TPostgresResourceConnectionDetails
|
||||
} from "../../postgres/postgres-resource-types";
|
||||
|
||||
export type TSqlResourceConnectionDetails = TPostgresResourceConnectionDetails;
|
||||
export type TSqlAccountCredentials = TPostgresAccountCredentials;
|
||||
export type TSqlResourceConnectionDetails = TPostgresResourceConnectionDetails | TMySQLResourceConnectionDetails;
|
||||
// eslint-disable-next-line @typescript-eslint/no-duplicate-type-constituents
|
||||
export type TSqlAccountCredentials = TPostgresAccountCredentials | TMySQLAccountCredentials;
|
||||
|
||||
@@ -337,6 +337,12 @@ export const permissionServiceFactory = ({
|
||||
throw new NotFoundError({ message: `Project with ${projectId} not found` });
|
||||
}
|
||||
|
||||
requestContext.set("projectDetails", {
|
||||
id: projectDetails.id,
|
||||
name: projectDetails.name,
|
||||
slug: projectDetails.slug
|
||||
});
|
||||
|
||||
if (projectDetails.orgId !== actorOrgId) {
|
||||
throw new ForbiddenRequestError({ name: "You are not logged into this organization" });
|
||||
}
|
||||
|
||||
@@ -1268,15 +1268,17 @@ export const relayServiceFactory = ({
|
||||
}))
|
||||
);
|
||||
|
||||
await smtpService.sendMail({
|
||||
recipients: admins.map((admin) => admin.user.email).filter((v): v is string => !!v),
|
||||
subjectLine: "Relay Health Alert",
|
||||
substitutions: {
|
||||
type: "relay",
|
||||
names: relayNames
|
||||
},
|
||||
template: SmtpTemplates.HealthAlert
|
||||
});
|
||||
// Temporarily disabled email notifications due to excessive noise. Will be revised later
|
||||
//
|
||||
// await smtpService.sendMail({
|
||||
// recipients: admins.map((admin) => admin.user.email).filter((v): v is string => !!v),
|
||||
// subjectLine: "Relay Health Alert",
|
||||
// substitutions: {
|
||||
// type: "relay",
|
||||
// names: relayNames
|
||||
// },
|
||||
// template: SmtpTemplates.HealthAlert
|
||||
// });
|
||||
}
|
||||
|
||||
await Promise.all(relays.map((r) => relayDAL.updateById(r.id, { healthAlertedAt: new Date() })));
|
||||
|
||||
@@ -769,7 +769,7 @@ export const samlConfigServiceFactory = ({
|
||||
});
|
||||
}
|
||||
|
||||
return { isUserCompleted, providerAuthToken };
|
||||
return { isUserCompleted, providerAuthToken, user, organization };
|
||||
};
|
||||
|
||||
return {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { TSamlConfigs } from "@app/db/schemas";
|
||||
import { TOrganizations, TSamlConfigs, TUsers } from "@app/db/schemas";
|
||||
import { TOrgPermission } from "@app/lib/types";
|
||||
import { ActorAuthMethod, ActorType } from "@app/services/auth/auth-type";
|
||||
|
||||
@@ -78,5 +78,7 @@ export type TSamlConfigServiceFactory = {
|
||||
samlLogin: (arg: TSamlLoginDTO) => Promise<{
|
||||
isUserCompleted: boolean;
|
||||
providerAuthToken: string;
|
||||
user: TUsers;
|
||||
organization: TOrganizations;
|
||||
}>;
|
||||
};
|
||||
|
||||
@@ -1517,7 +1517,7 @@ export const secretApprovalRequestServiceFactory = ({
|
||||
}))
|
||||
);
|
||||
if (secrets.length)
|
||||
throw new BadRequestError({ message: `Secret already exist: ${secrets.map((el) => el.key).join(",")}` });
|
||||
throw new BadRequestError({ message: `Secret already exists: ${secrets.map((el) => el.key).join(",")}` });
|
||||
|
||||
commits.push(
|
||||
...createdSecrets.map((createdSecret) => ({
|
||||
|
||||
@@ -2348,6 +2348,9 @@ export const AppConnections = {
|
||||
RAILWAY: {
|
||||
apiToken: "The API token used to authenticate with Railway."
|
||||
},
|
||||
NORTHFLANK: {
|
||||
apiToken: "The API token used to authenticate with Northflank."
|
||||
},
|
||||
CHECKLY: {
|
||||
apiKey: "The API key used to authenticate with Checkly."
|
||||
},
|
||||
@@ -2376,6 +2379,12 @@ export const AppConnections = {
|
||||
},
|
||||
LARAVEL_FORGE: {
|
||||
apiToken: "The API token used to authenticate with Laravel Forge."
|
||||
},
|
||||
CHEF: {
|
||||
serverUrl: "The URL of the Chef server to connect to.",
|
||||
orgName: "The short name of the Chef organization to connect to.",
|
||||
userName: "The username used to access Chef.",
|
||||
privateKey: "The private key used to access Chef."
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -2620,6 +2629,16 @@ export const SecretSyncs = {
|
||||
siteName: "The name of the Netlify site to sync secrets to.",
|
||||
siteId: "The ID of the Netlify site to sync secrets to.",
|
||||
context: "The Netlify context to sync secrets to."
|
||||
},
|
||||
CHEF: {
|
||||
dataBagName: "The name of the Chef data bag to sync secrets to.",
|
||||
dataBagItemName: "The name of the Chef data bag item to sync secrets to."
|
||||
},
|
||||
NORTHFLANK: {
|
||||
projectId: "The ID of the Northflank project to sync secrets to.",
|
||||
projectName: "The name of the Northflank project to sync secrets to.",
|
||||
secretGroupId: "The ID of the Northflank secret group to sync secrets to.",
|
||||
secretGroupName: "The name of the Northflank secret group to sync secrets to."
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -7,6 +7,7 @@ import https from "https";
|
||||
import { verifyHostInputValidity } from "@app/ee/services/dynamic-secret/dynamic-secret-fns";
|
||||
import { splitPemChain } from "@app/services/certificate/certificate-fns";
|
||||
|
||||
import { getConfig } from "../config/env";
|
||||
import { BadRequestError } from "../errors";
|
||||
import { GatewayProxyProtocol } from "../gateway/types";
|
||||
import { logger } from "../logger";
|
||||
@@ -80,6 +81,8 @@ const createGatewayConnection = async (
|
||||
gateway: { clientCertificate: string; clientPrivateKey: string; serverCertificateChain: string },
|
||||
protocol: GatewayProxyProtocol
|
||||
): Promise<net.Socket> => {
|
||||
const appCfg = getConfig();
|
||||
|
||||
const protocolToAlpn = {
|
||||
[GatewayProxyProtocol.Http]: "infisical-http-proxy",
|
||||
[GatewayProxyProtocol.Tcp]: "infisical-tcp-proxy",
|
||||
@@ -94,7 +97,8 @@ const createGatewayConnection = async (
|
||||
minVersion: "TLSv1.2",
|
||||
maxVersion: "TLSv1.3",
|
||||
rejectUnauthorized: true,
|
||||
ALPNProtocols: [protocolToAlpn[protocol]]
|
||||
ALPNProtocols: [protocolToAlpn[protocol]],
|
||||
checkServerIdentity: appCfg.isDevelopmentMode ? () => undefined : tls.checkServerIdentity
|
||||
};
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
import { requestContext } from "@fastify/request-context";
|
||||
import opentelemetry from "@opentelemetry/api";
|
||||
|
||||
import { getConfig } from "../config/env";
|
||||
|
||||
const infisicalMeter = opentelemetry.metrics.getMeter("Infisical");
|
||||
|
||||
export enum AuthAttemptAuthMethod {
|
||||
EMAIL = "email",
|
||||
SAML = "saml",
|
||||
OIDC = "oidc",
|
||||
GOOGLE = "google",
|
||||
GITHUB = "github",
|
||||
GITLAB = "gitlab",
|
||||
TOKEN_AUTH = "token-auth",
|
||||
UNIVERSAL_AUTH = "universal-auth",
|
||||
KUBERNETES_AUTH = "kubernetes-auth",
|
||||
GCP_AUTH = "gcp-auth",
|
||||
ALICLOUD_AUTH = "alicloud-auth",
|
||||
AWS_AUTH = "aws-auth",
|
||||
AZURE_AUTH = "azure-auth",
|
||||
TLS_CERT_AUTH = "tls-cert-auth",
|
||||
OCI_AUTH = "oci-auth",
|
||||
OIDC_AUTH = "oidc-auth",
|
||||
JWT_AUTH = "jwt-auth",
|
||||
LDAP_AUTH = "ldap-auth"
|
||||
}
|
||||
|
||||
export enum AuthAttemptAuthResult {
|
||||
SUCCESS = "success",
|
||||
FAILURE = "failure"
|
||||
}
|
||||
|
||||
export const authAttemptCounter = infisicalMeter.createCounter("infisical.auth.attempt.count", {
|
||||
description: "Authentication attempts (both successful and failed)",
|
||||
unit: "{attempt}"
|
||||
});
|
||||
|
||||
export const secretReadCounter = infisicalMeter.createCounter("infisical.secret.read.count", {
|
||||
description: "Number of secret read operations",
|
||||
unit: "{operation}"
|
||||
});
|
||||
|
||||
export const recordSecretReadMetric = (params: { environment: string; secretPath: string; name?: string }) => {
|
||||
const appCfg = getConfig();
|
||||
|
||||
if (appCfg.OTEL_TELEMETRY_COLLECTION_ENABLED) {
|
||||
const attributes: Record<string, string> = {
|
||||
"infisical.environment": params.environment,
|
||||
"infisical.secret.path": params.secretPath,
|
||||
...(params.name ? { "infisical.secret.name": params.name } : {})
|
||||
};
|
||||
|
||||
const orgId = requestContext.get("orgId");
|
||||
if (orgId) {
|
||||
attributes["infisical.organization.id"] = orgId;
|
||||
}
|
||||
|
||||
const orgName = requestContext.get("orgName");
|
||||
if (orgName) {
|
||||
attributes["infisical.organization.name"] = orgName;
|
||||
}
|
||||
|
||||
const projectDetails = requestContext.get("projectDetails");
|
||||
if (projectDetails?.id) {
|
||||
attributes["infisical.project.id"] = projectDetails.id;
|
||||
}
|
||||
if (projectDetails?.name) {
|
||||
attributes["infisical.project.name"] = projectDetails.name;
|
||||
}
|
||||
|
||||
const userAuthInfo = requestContext.get("userAuthInfo");
|
||||
if (userAuthInfo?.userId) {
|
||||
attributes["infisical.user.id"] = userAuthInfo.userId;
|
||||
}
|
||||
if (userAuthInfo?.email) {
|
||||
attributes["infisical.user.email"] = userAuthInfo.email;
|
||||
}
|
||||
|
||||
const identityAuthInfo = requestContext.get("identityAuthInfo");
|
||||
if (identityAuthInfo?.identityId) {
|
||||
attributes["infisical.identity.id"] = identityAuthInfo.identityId;
|
||||
}
|
||||
if (identityAuthInfo?.identityName) {
|
||||
attributes["infisical.identity.name"] = identityAuthInfo.identityName;
|
||||
}
|
||||
|
||||
const userAgent = requestContext.get("userAgent");
|
||||
if (userAgent) {
|
||||
attributes["user_agent.original"] = userAgent;
|
||||
}
|
||||
|
||||
const ip = requestContext.get("ip");
|
||||
if (ip) {
|
||||
attributes["client.address"] = ip;
|
||||
}
|
||||
|
||||
secretReadCounter.add(1, attributes);
|
||||
}
|
||||
};
|
||||
@@ -78,6 +78,7 @@ export enum QueueName {
|
||||
SecretReminderMigration = "secret-reminder-migration",
|
||||
UserNotification = "user-notification",
|
||||
HealthAlert = "health-alert",
|
||||
CertificateV3AutoRenewal = "certificate-v3-auto-renewal",
|
||||
PamAccountRotation = "pam-account-rotation"
|
||||
}
|
||||
|
||||
@@ -128,6 +129,7 @@ export enum QueueJobs {
|
||||
SecretReminderMigration = "secret-reminder-migration",
|
||||
UserNotification = "user-notification-job",
|
||||
HealthAlert = "health-alert",
|
||||
CertificateV3DailyAutoRenewal = "certificate-v3-daily-auto-renewal",
|
||||
PamAccountRotation = "pam-account-rotation"
|
||||
}
|
||||
|
||||
@@ -359,6 +361,10 @@ export type TQueueJobTypes = {
|
||||
name: QueueJobs.HealthAlert;
|
||||
payload: undefined;
|
||||
};
|
||||
[QueueName.CertificateV3AutoRenewal]: {
|
||||
name: QueueJobs.CertificateV3DailyAutoRenewal;
|
||||
payload: undefined;
|
||||
};
|
||||
[QueueName.PamAccountRotation]: {
|
||||
name: QueueJobs.PamAccountRotation;
|
||||
payload: undefined;
|
||||
|
||||
@@ -141,7 +141,9 @@ export const main = async ({
|
||||
await server.register(fastifyRequestContext, {
|
||||
defaultStoreValues: (req) => ({
|
||||
reqId: req.id,
|
||||
log: req.log.child({ reqId: req.id })
|
||||
log: req.log.child({ reqId: req.id }),
|
||||
ip: req.realIp,
|
||||
userAgent: req.headers["user-agent"]
|
||||
})
|
||||
});
|
||||
|
||||
|
||||
@@ -1,12 +1,26 @@
|
||||
import { requestContext } from "@fastify/request-context";
|
||||
import opentelemetry from "@opentelemetry/api";
|
||||
import fp from "fastify-plugin";
|
||||
|
||||
export const apiMetrics = fp(async (fastify) => {
|
||||
const apiMeter = opentelemetry.metrics.getMeter("API");
|
||||
const latencyHistogram = apiMeter.createHistogram("API_latency", {
|
||||
unit: "ms"
|
||||
});
|
||||
const apiMeter = opentelemetry.metrics.getMeter("API");
|
||||
|
||||
const latencyHistogram = apiMeter.createHistogram("API_latency", {
|
||||
unit: "ms"
|
||||
});
|
||||
|
||||
const infisicalMeter = opentelemetry.metrics.getMeter("Infisical");
|
||||
|
||||
const requestCounter = infisicalMeter.createCounter("infisical.http.server.request.count", {
|
||||
description: "Total number of API requests to Infisical (covers both human users and machine identities)",
|
||||
unit: "{request}"
|
||||
});
|
||||
|
||||
const requestDurationHistogram = infisicalMeter.createHistogram("infisical.http.server.request.duration", {
|
||||
description: "API request latency",
|
||||
unit: "s"
|
||||
});
|
||||
|
||||
export const apiMetrics = fp(async (fastify) => {
|
||||
fastify.addHook("onResponse", async (request, reply) => {
|
||||
const { method } = request;
|
||||
const route = request.routerPath;
|
||||
@@ -17,5 +31,67 @@ export const apiMetrics = fp(async (fastify) => {
|
||||
method,
|
||||
statusCode
|
||||
});
|
||||
|
||||
const orgId = requestContext.get("orgId");
|
||||
const orgName = requestContext.get("orgName");
|
||||
const userAuthInfo = requestContext.get("userAuthInfo");
|
||||
const identityAuthInfo = requestContext.get("identityAuthInfo");
|
||||
const projectDetails = requestContext.get("projectDetails");
|
||||
const userAgent = requestContext.get("userAgent");
|
||||
const ip = requestContext.get("ip");
|
||||
|
||||
const attributes: Record<string, string | number> = {
|
||||
"http.request.method": method,
|
||||
"http.route": route,
|
||||
"http.response.status_code": statusCode
|
||||
};
|
||||
|
||||
if (orgId) {
|
||||
attributes["infisical.organization.id"] = orgId;
|
||||
}
|
||||
if (orgName) {
|
||||
attributes["infisical.organization.name"] = orgName;
|
||||
}
|
||||
|
||||
if (userAuthInfo) {
|
||||
if (userAuthInfo.userId) {
|
||||
attributes["infisical.user.id"] = userAuthInfo.userId;
|
||||
}
|
||||
if (userAuthInfo.email) {
|
||||
attributes["infisical.user.email"] = userAuthInfo.email;
|
||||
}
|
||||
}
|
||||
|
||||
if (identityAuthInfo) {
|
||||
if (identityAuthInfo.identityId) {
|
||||
attributes["infisical.identity.id"] = identityAuthInfo.identityId;
|
||||
}
|
||||
if (identityAuthInfo.identityName) {
|
||||
attributes["infisical.identity.name"] = identityAuthInfo.identityName;
|
||||
}
|
||||
if (identityAuthInfo.authMethod) {
|
||||
attributes["infisical.auth.method"] = identityAuthInfo.authMethod;
|
||||
}
|
||||
}
|
||||
|
||||
if (projectDetails) {
|
||||
if (projectDetails.id) {
|
||||
attributes["infisical.project.id"] = projectDetails.id;
|
||||
}
|
||||
if (projectDetails.name) {
|
||||
attributes["infisical.project.name"] = projectDetails.name;
|
||||
}
|
||||
}
|
||||
|
||||
if (userAgent) {
|
||||
attributes["user_agent.original"] = userAgent;
|
||||
}
|
||||
|
||||
if (ip) {
|
||||
attributes["client.address"] = ip;
|
||||
}
|
||||
|
||||
requestCounter.add(1, attributes);
|
||||
requestDurationHistogram.record(reply.elapsedTime / 1000, attributes);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { requestContext } from "@fastify/request-context";
|
||||
import { requestContext, RequestContextData } from "@fastify/request-context";
|
||||
import { FastifyRequest } from "fastify";
|
||||
import fp from "fastify-plugin";
|
||||
import type { JwtPayload } from "jsonwebtoken";
|
||||
@@ -138,6 +138,11 @@ export const injectIdentity = fp(
|
||||
return;
|
||||
}
|
||||
|
||||
// Authentication is handled on a route-level
|
||||
if (req.url === "/api/v1/relays/heartbeat-instance-relay") {
|
||||
return;
|
||||
}
|
||||
|
||||
// Authentication is handled on a route-level here.
|
||||
if (req.url.includes("/api/v1/workflow-integrations/microsoft-teams/message-endpoint")) {
|
||||
return;
|
||||
@@ -154,10 +159,11 @@ export const injectIdentity = fp(
|
||||
|
||||
switch (authMode) {
|
||||
case AuthMode.JWT: {
|
||||
const { user, tokenVersionId, orgId, rootOrgId, parentOrgId } =
|
||||
const { user, tokenVersionId, orgId, orgName, rootOrgId, parentOrgId } =
|
||||
await server.services.authToken.fnValidateJwtIdentity(token, subOrganizationSelector);
|
||||
requestContext.set("orgId", orgId);
|
||||
|
||||
requestContext.set("orgName", orgName);
|
||||
requestContext.set("userAuthInfo", { userId: user.id, email: user.email || "" });
|
||||
req.auth = {
|
||||
authMode: AuthMode.JWT,
|
||||
user,
|
||||
@@ -181,6 +187,7 @@ export const injectIdentity = fp(
|
||||
);
|
||||
const serverCfg = await getServerCfg();
|
||||
requestContext.set("orgId", identity.orgId);
|
||||
requestContext.set("orgName", identity.orgName);
|
||||
req.auth = {
|
||||
authMode: AuthMode.IDENTITY_ACCESS_TOKEN,
|
||||
actor,
|
||||
@@ -193,24 +200,23 @@ export const injectIdentity = fp(
|
||||
isInstanceAdmin: serverCfg?.adminIdentityIds?.includes(identity.identityId),
|
||||
token
|
||||
};
|
||||
const identityAuthInfo: RequestContextData["identityAuthInfo"] = {
|
||||
identityId: identity.identityId,
|
||||
identityName: identity.name,
|
||||
authMethod: identity.authMethod
|
||||
};
|
||||
|
||||
if (token?.identityAuth?.oidc) {
|
||||
requestContext.set("identityAuthInfo", {
|
||||
identityId: identity.identityId,
|
||||
oidc: token?.identityAuth?.oidc
|
||||
});
|
||||
identityAuthInfo.oidc = token?.identityAuth?.oidc;
|
||||
}
|
||||
if (token?.identityAuth?.kubernetes) {
|
||||
requestContext.set("identityAuthInfo", {
|
||||
identityId: identity.identityId,
|
||||
kubernetes: token?.identityAuth?.kubernetes
|
||||
});
|
||||
identityAuthInfo.kubernetes = token?.identityAuth?.kubernetes;
|
||||
}
|
||||
if (token?.identityAuth?.aws) {
|
||||
requestContext.set("identityAuthInfo", {
|
||||
identityId: identity.identityId,
|
||||
aws: token?.identityAuth?.aws
|
||||
});
|
||||
identityAuthInfo.aws = token?.identityAuth?.aws;
|
||||
}
|
||||
|
||||
requestContext.set("identityAuthInfo", identityAuthInfo);
|
||||
break;
|
||||
}
|
||||
case AuthMode.SERVICE_TOKEN: {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { ForbiddenError, PureAbility } from "@casl/ability";
|
||||
import { requestContext } from "@fastify/request-context";
|
||||
import opentelemetry from "@opentelemetry/api";
|
||||
import fastifyPlugin from "fastify-plugin";
|
||||
import jwt from "jsonwebtoken";
|
||||
@@ -47,6 +48,12 @@ export const fastifyErrHandler = fastifyPlugin(async (server: FastifyZodProvider
|
||||
unit: "1"
|
||||
});
|
||||
|
||||
const infisicalMeter = opentelemetry.metrics.getMeter("Infisical");
|
||||
const errorCounter = infisicalMeter.createCounter("infisical.http.server.error.count", {
|
||||
description: "Total number of API errors in Infisical (covers both human users and machine identities)",
|
||||
unit: "{error}"
|
||||
});
|
||||
|
||||
server.setErrorHandler((error, req, res) => {
|
||||
req.log.error(error);
|
||||
if (appCfg.OTEL_TELEMETRY_COLLECTION_ENABLED) {
|
||||
@@ -61,6 +68,67 @@ export const fastifyErrHandler = fastifyPlugin(async (server: FastifyZodProvider
|
||||
type: errorType,
|
||||
name: error.name
|
||||
});
|
||||
|
||||
const orgId = requestContext.get("orgId");
|
||||
const orgName = requestContext.get("orgName");
|
||||
const userAuthInfo = requestContext.get("userAuthInfo");
|
||||
const identityAuthInfo = requestContext.get("identityAuthInfo");
|
||||
const projectDetails = requestContext.get("projectDetails");
|
||||
|
||||
const attributes: Record<string, string | number> = {
|
||||
"http.request.method": method,
|
||||
"http.route": route,
|
||||
"error.type": errorType,
|
||||
"error.name": error.name
|
||||
};
|
||||
|
||||
if (orgId) {
|
||||
attributes["infisical.organization.id"] = orgId;
|
||||
}
|
||||
if (orgName) {
|
||||
attributes["infisical.organization.name"] = orgName;
|
||||
}
|
||||
|
||||
if (userAuthInfo) {
|
||||
if (userAuthInfo.userId) {
|
||||
attributes["infisical.user.id"] = userAuthInfo.userId;
|
||||
}
|
||||
if (userAuthInfo.email) {
|
||||
attributes["infisical.user.email"] = userAuthInfo.email;
|
||||
}
|
||||
}
|
||||
|
||||
if (identityAuthInfo) {
|
||||
if (identityAuthInfo.identityId) {
|
||||
attributes["infisical.identity.id"] = identityAuthInfo.identityId;
|
||||
}
|
||||
if (identityAuthInfo.identityName) {
|
||||
attributes["infisical.identity.name"] = identityAuthInfo.identityName;
|
||||
}
|
||||
if (identityAuthInfo.authMethod) {
|
||||
attributes["infisical.auth.method"] = identityAuthInfo.authMethod;
|
||||
}
|
||||
}
|
||||
|
||||
if (projectDetails) {
|
||||
if (projectDetails.id) {
|
||||
attributes["infisical.project.id"] = projectDetails.id;
|
||||
}
|
||||
if (projectDetails.name) {
|
||||
attributes["infisical.project.name"] = projectDetails.name;
|
||||
}
|
||||
}
|
||||
|
||||
const userAgent = req.headers["user-agent"];
|
||||
if (userAgent) {
|
||||
attributes["user_agent.original"] = userAgent;
|
||||
}
|
||||
|
||||
if (req.realIp) {
|
||||
attributes["client.address"] = req.realIp;
|
||||
}
|
||||
|
||||
errorCounter.add(1, attributes);
|
||||
}
|
||||
|
||||
if (error instanceof BadRequestError) {
|
||||
|
||||
@@ -172,11 +172,13 @@ import { internalCertificateAuthorityServiceFactory } from "@app/services/certif
|
||||
import { certificateEstV3ServiceFactory } from "@app/services/certificate-est-v3/certificate-est-v3-service";
|
||||
import { certificateProfileDALFactory } from "@app/services/certificate-profile/certificate-profile-dal";
|
||||
import { certificateProfileServiceFactory } from "@app/services/certificate-profile/certificate-profile-service";
|
||||
import { certificateSyncDALFactory } from "@app/services/certificate-sync/certificate-sync-dal";
|
||||
import { certificateTemplateDALFactory } from "@app/services/certificate-template/certificate-template-dal";
|
||||
import { certificateTemplateEstConfigDALFactory } from "@app/services/certificate-template/certificate-template-est-config-dal";
|
||||
import { certificateTemplateServiceFactory } from "@app/services/certificate-template/certificate-template-service";
|
||||
import { certificateTemplateV2DALFactory } from "@app/services/certificate-template-v2/certificate-template-v2-dal";
|
||||
import { certificateTemplateV2ServiceFactory } from "@app/services/certificate-template-v2/certificate-template-v2-service";
|
||||
import { certificateV3QueueServiceFactory } from "@app/services/certificate-v3/certificate-v3-queue";
|
||||
import { certificateV3ServiceFactory } from "@app/services/certificate-v3/certificate-v3-service";
|
||||
import { cmekServiceFactory } from "@app/services/cmek/cmek-service";
|
||||
import { convertorServiceFactory } from "@app/services/convertor/convertor-service";
|
||||
@@ -607,6 +609,10 @@ export const registerRoutes = async (
|
||||
const membershipGroupService = membershipGroupServiceFactory({
|
||||
membershipGroupDAL,
|
||||
membershipRoleDAL,
|
||||
accessApprovalPolicyDAL,
|
||||
accessApprovalPolicyApproverDAL,
|
||||
secretApprovalPolicyDAL,
|
||||
secretApprovalPolicyApproverDAL: sapApproverDAL,
|
||||
roleDAL,
|
||||
permissionService,
|
||||
orgDAL
|
||||
@@ -1059,6 +1065,7 @@ export const registerRoutes = async (
|
||||
const certificateDAL = certificateDALFactory(db);
|
||||
const certificateBodyDAL = certificateBodyDALFactory(db);
|
||||
const certificateSecretDAL = certificateSecretDALFactory(db);
|
||||
const certificateSyncDAL = certificateSyncDALFactory(db);
|
||||
|
||||
const pkiAlertDAL = pkiAlertDALFactory(db);
|
||||
const pkiCollectionDAL = pkiCollectionDALFactory(db);
|
||||
@@ -1707,7 +1714,8 @@ export const registerRoutes = async (
|
||||
licenseService,
|
||||
permissionService,
|
||||
kmsService,
|
||||
membershipIdentityDAL
|
||||
membershipIdentityDAL,
|
||||
orgDAL
|
||||
});
|
||||
|
||||
const identityAwsAuthService = identityAwsAuthServiceFactory({
|
||||
@@ -1960,6 +1968,8 @@ export const registerRoutes = async (
|
||||
secretImportDAL,
|
||||
permissionService,
|
||||
appConnectionService,
|
||||
projectDAL,
|
||||
orgDAL,
|
||||
folderDAL,
|
||||
secretSyncQueue,
|
||||
projectBotService,
|
||||
@@ -2022,7 +2032,8 @@ export const registerRoutes = async (
|
||||
certificateBodyDAL,
|
||||
certificateSecretDAL,
|
||||
certificateAuthorityDAL,
|
||||
certificateAuthorityCertDAL
|
||||
certificateAuthorityCertDAL,
|
||||
certificateSyncDAL
|
||||
});
|
||||
|
||||
const pkiSyncCleanup = pkiSyncCleanupQueueServiceFactory({
|
||||
@@ -2133,17 +2144,29 @@ export const registerRoutes = async (
|
||||
permissionService,
|
||||
pkiCollectionDAL,
|
||||
pkiCollectionItemDAL,
|
||||
certificateSyncDAL,
|
||||
pkiSyncDAL,
|
||||
pkiSyncQueue
|
||||
});
|
||||
|
||||
const certificateV3Service = certificateV3ServiceFactory({
|
||||
certificateDAL,
|
||||
certificateSecretDAL,
|
||||
certificateAuthorityDAL,
|
||||
certificateProfileDAL,
|
||||
certificateTemplateV2Service,
|
||||
internalCaService: internalCertificateAuthorityService,
|
||||
permissionService
|
||||
permissionService,
|
||||
certificateSyncDAL,
|
||||
pkiSyncDAL,
|
||||
pkiSyncQueue
|
||||
});
|
||||
|
||||
const certificateV3Queue = certificateV3QueueServiceFactory({
|
||||
queueService,
|
||||
certificateDAL,
|
||||
certificateV3Service,
|
||||
auditLogService
|
||||
});
|
||||
|
||||
const certificateEstV3Service = certificateEstV3ServiceFactory({
|
||||
@@ -2178,6 +2201,8 @@ export const registerRoutes = async (
|
||||
|
||||
const pkiSyncService = pkiSyncServiceFactory({
|
||||
pkiSyncDAL,
|
||||
certificateDAL,
|
||||
certificateSyncDAL,
|
||||
pkiSubscriberDAL,
|
||||
appConnectionService,
|
||||
permissionService,
|
||||
@@ -2333,6 +2358,7 @@ export const registerRoutes = async (
|
||||
await dailyReminderQueueService.startSecretReminderMigrationJob();
|
||||
await dailyExpiringPkiItemAlert.startSendingAlerts();
|
||||
await pkiSubscriberQueue.startDailyAutoRenewalJob();
|
||||
await certificateV3Queue.init();
|
||||
await kmsService.startService(hsmStatus);
|
||||
await microsoftTeamsService.start();
|
||||
await dynamicSecretQueueService.init();
|
||||
|
||||
@@ -48,6 +48,7 @@ import {
|
||||
ChecklyConnectionListItemSchema,
|
||||
SanitizedChecklyConnectionSchema
|
||||
} from "@app/services/app-connection/checkly";
|
||||
import { ChefConnectionListItemSchema, SanitizedChefConnectionSchema } from "@app/services/app-connection/chef";
|
||||
import {
|
||||
CloudflareConnectionListItemSchema,
|
||||
SanitizedCloudflareConnectionSchema
|
||||
@@ -88,6 +89,10 @@ import {
|
||||
NetlifyConnectionListItemSchema,
|
||||
SanitizedNetlifyConnectionSchema
|
||||
} from "@app/services/app-connection/netlify";
|
||||
import {
|
||||
NorthflankConnectionListItemSchema,
|
||||
SanitizedNorthflankConnectionSchema
|
||||
} from "@app/services/app-connection/northflank";
|
||||
import { OktaConnectionListItemSchema, SanitizedOktaConnectionSchema } from "@app/services/app-connection/okta";
|
||||
import {
|
||||
PostgresConnectionListItemSchema,
|
||||
@@ -160,10 +165,12 @@ const SanitizedAppConnectionSchema = z.union([
|
||||
...SanitizedSupabaseConnectionSchema.options,
|
||||
...SanitizedDigitalOceanConnectionSchema.options,
|
||||
...SanitizedNetlifyConnectionSchema.options,
|
||||
...SanitizedNorthflankConnectionSchema.options,
|
||||
...SanitizedOktaConnectionSchema.options,
|
||||
...SanitizedAzureADCSConnectionSchema.options,
|
||||
...SanitizedRedisConnectionSchema.options,
|
||||
...SanitizedLaravelForgeConnectionSchema.options
|
||||
...SanitizedLaravelForgeConnectionSchema.options,
|
||||
...SanitizedChefConnectionSchema.options
|
||||
]);
|
||||
|
||||
const AppConnectionOptionsSchema = z.discriminatedUnion("app", [
|
||||
@@ -203,10 +210,12 @@ const AppConnectionOptionsSchema = z.discriminatedUnion("app", [
|
||||
SupabaseConnectionListItemSchema,
|
||||
DigitalOceanConnectionListItemSchema,
|
||||
NetlifyConnectionListItemSchema,
|
||||
NorthflankConnectionListItemSchema,
|
||||
OktaConnectionListItemSchema,
|
||||
AzureADCSConnectionListItemSchema,
|
||||
RedisConnectionListItemSchema,
|
||||
LaravelForgeConnectionListItemSchema
|
||||
LaravelForgeConnectionListItemSchema,
|
||||
ChefConnectionListItemSchema
|
||||
]);
|
||||
|
||||
export const registerAppConnectionRouter = async (server: FastifyZodProvider) => {
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
import z from "zod";
|
||||
|
||||
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 {
|
||||
CreateChefConnectionSchema,
|
||||
SanitizedChefConnectionSchema,
|
||||
UpdateChefConnectionSchema
|
||||
} from "@app/services/app-connection/chef";
|
||||
import { AuthMode } from "@app/services/auth/auth-type";
|
||||
|
||||
import { registerAppConnectionEndpoints } from "./app-connection-endpoints";
|
||||
|
||||
export const registerChefConnectionRouter = async (server: FastifyZodProvider) => {
|
||||
registerAppConnectionEndpoints({
|
||||
app: AppConnection.Chef,
|
||||
server,
|
||||
sanitizedResponseSchema: SanitizedChefConnectionSchema,
|
||||
createSchema: CreateChefConnectionSchema,
|
||||
updateSchema: UpdateChefConnectionSchema
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: `/:connectionId/data-bags`,
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
params: z.object({
|
||||
connectionId: z.string().uuid()
|
||||
}),
|
||||
response: {
|
||||
200: z
|
||||
.object({
|
||||
name: z.string()
|
||||
})
|
||||
.array()
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
handler: async (req) => {
|
||||
const { connectionId } = req.params;
|
||||
const dataBags = await server.services.appConnection.chef.listDataBags(connectionId, req.permission);
|
||||
|
||||
return dataBags;
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: `/:connectionId/data-bag-items`,
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
params: z.object({
|
||||
connectionId: z.string().uuid()
|
||||
}),
|
||||
querystring: z.object({
|
||||
dataBagName: z.string()
|
||||
}),
|
||||
response: {
|
||||
200: z
|
||||
.object({
|
||||
name: z.string()
|
||||
})
|
||||
.array()
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
handler: async (req) => {
|
||||
const { connectionId } = req.params;
|
||||
const { dataBagName } = req.query;
|
||||
const dataBagItems = await server.services.appConnection.chef.listDataBagItems(
|
||||
connectionId,
|
||||
dataBagName,
|
||||
req.permission
|
||||
);
|
||||
|
||||
return dataBagItems;
|
||||
}
|
||||
});
|
||||
};
|
||||
@@ -13,6 +13,7 @@ import { registerAzureKeyVaultConnectionRouter } from "./azure-key-vault-connect
|
||||
import { registerBitbucketConnectionRouter } from "./bitbucket-connection-router";
|
||||
import { registerCamundaConnectionRouter } from "./camunda-connection-router";
|
||||
import { registerChecklyConnectionRouter } from "./checkly-connection-router";
|
||||
import { registerChefConnectionRouter } from "./chef-connection-router";
|
||||
import { registerCloudflareConnectionRouter } from "./cloudflare-connection-router";
|
||||
import { registerDatabricksConnectionRouter } from "./databricks-connection-router";
|
||||
import { registerDigitalOceanConnectionRouter } from "./digital-ocean-connection-router";
|
||||
@@ -29,6 +30,7 @@ import { registerLdapConnectionRouter } from "./ldap-connection-router";
|
||||
import { registerMsSqlConnectionRouter } from "./mssql-connection-router";
|
||||
import { registerMySqlConnectionRouter } from "./mysql-connection-router";
|
||||
import { registerNetlifyConnectionRouter } from "./netlify-connection-router";
|
||||
import { registerNorthflankConnectionRouter } from "./northflank-connection-router";
|
||||
import { registerOktaConnectionRouter } from "./okta-connection-router";
|
||||
import { registerPostgresConnectionRouter } from "./postgres-connection-router";
|
||||
import { registerRailwayConnectionRouter } from "./railway-connection-router";
|
||||
@@ -83,6 +85,8 @@ export const APP_CONNECTION_REGISTER_ROUTER_MAP: Record<AppConnection, (server:
|
||||
[AppConnection.Supabase]: registerSupabaseConnectionRouter,
|
||||
[AppConnection.DigitalOcean]: registerDigitalOceanConnectionRouter,
|
||||
[AppConnection.Netlify]: registerNetlifyConnectionRouter,
|
||||
[AppConnection.Northflank]: registerNorthflankConnectionRouter,
|
||||
[AppConnection.Okta]: registerOktaConnectionRouter,
|
||||
[AppConnection.Redis]: registerRedisConnectionRouter
|
||||
[AppConnection.Redis]: registerRedisConnectionRouter,
|
||||
[AppConnection.Chef]: registerChefConnectionRouter
|
||||
};
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
import { z } from "zod";
|
||||
|
||||
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 {
|
||||
CreateNorthflankConnectionSchema,
|
||||
SanitizedNorthflankConnectionSchema,
|
||||
UpdateNorthflankConnectionSchema
|
||||
} from "@app/services/app-connection/northflank";
|
||||
import { AuthMode } from "@app/services/auth/auth-type";
|
||||
|
||||
import { registerAppConnectionEndpoints } from "./app-connection-endpoints";
|
||||
|
||||
export const registerNorthflankConnectionRouter = async (server: FastifyZodProvider) => {
|
||||
registerAppConnectionEndpoints({
|
||||
app: AppConnection.Northflank,
|
||||
server,
|
||||
sanitizedResponseSchema: SanitizedNorthflankConnectionSchema,
|
||||
createSchema: CreateNorthflankConnectionSchema,
|
||||
updateSchema: UpdateNorthflankConnectionSchema
|
||||
});
|
||||
|
||||
// The below endpoints are not exposed and for Infisical App use
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: `/:connectionId/projects`,
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
params: z.object({
|
||||
connectionId: z.string().uuid()
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
projects: z
|
||||
.object({
|
||||
name: z.string(),
|
||||
id: z.string()
|
||||
})
|
||||
.array()
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
handler: async (req) => {
|
||||
const { connectionId } = req.params;
|
||||
const projects = await server.services.appConnection.northflank.listProjects(connectionId, req.permission);
|
||||
return { projects };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: `/:connectionId/projects/:projectId/secret-groups`,
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
params: z.object({
|
||||
connectionId: z.string().uuid(),
|
||||
projectId: z.string()
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
secretGroups: z
|
||||
.object({
|
||||
name: z.string(),
|
||||
id: z.string()
|
||||
})
|
||||
.array()
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
handler: async (req) => {
|
||||
const { connectionId, projectId } = req.params;
|
||||
const secretGroups = await server.services.appConnection.northflank.listSecretGroups(
|
||||
connectionId,
|
||||
projectId,
|
||||
req.permission
|
||||
);
|
||||
return { secretGroups };
|
||||
}
|
||||
});
|
||||
};
|
||||
@@ -42,7 +42,7 @@ export const registerCertificateProfilesRouter = async (server: FastifyZodProvid
|
||||
apiConfig: z
|
||||
.object({
|
||||
autoRenew: z.boolean().default(false),
|
||||
autoRenewDays: z.number().min(1).max(365).optional()
|
||||
renewBeforeDays: z.number().min(1).max(30).optional()
|
||||
})
|
||||
.optional()
|
||||
})
|
||||
@@ -121,9 +121,7 @@ export const registerCertificateProfilesRouter = async (server: FastifyZodProvid
|
||||
limit: z.coerce.number().min(1).max(100).default(20),
|
||||
search: z.string().optional(),
|
||||
enrollmentType: z.nativeEnum(EnrollmentType).optional(),
|
||||
caId: z.string().uuid().optional(),
|
||||
includeMetrics: z.coerce.boolean().optional().default(false),
|
||||
expiringDays: z.coerce.number().min(1).max(365).optional().default(7)
|
||||
caId: z.string().uuid().optional()
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
@@ -150,7 +148,7 @@ export const registerCertificateProfilesRouter = async (server: FastifyZodProvid
|
||||
.object({
|
||||
id: z.string(),
|
||||
autoRenew: z.boolean(),
|
||||
autoRenewDays: z.number().optional()
|
||||
renewBeforeDays: z.number().optional()
|
||||
})
|
||||
.optional()
|
||||
}).array(),
|
||||
@@ -195,10 +193,6 @@ export const registerCertificateProfilesRouter = async (server: FastifyZodProvid
|
||||
params: z.object({
|
||||
id: z.string().uuid()
|
||||
}),
|
||||
querystring: z.object({
|
||||
includeMetrics: z.coerce.boolean().optional().default(false),
|
||||
expiringDays: z.coerce.number().min(1).max(365).optional().default(7)
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
certificateProfile: PkiCertificateProfilesSchema.extend({
|
||||
@@ -230,17 +224,7 @@ export const registerCertificateProfilesRouter = async (server: FastifyZodProvid
|
||||
.object({
|
||||
id: z.string(),
|
||||
autoRenew: z.boolean(),
|
||||
autoRenewDays: z.number().optional()
|
||||
})
|
||||
.optional(),
|
||||
metrics: z
|
||||
.object({
|
||||
profileId: z.string(),
|
||||
totalCertificates: z.number(),
|
||||
activeCertificates: z.number(),
|
||||
expiredCertificates: z.number(),
|
||||
expiringCertificates: z.number(),
|
||||
revokedCertificates: z.number()
|
||||
renewBeforeDays: z.number().optional()
|
||||
})
|
||||
.optional()
|
||||
})
|
||||
@@ -257,20 +241,6 @@ export const registerCertificateProfilesRouter = async (server: FastifyZodProvid
|
||||
profileId: req.params.id
|
||||
});
|
||||
|
||||
let result = certificateProfile;
|
||||
|
||||
if (req.query.includeMetrics) {
|
||||
const metrics = await server.services.certificateProfile.getProfileMetrics({
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId,
|
||||
profileId: req.params.id,
|
||||
expiringDays: req.query.expiringDays
|
||||
});
|
||||
result = { ...certificateProfile, metrics };
|
||||
}
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
projectId: certificateProfile.projectId,
|
||||
@@ -283,7 +253,7 @@ export const registerCertificateProfilesRouter = async (server: FastifyZodProvid
|
||||
}
|
||||
});
|
||||
|
||||
return { certificateProfile: result };
|
||||
return { certificateProfile };
|
||||
}
|
||||
});
|
||||
|
||||
@@ -355,7 +325,7 @@ export const registerCertificateProfilesRouter = async (server: FastifyZodProvid
|
||||
apiConfig: z
|
||||
.object({
|
||||
autoRenew: z.boolean().default(false),
|
||||
autoRenewDays: z.number().min(1).max(365).optional()
|
||||
renewBeforeDays: z.number().min(1).max(30).optional()
|
||||
})
|
||||
.optional()
|
||||
})
|
||||
|
||||
@@ -323,7 +323,11 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => {
|
||||
.min(1, "Max Shared Secret view count cannot be lower than 1")
|
||||
.max(1000, "Max Shared Secret view count cannot exceed 1000")
|
||||
.nullable()
|
||||
.optional(),
|
||||
blockDuplicateSecretSyncDestinations: z
|
||||
.boolean()
|
||||
.optional()
|
||||
.describe("Block duplicate secret sync destinations across the organization")
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
|
||||
@@ -26,7 +26,7 @@ export const registerSyncPkiEndpoints = ({
|
||||
syncOptions?: Record<string, unknown>;
|
||||
description?: string;
|
||||
isAutoSyncEnabled?: boolean;
|
||||
subscriberId?: string;
|
||||
subscriberId?: string | null;
|
||||
}>;
|
||||
updateSchema: z.ZodType<{
|
||||
connectionId?: string;
|
||||
@@ -35,7 +35,7 @@ export const registerSyncPkiEndpoints = ({
|
||||
syncOptions?: Record<string, unknown>;
|
||||
description?: string;
|
||||
isAutoSyncEnabled?: boolean;
|
||||
subscriberId?: string;
|
||||
subscriberId?: string | null;
|
||||
}>;
|
||||
responseSchema: z.ZodTypeAny;
|
||||
syncOptions: {
|
||||
|
||||
@@ -2,10 +2,11 @@ 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 { readLimit, writeLimit } 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 { CertificateSyncStatus } from "@app/services/certificate-sync/certificate-sync-enums";
|
||||
import { PkiSync } from "@app/services/pki-sync/pki-sync-enums";
|
||||
|
||||
const PkiSyncSchema = z.object({
|
||||
@@ -60,7 +61,8 @@ const PkiSyncSchema = z.object({
|
||||
name: z.string()
|
||||
})
|
||||
.nullable()
|
||||
.optional()
|
||||
.optional(),
|
||||
hasCertificate: z.boolean().optional()
|
||||
});
|
||||
|
||||
const PkiSyncOptionsSchema = z.object({
|
||||
@@ -76,6 +78,27 @@ const PkiSyncOptionsSchema = z.object({
|
||||
minCertificateNameLength: z.number().optional()
|
||||
});
|
||||
|
||||
const PkiSyncCertificateSchema = z.object({
|
||||
id: z.string().uuid(),
|
||||
pkiSyncId: z.string().uuid(),
|
||||
certificateId: z.string().uuid(),
|
||||
syncStatus: z.nativeEnum(CertificateSyncStatus),
|
||||
lastSyncMessage: z.string().nullable().optional(),
|
||||
lastSyncedAt: z.date().nullable().optional(),
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date(),
|
||||
certificateSerialNumber: z.string().optional(),
|
||||
certificateCommonName: z.string().optional(),
|
||||
certificateAltNames: z.string().optional(),
|
||||
certificateStatus: z.string().optional(),
|
||||
certificateNotBefore: z.date().optional(),
|
||||
certificateNotAfter: z.date().optional(),
|
||||
certificateRenewBeforeDays: z.number().nullish(),
|
||||
certificateRenewalError: z.string().nullish(),
|
||||
pkiSyncName: z.string().optional(),
|
||||
pkiSyncDestination: z.string().optional()
|
||||
});
|
||||
|
||||
export const registerPkiSyncRouter = async (server: FastifyZodProvider) => {
|
||||
server.route({
|
||||
method: "GET",
|
||||
@@ -111,7 +134,8 @@ export const registerPkiSyncRouter = async (server: FastifyZodProvider) => {
|
||||
tags: [ApiDocsTags.PkiSyncs],
|
||||
description: "List all the PKI Syncs for the specified project.",
|
||||
querystring: z.object({
|
||||
projectId: z.string().trim().min(1)
|
||||
projectId: z.string().trim().min(1),
|
||||
certificateId: z.string().uuid().optional()
|
||||
}),
|
||||
response: {
|
||||
200: z.object({ pkiSyncs: PkiSyncSchema.array() })
|
||||
@@ -120,11 +144,11 @@ export const registerPkiSyncRouter = async (server: FastifyZodProvider) => {
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const {
|
||||
query: { projectId },
|
||||
query: { projectId, certificateId },
|
||||
permission
|
||||
} = req;
|
||||
|
||||
const pkiSyncs = await server.services.pkiSync.listPkiSyncsByProjectId({ projectId }, permission);
|
||||
const pkiSyncs = await server.services.pkiSync.listPkiSyncsByProjectId({ projectId, certificateId }, permission);
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
@@ -179,4 +203,163 @@ export const registerPkiSyncRouter = async (server: FastifyZodProvider) => {
|
||||
return pkiSync;
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: "/:pkiSyncId/certificates",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
hide: false,
|
||||
tags: [ApiDocsTags.PkiSyncs],
|
||||
description: "List all certificates associated with a PKI Sync.",
|
||||
params: z.object({
|
||||
pkiSyncId: z.string().uuid()
|
||||
}),
|
||||
querystring: z.object({
|
||||
offset: z.coerce.number().min(0).default(0),
|
||||
limit: z.coerce.number().min(1).max(100).default(20)
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
certificates: PkiSyncCertificateSchema.array(),
|
||||
totalCount: z.number()
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const { pkiSyncId } = req.params;
|
||||
const { offset, limit } = req.query;
|
||||
|
||||
const { certificates, totalCount, pkiSyncInfo } = await server.services.pkiSync.listPkiSyncCertificates(
|
||||
{ pkiSyncId, offset, limit },
|
||||
req.permission
|
||||
);
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
projectId: pkiSyncInfo.projectId,
|
||||
event: {
|
||||
type: EventType.GET_PKI_SYNC_CERTIFICATES,
|
||||
metadata: {
|
||||
syncId: pkiSyncId,
|
||||
destination: pkiSyncInfo.destination,
|
||||
count: certificates.length,
|
||||
certificateIds: certificates.map((c) => c.certificateId)
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return { certificates, totalCount };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "POST",
|
||||
url: "/:pkiSyncId/certificates",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
schema: {
|
||||
hide: false,
|
||||
tags: [ApiDocsTags.PkiSyncs],
|
||||
description: "Add certificates to a PKI Sync.",
|
||||
params: z.object({
|
||||
pkiSyncId: z.string().uuid()
|
||||
}),
|
||||
body: z.object({
|
||||
certificateIds: z.array(z.string().uuid()).min(1, "At least one certificate ID is required")
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
addedCertificates: z.array(
|
||||
z.object({
|
||||
id: z.string().uuid(),
|
||||
pkiSyncId: z.string().uuid(),
|
||||
certificateId: z.string().uuid(),
|
||||
syncStatus: z.string().default("pending").optional().nullable(),
|
||||
lastSyncMessage: z.string().optional().nullable(),
|
||||
lastSyncedAt: z.date().optional().nullable(),
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date()
|
||||
})
|
||||
)
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const { pkiSyncId } = req.params;
|
||||
const { certificateIds } = req.body;
|
||||
|
||||
const { addedCertificates, pkiSyncInfo } = await server.services.pkiSync.addCertificatesToPkiSync(
|
||||
{ pkiSyncId, certificateIds },
|
||||
req.permission
|
||||
);
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
projectId: pkiSyncInfo.projectId,
|
||||
event: {
|
||||
type: EventType.UPDATE_PKI_SYNC,
|
||||
metadata: {
|
||||
pkiSyncId,
|
||||
name: pkiSyncInfo.name
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return { addedCertificates };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "DELETE",
|
||||
url: "/:pkiSyncId/certificates",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
schema: {
|
||||
hide: false,
|
||||
tags: [ApiDocsTags.PkiSyncs],
|
||||
description: "Remove certificates from a PKI Sync.",
|
||||
params: z.object({
|
||||
pkiSyncId: z.string().uuid()
|
||||
}),
|
||||
body: z.object({
|
||||
certificateIds: z.array(z.string().uuid()).min(1, "At least one certificate ID is required")
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
removedCount: z.number()
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const { pkiSyncId } = req.params;
|
||||
const { certificateIds } = req.body;
|
||||
|
||||
const { removedCount, pkiSyncInfo } = await server.services.pkiSync.removeCertificatesFromPkiSync(
|
||||
{ pkiSyncId, certificateIds },
|
||||
req.permission
|
||||
);
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
projectId: pkiSyncInfo.projectId,
|
||||
event: {
|
||||
type: EventType.UPDATE_PKI_SYNC,
|
||||
metadata: {
|
||||
pkiSyncId,
|
||||
name: pkiSyncInfo.name
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return { removedCount };
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
@@ -1201,12 +1201,17 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
|
||||
querystring: z.object({
|
||||
friendlyName: z.string().optional().describe(PROJECTS.LIST_CERTIFICATES.friendlyName),
|
||||
commonName: z.string().optional().describe(PROJECTS.LIST_CERTIFICATES.commonName),
|
||||
offset: z.coerce.number().min(0).max(100).default(0).describe(PROJECTS.LIST_CERTIFICATES.offset),
|
||||
limit: z.coerce.number().min(1).max(100).default(25).describe(PROJECTS.LIST_CERTIFICATES.limit)
|
||||
offset: z.coerce.number().min(0).default(0).describe(PROJECTS.LIST_CERTIFICATES.offset),
|
||||
limit: z.coerce.number().min(1).max(100).default(25).describe(PROJECTS.LIST_CERTIFICATES.limit),
|
||||
forPkiSync: z.coerce
|
||||
.boolean()
|
||||
.default(false)
|
||||
.optional()
|
||||
.describe("Retrieve only certificates available for PKI sync")
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
certificates: z.array(CertificatesSchema),
|
||||
certificates: z.array(CertificatesSchema.extend({ hasPrivateKey: z.boolean() })),
|
||||
totalCount: z.number()
|
||||
})
|
||||
}
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
import { ChefSyncSchema, CreateChefSyncSchema, UpdateChefSyncSchema } from "@app/services/secret-sync/chef";
|
||||
import { SecretSync } from "@app/services/secret-sync/secret-sync-enums";
|
||||
|
||||
import { registerSyncSecretsEndpoints } from "./secret-sync-endpoints";
|
||||
|
||||
export const registerChefSyncRouter = async (server: FastifyZodProvider) =>
|
||||
registerSyncSecretsEndpoints({
|
||||
destination: SecretSync.Chef,
|
||||
server,
|
||||
responseSchema: ChefSyncSchema,
|
||||
createSchema: CreateChefSyncSchema,
|
||||
updateSchema: UpdateChefSyncSchema
|
||||
});
|
||||
@@ -10,6 +10,7 @@ import { registerAzureKeyVaultSyncRouter } from "./azure-key-vault-sync-router";
|
||||
import { registerBitbucketSyncRouter } from "./bitbucket-sync-router";
|
||||
import { registerCamundaSyncRouter } from "./camunda-sync-router";
|
||||
import { registerChecklySyncRouter } from "./checkly-sync-router";
|
||||
import { registerChefSyncRouter } from "./chef-sync-router";
|
||||
import { registerCloudflarePagesSyncRouter } from "./cloudflare-pages-sync-router";
|
||||
import { registerCloudflareWorkersSyncRouter } from "./cloudflare-workers-sync-router";
|
||||
import { registerDatabricksSyncRouter } from "./databricks-sync-router";
|
||||
@@ -23,6 +24,7 @@ import { registerHerokuSyncRouter } from "./heroku-sync-router";
|
||||
import { registerHumanitecSyncRouter } from "./humanitec-sync-router";
|
||||
import { registerLaravelForgeSyncRouter } from "./laravel-forge-sync-router";
|
||||
import { registerNetlifySyncRouter } from "./netlify-sync-router";
|
||||
import { registerNorthflankSyncRouter } from "./northflank-sync-router";
|
||||
import { registerRailwaySyncRouter } from "./railway-sync-router";
|
||||
import { registerRenderSyncRouter } from "./render-sync-router";
|
||||
import { registerSupabaseSyncRouter } from "./supabase-sync-router";
|
||||
@@ -64,6 +66,8 @@ export const SECRET_SYNC_REGISTER_ROUTER_MAP: Record<SecretSync, (server: Fastif
|
||||
[SecretSync.Checkly]: registerChecklySyncRouter,
|
||||
[SecretSync.DigitalOceanAppPlatform]: registerDigitalOceanAppPlatformSyncRouter,
|
||||
[SecretSync.Netlify]: registerNetlifySyncRouter,
|
||||
[SecretSync.Northflank]: registerNorthflankSyncRouter,
|
||||
[SecretSync.Bitbucket]: registerBitbucketSyncRouter,
|
||||
[SecretSync.LaravelForge]: registerLaravelForgeSyncRouter
|
||||
[SecretSync.LaravelForge]: registerLaravelForgeSyncRouter,
|
||||
[SecretSync.Chef]: registerChefSyncRouter
|
||||
};
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
import {
|
||||
CreateNorthflankSyncSchema,
|
||||
NorthflankSyncSchema,
|
||||
UpdateNorthflankSyncSchema
|
||||
} from "@app/services/secret-sync/northflank";
|
||||
import { SecretSync } from "@app/services/secret-sync/secret-sync-enums";
|
||||
|
||||
import { registerSyncSecretsEndpoints } from "./secret-sync-endpoints";
|
||||
|
||||
export const registerNorthflankSyncRouter = async (server: FastifyZodProvider) =>
|
||||
registerSyncSecretsEndpoints({
|
||||
destination: SecretSync.Northflank,
|
||||
server,
|
||||
responseSchema: NorthflankSyncSchema,
|
||||
createSchema: CreateNorthflankSyncSchema,
|
||||
updateSchema: UpdateNorthflankSyncSchema
|
||||
});
|
||||
@@ -24,6 +24,7 @@ import { AzureKeyVaultSyncListItemSchema, AzureKeyVaultSyncSchema } from "@app/s
|
||||
import { BitbucketSyncListItemSchema, BitbucketSyncSchema } from "@app/services/secret-sync/bitbucket";
|
||||
import { CamundaSyncListItemSchema, CamundaSyncSchema } from "@app/services/secret-sync/camunda";
|
||||
import { ChecklySyncListItemSchema, ChecklySyncSchema } from "@app/services/secret-sync/checkly/checkly-sync-schemas";
|
||||
import { ChefSyncListItemSchema, ChefSyncSchema } from "@app/services/secret-sync/chef";
|
||||
import {
|
||||
CloudflarePagesSyncListItemSchema,
|
||||
CloudflarePagesSyncSchema
|
||||
@@ -46,6 +47,7 @@ import { HerokuSyncListItemSchema, HerokuSyncSchema } from "@app/services/secret
|
||||
import { HumanitecSyncListItemSchema, HumanitecSyncSchema } from "@app/services/secret-sync/humanitec";
|
||||
import { LaravelForgeSyncListItemSchema, LaravelForgeSyncSchema } from "@app/services/secret-sync/laravel-forge";
|
||||
import { NetlifySyncListItemSchema, NetlifySyncSchema } from "@app/services/secret-sync/netlify";
|
||||
import { NorthflankSyncListItemSchema, NorthflankSyncSchema } from "@app/services/secret-sync/northflank";
|
||||
import { RailwaySyncListItemSchema, RailwaySyncSchema } from "@app/services/secret-sync/railway/railway-sync-schemas";
|
||||
import { RenderSyncListItemSchema, RenderSyncSchema } from "@app/services/secret-sync/render/render-sync-schemas";
|
||||
import { SupabaseSyncListItemSchema, SupabaseSyncSchema } from "@app/services/secret-sync/supabase";
|
||||
@@ -85,8 +87,10 @@ const SecretSyncSchema = z.discriminatedUnion("destination", [
|
||||
ChecklySyncSchema,
|
||||
DigitalOceanAppPlatformSyncSchema,
|
||||
NetlifySyncSchema,
|
||||
NorthflankSyncSchema,
|
||||
BitbucketSyncSchema,
|
||||
LaravelForgeSyncSchema
|
||||
LaravelForgeSyncSchema,
|
||||
ChefSyncSchema
|
||||
]);
|
||||
|
||||
const SecretSyncOptionsSchema = z.discriminatedUnion("destination", [
|
||||
@@ -119,8 +123,10 @@ const SecretSyncOptionsSchema = z.discriminatedUnion("destination", [
|
||||
ChecklySyncListItemSchema,
|
||||
SupabaseSyncListItemSchema,
|
||||
NetlifySyncListItemSchema,
|
||||
NorthflankSyncListItemSchema,
|
||||
BitbucketSyncListItemSchema,
|
||||
LaravelForgeSyncListItemSchema
|
||||
LaravelForgeSyncListItemSchema,
|
||||
ChefSyncListItemSchema
|
||||
]);
|
||||
|
||||
export const registerSecretSyncRouter = async (server: FastifyZodProvider) => {
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
// All the any rules are disabled because passport typesense with fastify is really poor
|
||||
|
||||
import { Authenticator } from "@fastify/passport";
|
||||
import { requestContext } from "@fastify/request-context";
|
||||
import fastifySession from "@fastify/session";
|
||||
import RedisStore from "connect-redis";
|
||||
import { CronJob } from "cron";
|
||||
@@ -21,6 +22,7 @@ import { BadRequestError, NotFoundError } from "@app/lib/errors";
|
||||
import { logger } from "@app/lib/logger";
|
||||
import { ms } from "@app/lib/ms";
|
||||
import { fetchGithubEmails, fetchGithubUser } from "@app/lib/requests/github";
|
||||
import { AuthAttemptAuthMethod, AuthAttemptAuthResult, authAttemptCounter } from "@app/lib/telemetry/metrics";
|
||||
import { authRateLimit } from "@app/server/config/rateLimiter";
|
||||
import { addAuthOriginDomainCookie } from "@app/server/lib/cookie";
|
||||
import { AuthMethod } from "@app/services/auth/auth-type";
|
||||
@@ -51,30 +53,54 @@ export const registerOauthMiddlewares = (server: FastifyZodProvider) => {
|
||||
},
|
||||
// eslint-disable-next-line
|
||||
async (req, _accessToken, _refreshToken, profile, cb) => {
|
||||
try {
|
||||
// @ts-expect-error this is because this is express type and not fastify
|
||||
const callbackPort = req.session.get("callbackPort");
|
||||
// @ts-expect-error this is because this is express type and not fastify
|
||||
const orgSlug = req.session.get("orgSlug");
|
||||
// @ts-expect-error this is because this is express type and not fastify
|
||||
const callbackPort = req.session.get("callbackPort");
|
||||
// @ts-expect-error this is because this is express type and not fastify
|
||||
const orgSlug = req.session.get("orgSlug");
|
||||
|
||||
const email = profile?.emails?.[0]?.value;
|
||||
if (!email)
|
||||
throw new NotFoundError({
|
||||
message: "Email not found",
|
||||
name: "OauthGoogleRegister"
|
||||
const email = profile?.emails?.[0]?.value;
|
||||
if (!email)
|
||||
throw new NotFoundError({
|
||||
message: "Email not found",
|
||||
name: "OauthGoogleRegister"
|
||||
});
|
||||
|
||||
try {
|
||||
const { isUserCompleted, providerAuthToken, user, orgId, orgName } =
|
||||
await server.services.login.oauth2Login({
|
||||
email,
|
||||
firstName: profile?.name?.givenName || "",
|
||||
lastName: profile?.name?.familyName || "",
|
||||
authMethod: AuthMethod.GOOGLE,
|
||||
callbackPort,
|
||||
orgSlug
|
||||
});
|
||||
|
||||
const { isUserCompleted, providerAuthToken } = await server.services.login.oauth2Login({
|
||||
email,
|
||||
firstName: profile?.name?.givenName || "",
|
||||
lastName: profile?.name?.familyName || "",
|
||||
authMethod: AuthMethod.GOOGLE,
|
||||
callbackPort,
|
||||
orgSlug
|
||||
});
|
||||
if (appCfg.OTEL_TELEMETRY_COLLECTION_ENABLED) {
|
||||
authAttemptCounter.add(1, {
|
||||
"infisical.user.email": email,
|
||||
"infisical.user.id": user.id,
|
||||
"infisical.organization.id": orgId,
|
||||
"infisical.organization.name": orgName,
|
||||
"infisical.auth.method": AuthAttemptAuthMethod.GOOGLE,
|
||||
"infisical.auth.result": AuthAttemptAuthResult.SUCCESS,
|
||||
"client.address": requestContext.get("ip"),
|
||||
"user_agent.original": requestContext.get("userAgent")
|
||||
});
|
||||
}
|
||||
|
||||
cb(null, { isUserCompleted, providerAuthToken });
|
||||
} catch (error) {
|
||||
logger.error(error);
|
||||
if (appCfg.OTEL_TELEMETRY_COLLECTION_ENABLED) {
|
||||
authAttemptCounter.add(1, {
|
||||
"infisical.user.email": email,
|
||||
"infisical.auth.method": AuthAttemptAuthMethod.GOOGLE,
|
||||
"infisical.auth.result": AuthAttemptAuthResult.FAILURE,
|
||||
"client.address": requestContext.get("ip"),
|
||||
"user_agent.original": requestContext.get("userAgent")
|
||||
});
|
||||
}
|
||||
cb(error as Error, false);
|
||||
}
|
||||
}
|
||||
@@ -101,27 +127,50 @@ export const registerOauthMiddlewares = (server: FastifyZodProvider) => {
|
||||
},
|
||||
// eslint-disable-next-line
|
||||
async (req: any, accessToken: string, _refreshToken: string, _profile: any, done: Function) => {
|
||||
const ghEmails = await fetchGithubEmails(accessToken);
|
||||
const { email } = ghEmails.filter((gitHubEmail) => gitHubEmail.primary)[0];
|
||||
|
||||
if (!email) throw new Error("No primary email found");
|
||||
|
||||
try {
|
||||
const ghEmails = await fetchGithubEmails(accessToken);
|
||||
const { email } = ghEmails.filter((gitHubEmail) => gitHubEmail.primary)[0];
|
||||
|
||||
if (!email) throw new Error("No primary email found");
|
||||
|
||||
// profile does not get automatically populated so we need to manually fetch user info
|
||||
const user = await fetchGithubUser(accessToken);
|
||||
const githubUser = await fetchGithubUser(accessToken);
|
||||
|
||||
const callbackPort = req.session.get("callbackPort");
|
||||
|
||||
const { isUserCompleted, providerAuthToken } = await server.services.login.oauth2Login({
|
||||
email,
|
||||
firstName: user.name || user.login,
|
||||
lastName: "",
|
||||
authMethod: AuthMethod.GITHUB,
|
||||
callbackPort
|
||||
});
|
||||
const { isUserCompleted, providerAuthToken, user, orgId, orgName } =
|
||||
await server.services.login.oauth2Login({
|
||||
email,
|
||||
firstName: githubUser.name || githubUser.login,
|
||||
lastName: "",
|
||||
authMethod: AuthMethod.GITHUB,
|
||||
callbackPort
|
||||
});
|
||||
|
||||
if (appCfg.OTEL_TELEMETRY_COLLECTION_ENABLED) {
|
||||
authAttemptCounter.add(1, {
|
||||
"infisical.user.email": email,
|
||||
"infisical.user.id": user.id,
|
||||
"infisical.organization.id": orgId,
|
||||
"infisical.organization.name": orgName,
|
||||
"infisical.auth.method": AuthAttemptAuthMethod.GITHUB,
|
||||
"infisical.auth.result": AuthAttemptAuthResult.SUCCESS,
|
||||
"client.address": requestContext.get("ip"),
|
||||
"user_agent.original": requestContext.get("userAgent")
|
||||
});
|
||||
}
|
||||
|
||||
done(null, { isUserCompleted, providerAuthToken, externalProviderAccessToken: accessToken });
|
||||
} catch (err) {
|
||||
if (appCfg.OTEL_TELEMETRY_COLLECTION_ENABLED) {
|
||||
authAttemptCounter.add(1, {
|
||||
"infisical.user.email": email,
|
||||
"infisical.auth.method": AuthAttemptAuthMethod.GITHUB,
|
||||
"infisical.auth.result": AuthAttemptAuthResult.FAILURE,
|
||||
"client.address": requestContext.get("ip"),
|
||||
"user_agent.original": requestContext.get("userAgent")
|
||||
});
|
||||
}
|
||||
logger.error(err);
|
||||
done(err as Error, false);
|
||||
}
|
||||
@@ -147,20 +196,45 @@ export const registerOauthMiddlewares = (server: FastifyZodProvider) => {
|
||||
pkce: true
|
||||
},
|
||||
async (req: any, _accessToken: string, _refreshToken: string, profile: any, cb: any) => {
|
||||
const email = profile.emails[0].value;
|
||||
|
||||
try {
|
||||
const callbackPort = req.session.get("callbackPort");
|
||||
|
||||
const email = profile.emails[0].value;
|
||||
const { isUserCompleted, providerAuthToken } = await server.services.login.oauth2Login({
|
||||
email,
|
||||
firstName: profile.displayName || profile.username || "",
|
||||
lastName: "",
|
||||
authMethod: AuthMethod.GITLAB,
|
||||
callbackPort
|
||||
});
|
||||
const { isUserCompleted, providerAuthToken, user, orgId, orgName } =
|
||||
await server.services.login.oauth2Login({
|
||||
email,
|
||||
firstName: profile.displayName || profile.username || "",
|
||||
lastName: "",
|
||||
authMethod: AuthMethod.GITLAB,
|
||||
callbackPort
|
||||
});
|
||||
|
||||
if (appCfg.OTEL_TELEMETRY_COLLECTION_ENABLED) {
|
||||
authAttemptCounter.add(1, {
|
||||
"infisical.user.email": email,
|
||||
"infisical.user.id": user.id,
|
||||
"infisical.organization.id": orgId,
|
||||
"infisical.organization.name": orgName,
|
||||
"infisical.auth.method": AuthAttemptAuthMethod.GITLAB,
|
||||
"infisical.auth.result": AuthAttemptAuthResult.SUCCESS,
|
||||
"client.address": requestContext.get("ip"),
|
||||
"user_agent.original": requestContext.get("userAgent")
|
||||
});
|
||||
}
|
||||
|
||||
return cb(null, { isUserCompleted, providerAuthToken });
|
||||
} catch (error) {
|
||||
if (appCfg.OTEL_TELEMETRY_COLLECTION_ENABLED) {
|
||||
authAttemptCounter.add(1, {
|
||||
"infisical.user.email": email,
|
||||
"infisical.auth.method": AuthAttemptAuthMethod.GITLAB,
|
||||
"infisical.auth.result": AuthAttemptAuthResult.FAILURE,
|
||||
"client.address": requestContext.get("ip"),
|
||||
"user_agent.original": requestContext.get("userAgent")
|
||||
});
|
||||
}
|
||||
|
||||
logger.error(error);
|
||||
cb(error as Error, false);
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
CertKeyUsageType,
|
||||
CertSubjectAlternativeNameType
|
||||
} from "@app/services/certificate-common/certificate-constants";
|
||||
import { extractCertificateRequestFromCSR } from "@app/services/certificate-common/certificate-csr-utils";
|
||||
import { mapEnumsForValidation } from "@app/services/certificate-common/certificate-utils";
|
||||
import { validateTemplateRegexField } from "@app/services/certificate-template/certificate-template-validators";
|
||||
|
||||
@@ -84,8 +85,8 @@ export const registerCertificatesRouter = async (server: FastifyZodProvider) =>
|
||||
})
|
||||
)
|
||||
.optional(),
|
||||
signatureAlgorithm: z.nativeEnum(CertSignatureAlgorithm).optional(),
|
||||
keyAlgorithm: z.nativeEnum(CertKeyAlgorithm).optional()
|
||||
signatureAlgorithm: z.nativeEnum(CertSignatureAlgorithm),
|
||||
keyAlgorithm: z.nativeEnum(CertKeyAlgorithm)
|
||||
})
|
||||
.refine(validateTtlAndDateFields, {
|
||||
message:
|
||||
@@ -169,9 +170,7 @@ export const registerCertificatesRouter = async (server: FastifyZodProvider) =>
|
||||
.min(1, "TTL cannot be empty")
|
||||
.refine((val) => ms(val) > 0, "TTL must be a positive number"),
|
||||
notBefore: validateCaDateField.optional(),
|
||||
notAfter: validateCaDateField.optional(),
|
||||
signatureAlgorithm: z.nativeEnum(CertSignatureAlgorithm).optional(),
|
||||
keyAlgorithm: z.nativeEnum(CertKeyAlgorithm).optional()
|
||||
notAfter: validateCaDateField.optional()
|
||||
})
|
||||
.refine(validateTtlAndDateFields, {
|
||||
message:
|
||||
@@ -192,6 +191,8 @@ export const registerCertificatesRouter = async (server: FastifyZodProvider) =>
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const certificateRequest = extractCertificateRequestFromCSR(req.body.csr);
|
||||
|
||||
const data = await server.services.certificateV3.signCertificateFromProfile({
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
@@ -203,9 +204,7 @@ export const registerCertificatesRouter = async (server: FastifyZodProvider) =>
|
||||
ttl: req.body.ttl
|
||||
},
|
||||
notBefore: req.body.notBefore ? new Date(req.body.notBefore) : undefined,
|
||||
notAfter: req.body.notAfter ? new Date(req.body.notAfter) : undefined,
|
||||
signatureAlgorithm: req.body.signatureAlgorithm,
|
||||
keyAlgorithm: req.body.keyAlgorithm
|
||||
notAfter: req.body.notAfter ? new Date(req.body.notAfter) : undefined
|
||||
});
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
@@ -217,7 +216,7 @@ export const registerCertificatesRouter = async (server: FastifyZodProvider) =>
|
||||
certificateProfileId: req.body.profileId,
|
||||
certificateId: data.certificateId,
|
||||
profileName: data.profileName,
|
||||
commonName: ""
|
||||
commonName: certificateRequest.commonName || ""
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -260,8 +259,8 @@ export const registerCertificatesRouter = async (server: FastifyZodProvider) =>
|
||||
notBefore: validateCaDateField.optional(),
|
||||
notAfter: validateCaDateField.optional(),
|
||||
commonName: validateTemplateRegexField.optional(),
|
||||
signatureAlgorithm: z.nativeEnum(CertSignatureAlgorithm).optional(),
|
||||
keyAlgorithm: z.nativeEnum(CertKeyAlgorithm).optional()
|
||||
signatureAlgorithm: z.nativeEnum(CertSignatureAlgorithm),
|
||||
keyAlgorithm: z.nativeEnum(CertKeyAlgorithm)
|
||||
})
|
||||
.refine(validateTtlAndDateFields, {
|
||||
message:
|
||||
@@ -343,4 +342,145 @@ export const registerCertificatesRouter = async (server: FastifyZodProvider) =>
|
||||
return data;
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "POST",
|
||||
url: "/:certificateId/renew",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
schema: {
|
||||
hide: false,
|
||||
tags: [ApiDocsTags.PkiCertificates],
|
||||
params: z.object({
|
||||
certificateId: z.string().uuid()
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
certificate: z.string().trim(),
|
||||
issuingCaCertificate: z.string().trim(),
|
||||
certificateChain: z.string().trim(),
|
||||
privateKey: z.string().trim().optional(),
|
||||
serialNumber: z.string().trim(),
|
||||
certificateId: z.string()
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const data = await server.services.certificateV3.renewCertificate({
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId,
|
||||
certificateId: req.params.certificateId
|
||||
});
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
projectId: data.projectId,
|
||||
event: {
|
||||
type: EventType.RENEW_CERTIFICATE,
|
||||
metadata: {
|
||||
originalCertificateId: req.params.certificateId,
|
||||
newCertificateId: data.certificateId,
|
||||
profileName: data.profileName,
|
||||
commonName: data.commonName
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return data;
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "PATCH",
|
||||
url: "/:certificateId/config",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
schema: {
|
||||
hide: false,
|
||||
tags: [ApiDocsTags.PkiCertificates],
|
||||
params: z.object({
|
||||
certificateId: z.string().uuid()
|
||||
}),
|
||||
body: z
|
||||
.object({
|
||||
renewBeforeDays: z.number().int().min(1).max(30).optional(),
|
||||
enableAutoRenewal: z.boolean().optional()
|
||||
})
|
||||
.refine((data) => !(data.renewBeforeDays !== undefined && data.enableAutoRenewal === false), {
|
||||
message: "Cannot specify both renewBeforeDays and enableAutoRenewal=false"
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
message: z.string(),
|
||||
renewBeforeDays: z.number().optional()
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
if (req.body.enableAutoRenewal === false) {
|
||||
const data = await server.services.certificateV3.disableRenewalConfig({
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId,
|
||||
certificateId: req.params.certificateId
|
||||
});
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
projectId: data.projectId,
|
||||
event: {
|
||||
type: EventType.DISABLE_CERTIFICATE_RENEWAL_CONFIG,
|
||||
metadata: {
|
||||
certificateId: req.params.certificateId,
|
||||
commonName: data.commonName
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
message: "Auto-renewal disabled successfully"
|
||||
};
|
||||
}
|
||||
|
||||
if (req.body.renewBeforeDays !== undefined) {
|
||||
const data = await server.services.certificateV3.updateRenewalConfig({
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId,
|
||||
certificateId: req.params.certificateId,
|
||||
renewBeforeDays: req.body.renewBeforeDays
|
||||
});
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
projectId: data.projectId,
|
||||
event: {
|
||||
type: EventType.UPDATE_CERTIFICATE_RENEWAL_CONFIG,
|
||||
metadata: {
|
||||
certificateId: req.params.certificateId,
|
||||
renewBeforeDays: req.body.renewBeforeDays.toString(),
|
||||
commonName: data.commonName
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
message: "Certificate configuration updated successfully",
|
||||
renewBeforeDays: data.renewBeforeDays
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
message: "No configuration changes requested"
|
||||
};
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
@@ -393,6 +393,56 @@ export const registerExternalMigrationRouter = async (server: FastifyZodProvider
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: "/vault/kubernetes-roles",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
querystring: z.object({
|
||||
namespace: z.string(),
|
||||
mountPath: z.string()
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
roles: z.array(
|
||||
z.object({
|
||||
name: z.string(),
|
||||
mountPath: z.string(),
|
||||
allowed_kubernetes_namespaces: z.array(z.string()).nullish(),
|
||||
allowed_kubernetes_namespace_selector: z.string().nullish(),
|
||||
token_max_ttl: z.number().nullish(),
|
||||
token_default_ttl: z.number().nullish(),
|
||||
token_default_audiences: z.array(z.string()).nullish(),
|
||||
service_account_name: z.string().nullish(),
|
||||
kubernetes_role_name: z.string().nullish(),
|
||||
kubernetes_role_type: z.string().nullish(),
|
||||
generated_role_rules: z.string().nullish(),
|
||||
name_template: z.string().nullish(),
|
||||
extra_annotations: z.record(z.string()).nullish(),
|
||||
extra_labels: z.record(z.string()).nullish(),
|
||||
config: z.object({
|
||||
kubernetes_host: z.string(),
|
||||
kubernetes_ca_cert: z.string().nullish()
|
||||
})
|
||||
})
|
||||
)
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
handler: async (req) => {
|
||||
const roles = await server.services.migration.getVaultKubernetesRoles({
|
||||
actor: req.permission,
|
||||
namespace: req.query.namespace,
|
||||
mountPath: req.query.mountPath
|
||||
});
|
||||
|
||||
return { roles };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: "/vault/secret-paths",
|
||||
|
||||
@@ -38,7 +38,9 @@ export enum AppConnection {
|
||||
Netlify = "netlify",
|
||||
Okta = "okta",
|
||||
Redis = "redis",
|
||||
LaravelForge = "laravel-forge"
|
||||
LaravelForge = "laravel-forge",
|
||||
Chef = "chef",
|
||||
Northflank = "northflank"
|
||||
}
|
||||
|
||||
export enum AWSRegion {
|
||||
|
||||
@@ -68,6 +68,7 @@ import {
|
||||
} from "./bitbucket";
|
||||
import { CamundaConnectionMethod, getCamundaConnectionListItem, validateCamundaConnectionCredentials } from "./camunda";
|
||||
import { ChecklyConnectionMethod, getChecklyConnectionListItem, validateChecklyConnectionCredentials } from "./checkly";
|
||||
import { ChefConnectionMethod, getChefConnectionListItem, validateChefConnectionCredentials } from "./chef";
|
||||
import { CloudflareConnectionMethod } from "./cloudflare/cloudflare-connection-enum";
|
||||
import {
|
||||
getCloudflareConnectionListItem,
|
||||
@@ -113,6 +114,11 @@ import { getMsSqlConnectionListItem, MsSqlConnectionMethod } from "./mssql";
|
||||
import { MySqlConnectionMethod } from "./mysql/mysql-connection-enums";
|
||||
import { getMySqlConnectionListItem } from "./mysql/mysql-connection-fns";
|
||||
import { getNetlifyConnectionListItem, validateNetlifyConnectionCredentials } from "./netlify";
|
||||
import {
|
||||
getNorthflankConnectionListItem,
|
||||
NorthflankConnectionMethod,
|
||||
validateNorthflankConnectionCredentials
|
||||
} from "./northflank";
|
||||
import { getOktaConnectionListItem, OktaConnectionMethod, validateOktaConnectionCredentials } from "./okta";
|
||||
import { getPostgresConnectionListItem, PostgresConnectionMethod } from "./postgres";
|
||||
import { getRailwayConnectionListItem, validateRailwayConnectionCredentials } from "./railway";
|
||||
@@ -203,8 +209,10 @@ export const listAppConnectionOptions = (projectType?: ProjectType) => {
|
||||
getSupabaseConnectionListItem(),
|
||||
getDigitalOceanConnectionListItem(),
|
||||
getNetlifyConnectionListItem(),
|
||||
getNorthflankConnectionListItem(),
|
||||
getOktaConnectionListItem(),
|
||||
getRedisConnectionListItem()
|
||||
getRedisConnectionListItem(),
|
||||
getChefConnectionListItem()
|
||||
]
|
||||
.filter((option) => {
|
||||
switch (projectType) {
|
||||
@@ -332,8 +340,10 @@ export const validateAppConnectionCredentials = async (
|
||||
[AppConnection.Checkly]: validateChecklyConnectionCredentials as TAppConnectionCredentialsValidator,
|
||||
[AppConnection.Supabase]: validateSupabaseConnectionCredentials as TAppConnectionCredentialsValidator,
|
||||
[AppConnection.DigitalOcean]: validateDigitalOceanConnectionCredentials as TAppConnectionCredentialsValidator,
|
||||
[AppConnection.Okta]: validateOktaConnectionCredentials as TAppConnectionCredentialsValidator,
|
||||
[AppConnection.Netlify]: validateNetlifyConnectionCredentials as TAppConnectionCredentialsValidator,
|
||||
[AppConnection.Northflank]: validateNorthflankConnectionCredentials as TAppConnectionCredentialsValidator,
|
||||
[AppConnection.Okta]: validateOktaConnectionCredentials as TAppConnectionCredentialsValidator,
|
||||
[AppConnection.Chef]: validateChefConnectionCredentials as TAppConnectionCredentialsValidator,
|
||||
[AppConnection.Redis]: validateRedisConnectionCredentials as TAppConnectionCredentialsValidator
|
||||
};
|
||||
|
||||
@@ -345,6 +355,8 @@ export const getAppConnectionMethodName = (method: TAppConnection["method"]) =>
|
||||
case GitHubConnectionMethod.App:
|
||||
case GitHubRadarConnectionMethod.App:
|
||||
return "GitHub App";
|
||||
case GitHubConnectionMethod.Pat:
|
||||
return "Personal Access Token";
|
||||
case AzureKeyVaultConnectionMethod.OAuth:
|
||||
case AzureAppConfigurationConnectionMethod.OAuth:
|
||||
case AzureClientSecretsConnectionMethod.OAuth:
|
||||
@@ -374,6 +386,7 @@ export const getAppConnectionMethodName = (method: TAppConnection["method"]) =>
|
||||
case BitbucketConnectionMethod.ApiToken:
|
||||
case ZabbixConnectionMethod.ApiToken:
|
||||
case DigitalOceanConnectionMethod.ApiToken:
|
||||
case NorthflankConnectionMethod.ApiToken:
|
||||
case OktaConnectionMethod.ApiToken:
|
||||
case LaravelForgeConnectionMethod.ApiToken:
|
||||
return "API Token";
|
||||
@@ -399,6 +412,8 @@ export const getAppConnectionMethodName = (method: TAppConnection["method"]) =>
|
||||
case RenderConnectionMethod.ApiKey:
|
||||
case ChecklyConnectionMethod.ApiKey:
|
||||
return "API Key";
|
||||
case ChefConnectionMethod.UserKey:
|
||||
return "User Key";
|
||||
case SupabaseConnectionMethod.AccessToken:
|
||||
return "Access Token";
|
||||
default:
|
||||
@@ -470,9 +485,11 @@ export const TRANSITION_CONNECTION_CREDENTIALS_TO_PLATFORM: Record<
|
||||
[AppConnection.Supabase]: platformManagedCredentialsNotSupported,
|
||||
[AppConnection.DigitalOcean]: platformManagedCredentialsNotSupported,
|
||||
[AppConnection.Netlify]: platformManagedCredentialsNotSupported,
|
||||
[AppConnection.Northflank]: platformManagedCredentialsNotSupported,
|
||||
[AppConnection.Okta]: platformManagedCredentialsNotSupported,
|
||||
[AppConnection.Redis]: platformManagedCredentialsNotSupported,
|
||||
[AppConnection.LaravelForge]: platformManagedCredentialsNotSupported
|
||||
[AppConnection.LaravelForge]: platformManagedCredentialsNotSupported,
|
||||
[AppConnection.Chef]: platformManagedCredentialsNotSupported
|
||||
};
|
||||
|
||||
export const enterpriseAppCheck = async (
|
||||
|
||||
@@ -40,7 +40,9 @@ export const APP_CONNECTION_NAME_MAP: Record<AppConnection, string> = {
|
||||
[AppConnection.DigitalOcean]: "DigitalOcean App Platform",
|
||||
[AppConnection.Netlify]: "Netlify",
|
||||
[AppConnection.Okta]: "Okta",
|
||||
[AppConnection.Redis]: "Redis"
|
||||
[AppConnection.Redis]: "Redis",
|
||||
[AppConnection.Chef]: "Chef",
|
||||
[AppConnection.Northflank]: "Northflank"
|
||||
};
|
||||
|
||||
export const APP_CONNECTION_PLAN_MAP: Record<AppConnection, AppConnectionPlanType> = {
|
||||
@@ -83,5 +85,7 @@ export const APP_CONNECTION_PLAN_MAP: Record<AppConnection, AppConnectionPlanTyp
|
||||
[AppConnection.DigitalOcean]: AppConnectionPlanType.Regular,
|
||||
[AppConnection.Netlify]: AppConnectionPlanType.Regular,
|
||||
[AppConnection.Okta]: AppConnectionPlanType.Regular,
|
||||
[AppConnection.Redis]: AppConnectionPlanType.Regular
|
||||
[AppConnection.Redis]: AppConnectionPlanType.Regular,
|
||||
[AppConnection.Chef]: AppConnectionPlanType.Regular,
|
||||
[AppConnection.Northflank]: AppConnectionPlanType.Regular
|
||||
};
|
||||
|
||||
@@ -67,6 +67,8 @@ import { ValidateCamundaConnectionCredentialsSchema } from "./camunda";
|
||||
import { camundaConnectionService } from "./camunda/camunda-connection-service";
|
||||
import { ValidateChecklyConnectionCredentialsSchema } from "./checkly";
|
||||
import { checklyConnectionService } from "./checkly/checkly-connection-service";
|
||||
import { ValidateChefConnectionCredentialsSchema } from "./chef";
|
||||
import { chefConnectionService } from "./chef/chef-connection-service";
|
||||
import { ValidateCloudflareConnectionCredentialsSchema } from "./cloudflare/cloudflare-connection-schema";
|
||||
import { cloudflareConnectionService } from "./cloudflare/cloudflare-connection-service";
|
||||
import { ValidateDatabricksConnectionCredentialsSchema } from "./databricks";
|
||||
@@ -96,6 +98,8 @@ import { ValidateMsSqlConnectionCredentialsSchema } from "./mssql";
|
||||
import { ValidateMySqlConnectionCredentialsSchema } from "./mysql";
|
||||
import { ValidateNetlifyConnectionCredentialsSchema } from "./netlify";
|
||||
import { netlifyConnectionService } from "./netlify/netlify-connection-service";
|
||||
import { ValidateNorthflankConnectionCredentialsSchema } from "./northflank";
|
||||
import { northflankConnectionService } from "./northflank/northflank-connection-service";
|
||||
import { ValidateOktaConnectionCredentialsSchema } from "./okta";
|
||||
import { oktaConnectionService } from "./okta/okta-connection-service";
|
||||
import { ValidatePostgresConnectionCredentialsSchema } from "./postgres";
|
||||
@@ -170,8 +174,10 @@ const VALIDATE_APP_CONNECTION_CREDENTIALS_MAP: Record<AppConnection, TValidateAp
|
||||
[AppConnection.Supabase]: ValidateSupabaseConnectionCredentialsSchema,
|
||||
[AppConnection.DigitalOcean]: ValidateDigitalOceanConnectionCredentialsSchema,
|
||||
[AppConnection.Netlify]: ValidateNetlifyConnectionCredentialsSchema,
|
||||
[AppConnection.Northflank]: ValidateNorthflankConnectionCredentialsSchema,
|
||||
[AppConnection.Okta]: ValidateOktaConnectionCredentialsSchema,
|
||||
[AppConnection.Redis]: ValidateRedisConnectionCredentialsSchema
|
||||
[AppConnection.Redis]: ValidateRedisConnectionCredentialsSchema,
|
||||
[AppConnection.Chef]: ValidateChefConnectionCredentialsSchema
|
||||
};
|
||||
|
||||
export const appConnectionServiceFactory = ({
|
||||
@@ -876,7 +882,9 @@ export const appConnectionServiceFactory = ({
|
||||
supabase: supabaseConnectionService(connectAppConnectionById),
|
||||
digitalOcean: digitalOceanAppPlatformConnectionService(connectAppConnectionById),
|
||||
netlify: netlifyConnectionService(connectAppConnectionById),
|
||||
northflank: northflankConnectionService(connectAppConnectionById),
|
||||
okta: oktaConnectionService(connectAppConnectionById),
|
||||
laravelForge: laravelForgeConnectionService(connectAppConnectionById)
|
||||
laravelForge: laravelForgeConnectionService(connectAppConnectionById),
|
||||
chef: chefConnectionService(connectAppConnectionById)
|
||||
};
|
||||
};
|
||||
|
||||
@@ -82,6 +82,12 @@ import {
|
||||
TChecklyConnectionInput,
|
||||
TValidateChecklyConnectionCredentialsSchema
|
||||
} from "./checkly";
|
||||
import {
|
||||
TChefConnection,
|
||||
TChefConnectionConfig,
|
||||
TChefConnectionInput,
|
||||
TValidateChefConnectionCredentialsSchema
|
||||
} from "./chef";
|
||||
import {
|
||||
TCloudflareConnection,
|
||||
TCloudflareConnectionConfig,
|
||||
@@ -168,6 +174,12 @@ import {
|
||||
TNetlifyConnectionInput,
|
||||
TValidateNetlifyConnectionCredentialsSchema
|
||||
} from "./netlify";
|
||||
import {
|
||||
TNorthflankConnection,
|
||||
TNorthflankConnectionConfig,
|
||||
TNorthflankConnectionInput,
|
||||
TValidateNorthflankConnectionCredentialsSchema
|
||||
} from "./northflank";
|
||||
import {
|
||||
TOktaConnection,
|
||||
TOktaConnectionConfig,
|
||||
@@ -273,8 +285,10 @@ export type TAppConnection = { id: string } & (
|
||||
| TSupabaseConnection
|
||||
| TDigitalOceanConnection
|
||||
| TNetlifyConnection
|
||||
| TNorthflankConnection
|
||||
| TOktaConnection
|
||||
| TRedisConnection
|
||||
| TChefConnection
|
||||
);
|
||||
|
||||
export type TAppConnectionRaw = NonNullable<Awaited<ReturnType<TAppConnectionDALFactory["findById"]>>>;
|
||||
@@ -320,8 +334,10 @@ export type TAppConnectionInput = { id: string } & (
|
||||
| TSupabaseConnectionInput
|
||||
| TDigitalOceanConnectionInput
|
||||
| TNetlifyConnectionInput
|
||||
| TNorthflankConnectionInput
|
||||
| TOktaConnectionInput
|
||||
| TRedisConnectionInput
|
||||
| TChefConnectionInput
|
||||
);
|
||||
|
||||
export type TSqlConnectionInput =
|
||||
@@ -385,8 +401,10 @@ export type TAppConnectionConfig =
|
||||
| TSupabaseConnectionConfig
|
||||
| TDigitalOceanConnectionConfig
|
||||
| TNetlifyConnectionConfig
|
||||
| TNorthflankConnectionConfig
|
||||
| TOktaConnectionConfig
|
||||
| TRedisConnectionConfig;
|
||||
| TRedisConnectionConfig
|
||||
| TChefConnectionConfig;
|
||||
|
||||
export type TValidateAppConnectionCredentialsSchema =
|
||||
| TValidateAwsConnectionCredentialsSchema
|
||||
@@ -427,8 +445,10 @@ export type TValidateAppConnectionCredentialsSchema =
|
||||
| TValidateSupabaseConnectionCredentialsSchema
|
||||
| TValidateDigitalOceanCredentialsSchema
|
||||
| TValidateNetlifyConnectionCredentialsSchema
|
||||
| TValidateNorthflankConnectionCredentialsSchema
|
||||
| TValidateOktaConnectionCredentialsSchema
|
||||
| TValidateRedisConnectionCredentialsSchema;
|
||||
| TValidateRedisConnectionCredentialsSchema
|
||||
| TValidateChefConnectionCredentialsSchema;
|
||||
|
||||
export type TListAwsConnectionKmsKeys = {
|
||||
connectionId: string;
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
export enum ChefConnectionMethod {
|
||||
UserKey = "user-key"
|
||||
}
|
||||
@@ -0,0 +1,288 @@
|
||||
import { AxiosError } from "axios";
|
||||
import crypto from "crypto";
|
||||
|
||||
import { request } from "@app/lib/config/request";
|
||||
import { BadRequestError } from "@app/lib/errors";
|
||||
import { removeTrailingSlash } from "@app/lib/fn";
|
||||
import { blockLocalAndPrivateIpAddresses } from "@app/lib/validator";
|
||||
import { IntegrationUrls } from "@app/services/integration-auth/integration-list";
|
||||
|
||||
import { TChefDataBagItemContent } from "../../secret-sync/chef/chef-sync-types";
|
||||
import { AppConnection } from "../app-connection-enums";
|
||||
import { ChefConnectionMethod } from "./chef-connection-enums";
|
||||
import {
|
||||
TChefConnection,
|
||||
TChefConnectionConfig,
|
||||
TChefDataBag,
|
||||
TChefDataBagItem,
|
||||
TGetChefDataBagItem,
|
||||
TUpdateChefDataBagItem
|
||||
} from "./chef-connection-types";
|
||||
|
||||
export const getChefServerUrl = async (serverUrl?: string) => {
|
||||
const chefServerUrl = serverUrl ? removeTrailingSlash(serverUrl) : IntegrationUrls.CHEF_API_URL;
|
||||
|
||||
await blockLocalAndPrivateIpAddresses(chefServerUrl);
|
||||
|
||||
return chefServerUrl;
|
||||
};
|
||||
|
||||
// Helper to ensure private key is in proper PEM format
|
||||
const formatPrivateKey = (key: string): string => {
|
||||
let formattedKey = key.trim();
|
||||
|
||||
// Ensure proper line breaks in PEM format (handle escaped newlines)
|
||||
formattedKey = formattedKey.replace(/\\n/g, "\n");
|
||||
|
||||
// Remove any extra whitespace between lines
|
||||
formattedKey = formattedKey.replace(/\n\s+/g, "\n");
|
||||
|
||||
// If key doesn't have headers, add PKCS#1 RSA headers
|
||||
if (!formattedKey.includes("BEGIN")) {
|
||||
formattedKey = `-----BEGIN RSA PRIVATE KEY-----\n${formattedKey}\n-----END RSA PRIVATE KEY-----`;
|
||||
}
|
||||
|
||||
// Ensure the key has proper line breaks after headers and before footers
|
||||
formattedKey = formattedKey.replace(/(-----BEGIN[^-]+-----)\s*/g, "$1\n").replace(/\s*(-----END[^-]+-----)/g, "\n$1");
|
||||
|
||||
// Remove any duplicate newlines
|
||||
formattedKey = formattedKey.replace(/\n{3,}/g, "\n\n");
|
||||
|
||||
return formattedKey;
|
||||
};
|
||||
|
||||
const getChefAuthHeaders = (
|
||||
method: string,
|
||||
path: string,
|
||||
body: string,
|
||||
userId: string,
|
||||
privateKey: string,
|
||||
apiVersion: "1.0" | "1.3" = "1.3"
|
||||
) => {
|
||||
const timestamp = new Date().toISOString().replace(/\.\d{3}Z$/, "Z"); // Remove milliseconds from timestamp
|
||||
|
||||
// Calculate content hash based on version
|
||||
let contentHash: string;
|
||||
if (apiVersion === "1.3") {
|
||||
contentHash = crypto.createHash("sha256").update(body).digest("base64");
|
||||
} else {
|
||||
contentHash = crypto.createHash("sha1").update(body).digest("base64");
|
||||
}
|
||||
|
||||
// Build canonical request based on version
|
||||
let canonicalRequest: string;
|
||||
if (apiVersion === "1.3") {
|
||||
canonicalRequest = [
|
||||
`Method:${method}`,
|
||||
`Path:${path}`,
|
||||
`X-Ops-Content-Hash:${contentHash}`,
|
||||
"X-Ops-Sign:version=1.3",
|
||||
`X-Ops-Timestamp:${timestamp}`,
|
||||
`X-Ops-UserId:${userId}`,
|
||||
"X-Ops-Server-API-Version:1"
|
||||
].join("\n");
|
||||
} else {
|
||||
const hashedPath = crypto.createHash("sha1").update(path).digest("base64");
|
||||
canonicalRequest = [
|
||||
`Method:${method}`,
|
||||
`Hashed Path:${hashedPath}`,
|
||||
`X-Ops-Content-Hash:${contentHash}`,
|
||||
`X-Ops-Timestamp:${timestamp}`,
|
||||
`X-Ops-UserId:${userId}`
|
||||
].join("\n");
|
||||
}
|
||||
|
||||
// Format the private key properly
|
||||
const formattedKey = formatPrivateKey(privateKey);
|
||||
|
||||
// Sign the canonical request
|
||||
const sign = crypto.createSign(apiVersion === "1.3" ? "RSA-SHA256" : "RSA-SHA1");
|
||||
sign.update(canonicalRequest);
|
||||
const signature = sign.sign(formattedKey, "base64");
|
||||
|
||||
// Split signature into 60-character chunks
|
||||
const authHeaders: Record<string, string> = {};
|
||||
const signatureLines = signature.match(/.{1,60}/g) || [];
|
||||
signatureLines.forEach((line, index) => {
|
||||
authHeaders[`X-Ops-Authorization-${index + 1}`] = line;
|
||||
});
|
||||
|
||||
return {
|
||||
Accept: "application/json",
|
||||
"Content-Type": "application/json",
|
||||
"X-Chef-Version": "14.0.0",
|
||||
"X-Ops-Timestamp": timestamp,
|
||||
"X-Ops-UserId": userId,
|
||||
"X-Ops-Sign": apiVersion === "1.3" ? "version=1.3" : "algorithm=sha1;version=1.0",
|
||||
"X-Ops-Content-Hash": contentHash,
|
||||
...(apiVersion === "1.3" && { "X-Ops-Server-API-Version": "1" }),
|
||||
...authHeaders
|
||||
};
|
||||
};
|
||||
|
||||
export const getChefConnectionListItem = () => {
|
||||
return {
|
||||
name: "Chef" as const,
|
||||
app: AppConnection.Chef as const,
|
||||
methods: Object.values(ChefConnectionMethod) as [ChefConnectionMethod.UserKey]
|
||||
};
|
||||
};
|
||||
|
||||
export const validateChefConnectionCredentials = async (config: TChefConnectionConfig) => {
|
||||
const { credentials: inputCredentials } = config;
|
||||
|
||||
try {
|
||||
const path = `/organizations/${inputCredentials.orgName}/users/${inputCredentials.userName}`;
|
||||
|
||||
const hostServerUrl = await getChefServerUrl(inputCredentials.serverUrl);
|
||||
|
||||
const headers = getChefAuthHeaders("GET", path, "", inputCredentials.userName, inputCredentials.privateKey);
|
||||
|
||||
await request.get(`${hostServerUrl}${path}`, {
|
||||
headers
|
||||
});
|
||||
} catch (error: unknown) {
|
||||
if (error instanceof AxiosError) {
|
||||
throw new BadRequestError({
|
||||
message: `Failed to validate Chef credentials: ${error.message || "Unknown error"}`
|
||||
});
|
||||
}
|
||||
throw new BadRequestError({
|
||||
message: "Unable to validate Chef connection: verify credentials"
|
||||
});
|
||||
}
|
||||
|
||||
return inputCredentials;
|
||||
};
|
||||
|
||||
export const listChefDataBags = async (appConnection: TChefConnection): Promise<TChefDataBag[]> => {
|
||||
const {
|
||||
credentials: { serverUrl, userName, privateKey, orgName }
|
||||
} = appConnection;
|
||||
|
||||
try {
|
||||
const path = `/organizations/${orgName}/data`;
|
||||
const body = "";
|
||||
|
||||
const hostServerUrl = await getChefServerUrl(serverUrl);
|
||||
|
||||
const headers = getChefAuthHeaders("GET", path, body, userName, privateKey);
|
||||
|
||||
const res = await request.get<Record<string, string>>(`${hostServerUrl}${path}`, {
|
||||
headers
|
||||
});
|
||||
|
||||
return Object.keys(res.data).map((name) => ({
|
||||
name
|
||||
}));
|
||||
} catch (error) {
|
||||
if (error instanceof AxiosError) {
|
||||
throw new BadRequestError({
|
||||
message: `Failed to list Chef data bags: ${error.message || "Unknown error"}`
|
||||
});
|
||||
}
|
||||
throw new BadRequestError({
|
||||
message: "Unable to list Chef data bags"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export const listChefDataBagItems = async (
|
||||
appConnection: TChefConnection,
|
||||
dataBagName: string
|
||||
): Promise<TChefDataBagItem[]> => {
|
||||
const {
|
||||
credentials: { serverUrl, userName, privateKey, orgName }
|
||||
} = appConnection;
|
||||
|
||||
try {
|
||||
const path = `/organizations/${orgName}/data/${dataBagName}`;
|
||||
const body = "";
|
||||
|
||||
const hostServerUrl = await getChefServerUrl(serverUrl);
|
||||
|
||||
const headers = getChefAuthHeaders("GET", path, body, userName, privateKey);
|
||||
|
||||
const res = await request.get<Record<string, string>>(`${hostServerUrl}${path}`, {
|
||||
headers
|
||||
});
|
||||
|
||||
return Object.keys(res.data).map((name) => ({
|
||||
name
|
||||
}));
|
||||
} catch (error) {
|
||||
if (error instanceof AxiosError) {
|
||||
throw new BadRequestError({
|
||||
message: `Failed to list Chef data bag items: ${error.message || "Unknown error"}`
|
||||
});
|
||||
}
|
||||
throw new BadRequestError({
|
||||
message: "Unable to list Chef data bag items"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export const getChefDataBagItem = async ({
|
||||
serverUrl,
|
||||
userName,
|
||||
privateKey,
|
||||
orgName,
|
||||
dataBagName,
|
||||
dataBagItemName
|
||||
}: TGetChefDataBagItem): Promise<TChefDataBagItemContent> => {
|
||||
try {
|
||||
const path = `/organizations/${orgName}/data/${dataBagName}/${dataBagItemName}`;
|
||||
const body = "";
|
||||
|
||||
const hostServerUrl = await getChefServerUrl(serverUrl);
|
||||
|
||||
const headers = getChefAuthHeaders("GET", path, body, userName, privateKey);
|
||||
|
||||
const res = await request.get<TChefDataBagItemContent>(`${hostServerUrl}${path}`, {
|
||||
headers
|
||||
});
|
||||
|
||||
return res.data;
|
||||
} catch (error) {
|
||||
if (error instanceof AxiosError) {
|
||||
throw new BadRequestError({
|
||||
message: `Failed to get Chef data bag item: ${error.message || "Unknown error"}`
|
||||
});
|
||||
}
|
||||
throw new BadRequestError({
|
||||
message: "Unable to get Chef data bag item"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export const updateChefDataBagItem = async ({
|
||||
serverUrl,
|
||||
userName,
|
||||
privateKey,
|
||||
orgName,
|
||||
dataBagName,
|
||||
dataBagItemName,
|
||||
data
|
||||
}: TUpdateChefDataBagItem): Promise<void> => {
|
||||
try {
|
||||
const path = `/organizations/${orgName}/data/${dataBagName}/${dataBagItemName}`;
|
||||
const body = JSON.stringify(data);
|
||||
|
||||
const hostServerUrl = await getChefServerUrl(serverUrl);
|
||||
|
||||
const headers = getChefAuthHeaders("PUT", path, body, userName, privateKey);
|
||||
|
||||
await request.put(`${hostServerUrl}${path}`, data, {
|
||||
headers
|
||||
});
|
||||
} catch (error) {
|
||||
if (error instanceof AxiosError) {
|
||||
throw new BadRequestError({
|
||||
message: `Failed to update Chef data bag item: ${error.message || "Unknown error"}`
|
||||
});
|
||||
}
|
||||
throw new BadRequestError({
|
||||
message: "Unable to update Chef data bag item"
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,77 @@
|
||||
import z from "zod";
|
||||
|
||||
import { AppConnections } from "@app/lib/api-docs";
|
||||
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
||||
import {
|
||||
BaseAppConnectionSchema,
|
||||
GenericCreateAppConnectionFieldsSchema,
|
||||
GenericUpdateAppConnectionFieldsSchema
|
||||
} from "@app/services/app-connection/app-connection-schemas";
|
||||
|
||||
import { ChefConnectionMethod } from "./chef-connection-enums";
|
||||
|
||||
export const ChefConnectionUserKeyCredentialsSchema = z.object({
|
||||
serverUrl: z
|
||||
.string()
|
||||
.trim()
|
||||
.url("Valid Chef Server URL required")
|
||||
.optional()
|
||||
.describe(AppConnections.CREDENTIALS.CHEF.serverUrl),
|
||||
orgName: z
|
||||
.string()
|
||||
.trim()
|
||||
.min(1, "Organization name required")
|
||||
.max(256, "Organization name cannot exceed 256 characters")
|
||||
.describe(AppConnections.CREDENTIALS.CHEF.orgName),
|
||||
userName: z
|
||||
.string()
|
||||
.trim()
|
||||
.min(1, "User name required")
|
||||
.max(256, "User name cannot exceed 256 characters")
|
||||
.describe(AppConnections.CREDENTIALS.CHEF.userName),
|
||||
privateKey: z
|
||||
.string()
|
||||
.trim()
|
||||
.min(1, "Private key required")
|
||||
.max(16384, "Private key cannot exceed 16384 characters")
|
||||
.describe(AppConnections.CREDENTIALS.CHEF.privateKey)
|
||||
});
|
||||
|
||||
const BaseChefConnectionSchema = BaseAppConnectionSchema.extend({ app: z.literal(AppConnection.Chef) });
|
||||
|
||||
export const ChefConnectionSchema = BaseChefConnectionSchema.extend({
|
||||
method: z.literal(ChefConnectionMethod.UserKey),
|
||||
credentials: ChefConnectionUserKeyCredentialsSchema
|
||||
});
|
||||
|
||||
export const SanitizedChefConnectionSchema = z.discriminatedUnion("method", [
|
||||
BaseChefConnectionSchema.extend({
|
||||
method: z.literal(ChefConnectionMethod.UserKey),
|
||||
credentials: ChefConnectionUserKeyCredentialsSchema.pick({ serverUrl: true, orgName: true, userName: true })
|
||||
})
|
||||
]);
|
||||
|
||||
export const ValidateChefConnectionCredentialsSchema = z.discriminatedUnion("method", [
|
||||
z.object({
|
||||
method: z.literal(ChefConnectionMethod.UserKey).describe(AppConnections.CREATE(AppConnection.Chef).method),
|
||||
credentials: ChefConnectionUserKeyCredentialsSchema.describe(AppConnections.CREATE(AppConnection.Chef).credentials)
|
||||
})
|
||||
]);
|
||||
|
||||
export const CreateChefConnectionSchema = ValidateChefConnectionCredentialsSchema.and(
|
||||
GenericCreateAppConnectionFieldsSchema(AppConnection.Chef)
|
||||
);
|
||||
|
||||
export const UpdateChefConnectionSchema = z
|
||||
.object({
|
||||
credentials: ChefConnectionUserKeyCredentialsSchema.optional().describe(
|
||||
AppConnections.UPDATE(AppConnection.Chef).credentials
|
||||
)
|
||||
})
|
||||
.and(GenericUpdateAppConnectionFieldsSchema(AppConnection.Chef));
|
||||
|
||||
export const ChefConnectionListItemSchema = z.object({
|
||||
name: z.literal("Chef"),
|
||||
app: z.literal(AppConnection.Chef),
|
||||
methods: z.nativeEnum(ChefConnectionMethod).array()
|
||||
});
|
||||
@@ -0,0 +1,39 @@
|
||||
import { ForbiddenRequestError } from "@app/lib/errors";
|
||||
import { OrgServiceActor } from "@app/lib/types";
|
||||
|
||||
import { AppConnection } from "../app-connection-enums";
|
||||
import { listChefDataBagItems, listChefDataBags } from "./chef-connection-fns";
|
||||
import { TChefConnection } from "./chef-connection-types";
|
||||
|
||||
type TGetAppConnectionFunc = (
|
||||
app: AppConnection,
|
||||
connectionId: string,
|
||||
actor: OrgServiceActor
|
||||
) => Promise<TChefConnection>;
|
||||
|
||||
export const chefConnectionService = (getAppConnection: TGetAppConnectionFunc) => {
|
||||
const listDataBags = async (appConnectionId: string, actor: OrgServiceActor) => {
|
||||
const appConnection = await getAppConnection(AppConnection.Chef, appConnectionId, actor);
|
||||
|
||||
if (!appConnection) {
|
||||
throw new ForbiddenRequestError({ message: "App connection not found" });
|
||||
}
|
||||
|
||||
return listChefDataBags(appConnection);
|
||||
};
|
||||
|
||||
const listDataBagItems = async (appConnectionId: string, dataBagName: string, actor: OrgServiceActor) => {
|
||||
const appConnection = await getAppConnection(AppConnection.Chef, appConnectionId, actor);
|
||||
|
||||
if (!appConnection) {
|
||||
throw new ForbiddenRequestError({ message: "App connection not found" });
|
||||
}
|
||||
|
||||
return listChefDataBagItems(appConnection, dataBagName);
|
||||
};
|
||||
|
||||
return {
|
||||
listDataBags,
|
||||
listDataBagItems
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,50 @@
|
||||
import z from "zod";
|
||||
|
||||
import { DiscriminativePick } from "@app/lib/types";
|
||||
import { TChefDataBagItemContent } from "@app/services/secret-sync/chef";
|
||||
|
||||
import { AppConnection } from "../app-connection-enums";
|
||||
import {
|
||||
ChefConnectionSchema,
|
||||
CreateChefConnectionSchema,
|
||||
ValidateChefConnectionCredentialsSchema
|
||||
} from "./chef-connection-schemas";
|
||||
|
||||
export type TChefConnection = z.infer<typeof ChefConnectionSchema>;
|
||||
|
||||
export type TChefConnectionInput = z.infer<typeof CreateChefConnectionSchema> & {
|
||||
app: AppConnection.Chef;
|
||||
};
|
||||
|
||||
export type TValidateChefConnectionCredentialsSchema = typeof ValidateChefConnectionCredentialsSchema;
|
||||
|
||||
export type TChefConnectionConfig = DiscriminativePick<TChefConnectionInput, "method" | "app" | "credentials"> & {
|
||||
orgName: string;
|
||||
};
|
||||
|
||||
export type TChefDataBag = {
|
||||
name: string;
|
||||
};
|
||||
|
||||
export type TChefDataBagItem = {
|
||||
name: string;
|
||||
};
|
||||
|
||||
export type TGetChefDataBagItem = {
|
||||
serverUrl?: string;
|
||||
userName: string;
|
||||
privateKey: string;
|
||||
orgName: string;
|
||||
dataBagName: string;
|
||||
dataBagItemName: string;
|
||||
};
|
||||
|
||||
export type TUpdateChefDataBagItem = {
|
||||
serverUrl?: string;
|
||||
userName: string;
|
||||
privateKey: string;
|
||||
orgName: string;
|
||||
dataBagName: string;
|
||||
dataBagItemName: string;
|
||||
data: TChefDataBagItemContent;
|
||||
};
|
||||
@@ -0,0 +1,4 @@
|
||||
export * from "./chef-connection-enums";
|
||||
export * from "./chef-connection-fns";
|
||||
export * from "./chef-connection-schemas";
|
||||
export * from "./chef-connection-types";
|
||||
@@ -1,4 +1,5 @@
|
||||
export enum GitHubConnectionMethod {
|
||||
OAuth = "oauth",
|
||||
App = "github-app"
|
||||
App = "github-app",
|
||||
Pat = "pat"
|
||||
}
|
||||
|
||||
@@ -248,10 +248,18 @@ export const makePaginatedGitHubRequest = async <T, R = T[]>(
|
||||
): Promise<T[]> => {
|
||||
const { credentials, method } = appConnection;
|
||||
|
||||
const token =
|
||||
method === GitHubConnectionMethod.OAuth
|
||||
? credentials.accessToken
|
||||
: await getGitHubAppAuthToken(appConnection, gatewayService, gatewayV2Service);
|
||||
let token: string;
|
||||
|
||||
switch (method) {
|
||||
case GitHubConnectionMethod.OAuth:
|
||||
token = credentials.accessToken;
|
||||
break;
|
||||
case GitHubConnectionMethod.Pat:
|
||||
token = credentials.personalAccessToken;
|
||||
break;
|
||||
default:
|
||||
token = await getGitHubAppAuthToken(appConnection, gatewayService, gatewayV2Service);
|
||||
}
|
||||
|
||||
const baseUrl = `https://${await getGitHubInstanceApiUrl(appConnection)}${path}`;
|
||||
const initialUrlObj = new URL(baseUrl);
|
||||
@@ -460,6 +468,35 @@ export const validateGitHubConnectionCredentials = async (
|
||||
gatewayV2Service: Pick<TGatewayV2ServiceFactory, "getPlatformConnectionDetailsByGatewayId">
|
||||
) => {
|
||||
const { credentials, method } = config;
|
||||
|
||||
// PAT validation
|
||||
if (method === GitHubConnectionMethod.Pat) {
|
||||
try {
|
||||
const apiUrl = await getGitHubInstanceApiUrl(config);
|
||||
await requestWithGitHubGateway(config, gatewayService, gatewayV2Service, {
|
||||
url: `https://${apiUrl}/user`,
|
||||
method: "GET",
|
||||
headers: {
|
||||
Accept: "application/vnd.github+json",
|
||||
Authorization: `Bearer ${credentials.personalAccessToken}`,
|
||||
"X-GitHub-Api-Version": "2022-11-28"
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
personalAccessToken: credentials.personalAccessToken,
|
||||
instanceType: credentials.instanceType,
|
||||
host: credentials.host
|
||||
};
|
||||
} catch (e: unknown) {
|
||||
logger.error(e, "Unable to verify GitHub PAT connection");
|
||||
|
||||
throw new BadRequestError({
|
||||
message: "Unable to validate Personal Access Token: verify token has proper permissions"
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const {
|
||||
INF_APP_CONNECTION_GITHUB_OAUTH_CLIENT_ID,
|
||||
INF_APP_CONNECTION_GITHUB_OAUTH_CLIENT_SECRET,
|
||||
|
||||
@@ -38,6 +38,19 @@ export const GitHubConnectionAppInputCredentialsSchema = z.union([
|
||||
})
|
||||
]);
|
||||
|
||||
export const GitHubConnectionPatInputCredentialsSchema = z.union([
|
||||
z.object({
|
||||
personalAccessToken: z.string().trim().min(1, "Personal Access Token required"),
|
||||
instanceType: z.literal("server"),
|
||||
host: z.string().trim().min(1, "Host is required for server instance type")
|
||||
}),
|
||||
z.object({
|
||||
personalAccessToken: z.string().trim().min(1, "Personal Access Token required"),
|
||||
instanceType: z.literal("cloud").optional(),
|
||||
host: z.string().trim().optional()
|
||||
})
|
||||
]);
|
||||
|
||||
export const GitHubConnectionOAuthOutputCredentialsSchema = z.union([
|
||||
z.object({
|
||||
accessToken: z.string(),
|
||||
@@ -64,6 +77,19 @@ export const GitHubConnectionAppOutputCredentialsSchema = z.union([
|
||||
})
|
||||
]);
|
||||
|
||||
export const GitHubConnectionPatOutputCredentialsSchema = z.union([
|
||||
z.object({
|
||||
personalAccessToken: z.string(),
|
||||
instanceType: z.literal("server"),
|
||||
host: z.string().trim().min(1)
|
||||
}),
|
||||
z.object({
|
||||
personalAccessToken: z.string(),
|
||||
instanceType: z.literal("cloud").optional(),
|
||||
host: z.string().trim().optional()
|
||||
})
|
||||
]);
|
||||
|
||||
export const ValidateGitHubConnectionCredentialsSchema = z.discriminatedUnion("method", [
|
||||
z.object({
|
||||
method: z.literal(GitHubConnectionMethod.App).describe(AppConnections.CREATE(AppConnection.GitHub).method),
|
||||
@@ -76,6 +102,12 @@ export const ValidateGitHubConnectionCredentialsSchema = z.discriminatedUnion("m
|
||||
credentials: GitHubConnectionOAuthInputCredentialsSchema.describe(
|
||||
AppConnections.CREATE(AppConnection.GitHub).credentials
|
||||
)
|
||||
}),
|
||||
z.object({
|
||||
method: z.literal(GitHubConnectionMethod.Pat).describe(AppConnections.CREATE(AppConnection.GitHub).method),
|
||||
credentials: GitHubConnectionPatInputCredentialsSchema.describe(
|
||||
AppConnections.CREATE(AppConnection.GitHub).credentials
|
||||
)
|
||||
})
|
||||
]);
|
||||
|
||||
@@ -88,7 +120,11 @@ export const CreateGitHubConnectionSchema = ValidateGitHubConnectionCredentialsS
|
||||
export const UpdateGitHubConnectionSchema = z
|
||||
.object({
|
||||
credentials: z
|
||||
.union([GitHubConnectionAppInputCredentialsSchema, GitHubConnectionOAuthInputCredentialsSchema])
|
||||
.union([
|
||||
GitHubConnectionAppInputCredentialsSchema,
|
||||
GitHubConnectionOAuthInputCredentialsSchema,
|
||||
GitHubConnectionPatInputCredentialsSchema
|
||||
])
|
||||
.optional()
|
||||
.describe(AppConnections.UPDATE(AppConnection.GitHub).credentials)
|
||||
})
|
||||
@@ -110,6 +146,10 @@ export const GitHubConnectionSchema = z.intersection(
|
||||
z.object({
|
||||
method: z.literal(GitHubConnectionMethod.OAuth),
|
||||
credentials: GitHubConnectionOAuthOutputCredentialsSchema
|
||||
}),
|
||||
z.object({
|
||||
method: z.literal(GitHubConnectionMethod.Pat),
|
||||
credentials: GitHubConnectionPatOutputCredentialsSchema
|
||||
})
|
||||
])
|
||||
);
|
||||
@@ -128,6 +168,13 @@ export const SanitizedGitHubConnectionSchema = z.discriminatedUnion("method", [
|
||||
instanceType: z.union([z.literal("server"), z.literal("cloud")]).optional(),
|
||||
host: z.string().optional()
|
||||
})
|
||||
}),
|
||||
BaseGitHubConnectionSchema.extend({
|
||||
method: z.literal(GitHubConnectionMethod.Pat),
|
||||
credentials: z.object({
|
||||
instanceType: z.union([z.literal("server"), z.literal("cloud")]).optional(),
|
||||
host: z.string().optional()
|
||||
})
|
||||
})
|
||||
]);
|
||||
|
||||
|
||||
@@ -5,5 +5,6 @@ export enum GitLabConnectionMethod {
|
||||
|
||||
export enum GitLabAccessTokenType {
|
||||
Project = "project",
|
||||
Personal = "personal"
|
||||
Personal = "personal",
|
||||
Group = "group"
|
||||
}
|
||||
|
||||
@@ -21,6 +21,8 @@ import {
|
||||
THCVaultKubernetesAuthConfig,
|
||||
THCVaultKubernetesAuthRole,
|
||||
THCVaultKubernetesAuthRoleWithConfig,
|
||||
THCVaultKubernetesRole,
|
||||
THCVaultKubernetesSecretsConfig,
|
||||
THCVaultMount,
|
||||
THCVaultMountResponse
|
||||
} from "./hc-vault-connection-types";
|
||||
@@ -816,3 +818,122 @@ export const getHCVaultKubernetesAuthRoles = async (
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export const getHCVaultKubernetesRoles = async (
|
||||
namespace: string,
|
||||
mountPath: string,
|
||||
connection: THCVaultConnection,
|
||||
gatewayService: Pick<TGatewayServiceFactory, "fnGetGatewayClientTlsByGatewayId">
|
||||
): Promise<THCVaultKubernetesRole[]> => {
|
||||
// Remove trailing slash from mount path
|
||||
const cleanMountPath = mountPath.endsWith("/") ? mountPath.slice(0, -1) : mountPath;
|
||||
|
||||
try {
|
||||
const instanceUrl = await getHCVaultInstanceUrl(connection);
|
||||
const accessToken = await getHCVaultAccessToken(connection, gatewayService);
|
||||
// 1. Get the Kubernetes secrets engine configuration for this mount
|
||||
const { data: configResponse } = await requestWithHCVaultGateway<{ data: THCVaultKubernetesSecretsConfig }>(
|
||||
connection,
|
||||
gatewayService,
|
||||
{
|
||||
url: `${instanceUrl}/v1/${cleanMountPath}/config`,
|
||||
method: "GET",
|
||||
headers: {
|
||||
"X-Vault-Token": accessToken,
|
||||
"X-Vault-Namespace": namespace
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
const kubernetesConfig = configResponse.data;
|
||||
|
||||
// 2. List all roles in this mount
|
||||
let roleNames: string[] = [];
|
||||
try {
|
||||
const { data: roleListResponse } = await requestWithHCVaultGateway<{ data: { keys: string[] } }>(
|
||||
connection,
|
||||
gatewayService,
|
||||
{
|
||||
url: `${instanceUrl}/v1/${cleanMountPath}/roles?list=true`,
|
||||
method: "GET",
|
||||
headers: {
|
||||
"X-Vault-Token": accessToken,
|
||||
"X-Vault-Namespace": namespace
|
||||
}
|
||||
}
|
||||
);
|
||||
roleNames = roleListResponse.data.keys || [];
|
||||
} catch (error) {
|
||||
// Vault returns 404 when no roles are configured yet
|
||||
if (error && typeof error === "object" && "response" in error) {
|
||||
const axiosError = error as { response?: { status?: number } };
|
||||
if (axiosError.response?.status === 404) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
|
||||
if (!roleNames || roleNames.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// 3. Fetch details for each role with concurrency control
|
||||
const limiter = createConcurrencyLimiter(HC_VAULT_CONCURRENCY_LIMIT);
|
||||
|
||||
const roleDetailsPromises = roleNames.map((roleName) =>
|
||||
limiter(async () => {
|
||||
const { data: roleResponse } = await requestWithHCVaultGateway<{
|
||||
data: {
|
||||
allowed_kubernetes_namespaces?: string[];
|
||||
allowed_kubernetes_namespace_selector?: string;
|
||||
token_max_ttl?: number;
|
||||
token_default_ttl?: number;
|
||||
token_default_audiences?: string[];
|
||||
service_account_name?: string;
|
||||
kubernetes_role_name?: string;
|
||||
kubernetes_role_type?: string;
|
||||
generated_role_rules?: string;
|
||||
name_template?: string;
|
||||
extra_annotations?: Record<string, string>;
|
||||
extra_labels?: Record<string, string>;
|
||||
};
|
||||
}>(connection, gatewayService, {
|
||||
url: `${instanceUrl}/v1/${cleanMountPath}/roles/${roleName}`,
|
||||
method: "GET",
|
||||
headers: {
|
||||
"X-Vault-Token": accessToken,
|
||||
"X-Vault-Namespace": namespace
|
||||
}
|
||||
});
|
||||
|
||||
// 4. Merge the role with the config
|
||||
return {
|
||||
...roleResponse.data,
|
||||
name: roleName,
|
||||
config: kubernetesConfig,
|
||||
mountPath: cleanMountPath
|
||||
} as THCVaultKubernetesRole;
|
||||
})
|
||||
);
|
||||
|
||||
const roles = await Promise.all(roleDetailsPromises);
|
||||
|
||||
return roles;
|
||||
} catch (error: unknown) {
|
||||
logger.error(error, "Unable to list HC Vault Kubernetes secrets engine roles");
|
||||
|
||||
if (error instanceof AxiosError) {
|
||||
const errorMessage =
|
||||
(error.response?.data as { errors?: string[] })?.errors?.[0] || error.message || "Unknown error";
|
||||
throw new BadRequestError({
|
||||
message: `Failed to list Kubernetes secrets engine roles: ${errorMessage}`
|
||||
});
|
||||
}
|
||||
|
||||
throw new BadRequestError({
|
||||
message: "Unable to list Kubernetes secrets engine roles from HashiCorp Vault"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@@ -95,3 +95,26 @@ export type THCVaultKubernetesAuthRoleWithConfig = THCVaultKubernetesAuthRole &
|
||||
config: THCVaultKubernetesAuthConfig;
|
||||
mountPath: string;
|
||||
};
|
||||
|
||||
export type THCVaultKubernetesSecretsConfig = {
|
||||
kubernetes_host: string;
|
||||
kubernetes_ca_cert?: string;
|
||||
};
|
||||
|
||||
export type THCVaultKubernetesRole = {
|
||||
name: string;
|
||||
allowed_kubernetes_namespaces?: string[];
|
||||
allowed_kubernetes_namespace_selector?: string;
|
||||
token_max_ttl?: number;
|
||||
token_default_ttl?: number;
|
||||
token_default_audiences?: string[];
|
||||
service_account_name?: string;
|
||||
kubernetes_role_name?: string;
|
||||
kubernetes_role_type?: string;
|
||||
generated_role_rules?: string;
|
||||
name_template?: string;
|
||||
extra_annotations?: Record<string, string>;
|
||||
extra_labels?: Record<string, string>;
|
||||
config: THCVaultKubernetesSecretsConfig;
|
||||
mountPath: string;
|
||||
};
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
export * from "./northflank-connection-enums";
|
||||
export * from "./northflank-connection-fns";
|
||||
export * from "./northflank-connection-schemas";
|
||||
export * from "./northflank-connection-service";
|
||||
export * from "./northflank-connection-types";
|
||||
@@ -0,0 +1,3 @@
|
||||
export enum NorthflankConnectionMethod {
|
||||
ApiToken = "api-token"
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user