mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-11 01:29:26 +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 }}"
|
TAG_NAME="${{ github.ref_name }}"
|
||||||
echo "Checking for tag: $TAG_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
|
if [ "$EXACT_MATCH" == "refs/tags/$TAG_NAME" ]; then
|
||||||
echo "Tag $TAG_NAME already exists, skipping..."
|
echo "Tag $TAG_NAME already exists, skipping..."
|
||||||
else
|
else
|
||||||
echo "Creating tag in Infisical/infisical-omnibus: $TAG_NAME"
|
echo "Creating tag in Infisical/infisical-omnibus: $TAG_NAME"
|
||||||
LATEST_SHA=$(gh api repos/Infisical/infisical-omnibus/git/refs/heads/main --jq '.object.sha')
|
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
|
- name: Set up chart-testing
|
||||||
uses: helm/[email protected]
|
uses: helm/[email protected]
|
||||||
|
with:
|
||||||
|
yamale_version: "6.0.0"
|
||||||
|
|
||||||
- name: Run chart-testing (lint)
|
- name: Run chart-testing (lint)
|
||||||
run: ct lint --config ct.yaml --charts helm-charts/infisical-gateway
|
run: ct lint --config ct.yaml --charts helm-charts/infisical-gateway
|
||||||
|
|||||||
@@ -27,6 +27,8 @@ jobs:
|
|||||||
|
|
||||||
- name: Set up chart-testing
|
- name: Set up chart-testing
|
||||||
uses: helm/[email protected]
|
uses: helm/[email protected]
|
||||||
|
with:
|
||||||
|
yamale_version: "6.0.0"
|
||||||
|
|
||||||
- name: Run chart-testing (lint)
|
- name: Run chart-testing (lint)
|
||||||
run: ct lint --config ct.yaml --charts helm-charts/infisical-gateway
|
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",
|
"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: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'",
|
"lint": "node --max-old-space-size=8192 ./node_modules/.bin/eslint 'src/**/*.ts'",
|
||||||
"test:unit": "vitest run -c vitest.unit.config.ts",
|
"test:unit": "vitest run -c vitest.unit.config.mts",
|
||||||
"test:e2e": "vitest run -c vitest.e2e.config.ts --bail=1",
|
"test:e2e": "vitest run -c vitest.e2e.config.mts --bail=1",
|
||||||
"test:e2e-watch": "vitest -c vitest.e2e.config.ts --bail=1",
|
"test:e2e-watch": "vitest -c vitest.e2e.config.mts --bail=1",
|
||||||
"test:e2e-coverage": "vitest run --coverage -c vitest.e2e.config.ts",
|
"test:e2e-coverage": "vitest run --coverage -c vitest.e2e.config.mts",
|
||||||
"generate:component": "tsx ./scripts/create-backend-file.ts",
|
"generate:component": "tsx ./scripts/create-backend-file.ts",
|
||||||
"generate:schema": "tsx ./scripts/generate-schema-types.ts && eslint --fix --ext ts ./src/db/schemas",
|
"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",
|
"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/jsrp": "^0.2.6",
|
||||||
"@types/libsodium-wrappers": "^0.7.13",
|
"@types/libsodium-wrappers": "^0.7.13",
|
||||||
"@types/lodash.isequal": "^4.5.8",
|
"@types/lodash.isequal": "^4.5.8",
|
||||||
"@types/node": "^20.17.30",
|
"@types/node": "^20.19.0",
|
||||||
"@types/nodemailer": "^6.4.14",
|
"@types/nodemailer": "^6.4.14",
|
||||||
"@types/passport-google-oauth20": "^2.0.14",
|
"@types/passport-google-oauth20": "^2.0.14",
|
||||||
"@types/pg": "^8.10.9",
|
"@types/pg": "^8.10.9",
|
||||||
@@ -130,10 +130,10 @@
|
|||||||
"ts-node": "^10.9.2",
|
"ts-node": "^10.9.2",
|
||||||
"tsc-alias": "^1.8.8",
|
"tsc-alias": "^1.8.8",
|
||||||
"tsconfig-paths": "^4.2.0",
|
"tsconfig-paths": "^4.2.0",
|
||||||
"tsup": "^8.0.1",
|
"tsup": "^8.5.0",
|
||||||
"tsx": "^4.4.0",
|
"tsx": "^4.4.0",
|
||||||
"typescript": "^5.3.2",
|
"typescript": "^5.3.2",
|
||||||
"vitest": "^1.2.2"
|
"vitest": "^3.0.6"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@aws-sdk/client-elasticache": "^3.637.0",
|
"@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" {
|
declare module "@fastify/request-context" {
|
||||||
interface RequestContextData {
|
interface RequestContextData {
|
||||||
reqId: string;
|
reqId: string;
|
||||||
|
ip?: string;
|
||||||
|
userAgent?: string;
|
||||||
orgId?: string;
|
orgId?: string;
|
||||||
|
orgName?: string;
|
||||||
|
userAuthInfo?: {
|
||||||
|
userId: string;
|
||||||
|
email: string;
|
||||||
|
};
|
||||||
|
projectDetails?: {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
slug: string;
|
||||||
|
};
|
||||||
identityAuthInfo?: {
|
identityAuthInfo?: {
|
||||||
identityId: string;
|
identityId: string;
|
||||||
|
identityName: string;
|
||||||
|
authMethod: string;
|
||||||
oidc?: {
|
oidc?: {
|
||||||
claims: Record<string, string>;
|
claims: Record<string, string>;
|
||||||
};
|
};
|
||||||
|
|||||||
Vendored
+8
@@ -62,6 +62,9 @@ import {
|
|||||||
TCertificateSecretsUpdate,
|
TCertificateSecretsUpdate,
|
||||||
TCertificatesInsert,
|
TCertificatesInsert,
|
||||||
TCertificatesUpdate,
|
TCertificatesUpdate,
|
||||||
|
TCertificateSyncs,
|
||||||
|
TCertificateSyncsInsert,
|
||||||
|
TCertificateSyncsUpdate,
|
||||||
TCertificateTemplateEstConfigs,
|
TCertificateTemplateEstConfigs,
|
||||||
TCertificateTemplateEstConfigsInsert,
|
TCertificateTemplateEstConfigsInsert,
|
||||||
TCertificateTemplateEstConfigsUpdate,
|
TCertificateTemplateEstConfigsUpdate,
|
||||||
@@ -738,6 +741,11 @@ declare module "knex/types/tables" {
|
|||||||
TPkiSubscribersUpdate
|
TPkiSubscribersUpdate
|
||||||
>;
|
>;
|
||||||
[TableName.PkiSync]: KnexOriginal.CompositeTableType<TPkiSyncs, TPkiSyncsInsert, TPkiSyncsUpdate>;
|
[TableName.PkiSync]: KnexOriginal.CompositeTableType<TPkiSyncs, TPkiSyncsInsert, TPkiSyncsUpdate>;
|
||||||
|
[TableName.CertificateSync]: KnexOriginal.CompositeTableType<
|
||||||
|
TCertificateSyncs,
|
||||||
|
TCertificateSyncsInsert,
|
||||||
|
TCertificateSyncsUpdate
|
||||||
|
>;
|
||||||
[TableName.UserGroupMembership]: KnexOriginal.CompositeTableType<
|
[TableName.UserGroupMembership]: KnexOriginal.CompositeTableType<
|
||||||
TUserGroupMembership,
|
TUserGroupMembership,
|
||||||
TUserGroupMembershipInsert,
|
TUserGroupMembershipInsert,
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { Knex } from "knex";
|
|||||||
|
|
||||||
import { dropConstraintIfExists } from "@app/db/migrations/utils/dropConstraintIfExists";
|
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> {
|
export async function up(knex: Knex): Promise<void> {
|
||||||
const hasParentOrgId = await knex.schema.hasColumn(TableName.Organization, "parentOrgId");
|
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);
|
await dropConstraintIfExists(TableName.Organization, "organizations_slug_unique", knex);
|
||||||
t.unique(["rootOrgId", "parentOrgId", "slug"]);
|
t.unique(["rootOrgId", "parentOrgId", "slug"]);
|
||||||
});
|
});
|
||||||
|
|
||||||
// had to switch to raw for null not distinct
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const hasIdentityOrgCol = await knex.schema.hasColumn(TableName.Identity, "orgId");
|
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.uuid("orgId");
|
||||||
t.foreign("orgId").references("id").inTable(TableName.Organization).onDelete("CASCADE");
|
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(),
|
extendedKeyUsages: z.string().array().nullable().optional(),
|
||||||
projectId: z.string(),
|
projectId: z.string(),
|
||||||
pkiSubscriberId: z.string().uuid().nullable().optional(),
|
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>;
|
export type TCertificates = z.infer<typeof CertificatesSchema>;
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ export * from "./certificate-authority-crl";
|
|||||||
export * from "./certificate-authority-secret";
|
export * from "./certificate-authority-secret";
|
||||||
export * from "./certificate-bodies";
|
export * from "./certificate-bodies";
|
||||||
export * from "./certificate-secrets";
|
export * from "./certificate-secrets";
|
||||||
|
export * from "./certificate-syncs";
|
||||||
export * from "./certificate-template-est-configs";
|
export * from "./certificate-template-est-configs";
|
||||||
export * from "./certificate-templates";
|
export * from "./certificate-templates";
|
||||||
export * from "./certificates";
|
export * from "./certificates";
|
||||||
|
|||||||
@@ -161,6 +161,7 @@ export enum TableName {
|
|||||||
AppConnection = "app_connections",
|
AppConnection = "app_connections",
|
||||||
SecretSync = "secret_syncs",
|
SecretSync = "secret_syncs",
|
||||||
PkiSync = "pki_syncs",
|
PkiSync = "pki_syncs",
|
||||||
|
CertificateSync = "certificate_syncs",
|
||||||
KmipClient = "kmip_clients",
|
KmipClient = "kmip_clients",
|
||||||
KmipOrgConfig = "kmip_org_configs",
|
KmipOrgConfig = "kmip_org_configs",
|
||||||
KmipOrgServerCertificates = "kmip_org_server_certificates",
|
KmipOrgServerCertificates = "kmip_org_server_certificates",
|
||||||
|
|||||||
@@ -40,7 +40,8 @@ export const OrganizationsSchema = z.object({
|
|||||||
googleSsoAuthEnforced: z.boolean().default(false),
|
googleSsoAuthEnforced: z.boolean().default(false),
|
||||||
googleSsoAuthLastUsed: z.date().nullable().optional(),
|
googleSsoAuthLastUsed: z.date().nullable().optional(),
|
||||||
parentOrgId: z.string().uuid().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>;
|
export type TOrganizations = z.infer<typeof OrganizationsSchema>;
|
||||||
|
|||||||
@@ -21,7 +21,9 @@ export const PamAccountsSchema = z.object({
|
|||||||
updatedAt: z.date(),
|
updatedAt: z.date(),
|
||||||
rotationEnabled: z.boolean().default(false),
|
rotationEnabled: z.boolean().default(false),
|
||||||
rotationIntervalSeconds: z.number().nullable().optional(),
|
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>;
|
export type TPamAccounts = z.infer<typeof PamAccountsSchema>;
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import { TImmutableDBKeys } from "./models";
|
|||||||
export const PkiApiEnrollmentConfigsSchema = z.object({
|
export const PkiApiEnrollmentConfigsSchema = z.object({
|
||||||
id: z.string().uuid(),
|
id: z.string().uuid(),
|
||||||
autoRenew: z.boolean().default(false).nullable().optional(),
|
autoRenew: z.boolean().default(false).nullable().optional(),
|
||||||
autoRenewDays: z.number().nullable().optional(),
|
renewBeforeDays: z.number().nullable().optional(),
|
||||||
createdAt: z.date(),
|
createdAt: z.date(),
|
||||||
updatedAt: z.date()
|
updatedAt: z.date()
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import { z } from "zod";
|
|||||||
|
|
||||||
import { DynamicSecretLeasesSchema } from "@app/db/schemas";
|
import { DynamicSecretLeasesSchema } from "@app/db/schemas";
|
||||||
import { ApiDocsTags, DYNAMIC_SECRET_LEASES } from "@app/lib/api-docs";
|
import { ApiDocsTags, DYNAMIC_SECRET_LEASES } from "@app/lib/api-docs";
|
||||||
import { daysToMillisecond } from "@app/lib/dates";
|
|
||||||
import { removeTrailingSlash } from "@app/lib/fn";
|
import { removeTrailingSlash } from "@app/lib/fn";
|
||||||
import { ms } from "@app/lib/ms";
|
import { ms } from "@app/lib/ms";
|
||||||
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||||
@@ -32,8 +31,8 @@ export const registerDynamicSecretLeaseRouter = async (server: FastifyZodProvide
|
|||||||
const valMs = ms(val);
|
const valMs = ms(val);
|
||||||
if (valMs < 60 * 1000)
|
if (valMs < 60 * 1000)
|
||||||
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be a greater than 1min" });
|
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be a greater than 1min" });
|
||||||
if (valMs > daysToMillisecond(1))
|
if (valMs > ms("10y"))
|
||||||
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than a day" });
|
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),
|
path: z.string().trim().default("/").transform(removeTrailingSlash).describe(DYNAMIC_SECRET_LEASES.CREATE.path),
|
||||||
environmentSlug: z.string().min(1).describe(DYNAMIC_SECRET_LEASES.CREATE.environmentSlug),
|
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);
|
const valMs = ms(val);
|
||||||
if (valMs < 60 * 1000)
|
if (valMs < 60 * 1000)
|
||||||
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be a greater than 1min" });
|
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be a greater than 1min" });
|
||||||
if (valMs > daysToMillisecond(1))
|
if (valMs > ms("10y"))
|
||||||
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than a day" });
|
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),
|
projectSlug: z.string().min(1).describe(DYNAMIC_SECRET_LEASES.RENEW.projectSlug),
|
||||||
path: z
|
path: z
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import { z } from "zod";
|
|||||||
|
|
||||||
import { DynamicSecretLeasesSchema } from "@app/db/schemas";
|
import { DynamicSecretLeasesSchema } from "@app/db/schemas";
|
||||||
import { ApiDocsTags, DYNAMIC_SECRET_LEASES } from "@app/lib/api-docs";
|
import { ApiDocsTags, DYNAMIC_SECRET_LEASES } from "@app/lib/api-docs";
|
||||||
import { daysToMillisecond } from "@app/lib/dates";
|
|
||||||
import { removeTrailingSlash } from "@app/lib/fn";
|
import { removeTrailingSlash } from "@app/lib/fn";
|
||||||
import { ms } from "@app/lib/ms";
|
import { ms } from "@app/lib/ms";
|
||||||
import { writeLimit } from "@app/server/config/rateLimiter";
|
import { writeLimit } from "@app/server/config/rateLimiter";
|
||||||
@@ -32,8 +31,8 @@ export const registerKubernetesDynamicSecretLeaseRouter = async (server: Fastify
|
|||||||
const valMs = ms(val);
|
const valMs = ms(val);
|
||||||
if (valMs < 60 * 1000)
|
if (valMs < 60 * 1000)
|
||||||
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be greater than 1min" });
|
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be greater than 1min" });
|
||||||
if (valMs > daysToMillisecond(1))
|
if (valMs > ms("10y"))
|
||||||
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than a day" });
|
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),
|
path: z.string().trim().default("/").transform(removeTrailingSlash).describe(DYNAMIC_SECRET_LEASES.CREATE.path),
|
||||||
environmentSlug: z.string().min(1).describe(DYNAMIC_SECRET_LEASES.CREATE.environmentSlug),
|
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 { DynamicSecretLeasesSchema } from "@app/db/schemas";
|
||||||
import { DynamicSecretProviderSchema } from "@app/ee/services/dynamic-secret/providers/models";
|
import { DynamicSecretProviderSchema } from "@app/ee/services/dynamic-secret/providers/models";
|
||||||
import { ApiDocsTags, DYNAMIC_SECRETS } from "@app/lib/api-docs";
|
import { ApiDocsTags, DYNAMIC_SECRETS } from "@app/lib/api-docs";
|
||||||
import { daysToMillisecond } from "@app/lib/dates";
|
|
||||||
import { removeTrailingSlash } from "@app/lib/fn";
|
import { removeTrailingSlash } from "@app/lib/fn";
|
||||||
import { ms } from "@app/lib/ms";
|
import { ms } from "@app/lib/ms";
|
||||||
import { isValidHandleBarTemplate } from "@app/lib/template/validate-handlebars";
|
import { isValidHandleBarTemplate } from "@app/lib/template/validate-handlebars";
|
||||||
@@ -60,8 +59,8 @@ export const registerDynamicSecretRouter = async (server: FastifyZodProvider) =>
|
|||||||
const valMs = ms(val);
|
const valMs = ms(val);
|
||||||
if (valMs < 60 * 1000)
|
if (valMs < 60 * 1000)
|
||||||
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be a greater than 1min" });
|
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be a greater than 1min" });
|
||||||
if (valMs > daysToMillisecond(1))
|
if (valMs > ms("10y"))
|
||||||
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than a day" });
|
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than 10 years" });
|
||||||
}),
|
}),
|
||||||
maxTTL: z
|
maxTTL: z
|
||||||
.string()
|
.string()
|
||||||
@@ -72,8 +71,8 @@ export const registerDynamicSecretRouter = async (server: FastifyZodProvider) =>
|
|||||||
const valMs = ms(val);
|
const valMs = ms(val);
|
||||||
if (valMs < 60 * 1000)
|
if (valMs < 60 * 1000)
|
||||||
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be a greater than 1min" });
|
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be a greater than 1min" });
|
||||||
if (valMs > daysToMillisecond(1))
|
if (valMs > ms("10y"))
|
||||||
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than a day" });
|
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than 10 years" });
|
||||||
})
|
})
|
||||||
.nullable(),
|
.nullable(),
|
||||||
path: z.string().describe(DYNAMIC_SECRETS.CREATE.path).trim().default("/").transform(removeTrailingSlash),
|
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);
|
const valMs = ms(val);
|
||||||
if (valMs < 60 * 1000)
|
if (valMs < 60 * 1000)
|
||||||
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be a greater than 1min" });
|
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be a greater than 1min" });
|
||||||
if (valMs > daysToMillisecond(1))
|
if (valMs > ms("10y"))
|
||||||
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than a day" });
|
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than 10 years" });
|
||||||
}),
|
}),
|
||||||
maxTTL: z
|
maxTTL: z
|
||||||
.string()
|
.string()
|
||||||
@@ -142,8 +141,8 @@ export const registerDynamicSecretRouter = async (server: FastifyZodProvider) =>
|
|||||||
const valMs = ms(val);
|
const valMs = ms(val);
|
||||||
if (valMs < 60 * 1000)
|
if (valMs < 60 * 1000)
|
||||||
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be a greater than 1min" });
|
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be a greater than 1min" });
|
||||||
if (valMs > daysToMillisecond(1))
|
if (valMs > ms("10y"))
|
||||||
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than a day" });
|
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than 10 years" });
|
||||||
})
|
})
|
||||||
.nullable(),
|
.nullable(),
|
||||||
newName: z.string().describe(DYNAMIC_SECRETS.UPDATE.newName).optional(),
|
newName: z.string().describe(DYNAMIC_SECRETS.UPDATE.newName).optional(),
|
||||||
|
|||||||
@@ -182,7 +182,8 @@ export const registerKmipSpecRouter = async (server: FastifyZodProvider) => {
|
|||||||
algorithm: z.string(),
|
algorithm: z.string(),
|
||||||
isActive: z.boolean(),
|
isActive: z.boolean(),
|
||||||
createdAt: z.date(),
|
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(),
|
isActive: z.boolean(),
|
||||||
algorithm: z.string(),
|
algorithm: z.string(),
|
||||||
createdAt: z.date(),
|
createdAt: z.date(),
|
||||||
updatedAt: z.date()
|
updatedAt: z.date(),
|
||||||
|
kmipMetadata: z.record(z.any()).nullish()
|
||||||
})
|
})
|
||||||
.array()
|
.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 { PamResource } from "@app/ee/services/pam-resource/pam-resource-enums";
|
||||||
import {
|
import {
|
||||||
CreatePostgresAccountSchema,
|
CreatePostgresAccountSchema,
|
||||||
@@ -16,5 +21,14 @@ export const PAM_ACCOUNT_REGISTER_ROUTER_MAP: Record<PamResource, (server: Fasti
|
|||||||
createAccountSchema: CreatePostgresAccountSchema,
|
createAccountSchema: CreatePostgresAccountSchema,
|
||||||
updateAccountSchema: UpdatePostgresAccountSchema
|
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 { PamFoldersSchema } from "@app/db/schemas";
|
||||||
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
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 { PamResource } from "@app/ee/services/pam-resource/pam-resource-enums";
|
||||||
import { SanitizedPostgresAccountWithResourceSchema } from "@app/ee/services/pam-resource/postgres/postgres-resource-schemas";
|
import { SanitizedPostgresAccountWithResourceSchema } from "@app/ee/services/pam-resource/postgres/postgres-resource-schemas";
|
||||||
import { BadRequestError } from "@app/lib/errors";
|
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 { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||||
import { AuthMode } from "@app/services/auth/auth-type";
|
import { AuthMode } from "@app/services/auth/auth-type";
|
||||||
|
|
||||||
// Use z.union([...]) when more resources are added
|
const SanitizedAccountSchema = z.union([
|
||||||
const SanitizedAccountSchema = SanitizedPostgresAccountWithResourceSchema;
|
SanitizedPostgresAccountWithResourceSchema,
|
||||||
|
SanitizedMySQLAccountWithResourceSchema
|
||||||
|
]);
|
||||||
|
|
||||||
export const registerPamAccountRouter = async (server: FastifyZodProvider) => {
|
export const registerPamAccountRouter = async (server: FastifyZodProvider) => {
|
||||||
server.route({
|
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 { PamResource } from "@app/ee/services/pam-resource/pam-resource-enums";
|
||||||
import {
|
import {
|
||||||
CreatePostgresResourceSchema,
|
CreatePostgresResourceSchema,
|
||||||
@@ -16,5 +21,14 @@ export const PAM_RESOURCE_REGISTER_ROUTER_MAP: Record<PamResource, (server: Fast
|
|||||||
createResourceSchema: CreatePostgresResourceSchema,
|
createResourceSchema: CreatePostgresResourceSchema,
|
||||||
updateResourceSchema: UpdatePostgresResourceSchema
|
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 { z } from "zod";
|
||||||
|
|
||||||
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
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 {
|
import {
|
||||||
PostgresResourceListItemSchema,
|
PostgresResourceListItemSchema,
|
||||||
SanitizedPostgresResourceSchema
|
SanitizedPostgresResourceSchema
|
||||||
@@ -9,10 +13,12 @@ import { readLimit } from "@app/server/config/rateLimiter";
|
|||||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||||
import { AuthMode } from "@app/services/auth/auth-type";
|
import { AuthMode } from "@app/services/auth/auth-type";
|
||||||
|
|
||||||
// Use z.union([...]) when more resources are added
|
const SanitizedResourceSchema = z.union([SanitizedPostgresResourceSchema, SanitizedMySQLResourceSchema]);
|
||||||
const SanitizedResourceSchema = SanitizedPostgresResourceSchema;
|
|
||||||
|
|
||||||
const ResourceOptionsSchema = z.discriminatedUnion("resource", [PostgresResourceListItemSchema]);
|
const ResourceOptionsSchema = z.discriminatedUnion("resource", [
|
||||||
|
PostgresResourceListItemSchema,
|
||||||
|
MySQLResourceListItemSchema
|
||||||
|
]);
|
||||||
|
|
||||||
export const registerPamResourceRouter = async (server: FastifyZodProvider) => {
|
export const registerPamResourceRouter = async (server: FastifyZodProvider) => {
|
||||||
server.route({
|
server.route({
|
||||||
|
|||||||
@@ -2,14 +2,14 @@ import { z } from "zod";
|
|||||||
|
|
||||||
import { PamSessionsSchema } from "@app/db/schemas";
|
import { PamSessionsSchema } from "@app/db/schemas";
|
||||||
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
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 { PostgresSessionCredentialsSchema } from "@app/ee/services/pam-resource/postgres/postgres-resource-schemas";
|
||||||
import { PamSessionCommandLogSchema, SanitizedSessionSchema } from "@app/ee/services/pam-session/pam-session-schemas";
|
import { PamSessionCommandLogSchema, SanitizedSessionSchema } from "@app/ee/services/pam-session/pam-session-schemas";
|
||||||
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||||
import { AuthMode } from "@app/services/auth/auth-type";
|
import { AuthMode } from "@app/services/auth/auth-type";
|
||||||
|
|
||||||
// Use z.union([]) once there's multiple
|
const SessionCredentialsSchema = z.union([PostgresSessionCredentialsSchema, MySQLSessionCredentialsSchema]);
|
||||||
const SessionCredentialsSchema = PostgresSessionCredentialsSchema;
|
|
||||||
|
|
||||||
export const registerPamSessionRouter = async (server: FastifyZodProvider) => {
|
export const registerPamSessionRouter = async (server: FastifyZodProvider) => {
|
||||||
// Meant to be hit solely by gateway identities
|
// 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
|
// All the any rules are disabled because passport typesense with fastify is really poor
|
||||||
|
|
||||||
import { Authenticator } from "@fastify/passport";
|
import { Authenticator } from "@fastify/passport";
|
||||||
|
import { requestContext } from "@fastify/request-context";
|
||||||
import fastifySession from "@fastify/session";
|
import fastifySession from "@fastify/session";
|
||||||
import { MultiSamlStrategy } from "@node-saml/passport-saml";
|
import { MultiSamlStrategy } from "@node-saml/passport-saml";
|
||||||
import { FastifyRequest } from "fastify";
|
import { FastifyRequest } from "fastify";
|
||||||
@@ -17,6 +18,7 @@ import { ApiDocsTags, SamlSso } from "@app/lib/api-docs";
|
|||||||
import { getConfig } from "@app/lib/config/env";
|
import { getConfig } from "@app/lib/config/env";
|
||||||
import { BadRequestError } from "@app/lib/errors";
|
import { BadRequestError } from "@app/lib/errors";
|
||||||
import { logger } from "@app/lib/logger";
|
import { logger } from "@app/lib/logger";
|
||||||
|
import { AuthAttemptAuthMethod, AuthAttemptAuthResult, authAttemptCounter } from "@app/lib/telemetry/metrics";
|
||||||
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||||
import { SanitizedSamlConfigSchema } from "@app/server/routes/sanitizedSchema/directory-config";
|
import { SanitizedSamlConfigSchema } from "@app/server/routes/sanitizedSchema/directory-config";
|
||||||
@@ -102,15 +104,15 @@ export const registerSamlRouter = async (server: FastifyZodProvider) => {
|
|||||||
},
|
},
|
||||||
// eslint-disable-next-line
|
// eslint-disable-next-line
|
||||||
async (req, profile, cb) => {
|
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 {
|
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 ??
|
const firstName = (profile.firstName ??
|
||||||
// entra sends data in this format
|
// entra sends data in this format
|
||||||
profile["http://schemas.xmlsoap.org/ws/2005/05/identity/claims/firstName"]) as string;
|
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));
|
.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,
|
externalId: profile.nameID,
|
||||||
email: email.toLowerCase(),
|
email: email.toLowerCase(),
|
||||||
firstName,
|
firstName,
|
||||||
@@ -154,8 +156,32 @@ export const registerSamlRouter = async (server: FastifyZodProvider) => {
|
|||||||
orgId: (req as unknown as FastifyRequest).ssoConfig?.orgId,
|
orgId: (req as unknown as FastifyRequest).ssoConfig?.orgId,
|
||||||
metadata: userMetadata
|
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 });
|
cb(null, { isUserCompleted, providerAuthToken });
|
||||||
} catch (error) {
|
} 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);
|
logger.error(error);
|
||||||
cb(error as Error);
|
cb(error as Error);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -340,6 +340,8 @@ export enum EventType {
|
|||||||
ISSUE_PKI_SUBSCRIBER_CERT = "issue-pki-subscriber-cert",
|
ISSUE_PKI_SUBSCRIBER_CERT = "issue-pki-subscriber-cert",
|
||||||
SIGN_PKI_SUBSCRIBER_CERT = "sign-pki-subscriber-cert",
|
SIGN_PKI_SUBSCRIBER_CERT = "sign-pki-subscriber-cert",
|
||||||
AUTOMATED_RENEW_SUBSCRIBER_CERT = "automated-renew-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",
|
LIST_PKI_SUBSCRIBER_CERTS = "list-pki-subscriber-certs",
|
||||||
GET_SUBSCRIBER_ACTIVE_CERT_BUNDLE = "get-subscriber-active-cert-bundle",
|
GET_SUBSCRIBER_ACTIVE_CERT_BUNDLE = "get-subscriber-active-cert-bundle",
|
||||||
CREATE_KMS = "create-kms",
|
CREATE_KMS = "create-kms",
|
||||||
@@ -367,6 +369,9 @@ export enum EventType {
|
|||||||
ISSUE_CERTIFICATE_FROM_PROFILE = "issue-certificate-from-profile",
|
ISSUE_CERTIFICATE_FROM_PROFILE = "issue-certificate-from-profile",
|
||||||
SIGN_CERTIFICATE_FROM_PROFILE = "sign-certificate-from-profile",
|
SIGN_CERTIFICATE_FROM_PROFILE = "sign-certificate-from-profile",
|
||||||
ORDER_CERTIFICATE_FROM_PROFILE = "order-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_CREATE_SLACK_INTEGRATION = "attempt-create-slack-integration",
|
||||||
ATTEMPT_REINSTALL_SLACK_INTEGRATION = "attempt-reinstall-slack-integration",
|
ATTEMPT_REINSTALL_SLACK_INTEGRATION = "attempt-reinstall-slack-integration",
|
||||||
GET_PROJECT_SLACK_CONFIG = "get-project-slack-config",
|
GET_PROJECT_SLACK_CONFIG = "get-project-slack-config",
|
||||||
@@ -421,6 +426,7 @@ export enum EventType {
|
|||||||
SECRET_SYNC_REMOVE_SECRETS = "secret-sync-remove-secrets",
|
SECRET_SYNC_REMOVE_SECRETS = "secret-sync-remove-secrets",
|
||||||
GET_PKI_SYNCS = "get-pki-syncs",
|
GET_PKI_SYNCS = "get-pki-syncs",
|
||||||
GET_PKI_SYNC = "get-pki-sync",
|
GET_PKI_SYNC = "get-pki-sync",
|
||||||
|
GET_PKI_SYNC_CERTIFICATES = "get-pki-sync-certificates",
|
||||||
CREATE_PKI_SYNC = "create-pki-sync",
|
CREATE_PKI_SYNC = "create-pki-sync",
|
||||||
UPDATE_PKI_SYNC = "update-pki-sync",
|
UPDATE_PKI_SYNC = "update-pki-sync",
|
||||||
DELETE_PKI_SYNC = "delete-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 {
|
interface SignPkiSubscriberCert {
|
||||||
type: EventType.SIGN_PKI_SUBSCRIBER_CERT;
|
type: EventType.SIGN_PKI_SUBSCRIBER_CERT;
|
||||||
metadata: {
|
metadata: {
|
||||||
@@ -2720,6 +2749,16 @@ interface OrderCertificateFromProfile {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface RenewCertificate {
|
||||||
|
type: EventType.RENEW_CERTIFICATE;
|
||||||
|
metadata: {
|
||||||
|
originalCertificateId: string;
|
||||||
|
newCertificateId: string;
|
||||||
|
profileName: string;
|
||||||
|
commonName: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
interface AttemptCreateSlackIntegration {
|
interface AttemptCreateSlackIntegration {
|
||||||
type: EventType.ATTEMPT_CREATE_SLACK_INTEGRATION;
|
type: EventType.ATTEMPT_CREATE_SLACK_INTEGRATION;
|
||||||
metadata: {
|
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 {
|
interface CreatePkiSyncEvent {
|
||||||
type: EventType.CREATE_PKI_SYNC;
|
type: EventType.CREATE_PKI_SYNC;
|
||||||
metadata: {
|
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 =
|
export type Event =
|
||||||
| CreateSubOrganizationEvent
|
| CreateSubOrganizationEvent
|
||||||
| UpdateSubOrganizationEvent
|
| UpdateSubOrganizationEvent
|
||||||
@@ -4216,6 +4282,7 @@ export type Event =
|
|||||||
| IssueCertificateFromProfile
|
| IssueCertificateFromProfile
|
||||||
| SignCertificateFromProfile
|
| SignCertificateFromProfile
|
||||||
| OrderCertificateFromProfile
|
| OrderCertificateFromProfile
|
||||||
|
| RenewCertificate
|
||||||
| GetAzureAdCsTemplatesEvent
|
| GetAzureAdCsTemplatesEvent
|
||||||
| AttemptCreateSlackIntegration
|
| AttemptCreateSlackIntegration
|
||||||
| AttemptReinstallSlackIntegration
|
| AttemptReinstallSlackIntegration
|
||||||
@@ -4273,6 +4340,7 @@ export type Event =
|
|||||||
| SecretSyncRemoveSecretsEvent
|
| SecretSyncRemoveSecretsEvent
|
||||||
| GetPkiSyncsEvent
|
| GetPkiSyncsEvent
|
||||||
| GetPkiSyncEvent
|
| GetPkiSyncEvent
|
||||||
|
| GetPkiSyncCertificatesEvent
|
||||||
| CreatePkiSyncEvent
|
| CreatePkiSyncEvent
|
||||||
| UpdatePkiSyncEvent
|
| UpdatePkiSyncEvent
|
||||||
| DeletePkiSyncEvent
|
| DeletePkiSyncEvent
|
||||||
@@ -4373,4 +4441,8 @@ export type Event =
|
|||||||
| PamResourceGetEvent
|
| PamResourceGetEvent
|
||||||
| PamResourceCreateEvent
|
| PamResourceCreateEvent
|
||||||
| PamResourceUpdateEvent
|
| PamResourceUpdateEvent
|
||||||
| PamResourceDeleteEvent;
|
| PamResourceDeleteEvent
|
||||||
|
| UpdateCertificateRenewalConfigEvent
|
||||||
|
| DisableCertificateRenewalConfigEvent
|
||||||
|
| AutomatedRenewCertificate
|
||||||
|
| AutomatedRenewCertificateFailed;
|
||||||
|
|||||||
@@ -112,7 +112,7 @@ export const dynamicSecretServiceFactory = ({
|
|||||||
|
|
||||||
const existingDynamicSecret = await dynamicSecretDAL.findOne({ name, folderId: folder.id });
|
const existingDynamicSecret = await dynamicSecretDAL.findOne({ name, folderId: folder.id });
|
||||||
if (existingDynamicSecret)
|
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 selectedProvider = dynamicSecretProviders[provider.type];
|
||||||
const inputs = await selectedProvider.validateProviderInputs(provider.inputs, { projectId });
|
const inputs = await selectedProvider.validateProviderInputs(provider.inputs, { projectId });
|
||||||
@@ -265,7 +265,7 @@ export const dynamicSecretServiceFactory = ({
|
|||||||
if (newName) {
|
if (newName) {
|
||||||
const existingDynamicSecret = await dynamicSecretDAL.findOne({ name: newName, folderId: folder.id });
|
const existingDynamicSecret = await dynamicSecretDAL.findOne({ name: newName, folderId: folder.id });
|
||||||
if (existingDynamicSecret)
|
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 } =
|
const { encryptor: secretManagerEncryptor, decryptor: secretManagerDecryptor } =
|
||||||
await kmsService.createCipherPairWithDataKey({
|
await kmsService.createCipherPairWithDataKey({
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ import { KmsDataKey } from "@app/services/kms/kms-types";
|
|||||||
import { TNotificationServiceFactory } from "@app/services/notification/notification-service";
|
import { TNotificationServiceFactory } from "@app/services/notification/notification-service";
|
||||||
import { NotificationType } from "@app/services/notification/notification-types";
|
import { NotificationType } from "@app/services/notification/notification-types";
|
||||||
import { TOrgDALFactory } from "@app/services/org/org-dal";
|
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 { TLicenseServiceFactory } from "../license/license-service";
|
||||||
import { PamResource } from "../pam-resource/pam-resource-enums";
|
import { PamResource } from "../pam-resource/pam-resource-enums";
|
||||||
@@ -61,8 +61,7 @@ export const gatewayV2ServiceFactory = ({
|
|||||||
relayDAL,
|
relayDAL,
|
||||||
permissionService,
|
permissionService,
|
||||||
orgDAL,
|
orgDAL,
|
||||||
notificationService,
|
notificationService
|
||||||
smtpService
|
|
||||||
}: TGatewayV2ServiceFactoryDep) => {
|
}: TGatewayV2ServiceFactoryDep) => {
|
||||||
const $validateIdentityAccessToGateway = async (orgId: string, actorId: string, actorAuthMethod: ActorAuthMethod) => {
|
const $validateIdentityAccessToGateway = async (orgId: string, actorId: string, actorAuthMethod: ActorAuthMethod) => {
|
||||||
const orgLicensePlan = await licenseService.getPlan(orgId);
|
const orgLicensePlan = await licenseService.getPlan(orgId);
|
||||||
@@ -931,15 +930,17 @@ export const gatewayV2ServiceFactory = ({
|
|||||||
}))
|
}))
|
||||||
);
|
);
|
||||||
|
|
||||||
await smtpService.sendMail({
|
// Temporarily disabled email notifications due to excessive noise. Will be revised later
|
||||||
recipients: admins.map((admin) => admin.user.email).filter((v): v is string => !!v),
|
//
|
||||||
subjectLine: "Gateway Health Alert",
|
// await smtpService.sendMail({
|
||||||
substitutions: {
|
// recipients: admins.map((admin) => admin.user.email).filter((v): v is string => !!v),
|
||||||
type: "gateway",
|
// subjectLine: "Gateway Health Alert",
|
||||||
names: gatewayNames
|
// substitutions: {
|
||||||
},
|
// type: "gateway",
|
||||||
template: SmtpTemplates.HealthAlert
|
// names: gatewayNames
|
||||||
});
|
// },
|
||||||
|
// template: SmtpTemplates.HealthAlert
|
||||||
|
// });
|
||||||
|
|
||||||
await Promise.all(gateways.map((gw) => gatewayV2DAL.updateById(gw.id, { healthAlertedAt: new Date() })));
|
await Promise.all(gateways.map((gw) => gatewayV2DAL.updateById(gw.id, { healthAlertedAt: new Date() })));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -341,7 +341,8 @@ export const kmipOperationServiceFactory = ({
|
|||||||
algorithm: completeKeyDetails.internalKms.encryptionAlgorithm,
|
algorithm: completeKeyDetails.internalKms.encryptionAlgorithm,
|
||||||
isActive: !key.isDisabled,
|
isActive: !key.isDisabled,
|
||||||
createdAt: key.createdAt,
|
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 */
|
/* eslint-disable @typescript-eslint/no-unsafe-call */
|
||||||
import { ForbiddenError } from "@casl/ability";
|
import { ForbiddenError } from "@casl/ability";
|
||||||
|
import { requestContext } from "@fastify/request-context";
|
||||||
import { Issuer, Issuer as OpenIdIssuer, Strategy as OpenIdStrategy, TokenSet } from "openid-client";
|
import { Issuer, Issuer as OpenIdIssuer, Strategy as OpenIdStrategy, TokenSet } from "openid-client";
|
||||||
|
|
||||||
import { AccessScope, OrganizationActionScope, OrgMembershipStatus, TableName, TUsers } from "@app/db/schemas";
|
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 { getConfig } from "@app/lib/config/env";
|
||||||
import { crypto } from "@app/lib/crypto";
|
import { crypto } from "@app/lib/crypto";
|
||||||
import { BadRequestError, ForbiddenRequestError, NotFoundError, OidcAuthError } from "@app/lib/errors";
|
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 { OrgServiceActor } from "@app/lib/types";
|
||||||
import { ActorType, AuthMethod, AuthTokenType } from "@app/services/auth/auth-type";
|
import { ActorType, AuthMethod, AuthTokenType } from "@app/services/auth/auth-type";
|
||||||
import { TAuthTokenServiceFactory } from "@app/services/auth-token/auth-token-service";
|
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 ({
|
const updateOidcCfg = async ({
|
||||||
@@ -754,10 +756,35 @@ export const oidcConfigServiceFactory = ({
|
|||||||
callbackPort,
|
callbackPort,
|
||||||
manageGroupMemberships: oidcCfg.manageGroupMemberships
|
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 });
|
cb(null, { isUserCompleted, providerAuthToken });
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.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);
|
cb(error);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -45,17 +45,47 @@ export const decryptAccountCredentials = async ({
|
|||||||
return JSON.parse(decryptedPlainTextBlob.toString()) as TPamAccountCredentials;
|
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,
|
account: T,
|
||||||
projectId: string,
|
projectId: string,
|
||||||
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">
|
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">
|
||||||
): Promise<T & { credentials: TPamAccountCredentials }> => {
|
): Promise<T & { credentials: TPamAccountCredentials; lastRotationMessage: string | null }> => {
|
||||||
return {
|
return {
|
||||||
...account,
|
...account,
|
||||||
credentials: await decryptAccountCredentials({
|
credentials: await decryptAccountCredentials({
|
||||||
encryptedCredentials: account.encryptedCredentials,
|
encryptedCredentials: account.encryptedCredentials,
|
||||||
projectId,
|
projectId,
|
||||||
kmsService
|
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 { OrgServiceActor } from "@app/lib/types";
|
||||||
import { ActorType } from "@app/services/auth/auth-type";
|
import { ActorType } from "@app/services/auth/auth-type";
|
||||||
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
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 { TProjectDALFactory } from "@app/services/project/project-dal";
|
||||||
import { TUserDALFactory } from "@app/services/user/user-dal";
|
import { TUserDALFactory } from "@app/services/user/user-dal";
|
||||||
|
|
||||||
@@ -353,6 +354,7 @@ export const pamAccountServiceFactory = ({
|
|||||||
TPamAccounts & {
|
TPamAccounts & {
|
||||||
resource: Pick<TPamResources, "id" | "name" | "resourceType"> & { rotationCredentialsConfigured: boolean };
|
resource: Pick<TPamResources, "id" | "name" | "resourceType"> & { rotationCredentialsConfigured: boolean };
|
||||||
credentials: TPamAccountCredentials;
|
credentials: TPamAccountCredentials;
|
||||||
|
lastRotationMessage: string | null;
|
||||||
}
|
}
|
||||||
> = [];
|
> = [];
|
||||||
|
|
||||||
@@ -376,6 +378,7 @@ export const pamAccountServiceFactory = ({
|
|||||||
) {
|
) {
|
||||||
// Decrypt the account only if the user has permission to read it
|
// Decrypt the account only if the user has permission to read it
|
||||||
const decryptedAccount = await decryptAccount(account, account.projectId, kmsService);
|
const decryptedAccount = await decryptAccount(account, account.projectId, kmsService);
|
||||||
|
|
||||||
decryptedAndPermittedAccounts.push({
|
decryptedAndPermittedAccounts.push({
|
||||||
...decryptedAccount,
|
...decryptedAccount,
|
||||||
resource: {
|
resource: {
|
||||||
@@ -575,10 +578,10 @@ export const pamAccountServiceFactory = ({
|
|||||||
for (let i = 0; i < accounts.length; i += ROTATION_CONCURRENCY_LIMIT) {
|
for (let i = 0; i < accounts.length; i += ROTATION_CONCURRENCY_LIMIT) {
|
||||||
const batch = accounts.slice(i, i + ROTATION_CONCURRENCY_LIMIT);
|
const batch = accounts.slice(i, i + ROTATION_CONCURRENCY_LIMIT);
|
||||||
|
|
||||||
const rotationPromises = batch.map(async (account) =>
|
const rotationPromises = batch.map(async (account) => {
|
||||||
pamAccountDAL.transaction(async (tx) => {
|
let logResourceType = "unknown";
|
||||||
let logResourceType = "unknown";
|
try {
|
||||||
try {
|
await pamAccountDAL.transaction(async (tx) => {
|
||||||
const resource = await pamResourceDAL.findById(account.resourceId, tx);
|
const resource = await pamResourceDAL.findById(account.resourceId, tx);
|
||||||
if (!resource || !resource.encryptedRotationAccountCredentials) return;
|
if (!resource || !resource.encryptedRotationAccountCredentials) return;
|
||||||
logResourceType = resource.resourceType;
|
logResourceType = resource.resourceType;
|
||||||
@@ -619,7 +622,9 @@ export const pamAccountServiceFactory = ({
|
|||||||
account.id,
|
account.id,
|
||||||
{
|
{
|
||||||
encryptedCredentials,
|
encryptedCredentials,
|
||||||
lastRotatedAt: new Date()
|
lastRotatedAt: new Date(),
|
||||||
|
rotationStatus: "success",
|
||||||
|
encryptedLastRotationMessage: null
|
||||||
},
|
},
|
||||||
tx
|
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({
|
const { encryptor } = await kmsService.createCipherPairWithDataKey({
|
||||||
projectId: account.projectId,
|
type: KmsDataKey.SecretManager,
|
||||||
actor: {
|
projectId: account.projectId
|
||||||
type: ActorType.PLATFORM,
|
});
|
||||||
metadata: {}
|
|
||||||
},
|
const { cipherTextBlob: encryptedMessage } = encryptor({
|
||||||
event: {
|
plainText: Buffer.from(errorMessage)
|
||||||
type: EventType.PAM_ACCOUNT_CREDENTIAL_ROTATION_FAILED,
|
});
|
||||||
metadata: {
|
|
||||||
accountId: account.id,
|
await pamAccountDAL.updateById(account.id, {
|
||||||
accountName: account.name,
|
rotationStatus: "failed",
|
||||||
resourceId: account.resourceId,
|
encryptedLastRotationMessage: encryptedMessage
|
||||||
resourceType: logResourceType,
|
});
|
||||||
errorMessage
|
|
||||||
}
|
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
|
// eslint-disable-next-line no-await-in-loop
|
||||||
await Promise.all(rotationPromises);
|
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 {
|
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>;
|
type TPamResourceFactoryImplementation = TPamResourceFactory<TPamResourceConnectionDetails, TPamAccountCredentials>;
|
||||||
|
|
||||||
export const PAM_RESOURCE_FACTORY_MAP: Record<PamResource, TPamResourceFactoryImplementation> = {
|
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 { KmsDataKey } from "@app/services/kms/kms-types";
|
||||||
|
|
||||||
import { decryptAccountCredentials } from "../pam-account/pam-account-fns";
|
import { decryptAccountCredentials } from "../pam-account/pam-account-fns";
|
||||||
|
import { getMySQLResourceListItem } from "./mysql/mysql-resource-fns";
|
||||||
import { TPamResource, TPamResourceConnectionDetails } from "./pam-resource-types";
|
import { TPamResource, TPamResourceConnectionDetails } from "./pam-resource-types";
|
||||||
import { getPostgresResourceListItem } from "./postgres/postgres-resource-fns";
|
import { getPostgresResourceListItem } from "./postgres/postgres-resource-fns";
|
||||||
|
|
||||||
export const listResourceOptions = () => {
|
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
|
// Resource
|
||||||
|
|||||||
@@ -33,7 +33,9 @@ export const BasePamAccountSchemaWithResource = BasePamAccountSchema.extend({
|
|||||||
resourceType: true
|
resourceType: true
|
||||||
}).extend({
|
}).extend({
|
||||||
rotationCredentialsConfigured: z.boolean()
|
rotationCredentialsConfigured: z.boolean()
|
||||||
})
|
}),
|
||||||
|
lastRotationMessage: z.string().nullable().optional(),
|
||||||
|
rotationStatus: z.string().nullable().optional()
|
||||||
});
|
});
|
||||||
|
|
||||||
export const BaseCreatePamAccountSchema = z.object({
|
export const BaseCreatePamAccountSchema = z.object({
|
||||||
|
|||||||
@@ -1,4 +1,10 @@
|
|||||||
import { TGatewayV2ServiceFactory } from "../gateway-v2/gateway-v2-service";
|
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 { PamResource } from "./pam-resource-enums";
|
||||||
import {
|
import {
|
||||||
TPostgresAccount,
|
TPostgresAccount,
|
||||||
@@ -8,12 +14,13 @@ import {
|
|||||||
} from "./postgres/postgres-resource-types";
|
} from "./postgres/postgres-resource-types";
|
||||||
|
|
||||||
// Resource types
|
// Resource types
|
||||||
export type TPamResource = TPostgresResource;
|
export type TPamResource = TPostgresResource | TMySQLResource;
|
||||||
export type TPamResourceConnectionDetails = TPostgresResourceConnectionDetails;
|
export type TPamResourceConnectionDetails = TPostgresResourceConnectionDetails | TMySQLResourceConnectionDetails;
|
||||||
|
|
||||||
// Account types
|
// Account types
|
||||||
export type TPamAccount = TPostgresAccount;
|
export type TPamAccount = TPostgresAccount | TMySQLAccount;
|
||||||
export type TPamAccountCredentials = TPostgresAccountCredentials;
|
// eslint-disable-next-line @typescript-eslint/no-duplicate-type-constituents
|
||||||
|
export type TPamAccountCredentials = TPostgresAccountCredentials | TMySQLAccountCredentials;
|
||||||
|
|
||||||
// Resource DTOs
|
// Resource DTOs
|
||||||
export type TCreateResourceDTO = Pick<
|
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 tls, { PeerCertificate } from "tls";
|
||||||
|
|
||||||
import { verifyHostInputValidity } from "@app/ee/services/dynamic-secret/dynamic-secret-fns";
|
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_USERNAME = "infisical-gateway-connection-test";
|
||||||
const TEST_CONNECTION_PASSWORD = "infisical-gateway-connection-test-password";
|
const TEST_CONNECTION_PASSWORD = "infisical-gateway-connection-test-password";
|
||||||
|
const SIMPLE_QUERY = "select 1";
|
||||||
|
|
||||||
const SQL_CONNECTION_CLIENT_MAP = {
|
export interface SqlResourceConnection {
|
||||||
[PamResource.Postgres]: "pg"
|
/**
|
||||||
};
|
* 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,
|
* Rotate password and return the new credentials.
|
||||||
{ host, sslEnabled, sslRejectUnauthorized, sslCertificate }: TSqlResourceConnectionDetails
|
*
|
||||||
) => {
|
* @param currentCredentials the current credentials to rotate
|
||||||
switch (resourceType) {
|
*
|
||||||
|
* @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: {
|
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 {
|
return {
|
||||||
ssl: sslEnabled
|
validate: async (connectOnly) => {
|
||||||
? {
|
try {
|
||||||
rejectUnauthorized: sslRejectUnauthorized,
|
await client.raw(SIMPLE_QUERY);
|
||||||
ca: sslCertificate,
|
} catch (error) {
|
||||||
servername: host,
|
if (error instanceof pg.DatabaseError) {
|
||||||
// When using proxy, we need to bypass hostname validation since we connect to localhost
|
// Hacky way to know if we successfully hit the database.
|
||||||
// but validate the certificate against the actual hostname
|
// TODO: potentially two approaches to solve the problem.
|
||||||
checkServerIdentity: (hostname: string, cert: PeerCertificate) => {
|
// 1. change the work flow, add account first then resource
|
||||||
return tls.checkServerIdentity(host, cert);
|
// 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:
|
default:
|
||||||
@@ -62,10 +196,9 @@ export const executeWithGateway = async <T>(
|
|||||||
password?: string;
|
password?: string;
|
||||||
},
|
},
|
||||||
gatewayV2Service: Pick<TGatewayV2ServiceFactory, "getPlatformConnectionDetailsByGatewayId">,
|
gatewayV2Service: Pick<TGatewayV2ServiceFactory, "getPlatformConnectionDetailsByGatewayId">,
|
||||||
operation: (client: Knex) => Promise<T>
|
operation: (connection: SqlResourceConnection) => Promise<T>
|
||||||
): Promise<T> => {
|
): Promise<T> => {
|
||||||
const { connectionDetails, resourceType, gatewayId, username, password } = config;
|
const { connectionDetails, gatewayId } = config;
|
||||||
|
|
||||||
const [targetHost] = await verifyHostInputValidity(connectionDetails.host, true);
|
const [targetHost] = await verifyHostInputValidity(connectionDetails.host, true);
|
||||||
const platformConnectionDetails = await gatewayV2Service.getPlatformConnectionDetailsByGatewayId({
|
const platformConnectionDetails = await gatewayV2Service.getPlatformConnectionDetailsByGatewayId({
|
||||||
gatewayId,
|
gatewayId,
|
||||||
@@ -79,22 +212,11 @@ export const executeWithGateway = async <T>(
|
|||||||
|
|
||||||
return withGatewayV2Proxy(
|
return withGatewayV2Proxy(
|
||||||
async (proxyPort) => {
|
async (proxyPort) => {
|
||||||
const client = knex({
|
const connection = makeSqlConnection(proxyPort, config);
|
||||||
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)
|
|
||||||
}
|
|
||||||
});
|
|
||||||
try {
|
try {
|
||||||
return await operation(client);
|
return await operation(connection);
|
||||||
} finally {
|
} finally {
|
||||||
await client.destroy();
|
await connection.close();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -115,25 +237,14 @@ export const sqlResourceFactory: TPamResourceFactory<TSqlResourceConnectionDetai
|
|||||||
const validateConnection = async () => {
|
const validateConnection = async () => {
|
||||||
try {
|
try {
|
||||||
await executeWithGateway({ connectionDetails, gatewayId, resourceType }, gatewayV2Service, async (client) => {
|
await executeWithGateway({ connectionDetails, gatewayId, resourceType }, gatewayV2Service, async (client) => {
|
||||||
await client.raw("Select 1");
|
await client.validate(true);
|
||||||
});
|
});
|
||||||
return connectionDetails;
|
return connectionDetails;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// Hacky way to know if we successfully hit the database
|
if (error instanceof BadRequestError && error.message === "Connection terminated unexpectedly") {
|
||||||
if (error instanceof BadRequestError) {
|
throw new BadRequestError({
|
||||||
if (error.message === `password authentication failed for user "${TEST_CONNECTION_USERNAME}"`) {
|
message: "Connection terminated unexpectedly. Verify that host and port are correct"
|
||||||
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"
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new BadRequestError({
|
throw new BadRequestError({
|
||||||
@@ -156,11 +267,12 @@ export const sqlResourceFactory: TPamResourceFactory<TSqlResourceConnectionDetai
|
|||||||
},
|
},
|
||||||
gatewayV2Service,
|
gatewayV2Service,
|
||||||
async (client) => {
|
async (client) => {
|
||||||
await client.raw("Select 1");
|
await client.validate(false);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
return credentials;
|
return credentials;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
// TODO: extract these logic into each SQL connection
|
||||||
if (error instanceof BadRequestError) {
|
if (error instanceof BadRequestError) {
|
||||||
if (error.message === `password authentication failed for user "${credentials.username}"`) {
|
if (error.message === `password authentication failed for user "${credentials.username}"`) {
|
||||||
throw new BadRequestError({
|
throw new BadRequestError({
|
||||||
@@ -185,10 +297,9 @@ export const sqlResourceFactory: TPamResourceFactory<TSqlResourceConnectionDetai
|
|||||||
rotationAccountCredentials,
|
rotationAccountCredentials,
|
||||||
currentCredentials
|
currentCredentials
|
||||||
) => {
|
) => {
|
||||||
|
const newPassword = alphaNumericNanoId(32);
|
||||||
try {
|
try {
|
||||||
const newPassword = alphaNumericNanoId(32);
|
return await executeWithGateway(
|
||||||
|
|
||||||
await executeWithGateway(
|
|
||||||
{
|
{
|
||||||
connectionDetails,
|
connectionDetails,
|
||||||
gatewayId,
|
gatewayId,
|
||||||
@@ -197,20 +308,8 @@ export const sqlResourceFactory: TPamResourceFactory<TSqlResourceConnectionDetai
|
|||||||
password: rotationAccountCredentials.password
|
password: rotationAccountCredentials.password
|
||||||
},
|
},
|
||||||
gatewayV2Service,
|
gatewayV2Service,
|
||||||
async (client) => {
|
(client) => client.rotateCredentials(currentCredentials, newPassword)
|
||||||
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.`
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
);
|
||||||
|
|
||||||
return { username: currentCredentials.username, password: newPassword };
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof BadRequestError) {
|
if (error instanceof BadRequestError) {
|
||||||
if (error.message === `password authentication failed for user "${rotationAccountCredentials.username}"`) {
|
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({
|
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 {
|
import {
|
||||||
TPostgresAccountCredentials,
|
TPostgresAccountCredentials,
|
||||||
TPostgresResourceConnectionDetails
|
TPostgresResourceConnectionDetails
|
||||||
} from "../../postgres/postgres-resource-types";
|
} from "../../postgres/postgres-resource-types";
|
||||||
|
|
||||||
export type TSqlResourceConnectionDetails = TPostgresResourceConnectionDetails;
|
export type TSqlResourceConnectionDetails = TPostgresResourceConnectionDetails | TMySQLResourceConnectionDetails;
|
||||||
export type TSqlAccountCredentials = TPostgresAccountCredentials;
|
// 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` });
|
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) {
|
if (projectDetails.orgId !== actorOrgId) {
|
||||||
throw new ForbiddenRequestError({ name: "You are not logged into this organization" });
|
throw new ForbiddenRequestError({ name: "You are not logged into this organization" });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1268,15 +1268,17 @@ export const relayServiceFactory = ({
|
|||||||
}))
|
}))
|
||||||
);
|
);
|
||||||
|
|
||||||
await smtpService.sendMail({
|
// Temporarily disabled email notifications due to excessive noise. Will be revised later
|
||||||
recipients: admins.map((admin) => admin.user.email).filter((v): v is string => !!v),
|
//
|
||||||
subjectLine: "Relay Health Alert",
|
// await smtpService.sendMail({
|
||||||
substitutions: {
|
// recipients: admins.map((admin) => admin.user.email).filter((v): v is string => !!v),
|
||||||
type: "relay",
|
// subjectLine: "Relay Health Alert",
|
||||||
names: relayNames
|
// substitutions: {
|
||||||
},
|
// type: "relay",
|
||||||
template: SmtpTemplates.HealthAlert
|
// names: relayNames
|
||||||
});
|
// },
|
||||||
|
// template: SmtpTemplates.HealthAlert
|
||||||
|
// });
|
||||||
}
|
}
|
||||||
|
|
||||||
await Promise.all(relays.map((r) => relayDAL.updateById(r.id, { healthAlertedAt: new Date() })));
|
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 {
|
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 { TOrgPermission } from "@app/lib/types";
|
||||||
import { ActorAuthMethod, ActorType } from "@app/services/auth/auth-type";
|
import { ActorAuthMethod, ActorType } from "@app/services/auth/auth-type";
|
||||||
|
|
||||||
@@ -78,5 +78,7 @@ export type TSamlConfigServiceFactory = {
|
|||||||
samlLogin: (arg: TSamlLoginDTO) => Promise<{
|
samlLogin: (arg: TSamlLoginDTO) => Promise<{
|
||||||
isUserCompleted: boolean;
|
isUserCompleted: boolean;
|
||||||
providerAuthToken: string;
|
providerAuthToken: string;
|
||||||
|
user: TUsers;
|
||||||
|
organization: TOrganizations;
|
||||||
}>;
|
}>;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1517,7 +1517,7 @@ export const secretApprovalRequestServiceFactory = ({
|
|||||||
}))
|
}))
|
||||||
);
|
);
|
||||||
if (secrets.length)
|
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(
|
commits.push(
|
||||||
...createdSecrets.map((createdSecret) => ({
|
...createdSecrets.map((createdSecret) => ({
|
||||||
|
|||||||
@@ -2348,6 +2348,9 @@ export const AppConnections = {
|
|||||||
RAILWAY: {
|
RAILWAY: {
|
||||||
apiToken: "The API token used to authenticate with Railway."
|
apiToken: "The API token used to authenticate with Railway."
|
||||||
},
|
},
|
||||||
|
NORTHFLANK: {
|
||||||
|
apiToken: "The API token used to authenticate with Northflank."
|
||||||
|
},
|
||||||
CHECKLY: {
|
CHECKLY: {
|
||||||
apiKey: "The API key used to authenticate with Checkly."
|
apiKey: "The API key used to authenticate with Checkly."
|
||||||
},
|
},
|
||||||
@@ -2376,6 +2379,12 @@ export const AppConnections = {
|
|||||||
},
|
},
|
||||||
LARAVEL_FORGE: {
|
LARAVEL_FORGE: {
|
||||||
apiToken: "The API token used to authenticate with 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.",
|
siteName: "The name of the Netlify site to sync secrets to.",
|
||||||
siteId: "The ID 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."
|
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 { verifyHostInputValidity } from "@app/ee/services/dynamic-secret/dynamic-secret-fns";
|
||||||
import { splitPemChain } from "@app/services/certificate/certificate-fns";
|
import { splitPemChain } from "@app/services/certificate/certificate-fns";
|
||||||
|
|
||||||
|
import { getConfig } from "../config/env";
|
||||||
import { BadRequestError } from "../errors";
|
import { BadRequestError } from "../errors";
|
||||||
import { GatewayProxyProtocol } from "../gateway/types";
|
import { GatewayProxyProtocol } from "../gateway/types";
|
||||||
import { logger } from "../logger";
|
import { logger } from "../logger";
|
||||||
@@ -80,6 +81,8 @@ const createGatewayConnection = async (
|
|||||||
gateway: { clientCertificate: string; clientPrivateKey: string; serverCertificateChain: string },
|
gateway: { clientCertificate: string; clientPrivateKey: string; serverCertificateChain: string },
|
||||||
protocol: GatewayProxyProtocol
|
protocol: GatewayProxyProtocol
|
||||||
): Promise<net.Socket> => {
|
): Promise<net.Socket> => {
|
||||||
|
const appCfg = getConfig();
|
||||||
|
|
||||||
const protocolToAlpn = {
|
const protocolToAlpn = {
|
||||||
[GatewayProxyProtocol.Http]: "infisical-http-proxy",
|
[GatewayProxyProtocol.Http]: "infisical-http-proxy",
|
||||||
[GatewayProxyProtocol.Tcp]: "infisical-tcp-proxy",
|
[GatewayProxyProtocol.Tcp]: "infisical-tcp-proxy",
|
||||||
@@ -94,7 +97,8 @@ const createGatewayConnection = async (
|
|||||||
minVersion: "TLSv1.2",
|
minVersion: "TLSv1.2",
|
||||||
maxVersion: "TLSv1.3",
|
maxVersion: "TLSv1.3",
|
||||||
rejectUnauthorized: true,
|
rejectUnauthorized: true,
|
||||||
ALPNProtocols: [protocolToAlpn[protocol]]
|
ALPNProtocols: [protocolToAlpn[protocol]],
|
||||||
|
checkServerIdentity: appCfg.isDevelopmentMode ? () => undefined : tls.checkServerIdentity
|
||||||
};
|
};
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
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",
|
SecretReminderMigration = "secret-reminder-migration",
|
||||||
UserNotification = "user-notification",
|
UserNotification = "user-notification",
|
||||||
HealthAlert = "health-alert",
|
HealthAlert = "health-alert",
|
||||||
|
CertificateV3AutoRenewal = "certificate-v3-auto-renewal",
|
||||||
PamAccountRotation = "pam-account-rotation"
|
PamAccountRotation = "pam-account-rotation"
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -128,6 +129,7 @@ export enum QueueJobs {
|
|||||||
SecretReminderMigration = "secret-reminder-migration",
|
SecretReminderMigration = "secret-reminder-migration",
|
||||||
UserNotification = "user-notification-job",
|
UserNotification = "user-notification-job",
|
||||||
HealthAlert = "health-alert",
|
HealthAlert = "health-alert",
|
||||||
|
CertificateV3DailyAutoRenewal = "certificate-v3-daily-auto-renewal",
|
||||||
PamAccountRotation = "pam-account-rotation"
|
PamAccountRotation = "pam-account-rotation"
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -359,6 +361,10 @@ export type TQueueJobTypes = {
|
|||||||
name: QueueJobs.HealthAlert;
|
name: QueueJobs.HealthAlert;
|
||||||
payload: undefined;
|
payload: undefined;
|
||||||
};
|
};
|
||||||
|
[QueueName.CertificateV3AutoRenewal]: {
|
||||||
|
name: QueueJobs.CertificateV3DailyAutoRenewal;
|
||||||
|
payload: undefined;
|
||||||
|
};
|
||||||
[QueueName.PamAccountRotation]: {
|
[QueueName.PamAccountRotation]: {
|
||||||
name: QueueJobs.PamAccountRotation;
|
name: QueueJobs.PamAccountRotation;
|
||||||
payload: undefined;
|
payload: undefined;
|
||||||
|
|||||||
@@ -141,7 +141,9 @@ export const main = async ({
|
|||||||
await server.register(fastifyRequestContext, {
|
await server.register(fastifyRequestContext, {
|
||||||
defaultStoreValues: (req) => ({
|
defaultStoreValues: (req) => ({
|
||||||
reqId: req.id,
|
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 opentelemetry from "@opentelemetry/api";
|
||||||
import fp from "fastify-plugin";
|
import fp from "fastify-plugin";
|
||||||
|
|
||||||
export const apiMetrics = fp(async (fastify) => {
|
const apiMeter = opentelemetry.metrics.getMeter("API");
|
||||||
const apiMeter = opentelemetry.metrics.getMeter("API");
|
|
||||||
const latencyHistogram = apiMeter.createHistogram("API_latency", {
|
|
||||||
unit: "ms"
|
|
||||||
});
|
|
||||||
|
|
||||||
|
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) => {
|
fastify.addHook("onResponse", async (request, reply) => {
|
||||||
const { method } = request;
|
const { method } = request;
|
||||||
const route = request.routerPath;
|
const route = request.routerPath;
|
||||||
@@ -17,5 +31,67 @@ export const apiMetrics = fp(async (fastify) => {
|
|||||||
method,
|
method,
|
||||||
statusCode
|
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 { FastifyRequest } from "fastify";
|
||||||
import fp from "fastify-plugin";
|
import fp from "fastify-plugin";
|
||||||
import type { JwtPayload } from "jsonwebtoken";
|
import type { JwtPayload } from "jsonwebtoken";
|
||||||
@@ -138,6 +138,11 @@ export const injectIdentity = fp(
|
|||||||
return;
|
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.
|
// Authentication is handled on a route-level here.
|
||||||
if (req.url.includes("/api/v1/workflow-integrations/microsoft-teams/message-endpoint")) {
|
if (req.url.includes("/api/v1/workflow-integrations/microsoft-teams/message-endpoint")) {
|
||||||
return;
|
return;
|
||||||
@@ -154,10 +159,11 @@ export const injectIdentity = fp(
|
|||||||
|
|
||||||
switch (authMode) {
|
switch (authMode) {
|
||||||
case AuthMode.JWT: {
|
case AuthMode.JWT: {
|
||||||
const { user, tokenVersionId, orgId, rootOrgId, parentOrgId } =
|
const { user, tokenVersionId, orgId, orgName, rootOrgId, parentOrgId } =
|
||||||
await server.services.authToken.fnValidateJwtIdentity(token, subOrganizationSelector);
|
await server.services.authToken.fnValidateJwtIdentity(token, subOrganizationSelector);
|
||||||
requestContext.set("orgId", orgId);
|
requestContext.set("orgId", orgId);
|
||||||
|
requestContext.set("orgName", orgName);
|
||||||
|
requestContext.set("userAuthInfo", { userId: user.id, email: user.email || "" });
|
||||||
req.auth = {
|
req.auth = {
|
||||||
authMode: AuthMode.JWT,
|
authMode: AuthMode.JWT,
|
||||||
user,
|
user,
|
||||||
@@ -181,6 +187,7 @@ export const injectIdentity = fp(
|
|||||||
);
|
);
|
||||||
const serverCfg = await getServerCfg();
|
const serverCfg = await getServerCfg();
|
||||||
requestContext.set("orgId", identity.orgId);
|
requestContext.set("orgId", identity.orgId);
|
||||||
|
requestContext.set("orgName", identity.orgName);
|
||||||
req.auth = {
|
req.auth = {
|
||||||
authMode: AuthMode.IDENTITY_ACCESS_TOKEN,
|
authMode: AuthMode.IDENTITY_ACCESS_TOKEN,
|
||||||
actor,
|
actor,
|
||||||
@@ -193,24 +200,23 @@ export const injectIdentity = fp(
|
|||||||
isInstanceAdmin: serverCfg?.adminIdentityIds?.includes(identity.identityId),
|
isInstanceAdmin: serverCfg?.adminIdentityIds?.includes(identity.identityId),
|
||||||
token
|
token
|
||||||
};
|
};
|
||||||
|
const identityAuthInfo: RequestContextData["identityAuthInfo"] = {
|
||||||
|
identityId: identity.identityId,
|
||||||
|
identityName: identity.name,
|
||||||
|
authMethod: identity.authMethod
|
||||||
|
};
|
||||||
|
|
||||||
if (token?.identityAuth?.oidc) {
|
if (token?.identityAuth?.oidc) {
|
||||||
requestContext.set("identityAuthInfo", {
|
identityAuthInfo.oidc = token?.identityAuth?.oidc;
|
||||||
identityId: identity.identityId,
|
|
||||||
oidc: token?.identityAuth?.oidc
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
if (token?.identityAuth?.kubernetes) {
|
if (token?.identityAuth?.kubernetes) {
|
||||||
requestContext.set("identityAuthInfo", {
|
identityAuthInfo.kubernetes = token?.identityAuth?.kubernetes;
|
||||||
identityId: identity.identityId,
|
|
||||||
kubernetes: token?.identityAuth?.kubernetes
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
if (token?.identityAuth?.aws) {
|
if (token?.identityAuth?.aws) {
|
||||||
requestContext.set("identityAuthInfo", {
|
identityAuthInfo.aws = token?.identityAuth?.aws;
|
||||||
identityId: identity.identityId,
|
|
||||||
aws: token?.identityAuth?.aws
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
requestContext.set("identityAuthInfo", identityAuthInfo);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case AuthMode.SERVICE_TOKEN: {
|
case AuthMode.SERVICE_TOKEN: {
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { ForbiddenError, PureAbility } from "@casl/ability";
|
import { ForbiddenError, PureAbility } from "@casl/ability";
|
||||||
|
import { requestContext } from "@fastify/request-context";
|
||||||
import opentelemetry from "@opentelemetry/api";
|
import opentelemetry from "@opentelemetry/api";
|
||||||
import fastifyPlugin from "fastify-plugin";
|
import fastifyPlugin from "fastify-plugin";
|
||||||
import jwt from "jsonwebtoken";
|
import jwt from "jsonwebtoken";
|
||||||
@@ -47,6 +48,12 @@ export const fastifyErrHandler = fastifyPlugin(async (server: FastifyZodProvider
|
|||||||
unit: "1"
|
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) => {
|
server.setErrorHandler((error, req, res) => {
|
||||||
req.log.error(error);
|
req.log.error(error);
|
||||||
if (appCfg.OTEL_TELEMETRY_COLLECTION_ENABLED) {
|
if (appCfg.OTEL_TELEMETRY_COLLECTION_ENABLED) {
|
||||||
@@ -61,6 +68,67 @@ export const fastifyErrHandler = fastifyPlugin(async (server: FastifyZodProvider
|
|||||||
type: errorType,
|
type: errorType,
|
||||||
name: error.name
|
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) {
|
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 { certificateEstV3ServiceFactory } from "@app/services/certificate-est-v3/certificate-est-v3-service";
|
||||||
import { certificateProfileDALFactory } from "@app/services/certificate-profile/certificate-profile-dal";
|
import { certificateProfileDALFactory } from "@app/services/certificate-profile/certificate-profile-dal";
|
||||||
import { certificateProfileServiceFactory } from "@app/services/certificate-profile/certificate-profile-service";
|
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 { certificateTemplateDALFactory } from "@app/services/certificate-template/certificate-template-dal";
|
||||||
import { certificateTemplateEstConfigDALFactory } from "@app/services/certificate-template/certificate-template-est-config-dal";
|
import { certificateTemplateEstConfigDALFactory } from "@app/services/certificate-template/certificate-template-est-config-dal";
|
||||||
import { certificateTemplateServiceFactory } from "@app/services/certificate-template/certificate-template-service";
|
import { certificateTemplateServiceFactory } from "@app/services/certificate-template/certificate-template-service";
|
||||||
import { certificateTemplateV2DALFactory } from "@app/services/certificate-template-v2/certificate-template-v2-dal";
|
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 { 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 { certificateV3ServiceFactory } from "@app/services/certificate-v3/certificate-v3-service";
|
||||||
import { cmekServiceFactory } from "@app/services/cmek/cmek-service";
|
import { cmekServiceFactory } from "@app/services/cmek/cmek-service";
|
||||||
import { convertorServiceFactory } from "@app/services/convertor/convertor-service";
|
import { convertorServiceFactory } from "@app/services/convertor/convertor-service";
|
||||||
@@ -607,6 +609,10 @@ export const registerRoutes = async (
|
|||||||
const membershipGroupService = membershipGroupServiceFactory({
|
const membershipGroupService = membershipGroupServiceFactory({
|
||||||
membershipGroupDAL,
|
membershipGroupDAL,
|
||||||
membershipRoleDAL,
|
membershipRoleDAL,
|
||||||
|
accessApprovalPolicyDAL,
|
||||||
|
accessApprovalPolicyApproverDAL,
|
||||||
|
secretApprovalPolicyDAL,
|
||||||
|
secretApprovalPolicyApproverDAL: sapApproverDAL,
|
||||||
roleDAL,
|
roleDAL,
|
||||||
permissionService,
|
permissionService,
|
||||||
orgDAL
|
orgDAL
|
||||||
@@ -1059,6 +1065,7 @@ export const registerRoutes = async (
|
|||||||
const certificateDAL = certificateDALFactory(db);
|
const certificateDAL = certificateDALFactory(db);
|
||||||
const certificateBodyDAL = certificateBodyDALFactory(db);
|
const certificateBodyDAL = certificateBodyDALFactory(db);
|
||||||
const certificateSecretDAL = certificateSecretDALFactory(db);
|
const certificateSecretDAL = certificateSecretDALFactory(db);
|
||||||
|
const certificateSyncDAL = certificateSyncDALFactory(db);
|
||||||
|
|
||||||
const pkiAlertDAL = pkiAlertDALFactory(db);
|
const pkiAlertDAL = pkiAlertDALFactory(db);
|
||||||
const pkiCollectionDAL = pkiCollectionDALFactory(db);
|
const pkiCollectionDAL = pkiCollectionDALFactory(db);
|
||||||
@@ -1707,7 +1714,8 @@ export const registerRoutes = async (
|
|||||||
licenseService,
|
licenseService,
|
||||||
permissionService,
|
permissionService,
|
||||||
kmsService,
|
kmsService,
|
||||||
membershipIdentityDAL
|
membershipIdentityDAL,
|
||||||
|
orgDAL
|
||||||
});
|
});
|
||||||
|
|
||||||
const identityAwsAuthService = identityAwsAuthServiceFactory({
|
const identityAwsAuthService = identityAwsAuthServiceFactory({
|
||||||
@@ -1960,6 +1968,8 @@ export const registerRoutes = async (
|
|||||||
secretImportDAL,
|
secretImportDAL,
|
||||||
permissionService,
|
permissionService,
|
||||||
appConnectionService,
|
appConnectionService,
|
||||||
|
projectDAL,
|
||||||
|
orgDAL,
|
||||||
folderDAL,
|
folderDAL,
|
||||||
secretSyncQueue,
|
secretSyncQueue,
|
||||||
projectBotService,
|
projectBotService,
|
||||||
@@ -2022,7 +2032,8 @@ export const registerRoutes = async (
|
|||||||
certificateBodyDAL,
|
certificateBodyDAL,
|
||||||
certificateSecretDAL,
|
certificateSecretDAL,
|
||||||
certificateAuthorityDAL,
|
certificateAuthorityDAL,
|
||||||
certificateAuthorityCertDAL
|
certificateAuthorityCertDAL,
|
||||||
|
certificateSyncDAL
|
||||||
});
|
});
|
||||||
|
|
||||||
const pkiSyncCleanup = pkiSyncCleanupQueueServiceFactory({
|
const pkiSyncCleanup = pkiSyncCleanupQueueServiceFactory({
|
||||||
@@ -2133,17 +2144,29 @@ export const registerRoutes = async (
|
|||||||
permissionService,
|
permissionService,
|
||||||
pkiCollectionDAL,
|
pkiCollectionDAL,
|
||||||
pkiCollectionItemDAL,
|
pkiCollectionItemDAL,
|
||||||
|
certificateSyncDAL,
|
||||||
pkiSyncDAL,
|
pkiSyncDAL,
|
||||||
pkiSyncQueue
|
pkiSyncQueue
|
||||||
});
|
});
|
||||||
|
|
||||||
const certificateV3Service = certificateV3ServiceFactory({
|
const certificateV3Service = certificateV3ServiceFactory({
|
||||||
certificateDAL,
|
certificateDAL,
|
||||||
|
certificateSecretDAL,
|
||||||
certificateAuthorityDAL,
|
certificateAuthorityDAL,
|
||||||
certificateProfileDAL,
|
certificateProfileDAL,
|
||||||
certificateTemplateV2Service,
|
certificateTemplateV2Service,
|
||||||
internalCaService: internalCertificateAuthorityService,
|
internalCaService: internalCertificateAuthorityService,
|
||||||
permissionService
|
permissionService,
|
||||||
|
certificateSyncDAL,
|
||||||
|
pkiSyncDAL,
|
||||||
|
pkiSyncQueue
|
||||||
|
});
|
||||||
|
|
||||||
|
const certificateV3Queue = certificateV3QueueServiceFactory({
|
||||||
|
queueService,
|
||||||
|
certificateDAL,
|
||||||
|
certificateV3Service,
|
||||||
|
auditLogService
|
||||||
});
|
});
|
||||||
|
|
||||||
const certificateEstV3Service = certificateEstV3ServiceFactory({
|
const certificateEstV3Service = certificateEstV3ServiceFactory({
|
||||||
@@ -2178,6 +2201,8 @@ export const registerRoutes = async (
|
|||||||
|
|
||||||
const pkiSyncService = pkiSyncServiceFactory({
|
const pkiSyncService = pkiSyncServiceFactory({
|
||||||
pkiSyncDAL,
|
pkiSyncDAL,
|
||||||
|
certificateDAL,
|
||||||
|
certificateSyncDAL,
|
||||||
pkiSubscriberDAL,
|
pkiSubscriberDAL,
|
||||||
appConnectionService,
|
appConnectionService,
|
||||||
permissionService,
|
permissionService,
|
||||||
@@ -2333,6 +2358,7 @@ export const registerRoutes = async (
|
|||||||
await dailyReminderQueueService.startSecretReminderMigrationJob();
|
await dailyReminderQueueService.startSecretReminderMigrationJob();
|
||||||
await dailyExpiringPkiItemAlert.startSendingAlerts();
|
await dailyExpiringPkiItemAlert.startSendingAlerts();
|
||||||
await pkiSubscriberQueue.startDailyAutoRenewalJob();
|
await pkiSubscriberQueue.startDailyAutoRenewalJob();
|
||||||
|
await certificateV3Queue.init();
|
||||||
await kmsService.startService(hsmStatus);
|
await kmsService.startService(hsmStatus);
|
||||||
await microsoftTeamsService.start();
|
await microsoftTeamsService.start();
|
||||||
await dynamicSecretQueueService.init();
|
await dynamicSecretQueueService.init();
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ import {
|
|||||||
ChecklyConnectionListItemSchema,
|
ChecklyConnectionListItemSchema,
|
||||||
SanitizedChecklyConnectionSchema
|
SanitizedChecklyConnectionSchema
|
||||||
} from "@app/services/app-connection/checkly";
|
} from "@app/services/app-connection/checkly";
|
||||||
|
import { ChefConnectionListItemSchema, SanitizedChefConnectionSchema } from "@app/services/app-connection/chef";
|
||||||
import {
|
import {
|
||||||
CloudflareConnectionListItemSchema,
|
CloudflareConnectionListItemSchema,
|
||||||
SanitizedCloudflareConnectionSchema
|
SanitizedCloudflareConnectionSchema
|
||||||
@@ -88,6 +89,10 @@ import {
|
|||||||
NetlifyConnectionListItemSchema,
|
NetlifyConnectionListItemSchema,
|
||||||
SanitizedNetlifyConnectionSchema
|
SanitizedNetlifyConnectionSchema
|
||||||
} from "@app/services/app-connection/netlify";
|
} 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 { OktaConnectionListItemSchema, SanitizedOktaConnectionSchema } from "@app/services/app-connection/okta";
|
||||||
import {
|
import {
|
||||||
PostgresConnectionListItemSchema,
|
PostgresConnectionListItemSchema,
|
||||||
@@ -160,10 +165,12 @@ const SanitizedAppConnectionSchema = z.union([
|
|||||||
...SanitizedSupabaseConnectionSchema.options,
|
...SanitizedSupabaseConnectionSchema.options,
|
||||||
...SanitizedDigitalOceanConnectionSchema.options,
|
...SanitizedDigitalOceanConnectionSchema.options,
|
||||||
...SanitizedNetlifyConnectionSchema.options,
|
...SanitizedNetlifyConnectionSchema.options,
|
||||||
|
...SanitizedNorthflankConnectionSchema.options,
|
||||||
...SanitizedOktaConnectionSchema.options,
|
...SanitizedOktaConnectionSchema.options,
|
||||||
...SanitizedAzureADCSConnectionSchema.options,
|
...SanitizedAzureADCSConnectionSchema.options,
|
||||||
...SanitizedRedisConnectionSchema.options,
|
...SanitizedRedisConnectionSchema.options,
|
||||||
...SanitizedLaravelForgeConnectionSchema.options
|
...SanitizedLaravelForgeConnectionSchema.options,
|
||||||
|
...SanitizedChefConnectionSchema.options
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const AppConnectionOptionsSchema = z.discriminatedUnion("app", [
|
const AppConnectionOptionsSchema = z.discriminatedUnion("app", [
|
||||||
@@ -203,10 +210,12 @@ const AppConnectionOptionsSchema = z.discriminatedUnion("app", [
|
|||||||
SupabaseConnectionListItemSchema,
|
SupabaseConnectionListItemSchema,
|
||||||
DigitalOceanConnectionListItemSchema,
|
DigitalOceanConnectionListItemSchema,
|
||||||
NetlifyConnectionListItemSchema,
|
NetlifyConnectionListItemSchema,
|
||||||
|
NorthflankConnectionListItemSchema,
|
||||||
OktaConnectionListItemSchema,
|
OktaConnectionListItemSchema,
|
||||||
AzureADCSConnectionListItemSchema,
|
AzureADCSConnectionListItemSchema,
|
||||||
RedisConnectionListItemSchema,
|
RedisConnectionListItemSchema,
|
||||||
LaravelForgeConnectionListItemSchema
|
LaravelForgeConnectionListItemSchema,
|
||||||
|
ChefConnectionListItemSchema
|
||||||
]);
|
]);
|
||||||
|
|
||||||
export const registerAppConnectionRouter = async (server: FastifyZodProvider) => {
|
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 { registerBitbucketConnectionRouter } from "./bitbucket-connection-router";
|
||||||
import { registerCamundaConnectionRouter } from "./camunda-connection-router";
|
import { registerCamundaConnectionRouter } from "./camunda-connection-router";
|
||||||
import { registerChecklyConnectionRouter } from "./checkly-connection-router";
|
import { registerChecklyConnectionRouter } from "./checkly-connection-router";
|
||||||
|
import { registerChefConnectionRouter } from "./chef-connection-router";
|
||||||
import { registerCloudflareConnectionRouter } from "./cloudflare-connection-router";
|
import { registerCloudflareConnectionRouter } from "./cloudflare-connection-router";
|
||||||
import { registerDatabricksConnectionRouter } from "./databricks-connection-router";
|
import { registerDatabricksConnectionRouter } from "./databricks-connection-router";
|
||||||
import { registerDigitalOceanConnectionRouter } from "./digital-ocean-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 { registerMsSqlConnectionRouter } from "./mssql-connection-router";
|
||||||
import { registerMySqlConnectionRouter } from "./mysql-connection-router";
|
import { registerMySqlConnectionRouter } from "./mysql-connection-router";
|
||||||
import { registerNetlifyConnectionRouter } from "./netlify-connection-router";
|
import { registerNetlifyConnectionRouter } from "./netlify-connection-router";
|
||||||
|
import { registerNorthflankConnectionRouter } from "./northflank-connection-router";
|
||||||
import { registerOktaConnectionRouter } from "./okta-connection-router";
|
import { registerOktaConnectionRouter } from "./okta-connection-router";
|
||||||
import { registerPostgresConnectionRouter } from "./postgres-connection-router";
|
import { registerPostgresConnectionRouter } from "./postgres-connection-router";
|
||||||
import { registerRailwayConnectionRouter } from "./railway-connection-router";
|
import { registerRailwayConnectionRouter } from "./railway-connection-router";
|
||||||
@@ -83,6 +85,8 @@ export const APP_CONNECTION_REGISTER_ROUTER_MAP: Record<AppConnection, (server:
|
|||||||
[AppConnection.Supabase]: registerSupabaseConnectionRouter,
|
[AppConnection.Supabase]: registerSupabaseConnectionRouter,
|
||||||
[AppConnection.DigitalOcean]: registerDigitalOceanConnectionRouter,
|
[AppConnection.DigitalOcean]: registerDigitalOceanConnectionRouter,
|
||||||
[AppConnection.Netlify]: registerNetlifyConnectionRouter,
|
[AppConnection.Netlify]: registerNetlifyConnectionRouter,
|
||||||
|
[AppConnection.Northflank]: registerNorthflankConnectionRouter,
|
||||||
[AppConnection.Okta]: registerOktaConnectionRouter,
|
[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
|
apiConfig: z
|
||||||
.object({
|
.object({
|
||||||
autoRenew: z.boolean().default(false),
|
autoRenew: z.boolean().default(false),
|
||||||
autoRenewDays: z.number().min(1).max(365).optional()
|
renewBeforeDays: z.number().min(1).max(30).optional()
|
||||||
})
|
})
|
||||||
.optional()
|
.optional()
|
||||||
})
|
})
|
||||||
@@ -121,9 +121,7 @@ export const registerCertificateProfilesRouter = async (server: FastifyZodProvid
|
|||||||
limit: z.coerce.number().min(1).max(100).default(20),
|
limit: z.coerce.number().min(1).max(100).default(20),
|
||||||
search: z.string().optional(),
|
search: z.string().optional(),
|
||||||
enrollmentType: z.nativeEnum(EnrollmentType).optional(),
|
enrollmentType: z.nativeEnum(EnrollmentType).optional(),
|
||||||
caId: z.string().uuid().optional(),
|
caId: z.string().uuid().optional()
|
||||||
includeMetrics: z.coerce.boolean().optional().default(false),
|
|
||||||
expiringDays: z.coerce.number().min(1).max(365).optional().default(7)
|
|
||||||
}),
|
}),
|
||||||
response: {
|
response: {
|
||||||
200: z.object({
|
200: z.object({
|
||||||
@@ -150,7 +148,7 @@ export const registerCertificateProfilesRouter = async (server: FastifyZodProvid
|
|||||||
.object({
|
.object({
|
||||||
id: z.string(),
|
id: z.string(),
|
||||||
autoRenew: z.boolean(),
|
autoRenew: z.boolean(),
|
||||||
autoRenewDays: z.number().optional()
|
renewBeforeDays: z.number().optional()
|
||||||
})
|
})
|
||||||
.optional()
|
.optional()
|
||||||
}).array(),
|
}).array(),
|
||||||
@@ -195,10 +193,6 @@ export const registerCertificateProfilesRouter = async (server: FastifyZodProvid
|
|||||||
params: z.object({
|
params: z.object({
|
||||||
id: z.string().uuid()
|
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: {
|
response: {
|
||||||
200: z.object({
|
200: z.object({
|
||||||
certificateProfile: PkiCertificateProfilesSchema.extend({
|
certificateProfile: PkiCertificateProfilesSchema.extend({
|
||||||
@@ -230,17 +224,7 @@ export const registerCertificateProfilesRouter = async (server: FastifyZodProvid
|
|||||||
.object({
|
.object({
|
||||||
id: z.string(),
|
id: z.string(),
|
||||||
autoRenew: z.boolean(),
|
autoRenew: z.boolean(),
|
||||||
autoRenewDays: z.number().optional()
|
renewBeforeDays: 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()
|
|
||||||
})
|
})
|
||||||
.optional()
|
.optional()
|
||||||
})
|
})
|
||||||
@@ -257,20 +241,6 @@ export const registerCertificateProfilesRouter = async (server: FastifyZodProvid
|
|||||||
profileId: req.params.id
|
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({
|
await server.services.auditLog.createAuditLog({
|
||||||
...req.auditLogInfo,
|
...req.auditLogInfo,
|
||||||
projectId: certificateProfile.projectId,
|
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
|
apiConfig: z
|
||||||
.object({
|
.object({
|
||||||
autoRenew: z.boolean().default(false),
|
autoRenew: z.boolean().default(false),
|
||||||
autoRenewDays: z.number().min(1).max(365).optional()
|
renewBeforeDays: z.number().min(1).max(30).optional()
|
||||||
})
|
})
|
||||||
.optional()
|
.optional()
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -323,7 +323,11 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => {
|
|||||||
.min(1, "Max Shared Secret view count cannot be lower than 1")
|
.min(1, "Max Shared Secret view count cannot be lower than 1")
|
||||||
.max(1000, "Max Shared Secret view count cannot exceed 1000")
|
.max(1000, "Max Shared Secret view count cannot exceed 1000")
|
||||||
.nullable()
|
.nullable()
|
||||||
|
.optional(),
|
||||||
|
blockDuplicateSecretSyncDestinations: z
|
||||||
|
.boolean()
|
||||||
.optional()
|
.optional()
|
||||||
|
.describe("Block duplicate secret sync destinations across the organization")
|
||||||
}),
|
}),
|
||||||
response: {
|
response: {
|
||||||
200: z.object({
|
200: z.object({
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ export const registerSyncPkiEndpoints = ({
|
|||||||
syncOptions?: Record<string, unknown>;
|
syncOptions?: Record<string, unknown>;
|
||||||
description?: string;
|
description?: string;
|
||||||
isAutoSyncEnabled?: boolean;
|
isAutoSyncEnabled?: boolean;
|
||||||
subscriberId?: string;
|
subscriberId?: string | null;
|
||||||
}>;
|
}>;
|
||||||
updateSchema: z.ZodType<{
|
updateSchema: z.ZodType<{
|
||||||
connectionId?: string;
|
connectionId?: string;
|
||||||
@@ -35,7 +35,7 @@ export const registerSyncPkiEndpoints = ({
|
|||||||
syncOptions?: Record<string, unknown>;
|
syncOptions?: Record<string, unknown>;
|
||||||
description?: string;
|
description?: string;
|
||||||
isAutoSyncEnabled?: boolean;
|
isAutoSyncEnabled?: boolean;
|
||||||
subscriberId?: string;
|
subscriberId?: string | null;
|
||||||
}>;
|
}>;
|
||||||
responseSchema: z.ZodTypeAny;
|
responseSchema: z.ZodTypeAny;
|
||||||
syncOptions: {
|
syncOptions: {
|
||||||
|
|||||||
@@ -2,10 +2,11 @@ import { z } from "zod";
|
|||||||
|
|
||||||
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
||||||
import { ApiDocsTags } from "@app/lib/api-docs";
|
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 { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||||
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
||||||
import { AuthMode } from "@app/services/auth/auth-type";
|
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";
|
import { PkiSync } from "@app/services/pki-sync/pki-sync-enums";
|
||||||
|
|
||||||
const PkiSyncSchema = z.object({
|
const PkiSyncSchema = z.object({
|
||||||
@@ -60,7 +61,8 @@ const PkiSyncSchema = z.object({
|
|||||||
name: z.string()
|
name: z.string()
|
||||||
})
|
})
|
||||||
.nullable()
|
.nullable()
|
||||||
.optional()
|
.optional(),
|
||||||
|
hasCertificate: z.boolean().optional()
|
||||||
});
|
});
|
||||||
|
|
||||||
const PkiSyncOptionsSchema = z.object({
|
const PkiSyncOptionsSchema = z.object({
|
||||||
@@ -76,6 +78,27 @@ const PkiSyncOptionsSchema = z.object({
|
|||||||
minCertificateNameLength: z.number().optional()
|
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) => {
|
export const registerPkiSyncRouter = async (server: FastifyZodProvider) => {
|
||||||
server.route({
|
server.route({
|
||||||
method: "GET",
|
method: "GET",
|
||||||
@@ -111,7 +134,8 @@ export const registerPkiSyncRouter = async (server: FastifyZodProvider) => {
|
|||||||
tags: [ApiDocsTags.PkiSyncs],
|
tags: [ApiDocsTags.PkiSyncs],
|
||||||
description: "List all the PKI Syncs for the specified project.",
|
description: "List all the PKI Syncs for the specified project.",
|
||||||
querystring: z.object({
|
querystring: z.object({
|
||||||
projectId: z.string().trim().min(1)
|
projectId: z.string().trim().min(1),
|
||||||
|
certificateId: z.string().uuid().optional()
|
||||||
}),
|
}),
|
||||||
response: {
|
response: {
|
||||||
200: z.object({ pkiSyncs: PkiSyncSchema.array() })
|
200: z.object({ pkiSyncs: PkiSyncSchema.array() })
|
||||||
@@ -120,11 +144,11 @@ export const registerPkiSyncRouter = async (server: FastifyZodProvider) => {
|
|||||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
handler: async (req) => {
|
handler: async (req) => {
|
||||||
const {
|
const {
|
||||||
query: { projectId },
|
query: { projectId, certificateId },
|
||||||
permission
|
permission
|
||||||
} = req;
|
} = 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({
|
await server.services.auditLog.createAuditLog({
|
||||||
...req.auditLogInfo,
|
...req.auditLogInfo,
|
||||||
@@ -179,4 +203,163 @@ export const registerPkiSyncRouter = async (server: FastifyZodProvider) => {
|
|||||||
return pkiSync;
|
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({
|
querystring: z.object({
|
||||||
friendlyName: z.string().optional().describe(PROJECTS.LIST_CERTIFICATES.friendlyName),
|
friendlyName: z.string().optional().describe(PROJECTS.LIST_CERTIFICATES.friendlyName),
|
||||||
commonName: z.string().optional().describe(PROJECTS.LIST_CERTIFICATES.commonName),
|
commonName: z.string().optional().describe(PROJECTS.LIST_CERTIFICATES.commonName),
|
||||||
offset: z.coerce.number().min(0).max(100).default(0).describe(PROJECTS.LIST_CERTIFICATES.offset),
|
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)
|
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: {
|
response: {
|
||||||
200: z.object({
|
200: z.object({
|
||||||
certificates: z.array(CertificatesSchema),
|
certificates: z.array(CertificatesSchema.extend({ hasPrivateKey: z.boolean() })),
|
||||||
totalCount: z.number()
|
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 { registerBitbucketSyncRouter } from "./bitbucket-sync-router";
|
||||||
import { registerCamundaSyncRouter } from "./camunda-sync-router";
|
import { registerCamundaSyncRouter } from "./camunda-sync-router";
|
||||||
import { registerChecklySyncRouter } from "./checkly-sync-router";
|
import { registerChecklySyncRouter } from "./checkly-sync-router";
|
||||||
|
import { registerChefSyncRouter } from "./chef-sync-router";
|
||||||
import { registerCloudflarePagesSyncRouter } from "./cloudflare-pages-sync-router";
|
import { registerCloudflarePagesSyncRouter } from "./cloudflare-pages-sync-router";
|
||||||
import { registerCloudflareWorkersSyncRouter } from "./cloudflare-workers-sync-router";
|
import { registerCloudflareWorkersSyncRouter } from "./cloudflare-workers-sync-router";
|
||||||
import { registerDatabricksSyncRouter } from "./databricks-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 { registerHumanitecSyncRouter } from "./humanitec-sync-router";
|
||||||
import { registerLaravelForgeSyncRouter } from "./laravel-forge-sync-router";
|
import { registerLaravelForgeSyncRouter } from "./laravel-forge-sync-router";
|
||||||
import { registerNetlifySyncRouter } from "./netlify-sync-router";
|
import { registerNetlifySyncRouter } from "./netlify-sync-router";
|
||||||
|
import { registerNorthflankSyncRouter } from "./northflank-sync-router";
|
||||||
import { registerRailwaySyncRouter } from "./railway-sync-router";
|
import { registerRailwaySyncRouter } from "./railway-sync-router";
|
||||||
import { registerRenderSyncRouter } from "./render-sync-router";
|
import { registerRenderSyncRouter } from "./render-sync-router";
|
||||||
import { registerSupabaseSyncRouter } from "./supabase-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.Checkly]: registerChecklySyncRouter,
|
||||||
[SecretSync.DigitalOceanAppPlatform]: registerDigitalOceanAppPlatformSyncRouter,
|
[SecretSync.DigitalOceanAppPlatform]: registerDigitalOceanAppPlatformSyncRouter,
|
||||||
[SecretSync.Netlify]: registerNetlifySyncRouter,
|
[SecretSync.Netlify]: registerNetlifySyncRouter,
|
||||||
|
[SecretSync.Northflank]: registerNorthflankSyncRouter,
|
||||||
[SecretSync.Bitbucket]: registerBitbucketSyncRouter,
|
[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 { BitbucketSyncListItemSchema, BitbucketSyncSchema } from "@app/services/secret-sync/bitbucket";
|
||||||
import { CamundaSyncListItemSchema, CamundaSyncSchema } from "@app/services/secret-sync/camunda";
|
import { CamundaSyncListItemSchema, CamundaSyncSchema } from "@app/services/secret-sync/camunda";
|
||||||
import { ChecklySyncListItemSchema, ChecklySyncSchema } from "@app/services/secret-sync/checkly/checkly-sync-schemas";
|
import { ChecklySyncListItemSchema, ChecklySyncSchema } from "@app/services/secret-sync/checkly/checkly-sync-schemas";
|
||||||
|
import { ChefSyncListItemSchema, ChefSyncSchema } from "@app/services/secret-sync/chef";
|
||||||
import {
|
import {
|
||||||
CloudflarePagesSyncListItemSchema,
|
CloudflarePagesSyncListItemSchema,
|
||||||
CloudflarePagesSyncSchema
|
CloudflarePagesSyncSchema
|
||||||
@@ -46,6 +47,7 @@ import { HerokuSyncListItemSchema, HerokuSyncSchema } from "@app/services/secret
|
|||||||
import { HumanitecSyncListItemSchema, HumanitecSyncSchema } from "@app/services/secret-sync/humanitec";
|
import { HumanitecSyncListItemSchema, HumanitecSyncSchema } from "@app/services/secret-sync/humanitec";
|
||||||
import { LaravelForgeSyncListItemSchema, LaravelForgeSyncSchema } from "@app/services/secret-sync/laravel-forge";
|
import { LaravelForgeSyncListItemSchema, LaravelForgeSyncSchema } from "@app/services/secret-sync/laravel-forge";
|
||||||
import { NetlifySyncListItemSchema, NetlifySyncSchema } from "@app/services/secret-sync/netlify";
|
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 { RailwaySyncListItemSchema, RailwaySyncSchema } from "@app/services/secret-sync/railway/railway-sync-schemas";
|
||||||
import { RenderSyncListItemSchema, RenderSyncSchema } from "@app/services/secret-sync/render/render-sync-schemas";
|
import { RenderSyncListItemSchema, RenderSyncSchema } from "@app/services/secret-sync/render/render-sync-schemas";
|
||||||
import { SupabaseSyncListItemSchema, SupabaseSyncSchema } from "@app/services/secret-sync/supabase";
|
import { SupabaseSyncListItemSchema, SupabaseSyncSchema } from "@app/services/secret-sync/supabase";
|
||||||
@@ -85,8 +87,10 @@ const SecretSyncSchema = z.discriminatedUnion("destination", [
|
|||||||
ChecklySyncSchema,
|
ChecklySyncSchema,
|
||||||
DigitalOceanAppPlatformSyncSchema,
|
DigitalOceanAppPlatformSyncSchema,
|
||||||
NetlifySyncSchema,
|
NetlifySyncSchema,
|
||||||
|
NorthflankSyncSchema,
|
||||||
BitbucketSyncSchema,
|
BitbucketSyncSchema,
|
||||||
LaravelForgeSyncSchema
|
LaravelForgeSyncSchema,
|
||||||
|
ChefSyncSchema
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const SecretSyncOptionsSchema = z.discriminatedUnion("destination", [
|
const SecretSyncOptionsSchema = z.discriminatedUnion("destination", [
|
||||||
@@ -119,8 +123,10 @@ const SecretSyncOptionsSchema = z.discriminatedUnion("destination", [
|
|||||||
ChecklySyncListItemSchema,
|
ChecklySyncListItemSchema,
|
||||||
SupabaseSyncListItemSchema,
|
SupabaseSyncListItemSchema,
|
||||||
NetlifySyncListItemSchema,
|
NetlifySyncListItemSchema,
|
||||||
|
NorthflankSyncListItemSchema,
|
||||||
BitbucketSyncListItemSchema,
|
BitbucketSyncListItemSchema,
|
||||||
LaravelForgeSyncListItemSchema
|
LaravelForgeSyncListItemSchema,
|
||||||
|
ChefSyncListItemSchema
|
||||||
]);
|
]);
|
||||||
|
|
||||||
export const registerSecretSyncRouter = async (server: FastifyZodProvider) => {
|
export const registerSecretSyncRouter = async (server: FastifyZodProvider) => {
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
// All the any rules are disabled because passport typesense with fastify is really poor
|
// All the any rules are disabled because passport typesense with fastify is really poor
|
||||||
|
|
||||||
import { Authenticator } from "@fastify/passport";
|
import { Authenticator } from "@fastify/passport";
|
||||||
|
import { requestContext } from "@fastify/request-context";
|
||||||
import fastifySession from "@fastify/session";
|
import fastifySession from "@fastify/session";
|
||||||
import RedisStore from "connect-redis";
|
import RedisStore from "connect-redis";
|
||||||
import { CronJob } from "cron";
|
import { CronJob } from "cron";
|
||||||
@@ -21,6 +22,7 @@ import { BadRequestError, NotFoundError } from "@app/lib/errors";
|
|||||||
import { logger } from "@app/lib/logger";
|
import { logger } from "@app/lib/logger";
|
||||||
import { ms } from "@app/lib/ms";
|
import { ms } from "@app/lib/ms";
|
||||||
import { fetchGithubEmails, fetchGithubUser } from "@app/lib/requests/github";
|
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 { authRateLimit } from "@app/server/config/rateLimiter";
|
||||||
import { addAuthOriginDomainCookie } from "@app/server/lib/cookie";
|
import { addAuthOriginDomainCookie } from "@app/server/lib/cookie";
|
||||||
import { AuthMethod } from "@app/services/auth/auth-type";
|
import { AuthMethod } from "@app/services/auth/auth-type";
|
||||||
@@ -51,30 +53,54 @@ export const registerOauthMiddlewares = (server: FastifyZodProvider) => {
|
|||||||
},
|
},
|
||||||
// eslint-disable-next-line
|
// eslint-disable-next-line
|
||||||
async (req, _accessToken, _refreshToken, profile, cb) => {
|
async (req, _accessToken, _refreshToken, profile, cb) => {
|
||||||
try {
|
// @ts-expect-error this is because this is express type and not fastify
|
||||||
// @ts-expect-error this is because this is express type and not fastify
|
const callbackPort = req.session.get("callbackPort");
|
||||||
const callbackPort = req.session.get("callbackPort");
|
// @ts-expect-error this is because this is express type and not fastify
|
||||||
// @ts-expect-error this is because this is express type and not fastify
|
const orgSlug = req.session.get("orgSlug");
|
||||||
const orgSlug = req.session.get("orgSlug");
|
|
||||||
|
|
||||||
const email = profile?.emails?.[0]?.value;
|
const email = profile?.emails?.[0]?.value;
|
||||||
if (!email)
|
if (!email)
|
||||||
throw new NotFoundError({
|
throw new NotFoundError({
|
||||||
message: "Email not found",
|
message: "Email not found",
|
||||||
name: "OauthGoogleRegister"
|
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({
|
if (appCfg.OTEL_TELEMETRY_COLLECTION_ENABLED) {
|
||||||
email,
|
authAttemptCounter.add(1, {
|
||||||
firstName: profile?.name?.givenName || "",
|
"infisical.user.email": email,
|
||||||
lastName: profile?.name?.familyName || "",
|
"infisical.user.id": user.id,
|
||||||
authMethod: AuthMethod.GOOGLE,
|
"infisical.organization.id": orgId,
|
||||||
callbackPort,
|
"infisical.organization.name": orgName,
|
||||||
orgSlug
|
"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 });
|
cb(null, { isUserCompleted, providerAuthToken });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(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);
|
cb(error as Error, false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -101,27 +127,50 @@ export const registerOauthMiddlewares = (server: FastifyZodProvider) => {
|
|||||||
},
|
},
|
||||||
// eslint-disable-next-line
|
// eslint-disable-next-line
|
||||||
async (req: any, accessToken: string, _refreshToken: string, _profile: any, done: Function) => {
|
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 {
|
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
|
// 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 callbackPort = req.session.get("callbackPort");
|
||||||
|
|
||||||
const { isUserCompleted, providerAuthToken } = await server.services.login.oauth2Login({
|
const { isUserCompleted, providerAuthToken, user, orgId, orgName } =
|
||||||
email,
|
await server.services.login.oauth2Login({
|
||||||
firstName: user.name || user.login,
|
email,
|
||||||
lastName: "",
|
firstName: githubUser.name || githubUser.login,
|
||||||
authMethod: AuthMethod.GITHUB,
|
lastName: "",
|
||||||
callbackPort
|
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 });
|
done(null, { isUserCompleted, providerAuthToken, externalProviderAccessToken: accessToken });
|
||||||
} catch (err) {
|
} 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);
|
logger.error(err);
|
||||||
done(err as Error, false);
|
done(err as Error, false);
|
||||||
}
|
}
|
||||||
@@ -147,20 +196,45 @@ export const registerOauthMiddlewares = (server: FastifyZodProvider) => {
|
|||||||
pkce: true
|
pkce: true
|
||||||
},
|
},
|
||||||
async (req: any, _accessToken: string, _refreshToken: string, profile: any, cb: any) => {
|
async (req: any, _accessToken: string, _refreshToken: string, profile: any, cb: any) => {
|
||||||
|
const email = profile.emails[0].value;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const callbackPort = req.session.get("callbackPort");
|
const callbackPort = req.session.get("callbackPort");
|
||||||
|
|
||||||
const email = profile.emails[0].value;
|
const { isUserCompleted, providerAuthToken, user, orgId, orgName } =
|
||||||
const { isUserCompleted, providerAuthToken } = await server.services.login.oauth2Login({
|
await server.services.login.oauth2Login({
|
||||||
email,
|
email,
|
||||||
firstName: profile.displayName || profile.username || "",
|
firstName: profile.displayName || profile.username || "",
|
||||||
lastName: "",
|
lastName: "",
|
||||||
authMethod: AuthMethod.GITLAB,
|
authMethod: AuthMethod.GITLAB,
|
||||||
callbackPort
|
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 });
|
return cb(null, { isUserCompleted, providerAuthToken });
|
||||||
} catch (error) {
|
} 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);
|
logger.error(error);
|
||||||
cb(error as Error, false);
|
cb(error as Error, false);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import {
|
|||||||
CertKeyUsageType,
|
CertKeyUsageType,
|
||||||
CertSubjectAlternativeNameType
|
CertSubjectAlternativeNameType
|
||||||
} from "@app/services/certificate-common/certificate-constants";
|
} 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 { mapEnumsForValidation } from "@app/services/certificate-common/certificate-utils";
|
||||||
import { validateTemplateRegexField } from "@app/services/certificate-template/certificate-template-validators";
|
import { validateTemplateRegexField } from "@app/services/certificate-template/certificate-template-validators";
|
||||||
|
|
||||||
@@ -84,8 +85,8 @@ export const registerCertificatesRouter = async (server: FastifyZodProvider) =>
|
|||||||
})
|
})
|
||||||
)
|
)
|
||||||
.optional(),
|
.optional(),
|
||||||
signatureAlgorithm: z.nativeEnum(CertSignatureAlgorithm).optional(),
|
signatureAlgorithm: z.nativeEnum(CertSignatureAlgorithm),
|
||||||
keyAlgorithm: z.nativeEnum(CertKeyAlgorithm).optional()
|
keyAlgorithm: z.nativeEnum(CertKeyAlgorithm)
|
||||||
})
|
})
|
||||||
.refine(validateTtlAndDateFields, {
|
.refine(validateTtlAndDateFields, {
|
||||||
message:
|
message:
|
||||||
@@ -169,9 +170,7 @@ export const registerCertificatesRouter = async (server: FastifyZodProvider) =>
|
|||||||
.min(1, "TTL cannot be empty")
|
.min(1, "TTL cannot be empty")
|
||||||
.refine((val) => ms(val) > 0, "TTL must be a positive number"),
|
.refine((val) => ms(val) > 0, "TTL must be a positive number"),
|
||||||
notBefore: validateCaDateField.optional(),
|
notBefore: validateCaDateField.optional(),
|
||||||
notAfter: validateCaDateField.optional(),
|
notAfter: validateCaDateField.optional()
|
||||||
signatureAlgorithm: z.nativeEnum(CertSignatureAlgorithm).optional(),
|
|
||||||
keyAlgorithm: z.nativeEnum(CertKeyAlgorithm).optional()
|
|
||||||
})
|
})
|
||||||
.refine(validateTtlAndDateFields, {
|
.refine(validateTtlAndDateFields, {
|
||||||
message:
|
message:
|
||||||
@@ -192,6 +191,8 @@ export const registerCertificatesRouter = async (server: FastifyZodProvider) =>
|
|||||||
},
|
},
|
||||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
handler: async (req) => {
|
handler: async (req) => {
|
||||||
|
const certificateRequest = extractCertificateRequestFromCSR(req.body.csr);
|
||||||
|
|
||||||
const data = await server.services.certificateV3.signCertificateFromProfile({
|
const data = await server.services.certificateV3.signCertificateFromProfile({
|
||||||
actor: req.permission.type,
|
actor: req.permission.type,
|
||||||
actorId: req.permission.id,
|
actorId: req.permission.id,
|
||||||
@@ -203,9 +204,7 @@ export const registerCertificatesRouter = async (server: FastifyZodProvider) =>
|
|||||||
ttl: req.body.ttl
|
ttl: req.body.ttl
|
||||||
},
|
},
|
||||||
notBefore: req.body.notBefore ? new Date(req.body.notBefore) : undefined,
|
notBefore: req.body.notBefore ? new Date(req.body.notBefore) : undefined,
|
||||||
notAfter: req.body.notAfter ? new Date(req.body.notAfter) : undefined,
|
notAfter: req.body.notAfter ? new Date(req.body.notAfter) : undefined
|
||||||
signatureAlgorithm: req.body.signatureAlgorithm,
|
|
||||||
keyAlgorithm: req.body.keyAlgorithm
|
|
||||||
});
|
});
|
||||||
|
|
||||||
await server.services.auditLog.createAuditLog({
|
await server.services.auditLog.createAuditLog({
|
||||||
@@ -217,7 +216,7 @@ export const registerCertificatesRouter = async (server: FastifyZodProvider) =>
|
|||||||
certificateProfileId: req.body.profileId,
|
certificateProfileId: req.body.profileId,
|
||||||
certificateId: data.certificateId,
|
certificateId: data.certificateId,
|
||||||
profileName: data.profileName,
|
profileName: data.profileName,
|
||||||
commonName: ""
|
commonName: certificateRequest.commonName || ""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -260,8 +259,8 @@ export const registerCertificatesRouter = async (server: FastifyZodProvider) =>
|
|||||||
notBefore: validateCaDateField.optional(),
|
notBefore: validateCaDateField.optional(),
|
||||||
notAfter: validateCaDateField.optional(),
|
notAfter: validateCaDateField.optional(),
|
||||||
commonName: validateTemplateRegexField.optional(),
|
commonName: validateTemplateRegexField.optional(),
|
||||||
signatureAlgorithm: z.nativeEnum(CertSignatureAlgorithm).optional(),
|
signatureAlgorithm: z.nativeEnum(CertSignatureAlgorithm),
|
||||||
keyAlgorithm: z.nativeEnum(CertKeyAlgorithm).optional()
|
keyAlgorithm: z.nativeEnum(CertKeyAlgorithm)
|
||||||
})
|
})
|
||||||
.refine(validateTtlAndDateFields, {
|
.refine(validateTtlAndDateFields, {
|
||||||
message:
|
message:
|
||||||
@@ -343,4 +342,145 @@ export const registerCertificatesRouter = async (server: FastifyZodProvider) =>
|
|||||||
return data;
|
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({
|
server.route({
|
||||||
method: "GET",
|
method: "GET",
|
||||||
url: "/vault/secret-paths",
|
url: "/vault/secret-paths",
|
||||||
|
|||||||
@@ -38,7 +38,9 @@ export enum AppConnection {
|
|||||||
Netlify = "netlify",
|
Netlify = "netlify",
|
||||||
Okta = "okta",
|
Okta = "okta",
|
||||||
Redis = "redis",
|
Redis = "redis",
|
||||||
LaravelForge = "laravel-forge"
|
LaravelForge = "laravel-forge",
|
||||||
|
Chef = "chef",
|
||||||
|
Northflank = "northflank"
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum AWSRegion {
|
export enum AWSRegion {
|
||||||
|
|||||||
@@ -68,6 +68,7 @@ import {
|
|||||||
} from "./bitbucket";
|
} from "./bitbucket";
|
||||||
import { CamundaConnectionMethod, getCamundaConnectionListItem, validateCamundaConnectionCredentials } from "./camunda";
|
import { CamundaConnectionMethod, getCamundaConnectionListItem, validateCamundaConnectionCredentials } from "./camunda";
|
||||||
import { ChecklyConnectionMethod, getChecklyConnectionListItem, validateChecklyConnectionCredentials } from "./checkly";
|
import { ChecklyConnectionMethod, getChecklyConnectionListItem, validateChecklyConnectionCredentials } from "./checkly";
|
||||||
|
import { ChefConnectionMethod, getChefConnectionListItem, validateChefConnectionCredentials } from "./chef";
|
||||||
import { CloudflareConnectionMethod } from "./cloudflare/cloudflare-connection-enum";
|
import { CloudflareConnectionMethod } from "./cloudflare/cloudflare-connection-enum";
|
||||||
import {
|
import {
|
||||||
getCloudflareConnectionListItem,
|
getCloudflareConnectionListItem,
|
||||||
@@ -113,6 +114,11 @@ import { getMsSqlConnectionListItem, MsSqlConnectionMethod } from "./mssql";
|
|||||||
import { MySqlConnectionMethod } from "./mysql/mysql-connection-enums";
|
import { MySqlConnectionMethod } from "./mysql/mysql-connection-enums";
|
||||||
import { getMySqlConnectionListItem } from "./mysql/mysql-connection-fns";
|
import { getMySqlConnectionListItem } from "./mysql/mysql-connection-fns";
|
||||||
import { getNetlifyConnectionListItem, validateNetlifyConnectionCredentials } from "./netlify";
|
import { getNetlifyConnectionListItem, validateNetlifyConnectionCredentials } from "./netlify";
|
||||||
|
import {
|
||||||
|
getNorthflankConnectionListItem,
|
||||||
|
NorthflankConnectionMethod,
|
||||||
|
validateNorthflankConnectionCredentials
|
||||||
|
} from "./northflank";
|
||||||
import { getOktaConnectionListItem, OktaConnectionMethod, validateOktaConnectionCredentials } from "./okta";
|
import { getOktaConnectionListItem, OktaConnectionMethod, validateOktaConnectionCredentials } from "./okta";
|
||||||
import { getPostgresConnectionListItem, PostgresConnectionMethod } from "./postgres";
|
import { getPostgresConnectionListItem, PostgresConnectionMethod } from "./postgres";
|
||||||
import { getRailwayConnectionListItem, validateRailwayConnectionCredentials } from "./railway";
|
import { getRailwayConnectionListItem, validateRailwayConnectionCredentials } from "./railway";
|
||||||
@@ -203,8 +209,10 @@ export const listAppConnectionOptions = (projectType?: ProjectType) => {
|
|||||||
getSupabaseConnectionListItem(),
|
getSupabaseConnectionListItem(),
|
||||||
getDigitalOceanConnectionListItem(),
|
getDigitalOceanConnectionListItem(),
|
||||||
getNetlifyConnectionListItem(),
|
getNetlifyConnectionListItem(),
|
||||||
|
getNorthflankConnectionListItem(),
|
||||||
getOktaConnectionListItem(),
|
getOktaConnectionListItem(),
|
||||||
getRedisConnectionListItem()
|
getRedisConnectionListItem(),
|
||||||
|
getChefConnectionListItem()
|
||||||
]
|
]
|
||||||
.filter((option) => {
|
.filter((option) => {
|
||||||
switch (projectType) {
|
switch (projectType) {
|
||||||
@@ -332,8 +340,10 @@ export const validateAppConnectionCredentials = async (
|
|||||||
[AppConnection.Checkly]: validateChecklyConnectionCredentials as TAppConnectionCredentialsValidator,
|
[AppConnection.Checkly]: validateChecklyConnectionCredentials as TAppConnectionCredentialsValidator,
|
||||||
[AppConnection.Supabase]: validateSupabaseConnectionCredentials as TAppConnectionCredentialsValidator,
|
[AppConnection.Supabase]: validateSupabaseConnectionCredentials as TAppConnectionCredentialsValidator,
|
||||||
[AppConnection.DigitalOcean]: validateDigitalOceanConnectionCredentials as TAppConnectionCredentialsValidator,
|
[AppConnection.DigitalOcean]: validateDigitalOceanConnectionCredentials as TAppConnectionCredentialsValidator,
|
||||||
[AppConnection.Okta]: validateOktaConnectionCredentials as TAppConnectionCredentialsValidator,
|
|
||||||
[AppConnection.Netlify]: validateNetlifyConnectionCredentials 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
|
[AppConnection.Redis]: validateRedisConnectionCredentials as TAppConnectionCredentialsValidator
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -345,6 +355,8 @@ export const getAppConnectionMethodName = (method: TAppConnection["method"]) =>
|
|||||||
case GitHubConnectionMethod.App:
|
case GitHubConnectionMethod.App:
|
||||||
case GitHubRadarConnectionMethod.App:
|
case GitHubRadarConnectionMethod.App:
|
||||||
return "GitHub App";
|
return "GitHub App";
|
||||||
|
case GitHubConnectionMethod.Pat:
|
||||||
|
return "Personal Access Token";
|
||||||
case AzureKeyVaultConnectionMethod.OAuth:
|
case AzureKeyVaultConnectionMethod.OAuth:
|
||||||
case AzureAppConfigurationConnectionMethod.OAuth:
|
case AzureAppConfigurationConnectionMethod.OAuth:
|
||||||
case AzureClientSecretsConnectionMethod.OAuth:
|
case AzureClientSecretsConnectionMethod.OAuth:
|
||||||
@@ -374,6 +386,7 @@ export const getAppConnectionMethodName = (method: TAppConnection["method"]) =>
|
|||||||
case BitbucketConnectionMethod.ApiToken:
|
case BitbucketConnectionMethod.ApiToken:
|
||||||
case ZabbixConnectionMethod.ApiToken:
|
case ZabbixConnectionMethod.ApiToken:
|
||||||
case DigitalOceanConnectionMethod.ApiToken:
|
case DigitalOceanConnectionMethod.ApiToken:
|
||||||
|
case NorthflankConnectionMethod.ApiToken:
|
||||||
case OktaConnectionMethod.ApiToken:
|
case OktaConnectionMethod.ApiToken:
|
||||||
case LaravelForgeConnectionMethod.ApiToken:
|
case LaravelForgeConnectionMethod.ApiToken:
|
||||||
return "API Token";
|
return "API Token";
|
||||||
@@ -399,6 +412,8 @@ export const getAppConnectionMethodName = (method: TAppConnection["method"]) =>
|
|||||||
case RenderConnectionMethod.ApiKey:
|
case RenderConnectionMethod.ApiKey:
|
||||||
case ChecklyConnectionMethod.ApiKey:
|
case ChecklyConnectionMethod.ApiKey:
|
||||||
return "API Key";
|
return "API Key";
|
||||||
|
case ChefConnectionMethod.UserKey:
|
||||||
|
return "User Key";
|
||||||
case SupabaseConnectionMethod.AccessToken:
|
case SupabaseConnectionMethod.AccessToken:
|
||||||
return "Access Token";
|
return "Access Token";
|
||||||
default:
|
default:
|
||||||
@@ -470,9 +485,11 @@ export const TRANSITION_CONNECTION_CREDENTIALS_TO_PLATFORM: Record<
|
|||||||
[AppConnection.Supabase]: platformManagedCredentialsNotSupported,
|
[AppConnection.Supabase]: platformManagedCredentialsNotSupported,
|
||||||
[AppConnection.DigitalOcean]: platformManagedCredentialsNotSupported,
|
[AppConnection.DigitalOcean]: platformManagedCredentialsNotSupported,
|
||||||
[AppConnection.Netlify]: platformManagedCredentialsNotSupported,
|
[AppConnection.Netlify]: platformManagedCredentialsNotSupported,
|
||||||
|
[AppConnection.Northflank]: platformManagedCredentialsNotSupported,
|
||||||
[AppConnection.Okta]: platformManagedCredentialsNotSupported,
|
[AppConnection.Okta]: platformManagedCredentialsNotSupported,
|
||||||
[AppConnection.Redis]: platformManagedCredentialsNotSupported,
|
[AppConnection.Redis]: platformManagedCredentialsNotSupported,
|
||||||
[AppConnection.LaravelForge]: platformManagedCredentialsNotSupported
|
[AppConnection.LaravelForge]: platformManagedCredentialsNotSupported,
|
||||||
|
[AppConnection.Chef]: platformManagedCredentialsNotSupported
|
||||||
};
|
};
|
||||||
|
|
||||||
export const enterpriseAppCheck = async (
|
export const enterpriseAppCheck = async (
|
||||||
|
|||||||
@@ -40,7 +40,9 @@ export const APP_CONNECTION_NAME_MAP: Record<AppConnection, string> = {
|
|||||||
[AppConnection.DigitalOcean]: "DigitalOcean App Platform",
|
[AppConnection.DigitalOcean]: "DigitalOcean App Platform",
|
||||||
[AppConnection.Netlify]: "Netlify",
|
[AppConnection.Netlify]: "Netlify",
|
||||||
[AppConnection.Okta]: "Okta",
|
[AppConnection.Okta]: "Okta",
|
||||||
[AppConnection.Redis]: "Redis"
|
[AppConnection.Redis]: "Redis",
|
||||||
|
[AppConnection.Chef]: "Chef",
|
||||||
|
[AppConnection.Northflank]: "Northflank"
|
||||||
};
|
};
|
||||||
|
|
||||||
export const APP_CONNECTION_PLAN_MAP: Record<AppConnection, AppConnectionPlanType> = {
|
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.DigitalOcean]: AppConnectionPlanType.Regular,
|
||||||
[AppConnection.Netlify]: AppConnectionPlanType.Regular,
|
[AppConnection.Netlify]: AppConnectionPlanType.Regular,
|
||||||
[AppConnection.Okta]: 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 { camundaConnectionService } from "./camunda/camunda-connection-service";
|
||||||
import { ValidateChecklyConnectionCredentialsSchema } from "./checkly";
|
import { ValidateChecklyConnectionCredentialsSchema } from "./checkly";
|
||||||
import { checklyConnectionService } from "./checkly/checkly-connection-service";
|
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 { ValidateCloudflareConnectionCredentialsSchema } from "./cloudflare/cloudflare-connection-schema";
|
||||||
import { cloudflareConnectionService } from "./cloudflare/cloudflare-connection-service";
|
import { cloudflareConnectionService } from "./cloudflare/cloudflare-connection-service";
|
||||||
import { ValidateDatabricksConnectionCredentialsSchema } from "./databricks";
|
import { ValidateDatabricksConnectionCredentialsSchema } from "./databricks";
|
||||||
@@ -96,6 +98,8 @@ import { ValidateMsSqlConnectionCredentialsSchema } from "./mssql";
|
|||||||
import { ValidateMySqlConnectionCredentialsSchema } from "./mysql";
|
import { ValidateMySqlConnectionCredentialsSchema } from "./mysql";
|
||||||
import { ValidateNetlifyConnectionCredentialsSchema } from "./netlify";
|
import { ValidateNetlifyConnectionCredentialsSchema } from "./netlify";
|
||||||
import { netlifyConnectionService } from "./netlify/netlify-connection-service";
|
import { netlifyConnectionService } from "./netlify/netlify-connection-service";
|
||||||
|
import { ValidateNorthflankConnectionCredentialsSchema } from "./northflank";
|
||||||
|
import { northflankConnectionService } from "./northflank/northflank-connection-service";
|
||||||
import { ValidateOktaConnectionCredentialsSchema } from "./okta";
|
import { ValidateOktaConnectionCredentialsSchema } from "./okta";
|
||||||
import { oktaConnectionService } from "./okta/okta-connection-service";
|
import { oktaConnectionService } from "./okta/okta-connection-service";
|
||||||
import { ValidatePostgresConnectionCredentialsSchema } from "./postgres";
|
import { ValidatePostgresConnectionCredentialsSchema } from "./postgres";
|
||||||
@@ -170,8 +174,10 @@ const VALIDATE_APP_CONNECTION_CREDENTIALS_MAP: Record<AppConnection, TValidateAp
|
|||||||
[AppConnection.Supabase]: ValidateSupabaseConnectionCredentialsSchema,
|
[AppConnection.Supabase]: ValidateSupabaseConnectionCredentialsSchema,
|
||||||
[AppConnection.DigitalOcean]: ValidateDigitalOceanConnectionCredentialsSchema,
|
[AppConnection.DigitalOcean]: ValidateDigitalOceanConnectionCredentialsSchema,
|
||||||
[AppConnection.Netlify]: ValidateNetlifyConnectionCredentialsSchema,
|
[AppConnection.Netlify]: ValidateNetlifyConnectionCredentialsSchema,
|
||||||
|
[AppConnection.Northflank]: ValidateNorthflankConnectionCredentialsSchema,
|
||||||
[AppConnection.Okta]: ValidateOktaConnectionCredentialsSchema,
|
[AppConnection.Okta]: ValidateOktaConnectionCredentialsSchema,
|
||||||
[AppConnection.Redis]: ValidateRedisConnectionCredentialsSchema
|
[AppConnection.Redis]: ValidateRedisConnectionCredentialsSchema,
|
||||||
|
[AppConnection.Chef]: ValidateChefConnectionCredentialsSchema
|
||||||
};
|
};
|
||||||
|
|
||||||
export const appConnectionServiceFactory = ({
|
export const appConnectionServiceFactory = ({
|
||||||
@@ -876,7 +882,9 @@ export const appConnectionServiceFactory = ({
|
|||||||
supabase: supabaseConnectionService(connectAppConnectionById),
|
supabase: supabaseConnectionService(connectAppConnectionById),
|
||||||
digitalOcean: digitalOceanAppPlatformConnectionService(connectAppConnectionById),
|
digitalOcean: digitalOceanAppPlatformConnectionService(connectAppConnectionById),
|
||||||
netlify: netlifyConnectionService(connectAppConnectionById),
|
netlify: netlifyConnectionService(connectAppConnectionById),
|
||||||
|
northflank: northflankConnectionService(connectAppConnectionById),
|
||||||
okta: oktaConnectionService(connectAppConnectionById),
|
okta: oktaConnectionService(connectAppConnectionById),
|
||||||
laravelForge: laravelForgeConnectionService(connectAppConnectionById)
|
laravelForge: laravelForgeConnectionService(connectAppConnectionById),
|
||||||
|
chef: chefConnectionService(connectAppConnectionById)
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -82,6 +82,12 @@ import {
|
|||||||
TChecklyConnectionInput,
|
TChecklyConnectionInput,
|
||||||
TValidateChecklyConnectionCredentialsSchema
|
TValidateChecklyConnectionCredentialsSchema
|
||||||
} from "./checkly";
|
} from "./checkly";
|
||||||
|
import {
|
||||||
|
TChefConnection,
|
||||||
|
TChefConnectionConfig,
|
||||||
|
TChefConnectionInput,
|
||||||
|
TValidateChefConnectionCredentialsSchema
|
||||||
|
} from "./chef";
|
||||||
import {
|
import {
|
||||||
TCloudflareConnection,
|
TCloudflareConnection,
|
||||||
TCloudflareConnectionConfig,
|
TCloudflareConnectionConfig,
|
||||||
@@ -168,6 +174,12 @@ import {
|
|||||||
TNetlifyConnectionInput,
|
TNetlifyConnectionInput,
|
||||||
TValidateNetlifyConnectionCredentialsSchema
|
TValidateNetlifyConnectionCredentialsSchema
|
||||||
} from "./netlify";
|
} from "./netlify";
|
||||||
|
import {
|
||||||
|
TNorthflankConnection,
|
||||||
|
TNorthflankConnectionConfig,
|
||||||
|
TNorthflankConnectionInput,
|
||||||
|
TValidateNorthflankConnectionCredentialsSchema
|
||||||
|
} from "./northflank";
|
||||||
import {
|
import {
|
||||||
TOktaConnection,
|
TOktaConnection,
|
||||||
TOktaConnectionConfig,
|
TOktaConnectionConfig,
|
||||||
@@ -273,8 +285,10 @@ export type TAppConnection = { id: string } & (
|
|||||||
| TSupabaseConnection
|
| TSupabaseConnection
|
||||||
| TDigitalOceanConnection
|
| TDigitalOceanConnection
|
||||||
| TNetlifyConnection
|
| TNetlifyConnection
|
||||||
|
| TNorthflankConnection
|
||||||
| TOktaConnection
|
| TOktaConnection
|
||||||
| TRedisConnection
|
| TRedisConnection
|
||||||
|
| TChefConnection
|
||||||
);
|
);
|
||||||
|
|
||||||
export type TAppConnectionRaw = NonNullable<Awaited<ReturnType<TAppConnectionDALFactory["findById"]>>>;
|
export type TAppConnectionRaw = NonNullable<Awaited<ReturnType<TAppConnectionDALFactory["findById"]>>>;
|
||||||
@@ -320,8 +334,10 @@ export type TAppConnectionInput = { id: string } & (
|
|||||||
| TSupabaseConnectionInput
|
| TSupabaseConnectionInput
|
||||||
| TDigitalOceanConnectionInput
|
| TDigitalOceanConnectionInput
|
||||||
| TNetlifyConnectionInput
|
| TNetlifyConnectionInput
|
||||||
|
| TNorthflankConnectionInput
|
||||||
| TOktaConnectionInput
|
| TOktaConnectionInput
|
||||||
| TRedisConnectionInput
|
| TRedisConnectionInput
|
||||||
|
| TChefConnectionInput
|
||||||
);
|
);
|
||||||
|
|
||||||
export type TSqlConnectionInput =
|
export type TSqlConnectionInput =
|
||||||
@@ -385,8 +401,10 @@ export type TAppConnectionConfig =
|
|||||||
| TSupabaseConnectionConfig
|
| TSupabaseConnectionConfig
|
||||||
| TDigitalOceanConnectionConfig
|
| TDigitalOceanConnectionConfig
|
||||||
| TNetlifyConnectionConfig
|
| TNetlifyConnectionConfig
|
||||||
|
| TNorthflankConnectionConfig
|
||||||
| TOktaConnectionConfig
|
| TOktaConnectionConfig
|
||||||
| TRedisConnectionConfig;
|
| TRedisConnectionConfig
|
||||||
|
| TChefConnectionConfig;
|
||||||
|
|
||||||
export type TValidateAppConnectionCredentialsSchema =
|
export type TValidateAppConnectionCredentialsSchema =
|
||||||
| TValidateAwsConnectionCredentialsSchema
|
| TValidateAwsConnectionCredentialsSchema
|
||||||
@@ -427,8 +445,10 @@ export type TValidateAppConnectionCredentialsSchema =
|
|||||||
| TValidateSupabaseConnectionCredentialsSchema
|
| TValidateSupabaseConnectionCredentialsSchema
|
||||||
| TValidateDigitalOceanCredentialsSchema
|
| TValidateDigitalOceanCredentialsSchema
|
||||||
| TValidateNetlifyConnectionCredentialsSchema
|
| TValidateNetlifyConnectionCredentialsSchema
|
||||||
|
| TValidateNorthflankConnectionCredentialsSchema
|
||||||
| TValidateOktaConnectionCredentialsSchema
|
| TValidateOktaConnectionCredentialsSchema
|
||||||
| TValidateRedisConnectionCredentialsSchema;
|
| TValidateRedisConnectionCredentialsSchema
|
||||||
|
| TValidateChefConnectionCredentialsSchema;
|
||||||
|
|
||||||
export type TListAwsConnectionKmsKeys = {
|
export type TListAwsConnectionKmsKeys = {
|
||||||
connectionId: string;
|
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 {
|
export enum GitHubConnectionMethod {
|
||||||
OAuth = "oauth",
|
OAuth = "oauth",
|
||||||
App = "github-app"
|
App = "github-app",
|
||||||
|
Pat = "pat"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -248,10 +248,18 @@ export const makePaginatedGitHubRequest = async <T, R = T[]>(
|
|||||||
): Promise<T[]> => {
|
): Promise<T[]> => {
|
||||||
const { credentials, method } = appConnection;
|
const { credentials, method } = appConnection;
|
||||||
|
|
||||||
const token =
|
let token: string;
|
||||||
method === GitHubConnectionMethod.OAuth
|
|
||||||
? credentials.accessToken
|
switch (method) {
|
||||||
: await getGitHubAppAuthToken(appConnection, gatewayService, gatewayV2Service);
|
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 baseUrl = `https://${await getGitHubInstanceApiUrl(appConnection)}${path}`;
|
||||||
const initialUrlObj = new URL(baseUrl);
|
const initialUrlObj = new URL(baseUrl);
|
||||||
@@ -460,6 +468,35 @@ export const validateGitHubConnectionCredentials = async (
|
|||||||
gatewayV2Service: Pick<TGatewayV2ServiceFactory, "getPlatformConnectionDetailsByGatewayId">
|
gatewayV2Service: Pick<TGatewayV2ServiceFactory, "getPlatformConnectionDetailsByGatewayId">
|
||||||
) => {
|
) => {
|
||||||
const { credentials, method } = config;
|
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 {
|
const {
|
||||||
INF_APP_CONNECTION_GITHUB_OAUTH_CLIENT_ID,
|
INF_APP_CONNECTION_GITHUB_OAUTH_CLIENT_ID,
|
||||||
INF_APP_CONNECTION_GITHUB_OAUTH_CLIENT_SECRET,
|
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([
|
export const GitHubConnectionOAuthOutputCredentialsSchema = z.union([
|
||||||
z.object({
|
z.object({
|
||||||
accessToken: z.string(),
|
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", [
|
export const ValidateGitHubConnectionCredentialsSchema = z.discriminatedUnion("method", [
|
||||||
z.object({
|
z.object({
|
||||||
method: z.literal(GitHubConnectionMethod.App).describe(AppConnections.CREATE(AppConnection.GitHub).method),
|
method: z.literal(GitHubConnectionMethod.App).describe(AppConnections.CREATE(AppConnection.GitHub).method),
|
||||||
@@ -76,6 +102,12 @@ export const ValidateGitHubConnectionCredentialsSchema = z.discriminatedUnion("m
|
|||||||
credentials: GitHubConnectionOAuthInputCredentialsSchema.describe(
|
credentials: GitHubConnectionOAuthInputCredentialsSchema.describe(
|
||||||
AppConnections.CREATE(AppConnection.GitHub).credentials
|
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
|
export const UpdateGitHubConnectionSchema = z
|
||||||
.object({
|
.object({
|
||||||
credentials: z
|
credentials: z
|
||||||
.union([GitHubConnectionAppInputCredentialsSchema, GitHubConnectionOAuthInputCredentialsSchema])
|
.union([
|
||||||
|
GitHubConnectionAppInputCredentialsSchema,
|
||||||
|
GitHubConnectionOAuthInputCredentialsSchema,
|
||||||
|
GitHubConnectionPatInputCredentialsSchema
|
||||||
|
])
|
||||||
.optional()
|
.optional()
|
||||||
.describe(AppConnections.UPDATE(AppConnection.GitHub).credentials)
|
.describe(AppConnections.UPDATE(AppConnection.GitHub).credentials)
|
||||||
})
|
})
|
||||||
@@ -110,6 +146,10 @@ export const GitHubConnectionSchema = z.intersection(
|
|||||||
z.object({
|
z.object({
|
||||||
method: z.literal(GitHubConnectionMethod.OAuth),
|
method: z.literal(GitHubConnectionMethod.OAuth),
|
||||||
credentials: GitHubConnectionOAuthOutputCredentialsSchema
|
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(),
|
instanceType: z.union([z.literal("server"), z.literal("cloud")]).optional(),
|
||||||
host: z.string().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 {
|
export enum GitLabAccessTokenType {
|
||||||
Project = "project",
|
Project = "project",
|
||||||
Personal = "personal"
|
Personal = "personal",
|
||||||
|
Group = "group"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,6 +21,8 @@ import {
|
|||||||
THCVaultKubernetesAuthConfig,
|
THCVaultKubernetesAuthConfig,
|
||||||
THCVaultKubernetesAuthRole,
|
THCVaultKubernetesAuthRole,
|
||||||
THCVaultKubernetesAuthRoleWithConfig,
|
THCVaultKubernetesAuthRoleWithConfig,
|
||||||
|
THCVaultKubernetesRole,
|
||||||
|
THCVaultKubernetesSecretsConfig,
|
||||||
THCVaultMount,
|
THCVaultMount,
|
||||||
THCVaultMountResponse
|
THCVaultMountResponse
|
||||||
} from "./hc-vault-connection-types";
|
} 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;
|
config: THCVaultKubernetesAuthConfig;
|
||||||
mountPath: string;
|
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