diff --git a/backend/package-lock.json b/backend/package-lock.json index 7534ac1bd..f22fd0b7b 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -63,6 +63,7 @@ "argon2": "^0.31.2", "aws-sdk": "^2.1553.0", "axios": "^1.11.0", + "axios-ntlm": "^1.4.4", "axios-retry": "^4.0.0", "bcrypt": "^5.1.1", "botbuilder": "^4.23.2", @@ -12956,216 +12957,6 @@ "dev": true, "license": "MIT" }, - "node_modules/@swc/core": { - "version": "1.3.107", - "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.3.107.tgz", - "integrity": "sha512-zKhqDyFcTsyLIYK1iEmavljZnf4CCor5pF52UzLAz4B6Nu/4GLU+2LQVAf+oRHjusG39PTPjd2AlRT3f3QWfsQ==", - "dev": true, - "hasInstallScript": true, - "optional": true, - "peer": true, - "dependencies": { - "@swc/counter": "^0.1.1", - "@swc/types": "^0.1.5" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/swc" - }, - "optionalDependencies": { - "@swc/core-darwin-arm64": "1.3.107", - "@swc/core-darwin-x64": "1.3.107", - "@swc/core-linux-arm-gnueabihf": "1.3.107", - "@swc/core-linux-arm64-gnu": "1.3.107", - "@swc/core-linux-arm64-musl": "1.3.107", - "@swc/core-linux-x64-gnu": "1.3.107", - "@swc/core-linux-x64-musl": "1.3.107", - "@swc/core-win32-arm64-msvc": "1.3.107", - "@swc/core-win32-ia32-msvc": "1.3.107", - "@swc/core-win32-x64-msvc": "1.3.107" - }, - "peerDependencies": { - "@swc/helpers": "^0.5.0" - }, - "peerDependenciesMeta": { - "@swc/helpers": { - "optional": true - } - } - }, - "node_modules/@swc/core-darwin-arm64": { - "version": "1.3.107", - "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.3.107.tgz", - "integrity": "sha512-47tD/5vSXWxPd0j/ZllyQUg4bqalbQTsmqSw0J4dDdS82MWqCAwUErUrAZPRjBkjNQ6Kmrf5rpCWaGTtPw+ngw==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "darwin" - ], - "peer": true, - "engines": { - "node": ">=10" - } - }, - "node_modules/@swc/core-darwin-x64": { - "version": "1.3.107", - "resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.3.107.tgz", - "integrity": "sha512-hwiLJ2ulNkBGAh1m1eTfeY1417OAYbRGcb/iGsJ+LuVLvKAhU/itzsl535CvcwAlt2LayeCFfcI8gdeOLeZa9A==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "darwin" - ], - "peer": true, - "engines": { - "node": ">=10" - } - }, - "node_modules/@swc/core-linux-arm-gnueabihf": { - "version": "1.3.107", - "resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.3.107.tgz", - "integrity": "sha512-I2wzcC0KXqh0OwymCmYwNRgZ9nxX7DWnOOStJXV3pS0uB83TXAkmqd7wvMBuIl9qu4Hfomi9aDM7IlEEn9tumQ==", - "cpu": [ - "arm" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "peer": true, - "engines": { - "node": ">=10" - } - }, - "node_modules/@swc/core-linux-arm64-gnu": { - "version": "1.3.107", - "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.3.107.tgz", - "integrity": "sha512-HWgnn7JORYlOYnGsdunpSF8A+BCZKPLzLtEUA27/M/ZuANcMZabKL9Zurt7XQXq888uJFAt98Gy+59PU90aHKg==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "peer": true, - "engines": { - "node": ">=10" - } - }, - "node_modules/@swc/core-linux-arm64-musl": { - "version": "1.3.107", - "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.3.107.tgz", - "integrity": "sha512-vfPF74cWfAm8hyhS8yvYI94ucMHIo8xIYU+oFOW9uvDlGQRgnUf/6DEVbLyt/3yfX5723Ln57U8uiMALbX5Pyw==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "peer": true, - "engines": { - "node": ">=10" - } - }, - "node_modules/@swc/core-linux-x64-gnu": { - "version": "1.3.107", - "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.3.107.tgz", - "integrity": "sha512-uBVNhIg0ip8rH9OnOsCARUFZ3Mq3tbPHxtmWk9uAa5u8jQwGWeBx5+nTHpDOVd3YxKb6+5xDEI/edeeLpha/9g==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "peer": true, - "engines": { - "node": ">=10" - } - }, - "node_modules/@swc/core-linux-x64-musl": { - "version": "1.3.107", - "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.3.107.tgz", - "integrity": "sha512-mvACkUvzSIB12q1H5JtabWATbk3AG+pQgXEN95AmEX2ZA5gbP9+B+mijsg7Sd/3tboHr7ZHLz/q3SHTvdFJrEw==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "peer": true, - "engines": { - "node": ">=10" - } - }, - "node_modules/@swc/core-win32-arm64-msvc": { - "version": "1.3.107", - "resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.3.107.tgz", - "integrity": "sha512-J3P14Ngy/1qtapzbguEH41kY109t6DFxfbK4Ntz9dOWNuVY3o9/RTB841ctnJk0ZHEG+BjfCJjsD2n8H5HcaOA==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ], - "peer": true, - "engines": { - "node": ">=10" - } - }, - "node_modules/@swc/core-win32-ia32-msvc": { - "version": "1.3.107", - "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.3.107.tgz", - "integrity": "sha512-ZBUtgyjTHlz8TPJh7kfwwwFma+ktr6OccB1oXC8fMSopD0AxVnQasgun3l3099wIsAB9eEsJDQ/3lDkOLs1gBA==", - "cpu": [ - "ia32" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ], - "peer": true, - "engines": { - "node": ">=10" - } - }, - "node_modules/@swc/core-win32-x64-msvc": { - "version": "1.3.107", - "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.3.107.tgz", - "integrity": "sha512-Eyzo2XRqWOxqhE1gk9h7LWmUf4Bp4Xn2Ttb0ayAXFp6YSTxQIThXcT9kipXZqcpxcmDwoq8iWbbf2P8XL743EA==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ], - "peer": true, - "engines": { - "node": ">=10" - } - }, "node_modules/@swc/counter": { "version": "0.1.3", "resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz", @@ -13183,14 +12974,6 @@ "tslib": "^2.8.0" } }, - "node_modules/@swc/types": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.5.tgz", - "integrity": "sha512-myfUej5naTBWnqOCc/MdVOLVjXUXtIA+NpDrDBKJtLLg2shUjBu3cZmB/85RyitKc55+lUUyl7oRfLOvkr2hsw==", - "dev": true, - "optional": true, - "peer": true - }, "node_modules/@techteamer/ocsp": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@techteamer/ocsp/-/ocsp-1.0.1.tgz", @@ -15195,6 +14978,18 @@ "proxy-from-env": "^1.1.0" } }, + "node_modules/axios-ntlm": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/axios-ntlm/-/axios-ntlm-1.4.4.tgz", + "integrity": "sha512-kpCRdzMfL8gi0Z0o96P3QPAK4XuC8iciGgxGXe+PeQ4oyjI2LZN8WSOKbu0Y9Jo3T/A7pB81n6jYVPIpglEuRA==", + "license": "MIT", + "dependencies": { + "axios": "^1.8.4", + "des.js": "^1.1.0", + "dev-null": "^0.1.1", + "js-md4": "^0.3.2" + } + }, "node_modules/axios-retry": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/axios-retry/-/axios-retry-4.0.0.tgz", @@ -16954,6 +16749,16 @@ "resolved": "https://registry.npmjs.org/deprecation/-/deprecation-2.3.1.tgz", "integrity": "sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ==" }, + "node_modules/des.js": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/des.js/-/des.js-1.1.0.tgz", + "integrity": "sha512-r17GxjhUCjSRy8aiJpr8/UadFIzMzJGexI3Nmz4ADi9LYSFx4gTBp80+NaX/YsXWWLhpZ7v/v/ubEc/bCNfKwg==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.1", + "minimalistic-assert": "^1.0.0" + } + }, "node_modules/destroy": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", @@ -16981,6 +16786,12 @@ "node": ">=8" } }, + "node_modules/dev-null": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/dev-null/-/dev-null-0.1.1.tgz", + "integrity": "sha512-nMNZG0zfMgmdv8S5O0TM5cpwNbGKRGPCxVsr0SmA3NZZy9CYBbuNLL0PD3Acx9e5LIUgwONXtM9kM6RlawPxEQ==", + "license": "MIT" + }, "node_modules/diff": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", @@ -19029,49 +18840,6 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" }, - "node_modules/gcp-metadata": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-5.3.0.tgz", - "integrity": "sha512-FNTkdNEnBdlqF2oatizolQqNANMrcqJt6AAYt99B3y1aLLC8Hc5IOBb+ZnnzllodEEf6xMBp6wRcBbc16fa65w==", - "optional": true, - "peer": true, - "dependencies": { - "gaxios": "^5.0.0", - "json-bigint": "^1.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/gcp-metadata/node_modules/gaxios": { - "version": "5.1.3", - "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-5.1.3.tgz", - "integrity": "sha512-95hVgBRgEIRQQQHIbnxBXeHbW4TqFk4ZDJW7wmVtvYar72FdhRIo1UGOLS2eRAKCPEdPBWu+M7+A33D9CdX9rA==", - "optional": true, - "peer": true, - "dependencies": { - "extend": "^3.0.2", - "https-proxy-agent": "^5.0.0", - "is-stream": "^2.0.0", - "node-fetch": "^2.6.9" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/gcp-metadata/node_modules/is-stream": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", - "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", - "optional": true, - "peer": true, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/generate-function": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.3.1.tgz", diff --git a/backend/package.json b/backend/package.json index f84db13fc..cc02186e9 100644 --- a/backend/package.json +++ b/backend/package.json @@ -183,6 +183,7 @@ "argon2": "^0.31.2", "aws-sdk": "^2.1553.0", "axios": "^1.11.0", + "axios-ntlm": "^1.4.4", "axios-retry": "^4.0.0", "bcrypt": "^5.1.1", "botbuilder": "^4.23.2", diff --git a/backend/src/db/migrations/20250824173438_add-approval-secret-read-compat.ts b/backend/src/db/migrations/20250824173438_add-approval-secret-read-compat.ts new file mode 100644 index 000000000..7398920af --- /dev/null +++ b/backend/src/db/migrations/20250824173438_add-approval-secret-read-compat.ts @@ -0,0 +1,19 @@ +import { Knex } from "knex"; + +import { TableName } from "../schemas"; + +export async function up(knex: Knex): Promise { + if (!(await knex.schema.hasColumn(TableName.SecretApprovalPolicy, "shouldCheckSecretPermission"))) { + await knex.schema.alterTable(TableName.SecretApprovalPolicy, (t) => { + t.boolean("shouldCheckSecretPermission").nullable(); + }); + } +} + +export async function down(knex: Knex): Promise { + if (await knex.schema.hasColumn(TableName.SecretApprovalPolicy, "shouldCheckSecretPermission")) { + await knex.schema.alterTable(TableName.SecretApprovalPolicy, (t) => { + t.dropColumn("shouldCheckSecretPermission"); + }); + } +} diff --git a/backend/src/db/migrations/20250824192801_backfill-secret-read-compat-flag.ts b/backend/src/db/migrations/20250824192801_backfill-secret-read-compat-flag.ts new file mode 100644 index 000000000..7a629ca52 --- /dev/null +++ b/backend/src/db/migrations/20250824192801_backfill-secret-read-compat-flag.ts @@ -0,0 +1,29 @@ +import { Knex } from "knex"; + +import { selectAllTableCols } from "@app/lib/knex"; + +import { TableName } from "../schemas"; + +const BATCH_SIZE = 100; + +export async function up(knex: Knex): Promise { + if (await knex.schema.hasColumn(TableName.SecretApprovalPolicy, "shouldCheckSecretPermission")) { + // find all existing SecretApprovalPolicy rows to backfill shouldCheckSecretPermission flag + const rows = await knex(TableName.SecretApprovalPolicy).select(selectAllTableCols(TableName.SecretApprovalPolicy)); + + if (rows.length > 0) { + for (let i = 0; i < rows.length; i += BATCH_SIZE) { + const batch = rows.slice(i, i + BATCH_SIZE); + // eslint-disable-next-line no-await-in-loop + await knex(TableName.SecretApprovalPolicy) + .whereIn( + "id", + batch.map((row) => row.id) + ) + .update({ shouldCheckSecretPermission: true }); + } + } + } +} + +export async function down(): Promise {} diff --git a/backend/src/db/migrations/20250826190000_add-properties-to-pki-subscriber.ts b/backend/src/db/migrations/20250826190000_add-properties-to-pki-subscriber.ts new file mode 100644 index 000000000..83a85c94f --- /dev/null +++ b/backend/src/db/migrations/20250826190000_add-properties-to-pki-subscriber.ts @@ -0,0 +1,23 @@ +import { Knex } from "knex"; + +import { TableName } from "../schemas"; + +export async function up(knex: Knex): Promise { + const hasPropertiesCol = await knex.schema.hasColumn(TableName.PkiSubscriber, "properties"); + + if (!hasPropertiesCol) { + await knex.schema.alterTable(TableName.PkiSubscriber, (t) => { + t.jsonb("properties").nullable(); + }); + } +} + +export async function down(knex: Knex): Promise { + const hasPropertiesCol = await knex.schema.hasColumn(TableName.PkiSubscriber, "properties"); + + if (hasPropertiesCol) { + await knex.schema.alterTable(TableName.PkiSubscriber, (t) => { + t.dropColumn("properties"); + }); + } +} diff --git a/backend/src/db/schemas/pki-subscribers.ts b/backend/src/db/schemas/pki-subscribers.ts index 0cdff4250..1641ba528 100644 --- a/backend/src/db/schemas/pki-subscribers.ts +++ b/backend/src/db/schemas/pki-subscribers.ts @@ -25,7 +25,8 @@ export const PkiSubscribersSchema = z.object({ lastAutoRenewAt: z.date().nullable().optional(), lastOperationStatus: z.string().nullable().optional(), lastOperationMessage: z.string().nullable().optional(), - lastOperationAt: z.date().nullable().optional() + lastOperationAt: z.date().nullable().optional(), + properties: z.unknown().nullable().optional() }); export type TPkiSubscribers = z.infer; diff --git a/backend/src/db/schemas/secret-approval-policies.ts b/backend/src/db/schemas/secret-approval-policies.ts index 0273e617c..dbb881db3 100644 --- a/backend/src/db/schemas/secret-approval-policies.ts +++ b/backend/src/db/schemas/secret-approval-policies.ts @@ -17,7 +17,8 @@ export const SecretApprovalPoliciesSchema = z.object({ updatedAt: z.date(), enforcementLevel: z.string().default("hard"), deletedAt: z.date().nullable().optional(), - allowedSelfApprovals: z.boolean().default(true) + allowedSelfApprovals: z.boolean().default(true), + shouldCheckSecretPermission: z.boolean().nullable().optional() }); export type TSecretApprovalPolicies = z.infer; diff --git a/backend/src/ee/routes/v1/secret-approval-request-router.ts b/backend/src/ee/routes/v1/secret-approval-request-router.ts index 104738140..bdc9c2dcd 100644 --- a/backend/src/ee/routes/v1/secret-approval-request-router.ts +++ b/backend/src/ee/routes/v1/secret-approval-request-router.ts @@ -305,7 +305,8 @@ export const registerSecretApprovalRequestRouter = async (server: FastifyZodProv secretPath: z.string().optional().nullable(), enforcementLevel: z.string(), deletedAt: z.date().nullish(), - allowedSelfApprovals: z.boolean() + allowedSelfApprovals: z.boolean(), + shouldCheckSecretPermission: z.boolean().nullable().optional() }), environment: z.string(), statusChangedByUser: approvalRequestUser.optional(), diff --git a/backend/src/ee/services/audit-log/audit-log-types.ts b/backend/src/ee/services/audit-log/audit-log-types.ts index 82d7ff80c..fa5208ad4 100644 --- a/backend/src/ee/services/audit-log/audit-log-types.ts +++ b/backend/src/ee/services/audit-log/audit-log-types.ts @@ -283,6 +283,7 @@ export enum EventType { UPDATE_SSH_CERTIFICATE_TEMPLATE = "update-ssh-certificate-template", DELETE_SSH_CERTIFICATE_TEMPLATE = "delete-ssh-certificate-template", GET_SSH_CERTIFICATE_TEMPLATE = "get-ssh-certificate-template", + GET_AZURE_AD_TEMPLATES = "get-azure-ad-templates", GET_SSH_HOST = "get-ssh-host", CREATE_SSH_HOST = "create-ssh-host", UPDATE_SSH_HOST = "update-ssh-host", @@ -2529,6 +2530,14 @@ interface CreateCertificateTemplateEstConfig { }; } +interface GetAzureAdCsTemplatesEvent { + type: EventType.GET_AZURE_AD_TEMPLATES; + metadata: { + caId: string; + amount: number; + }; +} + interface UpdateCertificateTemplateEstConfig { type: EventType.UPDATE_CERTIFICATE_TEMPLATE_EST_CONFIG; metadata: { @@ -3670,6 +3679,7 @@ export type Event = | CreateCertificateTemplateEstConfig | UpdateCertificateTemplateEstConfig | GetCertificateTemplateEstConfig + | GetAzureAdCsTemplatesEvent | AttemptCreateSlackIntegration | AttemptReinstallSlackIntegration | UpdateSlackIntegration diff --git a/backend/src/ee/services/secret-approval-request/secret-approval-request-dal.ts b/backend/src/ee/services/secret-approval-request/secret-approval-request-dal.ts index 49512768c..fe4ca94e1 100644 --- a/backend/src/ee/services/secret-approval-request/secret-approval-request-dal.ts +++ b/backend/src/ee/services/secret-approval-request/secret-approval-request-dal.ts @@ -180,7 +180,11 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => { tx.ref("enforcementLevel").withSchema(TableName.SecretApprovalPolicy).as("policyEnforcementLevel"), tx.ref("allowedSelfApprovals").withSchema(TableName.SecretApprovalPolicy).as("policyAllowedSelfApprovals"), tx.ref("approvals").withSchema(TableName.SecretApprovalPolicy).as("policyApprovals"), - tx.ref("deletedAt").withSchema(TableName.SecretApprovalPolicy).as("policyDeletedAt") + tx.ref("deletedAt").withSchema(TableName.SecretApprovalPolicy).as("policyDeletedAt"), + tx + .ref("shouldCheckSecretPermission") + .withSchema(TableName.SecretApprovalPolicy) + .as("policySecretReadAccessCompat") ); const findById = async (id: string, tx?: Knex) => { @@ -220,7 +224,8 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => { enforcementLevel: el.policyEnforcementLevel, envId: el.policyEnvId, deletedAt: el.policyDeletedAt, - allowedSelfApprovals: el.policyAllowedSelfApprovals + allowedSelfApprovals: el.policyAllowedSelfApprovals, + shouldCheckSecretPermission: el.policySecretReadAccessCompat } }), childrenMapper: [ @@ -692,14 +697,15 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => { db.ref("firstName").withSchema("committerUser").as("committerUserFirstName"), db.ref("lastName").withSchema("committerUser").as("committerUserLastName") ) - .distinctOn(`${TableName.SecretApprovalRequest}.id`) .as("inner"); - const query = (tx || db) - .select("*") + const countQuery = (await (tx || db) .select(db.raw("count(*) OVER() as total_count")) - .from(innerQuery) - .orderBy("createdAt", "desc") as typeof innerQuery; + .from(innerQuery.clone().distinctOn(`${TableName.SecretApprovalRequest}.id`))) as Array<{ + total_count: number; + }>; + + const query = (tx || db).select("*").from(innerQuery).orderBy("createdAt", "desc") as typeof innerQuery; if (search) { void query.where((qb) => { @@ -725,8 +731,7 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => { .where("w.rank", ">=", rankOffset) .andWhere("w.rank", "<", rankOffset + limit); - // @ts-expect-error knex does not infer - const totalCount = Number(docs[0]?.total_count || 0); + const totalCount = Number(countQuery[0]?.total_count || 0); const formattedDoc = sqlNestRelationships({ data: docs, diff --git a/backend/src/ee/services/secret-approval-request/secret-approval-request-service.ts b/backend/src/ee/services/secret-approval-request/secret-approval-request-service.ts index 9e32ad7de..d485f7ea0 100644 --- a/backend/src/ee/services/secret-approval-request/secret-approval-request-service.ts +++ b/backend/src/ee/services/secret-approval-request/secret-approval-request-service.ts @@ -281,13 +281,22 @@ export const secretApprovalRequestServiceFactory = ({ ) { throw new ForbiddenRequestError({ message: "User has insufficient privileges" }); } - const getHasSecretReadAccess = (environment: string, tags: { slug: string }[], secretPath?: string) => { - const canRead = hasSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.ReadValue, { - environment, - secretPath: secretPath || "/", - secretTags: tags.map((i) => i.slug) - }); - return canRead; + const getHasSecretReadAccess = ( + shouldCheckSecretPermission: boolean | null | undefined, + environment: string, + tags: { slug: string }[], + secretPath?: string + ) => { + if (shouldCheckSecretPermission) { + const canRead = hasSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.ReadValue, { + environment, + secretPath: secretPath || "/", + secretTags: tags.map((i) => i.slug) + }); + return canRead; + } + + return true; }; let secrets; @@ -309,8 +318,18 @@ export const secretApprovalRequestServiceFactory = ({ version: el.version, secretMetadata: el.secretMetadata as ResourceMetadataDTO, isRotatedSecret: el.secret?.isRotatedSecret ?? false, - secretValueHidden: !getHasSecretReadAccess(secretApprovalRequest.environment, el.tags, secretPath?.[0]?.path), - secretValue: !getHasSecretReadAccess(secretApprovalRequest.environment, el.tags, secretPath?.[0]?.path) + secretValueHidden: !getHasSecretReadAccess( + secretApprovalRequest.policy.shouldCheckSecretPermission, + secretApprovalRequest.environment, + el.tags, + secretPath?.[0]?.path + ), + secretValue: !getHasSecretReadAccess( + secretApprovalRequest.policy.shouldCheckSecretPermission, + secretApprovalRequest.environment, + el.tags, + secretPath?.[0]?.path + ) ? INFISICAL_SECRET_VALUE_HIDDEN_MASK : el.secret && el.secret.isRotatedSecret ? undefined @@ -326,11 +345,17 @@ export const secretApprovalRequestServiceFactory = ({ id: el.secret.id, version: el.secret.version, secretValueHidden: !getHasSecretReadAccess( + secretApprovalRequest.policy.shouldCheckSecretPermission, secretApprovalRequest.environment, el.tags, secretPath?.[0]?.path ), - secretValue: !getHasSecretReadAccess(secretApprovalRequest.environment, el.tags, secretPath?.[0]?.path) + secretValue: !getHasSecretReadAccess( + secretApprovalRequest.policy.shouldCheckSecretPermission, + secretApprovalRequest.environment, + el.tags, + secretPath?.[0]?.path + ) ? INFISICAL_SECRET_VALUE_HIDDEN_MASK : el.secret.encryptedValue ? secretManagerDecryptor({ cipherTextBlob: el.secret.encryptedValue }).toString() @@ -346,11 +371,17 @@ export const secretApprovalRequestServiceFactory = ({ id: el.secretVersion.id, version: el.secretVersion.version, secretValueHidden: !getHasSecretReadAccess( + secretApprovalRequest.policy.shouldCheckSecretPermission, secretApprovalRequest.environment, el.tags, secretPath?.[0]?.path ), - secretValue: !getHasSecretReadAccess(secretApprovalRequest.environment, el.tags, secretPath?.[0]?.path) + secretValue: !getHasSecretReadAccess( + secretApprovalRequest.policy.shouldCheckSecretPermission, + secretApprovalRequest.environment, + el.tags, + secretPath?.[0]?.path + ) ? INFISICAL_SECRET_VALUE_HIDDEN_MASK : el.secretVersion.encryptedValue ? secretManagerDecryptor({ cipherTextBlob: el.secretVersion.encryptedValue }).toString() @@ -368,7 +399,12 @@ export const secretApprovalRequestServiceFactory = ({ const encryptedSecrets = await secretApprovalRequestSecretDAL.findByRequestId(secretApprovalRequest.id); secrets = encryptedSecrets.map((el) => ({ ...el, - secretValueHidden: !getHasSecretReadAccess(secretApprovalRequest.environment, el.tags, secretPath?.[0]?.path), + secretValueHidden: !getHasSecretReadAccess( + secretApprovalRequest.policy.shouldCheckSecretPermission, + secretApprovalRequest.environment, + el.tags, + secretPath?.[0]?.path + ), ...decryptSecretWithBot(el, botKey), secret: el.secret ? { diff --git a/backend/src/lib/api-docs/constants.ts b/backend/src/lib/api-docs/constants.ts index f5cdce862..80a742bfb 100644 --- a/backend/src/lib/api-docs/constants.ts +++ b/backend/src/lib/api-docs/constants.ts @@ -2338,6 +2338,15 @@ export const AppConnections = { OKTA: { instanceUrl: "The URL used to access your Okta organization.", apiToken: "The API token used to authenticate with Okta." + }, + AZURE_ADCS: { + adcsUrl: + "The HTTPS URL of the Azure ADCS instance to connect with (e.g., 'https://adcs.yourdomain.com/certsrv').", + username: "The username used to access Azure ADCS (format: 'DOMAIN\\username' or 'username@domain.com').", + password: "The password used to access Azure ADCS.", + sslRejectUnauthorized: + "Whether or not to reject unauthorized SSL certificates (true/false). Set to false only in test environments with self-signed certificates.", + sslCertificate: "The SSL certificate (PEM format) to use for secure connection." } } }; diff --git a/backend/src/server/routes/v1/app-connection-routers/app-connection-router.ts b/backend/src/server/routes/v1/app-connection-routers/app-connection-router.ts index f184f2001..c2033f4b4 100644 --- a/backend/src/server/routes/v1/app-connection-routers/app-connection-router.ts +++ b/backend/src/server/routes/v1/app-connection-routers/app-connection-router.ts @@ -15,6 +15,10 @@ import { } from "@app/services/app-connection/1password"; import { Auth0ConnectionListItemSchema, SanitizedAuth0ConnectionSchema } from "@app/services/app-connection/auth0"; import { AwsConnectionListItemSchema, SanitizedAwsConnectionSchema } from "@app/services/app-connection/aws"; +import { + AzureADCSConnectionListItemSchema, + SanitizedAzureADCSConnectionSchema +} from "@app/services/app-connection/azure-adcs/azure-adcs-connection-schemas"; import { AzureAppConfigurationConnectionListItemSchema, SanitizedAzureAppConfigurationConnectionSchema @@ -150,7 +154,8 @@ const SanitizedAppConnectionSchema = z.union([ ...SanitizedSupabaseConnectionSchema.options, ...SanitizedDigitalOceanConnectionSchema.options, ...SanitizedNetlifyConnectionSchema.options, - ...SanitizedOktaConnectionSchema.options + ...SanitizedOktaConnectionSchema.options, + ...SanitizedAzureADCSConnectionSchema.options ]); const AppConnectionOptionsSchema = z.discriminatedUnion("app", [ @@ -190,7 +195,8 @@ const AppConnectionOptionsSchema = z.discriminatedUnion("app", [ SupabaseConnectionListItemSchema, DigitalOceanConnectionListItemSchema, NetlifyConnectionListItemSchema, - OktaConnectionListItemSchema + OktaConnectionListItemSchema, + AzureADCSConnectionListItemSchema ]); export const registerAppConnectionRouter = async (server: FastifyZodProvider) => { diff --git a/backend/src/server/routes/v1/app-connection-routers/azure-adcs-connection-router.ts b/backend/src/server/routes/v1/app-connection-routers/azure-adcs-connection-router.ts new file mode 100644 index 000000000..cd4d4d1fd --- /dev/null +++ b/backend/src/server/routes/v1/app-connection-routers/azure-adcs-connection-router.ts @@ -0,0 +1,18 @@ +import { AppConnection } from "@app/services/app-connection/app-connection-enums"; +import { + CreateAzureADCSConnectionSchema, + SanitizedAzureADCSConnectionSchema, + UpdateAzureADCSConnectionSchema +} from "@app/services/app-connection/azure-adcs"; + +import { registerAppConnectionEndpoints } from "./app-connection-endpoints"; + +export const registerAzureADCSConnectionRouter = async (server: FastifyZodProvider) => { + registerAppConnectionEndpoints({ + app: AppConnection.AzureADCS, + server, + sanitizedResponseSchema: SanitizedAzureADCSConnectionSchema, + createSchema: CreateAzureADCSConnectionSchema, + updateSchema: UpdateAzureADCSConnectionSchema + }); +}; diff --git a/backend/src/server/routes/v1/app-connection-routers/checkly-connection-router.ts b/backend/src/server/routes/v1/app-connection-routers/checkly-connection-router.ts index bbe3fbbfb..668bea065 100644 --- a/backend/src/server/routes/v1/app-connection-routers/checkly-connection-router.ts +++ b/backend/src/server/routes/v1/app-connection-routers/checkly-connection-router.ts @@ -53,4 +53,36 @@ export const registerChecklyConnectionRouter = async (server: FastifyZodProvider return { accounts }; } }); + + server.route({ + method: "GET", + url: `/:connectionId/accounts/:accountId/groups`, + config: { + rateLimit: readLimit + }, + schema: { + params: z.object({ + connectionId: z.string().uuid(), + accountId: z.string() + }), + response: { + 200: z.object({ + groups: z + .object({ + name: z.string(), + id: z.string() + }) + .array() + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT]), + handler: async (req) => { + const { connectionId, accountId } = req.params; + + const groups = await server.services.appConnection.checkly.listGroups(connectionId, accountId, req.permission); + + return { groups }; + } + }); }; diff --git a/backend/src/server/routes/v1/app-connection-routers/index.ts b/backend/src/server/routes/v1/app-connection-routers/index.ts index 28e3b4e49..70804d173 100644 --- a/backend/src/server/routes/v1/app-connection-routers/index.ts +++ b/backend/src/server/routes/v1/app-connection-routers/index.ts @@ -5,6 +5,7 @@ import { AppConnection } from "@app/services/app-connection/app-connection-enums import { registerOnePassConnectionRouter } from "./1password-connection-router"; import { registerAuth0ConnectionRouter } from "./auth0-connection-router"; import { registerAwsConnectionRouter } from "./aws-connection-router"; +import { registerAzureADCSConnectionRouter } from "./azure-adcs-connection-router"; import { registerAzureAppConfigurationConnectionRouter } from "./azure-app-configuration-connection-router"; import { registerAzureClientSecretsConnectionRouter } from "./azure-client-secrets-connection-router"; import { registerAzureDevOpsConnectionRouter } from "./azure-devops-connection-router"; @@ -50,6 +51,7 @@ export const APP_CONNECTION_REGISTER_ROUTER_MAP: Record { + registerCertificateAuthorityEndpoints({ + caType: CaType.AZURE_AD_CS, + server, + responseSchema: AzureAdCsCertificateAuthoritySchema, + createSchema: CreateAzureAdCsCertificateAuthoritySchema, + updateSchema: UpdateAzureAdCsCertificateAuthoritySchema + }); + + server.route({ + method: "GET", + url: "/:caId/templates", + config: { + rateLimit: readLimit + }, + schema: { + hide: false, + description: "Get available certificate templates from Azure AD CS CA", + params: z.object({ + caId: z.string().describe("Azure AD CS CA ID") + }), + querystring: z.object({ + projectId: z.string().describe("Project ID") + }), + response: { + 200: z.object({ + templates: z.array( + z.object({ + id: z.string().describe("Template identifier"), + name: z.string().describe("Template display name"), + description: z.string().optional().describe("Template description") + }) + ) + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const templates = await server.services.certificateAuthority.getAzureAdcsTemplates({ + caId: req.params.caId, + projectId: req.query.projectId, + actor: req.permission.type, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId + }); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + projectId: req.query.projectId, + event: { + type: EventType.GET_AZURE_AD_TEMPLATES, + metadata: { + caId: req.params.caId, + amount: templates.length + } + } + }); + + return { templates }; + } + }); +}; diff --git a/backend/src/server/routes/v1/certificate-authority-routers/index.ts b/backend/src/server/routes/v1/certificate-authority-routers/index.ts index 56a236911..d146e68be 100644 --- a/backend/src/server/routes/v1/certificate-authority-routers/index.ts +++ b/backend/src/server/routes/v1/certificate-authority-routers/index.ts @@ -1,6 +1,7 @@ import { CaType } from "@app/services/certificate-authority/certificate-authority-enums"; import { registerAcmeCertificateAuthorityRouter } from "./acme-certificate-authority-router"; +import { registerAzureAdCsCertificateAuthorityRouter } from "./azure-ad-cs-certificate-authority-router"; import { registerInternalCertificateAuthorityRouter } from "./internal-certificate-authority-router"; export * from "./internal-certificate-authority-router"; @@ -8,5 +9,6 @@ export * from "./internal-certificate-authority-router"; export const CERTIFICATE_AUTHORITY_REGISTER_ROUTER_MAP: Record Promise> = { [CaType.INTERNAL]: registerInternalCertificateAuthorityRouter, - [CaType.ACME]: registerAcmeCertificateAuthorityRouter + [CaType.ACME]: registerAcmeCertificateAuthorityRouter, + [CaType.AZURE_AD_CS]: registerAzureAdCsCertificateAuthorityRouter }; diff --git a/backend/src/server/routes/v1/pki-subscriber-router.ts b/backend/src/server/routes/v1/pki-subscriber-router.ts index 0e9ec6e0c..30202af18 100644 --- a/backend/src/server/routes/v1/pki-subscriber-router.ts +++ b/backend/src/server/routes/v1/pki-subscriber-router.ts @@ -1,3 +1,4 @@ +import RE2 from "re2"; import { z } from "zod"; import { CertificatesSchema } from "@app/db/schemas"; @@ -112,7 +113,88 @@ export const registerPkiSubscriberRouter = async (server: FastifyZodProvider) => .transform((arr) => Array.from(new Set(arr))) .describe(PKI_SUBSCRIBERS.CREATE.extendedKeyUsages), enableAutoRenewal: z.boolean().optional().describe(PKI_SUBSCRIBERS.CREATE.enableAutoRenewal), - autoRenewalPeriodInDays: z.number().min(1).optional().describe(PKI_SUBSCRIBERS.CREATE.autoRenewalPeriodInDays) + autoRenewalPeriodInDays: z.number().min(1).optional().describe(PKI_SUBSCRIBERS.CREATE.autoRenewalPeriodInDays), + properties: z + .object({ + azureTemplateType: z.string().optional().describe("Azure ADCS Certificate Template Type"), + organization: z + .string() + .trim() + .min(1) + .max(64, "Organization cannot exceed 64 characters") + .regex( + new RE2('^[^,=+<>#;\\\\"/\\r\\n\\t]*$'), + 'Organization contains invalid characters: , = + < > # ; \\ " / \\r \\n \\t' + ) + .regex( + new RE2("^[^\\\\s\\\\-_.]+.*[^\\\\s\\\\-_.]+$|^[^\\\\s\\\\-_.]{1}$"), + "Organization cannot start or end with spaces, hyphens, underscores, or periods" + ) + .optional() + .describe("Organization (O) - Maximum 64 characters, no special DN characters"), + organizationalUnit: z + .string() + .trim() + .min(1) + .max(64, "Organizational Unit cannot exceed 64 characters") + .regex( + new RE2('^[^,=+<>#;\\\\"/\\r\\n\\t]*$'), + 'Organizational Unit contains invalid characters: , = + < > # ; \\ " / \\r \\n \\t' + ) + .regex( + new RE2("^[^\\\\s\\\\-_.]+.*[^\\\\s\\\\-_.]+$|^[^\\\\s\\\\-_.]{1}$"), + "Organizational Unit cannot start or end with spaces, hyphens, underscores, or periods" + ) + .optional() + .describe("Organizational Unit (OU) - Maximum 64 characters, no special DN characters"), + country: z + .string() + .trim() + .length(2, "Country must be exactly 2 characters") + .regex(new RE2("^[A-Z]{2}$"), "Country must be exactly 2 uppercase letters") + .optional() + .describe("Country (C) - Two uppercase letter country code (e.g., US, CA, GB)"), + state: z + .string() + .trim() + .min(1) + .max(64, "State cannot exceed 64 characters") + .regex( + new RE2('^[^,=+<>#;\\\\"/\\r\\n\\t]*$'), + 'State contains invalid characters: , = + < > # ; \\ " / \\r \\n \\t' + ) + .regex( + new RE2("^[^\\\\s\\\\-_.]+.*[^\\\\s\\\\-_.]+$|^[^\\\\s\\\\-_.]{1}$"), + "State cannot start or end with spaces, hyphens, underscores, or periods" + ) + .optional() + .describe("State/Province (ST) - Maximum 64 characters, no special DN characters"), + locality: z + .string() + .trim() + .min(1) + .max(64, "Locality cannot exceed 64 characters") + .regex( + new RE2('^[^,=+<>#;\\\\"/\\r\\n\\t]*$'), + 'Locality contains invalid characters: , = + < > # ; \\ " / \\r \\n \\t' + ) + .regex( + new RE2("^[^\\\\s\\\\-_.]+.*[^\\\\s\\\\-_.]+$|^[^\\\\s\\\\-_.]{1}$"), + "Locality cannot start or end with spaces, hyphens, underscores, or periods" + ) + .optional() + .describe("Locality (L) - Maximum 64 characters, no special DN characters"), + emailAddress: z + .string() + .trim() + .email("Email Address must be a valid email format") + .min(6, "Email Address must be at least 6 characters") + .max(64, "Email Address cannot exceed 64 characters") + .optional() + .describe("Email Address - Valid email format between 6 and 64 characters") + }) + .optional() + .describe("Additional subscriber properties and subject fields") }), response: { 200: sanitizedPkiSubscriber @@ -199,7 +281,88 @@ export const registerPkiSubscriberRouter = async (server: FastifyZodProvider) => .optional() .describe(PKI_SUBSCRIBERS.UPDATE.extendedKeyUsages), enableAutoRenewal: z.boolean().optional().describe(PKI_SUBSCRIBERS.UPDATE.enableAutoRenewal), - autoRenewalPeriodInDays: z.number().min(1).optional().describe(PKI_SUBSCRIBERS.UPDATE.autoRenewalPeriodInDays) + autoRenewalPeriodInDays: z.number().min(1).optional().describe(PKI_SUBSCRIBERS.UPDATE.autoRenewalPeriodInDays), + properties: z + .object({ + azureTemplateType: z.string().optional().describe("Azure ADCS Certificate Template Type"), + organization: z + .string() + .trim() + .min(1) + .max(64, "Organization cannot exceed 64 characters") + .regex( + new RE2('^[^,=+<>#;\\\\"/\\r\\n\\t]*$'), + 'Organization contains invalid characters: , = + < > # ; \\ " / \\r \\n \\t' + ) + .regex( + new RE2("^[^\\\\s\\\\-_.]+.*[^\\\\s\\\\-_.]+$|^[^\\\\s\\\\-_.]{1}$"), + "Organization cannot start or end with spaces, hyphens, underscores, or periods" + ) + .optional() + .describe("Organization (O) - Maximum 64 characters, no special DN characters"), + organizationalUnit: z + .string() + .trim() + .min(1) + .max(64, "Organizational Unit cannot exceed 64 characters") + .regex( + new RE2('^[^,=+<>#;\\\\"/\\r\\n\\t]*$'), + 'Organizational Unit contains invalid characters: , = + < > # ; \\ " / \\r \\n \\t' + ) + .regex( + new RE2("^[^\\\\s\\\\-_.]+.*[^\\\\s\\\\-_.]+$|^[^\\\\s\\\\-_.]{1}$"), + "Organizational Unit cannot start or end with spaces, hyphens, underscores, or periods" + ) + .optional() + .describe("Organizational Unit (OU) - Maximum 64 characters, no special DN characters"), + country: z + .string() + .trim() + .length(2, "Country must be exactly 2 characters") + .regex(new RE2("^[A-Z]{2}$"), "Country must be exactly 2 uppercase letters") + .optional() + .describe("Country (C) - Two uppercase letter country code (e.g., US, CA, GB)"), + state: z + .string() + .trim() + .min(1) + .max(64, "State cannot exceed 64 characters") + .regex( + new RE2('^[^,=+<>#;\\\\"/\\r\\n\\t]*$'), + 'State contains invalid characters: , = + < > # ; \\ " / \\r \\n \\t' + ) + .regex( + new RE2("^[^\\\\s\\\\-_.]+.*[^\\\\s\\\\-_.]+$|^[^\\\\s\\\\-_.]{1}$"), + "State cannot start or end with spaces, hyphens, underscores, or periods" + ) + .optional() + .describe("State/Province (ST) - Maximum 64 characters, no special DN characters"), + locality: z + .string() + .trim() + .min(1) + .max(64, "Locality cannot exceed 64 characters") + .regex( + new RE2('^[^,=+<>#;\\\\"/\\r\\n\\t]*$'), + 'Locality contains invalid characters: , = + < > # ; \\ " / \\r \\n \\t' + ) + .regex( + new RE2("^[^\\\\s\\\\-_.]+.*[^\\\\s\\\\-_.]+$|^[^\\\\s\\\\-_.]{1}$"), + "Locality cannot start or end with spaces, hyphens, underscores, or periods" + ) + .optional() + .describe("Locality (L) - Maximum 64 characters, no special DN characters"), + emailAddress: z + .string() + .trim() + .email("Email Address must be a valid email format") + .min(6, "Email Address must be at least 6 characters") + .max(64, "Email Address cannot exceed 64 characters") + .optional() + .describe("Email Address - Valid email format between 6 and 64 characters") + }) + .optional() + .describe("Additional subscriber properties and subject fields") }), response: { 200: sanitizedPkiSubscriber diff --git a/backend/src/server/routes/v2/certificate-authority-router.ts b/backend/src/server/routes/v2/certificate-authority-router.ts index d8b434fdf..28d5527a8 100644 --- a/backend/src/server/routes/v2/certificate-authority-router.ts +++ b/backend/src/server/routes/v2/certificate-authority-router.ts @@ -6,12 +6,14 @@ import { readLimit } from "@app/server/config/rateLimiter"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { AuthMode } from "@app/services/auth/auth-type"; import { AcmeCertificateAuthoritySchema } from "@app/services/certificate-authority/acme/acme-certificate-authority-schemas"; +import { AzureAdCsCertificateAuthoritySchema } from "@app/services/certificate-authority/azure-ad-cs/azure-ad-cs-certificate-authority-schemas"; import { CaType } from "@app/services/certificate-authority/certificate-authority-enums"; import { InternalCertificateAuthoritySchema } from "@app/services/certificate-authority/internal/internal-certificate-authority-schemas"; const CertificateAuthoritySchema = z.discriminatedUnion("type", [ InternalCertificateAuthoritySchema, - AcmeCertificateAuthoritySchema + AcmeCertificateAuthoritySchema, + AzureAdCsCertificateAuthoritySchema ]); export const registerCaRouter = async (server: FastifyZodProvider) => { @@ -52,19 +54,31 @@ export const registerCaRouter = async (server: FastifyZodProvider) => { req.permission ); + const azureAdCsCas = await server.services.certificateAuthority.listCertificateAuthoritiesByProjectId( + { + projectId: req.query.projectId, + type: CaType.AZURE_AD_CS + }, + req.permission + ); + await server.services.auditLog.createAuditLog({ ...req.auditLogInfo, projectId: req.query.projectId, event: { type: EventType.GET_CAS, metadata: { - caIds: [...(internalCas ?? []).map((ca) => ca.id), ...(acmeCas ?? []).map((ca) => ca.id)] + caIds: [ + ...(internalCas ?? []).map((ca) => ca.id), + ...(acmeCas ?? []).map((ca) => ca.id), + ...(azureAdCsCas ?? []).map((ca) => ca.id) + ] } } }); return { - certificateAuthorities: [...(internalCas ?? []), ...(acmeCas ?? [])] + certificateAuthorities: [...(internalCas ?? []), ...(acmeCas ?? []), ...(azureAdCsCas ?? [])] }; } }); diff --git a/backend/src/services/app-connection/app-connection-enums.ts b/backend/src/services/app-connection/app-connection-enums.ts index 263cbdd9b..76dcdd5f0 100644 --- a/backend/src/services/app-connection/app-connection-enums.ts +++ b/backend/src/services/app-connection/app-connection-enums.ts @@ -8,6 +8,7 @@ export enum AppConnection { AzureAppConfiguration = "azure-app-configuration", AzureClientSecrets = "azure-client-secrets", AzureDevOps = "azure-devops", + AzureADCS = "azure-adcs", Humanitec = "humanitec", TerraformCloud = "terraform-cloud", Vercel = "vercel", diff --git a/backend/src/services/app-connection/app-connection-fns.ts b/backend/src/services/app-connection/app-connection-fns.ts index 9ffc358b6..94de51f1e 100644 --- a/backend/src/services/app-connection/app-connection-fns.ts +++ b/backend/src/services/app-connection/app-connection-fns.ts @@ -31,6 +31,11 @@ import { } from "./app-connection-types"; import { Auth0ConnectionMethod, getAuth0ConnectionListItem, validateAuth0ConnectionCredentials } from "./auth0"; import { AwsConnectionMethod, getAwsConnectionListItem, validateAwsConnectionCredentials } from "./aws"; +import { AzureADCSConnectionMethod } from "./azure-adcs"; +import { + getAzureADCSConnectionListItem, + validateAzureADCSConnectionCredentials +} from "./azure-adcs/azure-adcs-connection-fns"; import { AzureAppConfigurationConnectionMethod, getAzureAppConfigurationConnectionListItem, @@ -136,6 +141,7 @@ export const listAppConnectionOptions = () => { getAzureKeyVaultConnectionListItem(), getAzureAppConfigurationConnectionListItem(), getAzureDevopsConnectionListItem(), + getAzureADCSConnectionListItem(), getDatabricksConnectionListItem(), getHumanitecConnectionListItem(), getTerraformCloudConnectionListItem(), @@ -227,6 +233,7 @@ export const validateAppConnectionCredentials = async ( [AppConnection.AzureClientSecrets]: validateAzureClientSecretsConnectionCredentials as TAppConnectionCredentialsValidator, [AppConnection.AzureDevOps]: validateAzureDevOpsConnectionCredentials as TAppConnectionCredentialsValidator, + [AppConnection.AzureADCS]: validateAzureADCSConnectionCredentials as TAppConnectionCredentialsValidator, [AppConnection.Humanitec]: validateHumanitecConnectionCredentials as TAppConnectionCredentialsValidator, [AppConnection.Postgres]: validateSqlConnectionCredentials as TAppConnectionCredentialsValidator, [AppConnection.MsSql]: validateSqlConnectionCredentials as TAppConnectionCredentialsValidator, @@ -300,6 +307,7 @@ export const getAppConnectionMethodName = (method: TAppConnection["method"]) => case MsSqlConnectionMethod.UsernameAndPassword: case MySqlConnectionMethod.UsernameAndPassword: case OracleDBConnectionMethod.UsernameAndPassword: + case AzureADCSConnectionMethod.UsernamePassword: return "Username & Password"; case WindmillConnectionMethod.AccessToken: case HCVaultConnectionMethod.AccessToken: @@ -357,6 +365,7 @@ export const TRANSITION_CONNECTION_CREDENTIALS_TO_PLATFORM: Record< [AppConnection.AzureKeyVault]: platformManagedCredentialsNotSupported, [AppConnection.AzureAppConfiguration]: platformManagedCredentialsNotSupported, [AppConnection.AzureDevOps]: platformManagedCredentialsNotSupported, + [AppConnection.AzureADCS]: platformManagedCredentialsNotSupported, [AppConnection.Humanitec]: platformManagedCredentialsNotSupported, [AppConnection.Postgres]: transferSqlConnectionCredentialsToPlatform as TAppConnectionTransitionCredentialsToPlatform, [AppConnection.MsSql]: transferSqlConnectionCredentialsToPlatform as TAppConnectionTransitionCredentialsToPlatform, diff --git a/backend/src/services/app-connection/app-connection-maps.ts b/backend/src/services/app-connection/app-connection-maps.ts index 155b68ce1..a2ce02669 100644 --- a/backend/src/services/app-connection/app-connection-maps.ts +++ b/backend/src/services/app-connection/app-connection-maps.ts @@ -9,6 +9,7 @@ export const APP_CONNECTION_NAME_MAP: Record = { [AppConnection.AzureAppConfiguration]: "Azure App Configuration", [AppConnection.AzureClientSecrets]: "Azure Client Secrets", [AppConnection.AzureDevOps]: "Azure DevOps", + [AppConnection.AzureADCS]: "Azure ADCS", [AppConnection.Databricks]: "Databricks", [AppConnection.Humanitec]: "Humanitec", [AppConnection.TerraformCloud]: "Terraform Cloud", @@ -49,6 +50,7 @@ export const APP_CONNECTION_PLAN_MAP: Record; +} + +interface AxiosNtlmResponse { + status: number; + data: string; + headers: unknown; +} + +// Types for credential parsing +interface ParsedCredentials { + domain: string; + username: string; + fullUsername: string; // domain\username format +} + +// Helper function to parse and normalize credentials for Windows authentication +const parseCredentials = (inputUsername: string): ParsedCredentials => { + // Ensure inputUsername is a string + if (typeof inputUsername !== "string" || !inputUsername.trim()) { + throw new BadRequestError({ + message: "Username must be a non-empty string" + }); + } + + let domain = ""; + let username = ""; + let fullUsername = ""; + + if (inputUsername.includes("\\")) { + // Already in domain\username format + const parts = inputUsername.split("\\"); + if (parts.length === 2) { + [domain, username] = parts; + fullUsername = inputUsername; + } else { + throw new BadRequestError({ + message: "Invalid domain\\username format. Expected format: DOMAIN\\username" + }); + } + } else if (inputUsername.includes("@")) { + // UPN format: user@domain.com + const [user, domainPart] = inputUsername.split("@"); + if (!user || !domainPart) { + throw new BadRequestError({ + message: "Invalid UPN format. Expected format: user@domain.com" + }); + } + + username = user; + // Extract NetBIOS name from FQDN + domain = domainPart.split(".")[0].toUpperCase(); + fullUsername = `${domain}\\${username}`; + } else { + // Plain username - assume local account or current domain + username = inputUsername; + domain = ""; + fullUsername = inputUsername; + } + + return { domain, username, fullUsername }; +}; + +// Helper to normalize URL +const normalizeAdcsUrl = (url: string): string => { + let normalizedUrl = url.trim(); + + // Remove trailing slash + normalizedUrl = normalizedUrl.replace(/\/$/, ""); + + // Ensure HTTPS protocol + if (normalizedUrl.startsWith("http://")) { + normalizedUrl = normalizedUrl.replace("http://", "https://"); + } else if (!normalizedUrl.startsWith("https://")) { + normalizedUrl = `https://${normalizedUrl}`; + } + + return normalizedUrl; +}; + +// NTLM request wrapper +const createHttpsAgent = (sslRejectUnauthorized: boolean, sslCertificate?: string): https.Agent => { + const agentOptions: https.AgentOptions = { + rejectUnauthorized: sslRejectUnauthorized, + keepAlive: true, // axios-ntlm needs keepAlive for NTLM handshake + ca: sslCertificate ? [sslCertificate.trim()] : undefined, + // Disable hostname verification as Microsoft servers by default use local IPs for certificates + // which may not match the hostname used to connect + checkServerIdentity: () => undefined + }; + + return new https.Agent(agentOptions); +}; + +const axiosNtlmRequest = async (config: AxiosNtlmConfig): Promise => { + const method = config.method || "GET"; + + const credentials = { + username: config.ntlm.username, + password: config.ntlm.password, + domain: config.ntlm.domain || "", + workstation: "" + }; + + const axiosConfig = { + httpsAgent: config.httpsAgent, + timeout: 30000 + }; + + const client = NtlmClient(credentials, axiosConfig); + + const requestOptions: { url: string; method: string; data?: string; headers?: Record } = { + url: config.url, + method + }; + + if (config.data) { + requestOptions.data = config.data; + } + + if (config.headers) { + requestOptions.headers = config.headers; + } + + const response = await client(requestOptions); + + return { + status: response.status, + data: response.data, + headers: response.headers + }; +}; + +// Test ADCS connectivity and authentication using NTLM +const testAdcsConnection = async ( + credentials: ParsedCredentials, + password: string, + baseUrl: string, + sslRejectUnauthorized: boolean = true, + sslCertificate?: string +): Promise => { + // Test endpoints in order of preference + const testEndpoints = [ + "/certsrv/certrqus.asp", // Certificate request status (most reliable) + "/certsrv/certfnsh.asp", // Certificate finalization + "/certsrv/default.asp", // Main ADCS page + "/certsrv/" // Root certsrv + ]; + + for (const endpoint of testEndpoints) { + try { + const testUrl = `${baseUrl}${endpoint}`; + + const shouldRejectUnauthorized = sslRejectUnauthorized; + + const httpsAgent = createHttpsAgent(shouldRejectUnauthorized, sslCertificate); + + const response = await axiosNtlmRequest({ + url: testUrl, + method: "GET", + httpsAgent, + ntlm: { + domain: credentials.domain, + username: credentials.username, + password + } + }); + + // Check if we got a successful response + if (response.status === 200) { + const responseText = response.data; + + // Verify this is actually an ADCS server by checking content + const adcsIndicators = [ + "Microsoft Active Directory Certificate Services", + "Certificate Services", + "Request a certificate", + "certsrv", + "Certificate Template", + "Web Enrollment" + ]; + + const isAdcsServer = adcsIndicators.some((indicator) => + responseText.toLowerCase().includes(indicator.toLowerCase()) + ); + + if (isAdcsServer) { + // Successfully authenticated and confirmed ADCS + return true; + } + } + + if (response.status === 401) { + throw new BadRequestError({ + message: "Authentication failed. Please verify your credentials are correct." + }); + } + + if (response.status === 403) { + throw new BadRequestError({ + message: "Access denied. Your account may not have permission to access ADCS web enrollment." + }); + } + } catch (error) { + if (error instanceof BadRequestError) { + throw error; + } + + // Handle network and connection errors + if (error instanceof Error) { + if (error.message.includes("ENOTFOUND")) { + throw new BadRequestError({ + message: "Cannot resolve ADCS server hostname. Please verify the URL is correct." + }); + } + if (error.message.includes("ECONNREFUSED")) { + throw new BadRequestError({ + message: "Connection refused by ADCS server. Please verify the server is running and accessible." + }); + } + if (error.message.includes("ETIMEDOUT") || error.message.includes("timeout")) { + throw new BadRequestError({ + message: "Connection timeout. Please verify the server is accessible and not blocked by firewall." + }); + } + if (error.message.includes("certificate") || error.message.includes("SSL") || error.message.includes("TLS")) { + throw new BadRequestError({ + message: `SSL/TLS certificate error: ${error.message}. This may indicate a certificate verification failure.` + }); + } + if (error.message.includes("DEPTH_ZERO_SELF_SIGNED_CERT")) { + throw new BadRequestError({ + message: + "Self-signed certificate detected. Either provide the server's certificate or set 'sslRejectUnauthorized' to false." + }); + } + if (error.message.includes("UNABLE_TO_VERIFY_LEAF_SIGNATURE")) { + throw new BadRequestError({ + message: "Unable to verify certificate signature. Please provide the correct CA certificate." + }); + } + } + + // Continue to next endpoint for other errors + continue; + } + } + + // If we get here, no endpoint worked + throw new BadRequestError({ + message: "Could not connect to ADCS server. Please verify the server URL and that Web Enrollment is enabled." + }); +}; + +// Create authenticated NTLM client for ADCS operations +const createNtlmClient = ( + username: string, + password: string, + baseUrl: string, + sslRejectUnauthorized: boolean = true, + sslCertificate?: string +) => { + const parsedCredentials = parseCredentials(username); + const normalizedUrl = normalizeAdcsUrl(baseUrl); + + return { + get: async (endpoint: string, additionalHeaders: Record = {}) => { + const shouldRejectUnauthorized = sslRejectUnauthorized; + + const httpsAgent = createHttpsAgent(shouldRejectUnauthorized, sslCertificate); + + return axiosNtlmRequest({ + url: `${normalizedUrl}${endpoint}`, + method: "GET", + httpsAgent, + headers: additionalHeaders, + ntlm: { + domain: parsedCredentials.domain, + username: parsedCredentials.username, + password + } + }); + }, + post: async (endpoint: string, body: string, additionalHeaders: Record = {}) => { + const shouldRejectUnauthorized = sslRejectUnauthorized; + + const httpsAgent = createHttpsAgent(shouldRejectUnauthorized, sslCertificate); + + return axiosNtlmRequest({ + url: `${normalizedUrl}${endpoint}`, + method: "POST", + httpsAgent, + data: body, + headers: { + "Content-Type": "application/x-www-form-urlencoded", + ...additionalHeaders + }, + ntlm: { + domain: parsedCredentials.domain, + username: parsedCredentials.username, + password + } + }); + }, + baseUrl: normalizedUrl, + credentials: parsedCredentials + }; +}; + +export const getAzureADCSConnectionCredentials = async ( + connectionId: string, + appConnectionDAL: Pick, + kmsService: Pick +) => { + const appConnection = await appConnectionDAL.findById(connectionId); + + if (!appConnection) { + throw new NotFoundError({ message: `Connection with ID '${connectionId}' not found` }); + } + + if (appConnection.app !== AppConnection.AzureADCS) { + throw new BadRequestError({ message: `Connection with ID '${connectionId}' is not an Azure ADCS connection` }); + } + + switch (appConnection.method) { + case AzureADCSConnectionMethod.UsernamePassword: + const credentials = (await decryptAppConnectionCredentials({ + orgId: appConnection.orgId, + kmsService, + encryptedCredentials: appConnection.encryptedCredentials + })) as { + username: string; + password: string; + adcsUrl: string; + sslRejectUnauthorized?: boolean; + sslCertificate?: string; + }; + + return { + username: credentials.username, + password: credentials.password, + adcsUrl: credentials.adcsUrl, + sslRejectUnauthorized: credentials.sslRejectUnauthorized ?? true, + sslCertificate: credentials.sslCertificate + }; + + default: + throw new BadRequestError({ + message: `Unsupported Azure ADCS connection method: ${appConnection.method}` + }); + } +}; + +export const validateAzureADCSConnectionCredentials = async (appConnection: TAzureADCSConnectionConfig) => { + const { credentials } = appConnection; + + try { + // Parse and validate credentials + const parsedCredentials = parseCredentials(credentials.username); + const normalizedUrl = normalizeAdcsUrl(credentials.adcsUrl); + + // Validate URL to prevent DNS manipulation attacks and SSRF + await blockLocalAndPrivateIpAddresses(normalizedUrl); + + // Test the connection using NTLM + await testAdcsConnection( + parsedCredentials, + credentials.password, + normalizedUrl, + credentials.sslRejectUnauthorized ?? true, + credentials.sslCertificate + ); + + // If we get here, authentication was successful + return { + username: credentials.username, + password: credentials.password, + adcsUrl: credentials.adcsUrl, + sslRejectUnauthorized: credentials.sslRejectUnauthorized ?? true, + sslCertificate: credentials.sslCertificate + }; + } catch (error) { + if (error instanceof BadRequestError) { + throw error; + } + + // Handle unexpected errors + let errorMessage = "Unable to validate ADCS connection."; + if (error instanceof Error) { + if (error.message.includes("401") || error.message.includes("Unauthorized")) { + errorMessage = "NTLM authentication failed. Please verify your username, password, and domain are correct."; + } else if (error.message.includes("ENOTFOUND") || error.message.includes("ECONNREFUSED")) { + errorMessage = "Cannot connect to the ADCS server. Please verify the server URL is correct and accessible."; + } else if (error.message.includes("timeout")) { + errorMessage = "Connection to ADCS server timed out. Please verify the server is accessible."; + } else if ( + error.message.includes("certificate") || + error.message.includes("SSL") || + error.message.includes("TLS") || + error.message.includes("DEPTH_ZERO_SELF_SIGNED_CERT") || + error.message.includes("UNABLE_TO_VERIFY_LEAF_SIGNATURE") + ) { + errorMessage = `SSL/TLS certificate error: ${error.message}. The server certificate may be self-signed or the CA certificate may be incorrect.`; + } + } + + throw new BadRequestError({ + message: `Failed to validate Azure ADCS connection: ${errorMessage} Details: ${ + error instanceof Error ? error.message : "Unknown error" + }` + }); + } +}; + +export const getAzureADCSConnectionListItem = () => ({ + name: "Azure ADCS" as const, + app: AppConnection.AzureADCS as const, + methods: [AzureADCSConnectionMethod.UsernamePassword] as [AzureADCSConnectionMethod.UsernamePassword] +}); + +// Export helper functions for use in certificate ordering +export const createAdcsHttpClient = ( + username: string, + password: string, + baseUrl: string, + sslRejectUnauthorized: boolean = true, + sslCertificate?: string +) => { + return createNtlmClient(username, password, baseUrl, sslRejectUnauthorized, sslCertificate); +}; diff --git a/backend/src/services/app-connection/azure-adcs/azure-adcs-connection-schemas.ts b/backend/src/services/app-connection/azure-adcs/azure-adcs-connection-schemas.ts new file mode 100644 index 000000000..a43bb5954 --- /dev/null +++ b/backend/src/services/app-connection/azure-adcs/azure-adcs-connection-schemas.ts @@ -0,0 +1,88 @@ +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 { AzureADCSConnectionMethod } from "./azure-adcs-connection-enums"; + +export const AzureADCSUsernamePasswordCredentialsSchema = z.object({ + adcsUrl: z + .string() + .trim() + .min(1, "ADCS URL required") + .max(255) + .refine((value) => value.startsWith("https://"), "ADCS URL must use HTTPS") + .describe(AppConnections.CREDENTIALS.AZURE_ADCS.adcsUrl), + username: z + .string() + .trim() + .min(1, "Username required") + .max(255) + .describe(AppConnections.CREDENTIALS.AZURE_ADCS.username), + password: z + .string() + .trim() + .min(1, "Password required") + .max(255) + .describe(AppConnections.CREDENTIALS.AZURE_ADCS.password), + sslRejectUnauthorized: z.boolean().optional().describe(AppConnections.CREDENTIALS.AZURE_ADCS.sslRejectUnauthorized), + sslCertificate: z + .string() + .trim() + .transform((value) => value || undefined) + .optional() + .describe(AppConnections.CREDENTIALS.AZURE_ADCS.sslCertificate) +}); + +const BaseAzureADCSConnectionSchema = BaseAppConnectionSchema.extend({ app: z.literal(AppConnection.AzureADCS) }); + +export const AzureADCSConnectionSchema = BaseAzureADCSConnectionSchema.extend({ + method: z.literal(AzureADCSConnectionMethod.UsernamePassword), + credentials: AzureADCSUsernamePasswordCredentialsSchema +}); + +export const SanitizedAzureADCSConnectionSchema = z.discriminatedUnion("method", [ + BaseAzureADCSConnectionSchema.extend({ + method: z.literal(AzureADCSConnectionMethod.UsernamePassword), + credentials: AzureADCSUsernamePasswordCredentialsSchema.pick({ + username: true, + adcsUrl: true, + sslRejectUnauthorized: true, + sslCertificate: true + }) + }) +]); + +export const ValidateAzureADCSConnectionCredentialsSchema = z.discriminatedUnion("method", [ + z.object({ + method: z + .literal(AzureADCSConnectionMethod.UsernamePassword) + .describe(AppConnections.CREATE(AppConnection.AzureADCS).method), + credentials: AzureADCSUsernamePasswordCredentialsSchema.describe( + AppConnections.CREATE(AppConnection.AzureADCS).credentials + ) + }) +]); + +export const CreateAzureADCSConnectionSchema = ValidateAzureADCSConnectionCredentialsSchema.and( + GenericCreateAppConnectionFieldsSchema(AppConnection.AzureADCS) +); + +export const UpdateAzureADCSConnectionSchema = z + .object({ + credentials: AzureADCSUsernamePasswordCredentialsSchema.optional().describe( + AppConnections.UPDATE(AppConnection.AzureADCS).credentials + ) + }) + .and(GenericUpdateAppConnectionFieldsSchema(AppConnection.AzureADCS)); + +export const AzureADCSConnectionListItemSchema = z.object({ + name: z.literal("Azure ADCS"), + app: z.literal(AppConnection.AzureADCS), + methods: z.nativeEnum(AzureADCSConnectionMethod).array() +}); diff --git a/backend/src/services/app-connection/azure-adcs/azure-adcs-connection-types.ts b/backend/src/services/app-connection/azure-adcs/azure-adcs-connection-types.ts new file mode 100644 index 000000000..051d13c0d --- /dev/null +++ b/backend/src/services/app-connection/azure-adcs/azure-adcs-connection-types.ts @@ -0,0 +1,23 @@ +import z from "zod"; + +import { DiscriminativePick } from "@app/lib/types"; + +import { AppConnection } from "../app-connection-enums"; +import { + AzureADCSConnectionSchema, + CreateAzureADCSConnectionSchema, + ValidateAzureADCSConnectionCredentialsSchema +} from "./azure-adcs-connection-schemas"; + +export type TAzureADCSConnection = z.infer; + +export type TAzureADCSConnectionInput = z.infer & { + app: AppConnection.AzureADCS; +}; + +export type TValidateAzureADCSConnectionCredentialsSchema = typeof ValidateAzureADCSConnectionCredentialsSchema; + +export type TAzureADCSConnectionConfig = DiscriminativePick< + TAzureADCSConnectionInput, + "method" | "app" | "credentials" +>; diff --git a/backend/src/services/app-connection/azure-adcs/index.ts b/backend/src/services/app-connection/azure-adcs/index.ts new file mode 100644 index 000000000..3cefd9090 --- /dev/null +++ b/backend/src/services/app-connection/azure-adcs/index.ts @@ -0,0 +1,4 @@ +export * from "./azure-adcs-connection-enums"; +export * from "./azure-adcs-connection-fns"; +export * from "./azure-adcs-connection-schemas"; +export * from "./azure-adcs-connection-types"; diff --git a/backend/src/services/app-connection/checkly/checkly-connection-public-client.ts b/backend/src/services/app-connection/checkly/checkly-connection-public-client.ts index 4e5db231f..35279d007 100644 --- a/backend/src/services/app-connection/checkly/checkly-connection-public-client.ts +++ b/backend/src/services/app-connection/checkly/checkly-connection-public-client.ts @@ -4,6 +4,7 @@ import { AxiosInstance, AxiosRequestConfig, AxiosResponse, HttpStatusCode, isAxi import { createRequestClient } from "@app/lib/config/request"; import { IntegrationUrls } from "@app/services/integration-auth/integration-list"; +import { SecretSyncError } from "@app/services/secret-sync/secret-sync-errors"; import { ChecklyConnectionMethod } from "./checkly-connection-constants"; import { TChecklyAccount, TChecklyConnectionConfig, TChecklyVariable } from "./checkly-connection-types"; @@ -181,6 +182,122 @@ class ChecklyPublicClient { return res; } + + async getCheckGroups(connection: TChecklyConnectionConfig, accountId: string, limit = 50, page = 1) { + const res = await this.send<{ id: number; name: string }[]>(connection, { + accountId, + method: "GET", + url: `/v1/check-groups`, + params: { limit, page } + }); + + return res?.map((group) => ({ + id: group.id.toString(), + name: group.name + })); + } + + async getCheckGroup(connection: TChecklyConnectionConfig, accountId: string, groupId: string) { + try { + type ChecklyGroupResponse = { + id: number; + name: string; + environmentVariables: Array<{ + key: string; + value: string; + locked: boolean; + }>; + }; + + const res = await this.send(connection, { + accountId, + method: "GET", + url: `/v1/check-groups/${groupId}` + }); + + if (!res) return null; + + return { + id: res.id.toString(), + name: res.name, + environmentVariables: res.environmentVariables + }; + } catch (error) { + if (isAxiosError(error) && error.response?.status === HttpStatusCode.NotFound) { + return null; + } + throw error; + } + } + + async updateCheckGroupEnvironmentVariables( + connection: TChecklyConnectionConfig, + accountId: string, + groupId: string, + environmentVariables: Array<{ key: string; value: string; locked?: boolean }> + ) { + if (environmentVariables.length > 50) { + throw new SecretSyncError({ + message: "Checkly does not support syncing more than 50 variables to Check Group", + shouldRetry: false + }); + } + + const apiVariables = environmentVariables.map((v) => ({ + key: v.key, + value: v.value, + locked: v.locked ?? false, + secret: true + })); + + const group = await this.getCheckGroup(connection, accountId, groupId); + + await this.send(connection, { + accountId, + method: "PUT", + url: `/v2/check-groups/${groupId}`, + data: { name: group?.name, environmentVariables: apiVariables } + }); + + return this.getCheckGroup(connection, accountId, groupId); + } + + async getCheckGroupEnvironmentVariables(connection: TChecklyConnectionConfig, accountId: string, groupId: string) { + const group = await this.getCheckGroup(connection, accountId, groupId); + return group?.environmentVariables || []; + } + + async upsertCheckGroupEnvironmentVariables( + connection: TChecklyConnectionConfig, + accountId: string, + groupId: string, + variables: Array<{ key: string; value: string; locked?: boolean }> + ) { + const existingVars = await this.getCheckGroupEnvironmentVariables(connection, accountId, groupId); + const varMap = new Map(existingVars.map((v) => [v.key, v])); + + for (const newVar of variables) { + varMap.set(newVar.key, { + key: newVar.key, + value: newVar.value, + locked: newVar.locked ?? false + }); + } + + return this.updateCheckGroupEnvironmentVariables(connection, accountId, groupId, Array.from(varMap.values())); + } + + async deleteCheckGroupEnvironmentVariable( + connection: TChecklyConnectionConfig, + accountId: string, + groupId: string, + variableKey: string + ) { + const existingVars = await this.getCheckGroupEnvironmentVariables(connection, accountId, groupId); + const filteredVars = existingVars.filter((v) => v.key !== variableKey); + + return this.updateCheckGroupEnvironmentVariables(connection, accountId, groupId, filteredVars); + } } export const ChecklyPublicAPI = new ChecklyPublicClient(); diff --git a/backend/src/services/app-connection/checkly/checkly-connection-service.ts b/backend/src/services/app-connection/checkly/checkly-connection-service.ts index c3598320f..1312b4590 100644 --- a/backend/src/services/app-connection/checkly/checkly-connection-service.ts +++ b/backend/src/services/app-connection/checkly/checkly-connection-service.ts @@ -24,7 +24,19 @@ export const checklyConnectionService = (getAppConnection: TGetAppConnectionFunc } }; + const listGroups = async (connectionId: string, accountId: string, actor: OrgServiceActor) => { + const appConnection = await getAppConnection(AppConnection.Checkly, connectionId, actor); + try { + const groups = await ChecklyPublicAPI.getCheckGroups(appConnection, accountId); + return groups!; + } catch (error) { + logger.error(error, "Failed to list accounts on Checkly"); + return []; + } + }; + return { - listAccounts + listAccounts, + listGroups }; }; diff --git a/backend/src/services/app-connection/checkly/checkly-connection-types.ts b/backend/src/services/app-connection/checkly/checkly-connection-types.ts index e8bb242ba..195d319d6 100644 --- a/backend/src/services/app-connection/checkly/checkly-connection-types.ts +++ b/backend/src/services/app-connection/checkly/checkly-connection-types.ts @@ -33,3 +33,15 @@ export type TChecklyAccount = { name: string; runtimeId: string; }; + +export type TChecklyGroupEnvironmentVariable = { + key: string; + value: string; + locked: boolean; +}; + +export type TChecklyGroup = { + id: string; + name: string; + environmentVariables?: TChecklyGroupEnvironmentVariable[]; +}; diff --git a/backend/src/services/certificate-authority/azure-ad-cs/azure-ad-cs-certificate-authority-fns.ts b/backend/src/services/certificate-authority/azure-ad-cs/azure-ad-cs-certificate-authority-fns.ts new file mode 100644 index 000000000..0e2619a27 --- /dev/null +++ b/backend/src/services/certificate-authority/azure-ad-cs/azure-ad-cs-certificate-authority-fns.ts @@ -0,0 +1,1172 @@ +/* eslint-disable no-await-in-loop */ +import * as x509 from "@peculiar/x509"; +import RE2 from "re2"; + +import { TableName } from "@app/db/schemas"; +import { crypto } from "@app/lib/crypto/cryptography"; +import { BadRequestError, NotFoundError } from "@app/lib/errors"; +import { ms } from "@app/lib/ms"; +import { OrgServiceActor } from "@app/lib/types"; +import { TAppConnectionDALFactory } from "@app/services/app-connection/app-connection-dal"; +import { AppConnection } from "@app/services/app-connection/app-connection-enums"; +import { TAppConnectionServiceFactory } from "@app/services/app-connection/app-connection-service"; +import { + createAdcsHttpClient, + getAzureADCSConnectionCredentials +} from "@app/services/app-connection/azure-adcs/azure-adcs-connection-fns"; +import { TCertificateBodyDALFactory } from "@app/services/certificate/certificate-body-dal"; +import { TCertificateDALFactory } from "@app/services/certificate/certificate-dal"; +import { TCertificateSecretDALFactory } from "@app/services/certificate/certificate-secret-dal"; +import { + CertExtendedKeyUsage, + CertKeyAlgorithm, + CertKeyUsage, + CertStatus +} from "@app/services/certificate/certificate-types"; +import { TKmsServiceFactory } from "@app/services/kms/kms-service"; +import { TPkiSubscriberDALFactory } from "@app/services/pki-subscriber/pki-subscriber-dal"; +import { TPkiSubscriberProperties } from "@app/services/pki-subscriber/pki-subscriber-types"; +import { TProjectDALFactory } from "@app/services/project/project-dal"; +import { getProjectKmsCertificateKeyId } from "@app/services/project/project-fns"; + +import { TCertificateAuthorityDALFactory } from "../certificate-authority-dal"; +import { CaStatus, CaType } from "../certificate-authority-enums"; +import { keyAlgorithmToAlgCfg } from "../certificate-authority-fns"; +import { TExternalCertificateAuthorityDALFactory } from "../external-certificate-authority-dal"; +import { + TAzureAdCsCertificateAuthority, + TCreateAzureAdCsCertificateAuthorityDTO, + TUpdateAzureAdCsCertificateAuthorityDTO +} from "./azure-ad-cs-certificate-authority-types"; + +type TAzureAdCsCertificateAuthorityFnsDeps = { + appConnectionDAL: Pick; + appConnectionService: Pick; + certificateAuthorityDAL: Pick< + TCertificateAuthorityDALFactory, + "create" | "transaction" | "findByIdWithAssociatedCa" | "updateById" | "findWithAssociatedCa" + >; + externalCertificateAuthorityDAL: Pick; + certificateDAL: Pick; + certificateBodyDAL: Pick; + certificateSecretDAL: Pick; + kmsService: Pick< + TKmsServiceFactory, + "encryptWithKmsKey" | "generateKmsKey" | "createCipherPairWithDataKey" | "decryptWithKmsKey" + >; + pkiSubscriberDAL: Pick; + projectDAL: Pick; +}; + +type AzureCertificateRequest = { + csr: string; + template: string; + attributes?: Record; +}; + +type AzureCertificateResponse = { + certificateId: string; + certificate: string; + certificateChain?: string; + status: "issued" | "pending" | "denied"; + disposition?: string; +}; + +const buildSubjectDN = (commonName: string, properties?: TPkiSubscriberProperties): string => { + // Validate and sanitize common name - it's required and cannot be empty + if (!commonName || !commonName.trim()) { + throw new BadRequestError({ message: "Common Name is required and cannot be empty" }); + } + + const trimmedCN = commonName.trim(); + + const invalidCharsRegex = new RE2("[,=+<>#;\\\\\\]]"); + if (invalidCharsRegex.test(trimmedCN)) { + throw new BadRequestError({ + message: "Common Name contains invalid characters: , = + < > # ; \\ ]" + }); + } + + let subject = `CN=${trimmedCN}`; + + const validateComponent = (value: string | undefined, componentName: string): string | null => { + if (!value || typeof value !== "string") return null; + const trimmed = value.trim(); + if (!trimmed) return null; + + const componentInvalidCharsRegex = new RE2('[,=+<>#;\\\\"\\/\\r\\n\\t]'); + if (componentInvalidCharsRegex.test(trimmed)) { + throw new BadRequestError({ + message: `${componentName} contains invalid characters: , = + < > # ; \\ " / \\r \\n \\t` + }); + } + + const problematicCharsRegex = new RE2("^[\\\\s\\\\-_.]+|[\\\\s\\\\-_.]+$"); + if (problematicCharsRegex.test(trimmed)) { + throw new BadRequestError({ + message: `${componentName} cannot start or end with spaces, hyphens, underscores, or periods` + }); + } + + return trimmed; + }; + + const emailAddress = validateComponent(properties?.emailAddress, "Email Address"); + if (emailAddress) { + const emailRegex = new RE2(/^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/); + if (!emailRegex.test(emailAddress) || emailAddress.length <= 5 || emailAddress.length >= 64) { + throw new BadRequestError({ + message: "Email Address must be a valid email format between 5 and 64 characters" + }); + } + subject += `,E=${emailAddress}`; + } + + const organizationalUnit = validateComponent(properties?.organizationalUnit, "Organizational Unit"); + if (organizationalUnit) { + if (organizationalUnit.length > 64) { + throw new BadRequestError({ + message: "Organizational Unit cannot exceed 64 characters" + }); + } + subject += `,OU=${organizationalUnit}`; + } + + const organization = validateComponent(properties?.organization, "Organization"); + if (organization) { + if (organization.length > 64) { + throw new BadRequestError({ + message: "Organization cannot exceed 64 characters" + }); + } + subject += `,O=${organization}`; + } + + const locality = validateComponent(properties?.locality, "Locality"); + if (locality) { + if (locality.length > 64) { + throw new BadRequestError({ + message: "Locality cannot exceed 64 characters" + }); + } + subject += `,L=${locality}`; + } + + const state = validateComponent(properties?.state, "State"); + if (state) { + if (state.length > 64) { + throw new BadRequestError({ + message: "State cannot exceed 64 characters" + }); + } + subject += `,ST=${state}`; + } + + const country = validateComponent(properties?.country, "Country"); + if (country) { + // Country code must be exactly 2 uppercase letters + const countryCode = country.toUpperCase(); + const invalidCountryRegex = new RE2("[^A-Z]"); + if (invalidCountryRegex.test(countryCode)) { + throw new BadRequestError({ + message: "Country must contain only uppercase letters" + }); + } + if (countryCode.length !== 2) { + throw new BadRequestError({ + message: "Country must be exactly 2 characters" + }); + } + subject += `,C=${countryCode}`; + } + + return subject; +}; + +export const castDbEntryToAzureAdCsCertificateAuthority = ( + ca: Awaited> +): TAzureAdCsCertificateAuthority & { credentials: unknown } => { + if (!ca.externalCa?.id) { + throw new BadRequestError({ message: "Malformed Azure AD Certificate Service certificate authority" }); + } + + if (!ca.externalCa.dnsAppConnectionId) { + throw new BadRequestError({ + message: "Azure ADCS connection ID is missing from certificate authority configuration" + }); + } + + return { + id: ca.id, + type: CaType.AZURE_AD_CS, + enableDirectIssuance: ca.enableDirectIssuance, + name: ca.name, + projectId: ca.projectId, + credentials: ca.externalCa.credentials, + configuration: { + azureAdcsConnectionId: ca.externalCa.dnsAppConnectionId + }, + status: ca.status as CaStatus + }; +}; + +const submitCertificateRequest = async ( + credentials: { username: string; password: string; sslRejectUnauthorized?: boolean; sslCertificate?: string }, + caServiceUrl: string, + certificateRequest: AzureCertificateRequest +): Promise => { + try { + const adcsClient = createAdcsHttpClient( + credentials.username, + credentials.password, + caServiceUrl, + credentials.sslRejectUnauthorized ?? true, + credentials.sslCertificate + ); + + // Clean CSR by removing headers and newlines for ADCS submission + const cleanCsr = certificateRequest.csr + .replace(new RE2("-----BEGIN CERTIFICATE REQUEST-----", "g"), "") + .replace(new RE2("-----END CERTIFICATE REQUEST-----", "g"), "") + .replace(new RE2("\\\\r?\\\\n", "g"), ""); + + // Build certificate attributes including template and validity period + const certAttribParts: string[] = []; + + // Add template - this is required + if (certificateRequest.template && certificateRequest.template.trim()) { + certAttribParts.push(`CertificateTemplate:${certificateRequest.template.trim()}`); + } + + // Add validity period if specified by the user + if (certificateRequest.attributes?.validityPeriod) { + try { + const ttlMs = ms(certificateRequest.attributes.validityPeriod); + const expirationDate = new Date(Date.now() + ttlMs); + + // Format expiration date in RFC 2616 format for ADCS + const rfc2616Date = expirationDate.toUTCString(); + + // Add ExpirationDate attribute (requires EDITF_ATTRIBUTEENDDATE flag on CA) + certAttribParts.push(`ExpirationDate:${rfc2616Date}`); + } catch (error) { + throw new BadRequestError({ + message: "Invalid validity period format" + }); + } + } + + // Join all attributes with proper CRLF ending + const certAttrib = certAttribParts.length > 0 ? `${certAttribParts.join("\r\n")}\r\n` : ""; + + // Prepare form data for ADCS web interface - these parameters are required by Microsoft ADCS + // Mode: "newreq" indicates a new certificate request + // CertRequest: the base64-encoded CSR without headers + // CertAttrib: certificate template and other attributes in CRLF format + // FriendlyType: display name for the certificate type + // TargetStoreFlags: certificate store flags (0 = default) + // SaveCert: "yes" to save the certificate to the server + const formData = new URLSearchParams({ + Mode: "newreq", + CertRequest: cleanCsr, + CertAttrib: certAttrib, + FriendlyType: "Saved-Request Certificate", + TargetStoreFlags: "0", + SaveCert: "yes" + }); + + const response = await adcsClient.post("/certsrv/certfnsh.asp", formData.toString()); + + const responseText = response.data; + + // Parse the HTML response to extract certificate information + let requestId: string | undefined; + let status: "issued" | "pending" | "denied" = "pending"; + let certificate = ""; + + // Look for request ID in various formats + const requestIdMatches = [ + new RE2("reqid[=:](\\d+)", "i"), + new RE2("request\\s+id[:\\s]+(\\d+)", "i"), + new RE2("certificate\\s+request\\s+(\\d+)", "i"), + new RE2("\\breqid=(\\d+)\\b", "i"), + new RE2("requestid[:\\s]*(\\d+)", "i") + ]; + + for (const regex of requestIdMatches) { + const match = responseText.match(regex); + if (match) { + [, requestId] = match; + break; + } + } + + // Check for immediate certificate issuance + const certMatch = responseText.match(new RE2("-----BEGIN CERTIFICATE-----[\\s\\S]*?-----END CERTIFICATE-----")); + if (certMatch) { + // Clean up the certificate format + certificate = certMatch[0].replace(new RE2("\\\\r\\\\n", "g"), "\n").replace(new RE2("\\\\r", "g"), "\n").trim(); + + // Validate the certificate format before using it + try { + status = "issued"; + } catch (error) { + certificate = ""; + status = "pending"; + } + } + + // Check disposition message for status + if (responseText.includes("taken under submission") || responseText.includes("pending")) { + status = "pending"; + } else if (responseText.includes("denied") || responseText.includes("rejected")) { + status = "denied"; + } else if (responseText.includes("issued") || certificate) { + status = "issued"; + } + + // If we couldn't parse a request ID and don't have a certificate, something went wrong + if (!requestId && !certificate) { + // Check for specific error types first + let errorMessage = "Unknown error occurred"; + + // ASN.1 parsing errors (CSR format issues) + if ( + responseText.includes("ASN1") || + responseText.includes("Error Parsing Request") || + responseText.includes("unexpected end of data") || + responseText.includes("bad tag value met") || + responseText.includes("0x80093102") + ) { + const asn1Patterns = [ + new RE2("Error Parsing Request\\s+ASN1[^.]*\\.?", "i"), + new RE2("ASN1[^.]*\\.?", "i"), + new RE2("Error Parsing Request[^.]*\\.?", "i") + ]; + + for (const pattern of asn1Patterns) { + const match = responseText.match(pattern); + if (match) { + errorMessage = match[0].trim(); + break; + } + } + + errorMessage = `Certificate request format error: ${errorMessage}. This indicates the CSR (Certificate Signing Request) format is incompatible with ADCS.`; + } + // Template permission errors (policy denials) + else if ( + responseText.includes("Denied by Policy Module") || + responseText.includes("0x80094800") || + responseText.includes("template that is not supported") + ) { + const policyMatch = + responseText.match(new RE2('Denied by Policy Module[^"]*"([^"]*)')) || + responseText.match(new RE2('The disposition message is "([^"]*)')); + + if (policyMatch) { + errorMessage = policyMatch[1].trim(); + } + + errorMessage = `Certificate template permission error: ${errorMessage}. Verify that the connection account has enrollment permissions for the selected certificate template.`; + } + // General error extraction + else { + const errorPatterns = [ + new RE2('The disposition message is "([^"]*)"', "i"), + new RE2('Denied by Policy Module[^"]*"([^"]*)"', "i"), + new RE2("]*class[^>]*error[^>]*>(.*?)<\\/p>", "i"), + new RE2("]*class[^>]*error[^>]*>(.*?)<\\/div>", "i"), + new RE2("]*class[^>]*error[^>]*>(.*?)<\\/span>", "i"), + new RE2("error[^<]*:([^<]*)", "i"), + new RE2("denied[^<]*:([^<]*)", "i"), + new RE2("The\\s+request\\s+contains\\s+no\\s+certificate\\s+template\\s+information", "i"), + new RE2("The\\s+template\\s+is\\s+missing", "i") + ]; + + // Try each pattern to find the error message + for (const pattern of errorPatterns) { + const match = responseText.match(pattern); + if (match) { + errorMessage = match[1] ? match[1].trim() : match[0].trim(); + break; + } + } + } + + // Clean up HTML entities and tags from error message + errorMessage = errorMessage + .replace(new RE2(""", "g"), '"') + .replace(new RE2("<", "g"), "<") + .replace(new RE2(">", "g"), ">") + .replace(new RE2("&", "g"), "&") + .replace(new RE2("<[^>]*>", "g"), "") // Remove HTML tags + .replace(new RE2("\\r\\n", "g"), " ") + .replace(new RE2("\\n", "g"), " ") + .replace(new RE2("\\r", "g"), " ") + .replace(new RE2("\\s+", "g"), " ") + .replace(new RE2('\\s*[".]*\\s*[".]*\\s*$'), "") // Remove trailing quotes and periods + .replace(new RE2('^\\s*[".]*\\s*'), "") // Remove leading quotes and periods + .trim(); + + // Handle specific Microsoft ADCS OID-related errors + if ( + responseText.includes("Cannot get OID for name type") || + responseText.includes("OID for name type") || + responseText.includes("name type ''") + ) { + errorMessage = + "Certificate template OID resolution error. This may be caused by: " + + "1) Certificate template name doesn't exist or is not published, " + + "2) ADCS OID cache needs refresh, or " + + "3) Template permissions are insufficient. " + + "Please verify the template exists, is published, and you have enrollment permissions."; + } else if (responseText.includes("template") && responseText.includes("not found")) { + errorMessage = `Certificate template not found. Please verify the template name '${certificateRequest.template}' exists and is published on the ADCS server.`; + } else if (responseText.includes("access denied") || responseText.includes("permission")) { + errorMessage = "Access denied. You may not have permission to request certificates with this template."; + } else if (responseText.includes("subject") || responseText.includes("DN")) { + errorMessage = + "Invalid subject DN format. Please check that all subject field values contain only valid characters."; + } else if (responseText.includes("Computer") && responseText.includes("Machine")) { + errorMessage = + "Computer/Machine template error. These templates may require domain-joined machines or specific subject name formats."; + } else if ( + certificateRequest.template.toLowerCase().includes("computer") || + certificateRequest.template.toLowerCase().includes("machine") + ) { + errorMessage = + `Template '${certificateRequest.template}' failed because it requires domain authentication and automatic enrollment. ` + + `Computer/Machine templates are designed for domain-joined computers, not manual requests. ` + + `Solutions: 1) Use 'User' or 'WebServer' templates instead, 2) Create a custom template based on ${certificateRequest.template} but configured for manual enrollment, ` + + `3) Ask your ADCS administrator to modify the template to allow manual enrollment and 'Supply subject in request'.`; + } else if (errorMessage.length < 10 || errorMessage === certificateRequest.template) { + errorMessage = `Certificate request failed with template '${certificateRequest.template}'. This may indicate a template configuration issue, permission problem, or invalid subject information.`; + } + + throw new BadRequestError({ + message: `Certificate request failed: ${errorMessage}` + }); + } + + return { + certificateId: requestId || "immediate", + certificate, + certificateChain: "", + status + }; + } catch (error) { + if (error instanceof BadRequestError) { + throw error; + } + + if (error instanceof Error) { + let errorMessage = `Failed to submit certificate request to ADCS: ${error.message}`; + + if (error.message.includes("401") || error.message.includes("Unauthorized")) { + errorMessage = "Authentication failed. Please verify your username and password are correct."; + } else if (error.message.includes("403") || error.message.includes("Forbidden")) { + errorMessage = "Access denied. You may not have permission to request certificates with this template."; + } else if (error.message.includes("404") || error.message.includes("Not Found")) { + errorMessage = "ADCS endpoint not found. Please verify the ADCS URL is correct."; + } else if (error.message.includes("ENOTFOUND")) { + errorMessage = "Cannot connect to ADCS server. Please verify the server URL and network connectivity."; + } else if (error.message.includes("ETIMEDOUT")) { + errorMessage = "Request timed out. The ADCS server may be overloaded or unreachable."; + } + + throw new BadRequestError({ message: errorMessage }); + } + + throw new BadRequestError({ + message: `Failed to submit certificate request to ADCS: ${error instanceof Error ? error.message : "Unknown error"}` + }); + } +}; + +const retrieveCertificate = async ( + credentials: { username: string; password: string; sslRejectUnauthorized?: boolean; sslCertificate?: string }, + caServiceUrl: string, + certificateId: string +): Promise => { + try { + const adcsClient = createAdcsHttpClient( + credentials.username, + credentials.password, + caServiceUrl, + credentials.sslRejectUnauthorized ?? true, + credentials.sslCertificate + ); + + const response = await adcsClient.get(`/certsrv/certnew.cer?ReqID=${certificateId}&Enc=b64`, { + Accept: "application/pkix-cert,application/x-x509-ca-cert,application/octet-stream,*/*" + }); + + const certData = response.data; + + // Check if the response contains HTML indicating the certificate is not ready + if (certData.includes("") || certData.includes("taken under submission") || certData.includes("pending")) { + throw new BadRequestError({ + message: `Certificate with ID ${certificateId} is still pending approval or processing` + }); + } + + // If certificate is already in PEM format, return as-is + if (certData.includes("-----BEGIN CERTIFICATE-----")) { + return certData.trim(); + } + + // Handle base64-encoded certificate data + let cleanCertData = certData.trim(); + + // Remove any HTML artifacts or unwanted characters, keeping only base64 + cleanCertData = cleanCertData.replace(new RE2("[^A-Za-z0-9+/=\\s]", "g"), "").replace(new RE2("\\s", "g"), ""); + + if (cleanCertData.length < 100) { + throw new BadRequestError({ + message: `Certificate data appears invalid or too short (${cleanCertData.length} characters). The certificate may still be pending.` + }); + } + + // Format as proper PEM certificate with 64 character lines + const formattedCert = cleanCertData.replace(new RE2("(.{64})", "g"), "$1\n").trim(); + const pemCert = `-----BEGIN CERTIFICATE-----\n${formattedCert}\n-----END CERTIFICATE-----`; + + // Validate the constructed PEM before returning + try { + // Test parse to ensure it's valid + const testCert = new x509.X509Certificate(pemCert); + // If we get here, the certificate is valid + if (testCert) { + return pemCert; + } + throw new Error("Certificate validation failed"); + } catch (error) { + throw new BadRequestError({ + message: `Failed to format certificate data from ADCS into valid PEM format: ${error instanceof Error ? error.message : "Unknown error"}` + }); + } + } catch (error) { + if (error instanceof BadRequestError) { + throw error; + } + + if (error instanceof Error) { + let errorMessage = `Failed to retrieve certificate with ID ${certificateId} from ADCS: ${error.message}`; + + if (error.message.includes("404") || error.message.includes("Not Found")) { + errorMessage = `Certificate with ID ${certificateId} not found. It may have been rejected or the ID is invalid.`; + } else if (error.message.includes("401") || error.message.includes("Unauthorized")) { + errorMessage = "Authentication failed while retrieving certificate. Please verify your credentials."; + } else if (error.message.includes("403") || error.message.includes("Forbidden")) { + errorMessage = + "Access denied while retrieving certificate. You may not have permission to access this certificate."; + } else if (error.message.includes("ETIMEDOUT")) { + errorMessage = "Timeout while retrieving certificate. The ADCS server may be overloaded."; + } + + throw new BadRequestError({ message: errorMessage }); + } + + throw new BadRequestError({ + message: `Failed to retrieve certificate with ID ${certificateId} from ADCS: ${error instanceof Error ? error.message : "Unknown error"}` + }); + } +}; + +export const AzureAdCsCertificateAuthorityFns = ({ + appConnectionDAL, + appConnectionService, + certificateAuthorityDAL, + externalCertificateAuthorityDAL, + certificateDAL, + certificateBodyDAL, + certificateSecretDAL, + kmsService, + projectDAL, + pkiSubscriberDAL +}: TAzureAdCsCertificateAuthorityFnsDeps) => { + const createCertificateAuthority = async ({ + name, + projectId, + configuration, + enableDirectIssuance, + actor, + status + }: { + status: CaStatus; + name: string; + projectId: string; + configuration: TCreateAzureAdCsCertificateAuthorityDTO["configuration"]; + enableDirectIssuance: boolean; + actor: OrgServiceActor; + }) => { + // Azure ADCS does not support direct issuance - enforce this restriction + if (enableDirectIssuance) { + throw new BadRequestError({ + message: "Azure ADCS Certificate Authorities do not support direct issuance" + }); + } + + const { azureAdcsConnectionId } = configuration; + const appConnection = await appConnectionDAL.findById(azureAdcsConnectionId); + + if (!appConnection) { + throw new NotFoundError({ message: `App connection with ID '${azureAdcsConnectionId}' not found` }); + } + + if (appConnection.app !== AppConnection.AzureADCS) { + throw new BadRequestError({ + message: `App connection with ID '${azureAdcsConnectionId}' is not an Azure ADCS connection` + }); + } + + await appConnectionService.connectAppConnectionById( + appConnection.app as AppConnection, + azureAdcsConnectionId, + actor + ); + + const caEntity = await certificateAuthorityDAL.transaction(async (tx) => { + try { + const ca = await certificateAuthorityDAL.create( + { + projectId, + enableDirectIssuance: false, // Always false for Azure ADCS CAs + name, + status + }, + tx + ); + + await externalCertificateAuthorityDAL.create( + { + caId: ca.id, + dnsAppConnectionId: azureAdcsConnectionId, + type: CaType.AZURE_AD_CS, + configuration: {} + }, + tx + ); + + return await certificateAuthorityDAL.findByIdWithAssociatedCa(ca.id, tx); + } catch (error) { + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-explicit-any + if ((error as any)?.error?.code === "23505") { + throw new BadRequestError({ + message: "Certificate authority with the same name already exists in your project" + }); + } + throw error; + } + }); + + if (!caEntity.externalCa?.id) { + throw new BadRequestError({ message: "Failed to create external certificate authority" }); + } + + return castDbEntryToAzureAdCsCertificateAuthority(caEntity); + }; + + const updateCertificateAuthority = async ({ + id, + status, + configuration, + enableDirectIssuance, + actor, + name + }: { + id: string; + status?: CaStatus; + configuration: TUpdateAzureAdCsCertificateAuthorityDTO["configuration"]; + enableDirectIssuance?: boolean; + actor: OrgServiceActor; + name?: string; + }) => { + // Azure ADCS does not support direct issuance - enforce this restriction + if (enableDirectIssuance) { + throw new BadRequestError({ + message: "Azure ADCS Certificate Authorities do not support direct issuance" + }); + } + + const updatedCa = await certificateAuthorityDAL.transaction(async (tx) => { + if (configuration) { + const { azureAdcsConnectionId } = configuration; + const appConnection = await appConnectionDAL.findById(azureAdcsConnectionId); + + if (!appConnection) { + throw new NotFoundError({ message: `App connection with ID '${azureAdcsConnectionId}' not found` }); + } + + if (appConnection.app !== AppConnection.AzureADCS) { + throw new BadRequestError({ + message: `App connection with ID '${azureAdcsConnectionId}' is not an Azure ADCS connection` + }); + } + + await appConnectionService.connectAppConnectionById( + appConnection.app as AppConnection, + azureAdcsConnectionId, + actor + ); + + await externalCertificateAuthorityDAL.update( + { + caId: id, + type: CaType.AZURE_AD_CS + }, + { + appConnectionId: azureAdcsConnectionId, + configuration: {} + }, + tx + ); + } + + if (name || status || enableDirectIssuance !== undefined) { + await certificateAuthorityDAL.updateById( + id, + { + name, + status, + enableDirectIssuance: false // Always false for Azure ADCS CAs + }, + tx + ); + } + + return certificateAuthorityDAL.findByIdWithAssociatedCa(id, tx); + }); + + if (!updatedCa.externalCa?.id) { + throw new BadRequestError({ message: "Failed to update external certificate authority" }); + } + + return castDbEntryToAzureAdCsCertificateAuthority(updatedCa); + }; + + const listCertificateAuthorities = async ({ projectId }: { projectId: string }) => { + const cas = await certificateAuthorityDAL.findWithAssociatedCa({ + [`${TableName.CertificateAuthority}.projectId` as "projectId"]: projectId, + [`${TableName.ExternalCertificateAuthority}.type` as "type"]: CaType.AZURE_AD_CS + }); + + return cas.map(castDbEntryToAzureAdCsCertificateAuthority); + }; + + const orderSubscriberCertificate = async (subscriberId: string) => { + const subscriber = await pkiSubscriberDAL.findById(subscriberId); + if (!subscriber.caId) { + throw new BadRequestError({ message: "Subscriber does not have a CA" }); + } + + const ca = await certificateAuthorityDAL.findByIdWithAssociatedCa(subscriber.caId); + if (!ca.externalCa || ca.externalCa.type !== CaType.AZURE_AD_CS) { + throw new BadRequestError({ message: "CA is not an Azure AD Certificate Service CA" }); + } + + const azureCa = castDbEntryToAzureAdCsCertificateAuthority(ca); + if (azureCa.status !== CaStatus.ACTIVE) { + throw new BadRequestError({ message: "CA is disabled" }); + } + + const certificateManagerKmsId = await getProjectKmsCertificateKeyId({ + projectId: ca.projectId, + projectDAL, + kmsService + }); + + const kmsEncryptor = await kmsService.encryptWithKmsKey({ + kmsId: certificateManagerKmsId + }); + + // Get credentials from the Azure ADCS connection + const { username, password, adcsUrl, sslRejectUnauthorized, sslCertificate } = + await getAzureADCSConnectionCredentials( + azureCa.configuration.azureAdcsConnectionId, + appConnectionDAL, + kmsService + ); + + const credentials: { + username: string; + password: string; + sslRejectUnauthorized?: boolean; + sslCertificate?: string; + } = { + username, + password, + sslRejectUnauthorized, + sslCertificate + }; + + const alg = keyAlgorithmToAlgCfg(CertKeyAlgorithm.RSA_2048); + const leafKeys = await crypto.nativeCrypto.subtle.generateKey(alg, true, ["sign", "verify"]); + const skLeafObj = crypto.nativeCrypto.KeyObject.from(leafKeys.privateKey); + const skLeaf = skLeafObj.export({ format: "pem", type: "pkcs8" }) as string; + + const subjectDN = buildSubjectDN( + subscriber.commonName, + subscriber.properties as TPkiSubscriberProperties | undefined + ); + + const csrObj = await x509.Pkcs10CertificateRequestGenerator.create({ + name: subjectDN, + keys: leafKeys, + signingAlgorithm: alg + }); + + const csrPem = csrObj.toString("pem"); + + const properties = subscriber.properties as TPkiSubscriberProperties | undefined; + const azureTemplateType = properties?.azureTemplateType; + if (!azureTemplateType || typeof azureTemplateType !== "string") { + throw new BadRequestError({ + message: "Subscriber must have an Azure certificate template configured for Azure ADCS CA" + }); + } + + const templateInput = azureTemplateType.trim(); + if (!templateInput || templateInput.length === 0) { + throw new BadRequestError({ + message: "Certificate template name cannot be empty" + }); + } + + const templateValue = templateInput; + + const certificateRequest: AzureCertificateRequest = { + csr: csrPem, + template: templateValue, + attributes: { + subject: buildSubjectDN(subscriber.commonName, subscriber.properties as TPkiSubscriberProperties | undefined), + subjectAlternativeName: subscriber.subjectAlternativeNames.join(","), + ...(subscriber.ttl && { validityPeriod: subscriber.ttl }) + } + }; + + // Add retry logic for OID caching issues + let submissionResponse; + const maxOidRetries = 3; + let oidRetryCount = 0; + + while (oidRetryCount <= maxOidRetries) { + try { + submissionResponse = await submitCertificateRequest(credentials, adcsUrl, certificateRequest); + break; // Success, exit retry loop + } catch (error) { + const isOidError = + error instanceof BadRequestError && + (error.message.includes("OID resolution error") || error.message.includes("Cannot get OID for name type")); + + if (isOidError && oidRetryCount < maxOidRetries) { + oidRetryCount += 1; + + // Wait before retry with increasing delays: 3s, 6s, 9s + const delay = 3000 * oidRetryCount; + await new Promise((resolve) => { + setTimeout(resolve, delay); + }); + // eslint-disable-next-line no-continue + continue; + } + + // If not an OID error or we've exhausted retries, re-throw the error + throw error; + } + } + + if (!submissionResponse) { + throw new BadRequestError({ + message: "Failed to submit certificate request after multiple attempts due to OID resolution issues" + }); + } + + // Handle both "issued" and "pending" status - ADCS may auto-approve or require manual approval + if (submissionResponse.status === "denied") { + throw new BadRequestError({ message: "Certificate request was denied by ADCS" }); + } + + let certificatePem = ""; + + if (submissionResponse.status === "issued" && submissionResponse.certificate) { + certificatePem = submissionResponse.certificate; + } else { + // For pending certificates, implement a retry mechanism with exponential backoff + const maxRetries = 5; + const initialDelay = 2000; // 2 seconds + let retryCount = 0; + let lastError: Error | null = null; + + // eslint-disable-next-line no-await-in-loop + while (retryCount < maxRetries) { + try { + // eslint-disable-next-line no-await-in-loop + certificatePem = await retrieveCertificate(credentials, adcsUrl, submissionResponse.certificateId); + break; // Success, exit retry loop + } catch (error) { + lastError = error as Error; + // eslint-disable-next-line no-plusplus + retryCount++; + + if (retryCount < maxRetries) { + // Wait with exponential backoff: 2s, 4s, 8s, 16s, 32s + const delay = initialDelay * 2 ** (retryCount - 1); + // eslint-disable-next-line no-await-in-loop + await new Promise((resolve) => { + setTimeout(resolve, delay); + }); + } + } + } + + if (retryCount === maxRetries) { + throw new BadRequestError({ + message: `Certificate request submitted with ID ${submissionResponse.certificateId} but failed to retrieve after ${maxRetries} attempts. The certificate may still be pending approval or processing. Last error: ${lastError?.message || "Unknown error"}.` + }); + } + } + + // Ensure we have a valid certificate before proceeding + if (!certificatePem) { + throw new BadRequestError({ + message: "Failed to obtain certificate from ADCS. The certificate may still be pending processing." + }); + } + + // Clean and validate the certificate PEM format + let cleanedCertificatePem = certificatePem.trim(); + + // Ensure proper PEM format + if (!cleanedCertificatePem.includes("-----BEGIN CERTIFICATE-----")) { + throw new BadRequestError({ + message: "Invalid certificate format received from ADCS. Expected PEM format." + }); + } + + // Remove any extra whitespace and ensure proper line endings + cleanedCertificatePem = cleanedCertificatePem + .replace(new RE2("\\r\\n", "g"), "\n") + .replace(new RE2("\\r", "g"), "\n") + .trim(); + + // Validate that we have both begin and end markers + if (!cleanedCertificatePem.includes("-----END CERTIFICATE-----")) { + throw new BadRequestError({ + message: "Invalid certificate format received from ADCS. Missing end marker." + }); + } + + let certObj: x509.X509Certificate; + try { + certObj = new x509.X509Certificate(cleanedCertificatePem); + } catch (error) { + throw new BadRequestError({ + message: `Failed to parse certificate from ADCS: ${error instanceof Error ? error.message : "Unknown error"}. Certificate data may be corrupted.` + }); + } + + const { cipherTextBlob: encryptedCertificate } = await kmsEncryptor({ + plainText: Buffer.from(new Uint8Array(certObj.rawData)) + }); + + const certificateChainPem = submissionResponse.certificateChain || ""; + + const { cipherTextBlob: encryptedCertificateChain } = await kmsEncryptor({ + plainText: Buffer.from(certificateChainPem) + }); + + const { cipherTextBlob: encryptedPrivateKey } = await kmsEncryptor({ + plainText: Buffer.from(skLeaf) + }); + + await certificateDAL.transaction(async (tx) => { + const cert = await certificateDAL.create( + { + caId: ca.id, + pkiSubscriberId: subscriber.id, + status: CertStatus.ACTIVE, + friendlyName: subscriber.commonName, + commonName: subscriber.commonName, + altNames: subscriber.subjectAlternativeNames.join(","), + serialNumber: certObj.serialNumber, + notBefore: certObj.notBefore, + notAfter: certObj.notAfter, + keyUsages: subscriber.keyUsages as CertKeyUsage[], + extendedKeyUsages: subscriber.extendedKeyUsages as CertExtendedKeyUsage[], + projectId: ca.projectId + }, + tx + ); + + await certificateBodyDAL.create( + { + certId: cert.id, + encryptedCertificate, + encryptedCertificateChain + }, + tx + ); + + await certificateSecretDAL.create( + { + certId: cert.id, + encryptedPrivateKey + }, + tx + ); + }); + + return { + certificate: certificatePem, + certificateChain: certificateChainPem, + privateKey: skLeaf, + serialNumber: certObj.serialNumber, + ca: azureCa, + subscriber + }; + }; + + const getTemplates = async ({ caId, projectId }: { caId: string; projectId: string }) => { + const ca = await certificateAuthorityDAL.findByIdWithAssociatedCa(caId); + if (!ca || ca.projectId !== projectId) { + throw new NotFoundError({ message: "Certificate Authority not found" }); + } + + const azureCa = castDbEntryToAzureAdCsCertificateAuthority(ca); + const { azureAdcsConnectionId } = azureCa.configuration; + + const appConnection = await appConnectionDAL.findById(azureAdcsConnectionId); + if (!appConnection) { + throw new NotFoundError({ message: `App connection with ID '${azureAdcsConnectionId}' not found` }); + } + + // Get credentials from the Azure ADCS connection + const { username, password, adcsUrl, sslRejectUnauthorized, sslCertificate } = + await getAzureADCSConnectionCredentials(azureAdcsConnectionId, appConnectionDAL, kmsService); + + const credentials: { + username: string; + password: string; + sslRejectUnauthorized?: boolean; + sslCertificate?: string; + } = { + username, + password, + sslRejectUnauthorized, + sslCertificate + }; + + const client = createAdcsHttpClient( + credentials.username, + credentials.password, + adcsUrl, + credentials.sslRejectUnauthorized ?? true, + credentials.sslCertificate + ); + + try { + // Get available templates from ADCS web interface and filter to only usable ones + let availableTemplates: Array<{ id: string; name: string; description: string }> = []; + + try { + const requestFormResponse = await client.get("/certsrv/certrqxt.asp"); + const responseText = requestFormResponse.data; + + // ADCS returns JavaScript-based template info instead of HTML options + // Look for patterns like: getTemplateStringInfo(CTINFO_INDEX_REALNAME, null) and sRealName assignments + const parsedTemplates: Array<{ id: string; name: string }> = []; + + // Extract template names from JavaScript variable assignments like sRealName="WebServer" + const nameRegex = new RE2('sRealName\\s*=\\s*"([^"]+)"', "gi"); + const names: string[] = []; + let nameMatch = nameRegex.exec(responseText); + while (nameMatch !== null) { + names.push(nameMatch[1]); + nameMatch = nameRegex.exec(responseText); + } + + // Extract template IDs/values from encoded strings or other patterns + const valueRegex = new RE2('CertificateTemplate\\s*\\+\\s*"([^"]+)"', "gi"); + const values: string[] = []; + let valueMatch = valueRegex.exec(responseText); + while (valueMatch !== null) { + values.push(valueMatch[1]); + valueMatch = valueRegex.exec(responseText); + } + + // Also look for any remaining HTML option patterns as fallback + const optionRegex = new RE2(']+value="([^"]*)"[^>]*>([^<]*)', "gi"); + let optionMatch = optionRegex.exec(responseText); + while (optionMatch !== null) { + const templateValue = optionMatch[1].trim(); + const templateDisplayName = optionMatch[2].trim(); + + if ( + templateValue && + templateDisplayName && + templateValue !== "" && + templateValue !== "0" && + !templateDisplayName.toLowerCase().includes("select") + ) { + // Parse the encoded template value format: "E;User;1;1;41;16;-1509949440;0;..." + // The template ID is the second semicolon-delimited value + const templateParts = templateValue.split(";"); + const templateId = templateParts.length >= 2 ? templateParts[1] : templateDisplayName; + + parsedTemplates.push({ + id: templateId, + name: templateDisplayName + }); + } + optionMatch = optionRegex.exec(responseText); + } + + // Combine JavaScript-extracted names with any found values + if (names.length > 0) { + names.forEach((name) => { + parsedTemplates.push({ + id: name, + name + }); + }); + } + + // If we successfully parsed templates, use them; otherwise fall back to common ones + if (parsedTemplates.length > 0) { + availableTemplates = parsedTemplates.map((template) => ({ + id: template.id, + name: template.name, + description: `Certificate template: ${template.name}` + })); + } else { + // Fallback to known working templates + availableTemplates = [ + { id: "User", name: "User", description: "User authentication certificate" }, + { id: "WebServer", name: "Web Server", description: "Web server certificate" } + ]; + } + } catch (requestError) { + // Fallback to known working templates if we can't parse the form + availableTemplates = [ + { id: "User", name: "User", description: "User authentication certificate" }, + { id: "WebServer", name: "Web Server", description: "Web server certificate" } + ]; + } + + // Return all available templates - let user decide what to use + return availableTemplates; + } catch (error) { + throw new BadRequestError({ message: "Failed to retrieve certificate templates from Azure ADCS" }); + } + }; + + return { + createCertificateAuthority, + updateCertificateAuthority, + listCertificateAuthorities, + orderSubscriberCertificate, + getTemplates + }; +}; diff --git a/backend/src/services/certificate-authority/azure-ad-cs/azure-ad-cs-certificate-authority-schemas.ts b/backend/src/services/certificate-authority/azure-ad-cs/azure-ad-cs-certificate-authority-schemas.ts new file mode 100644 index 000000000..2c2dfe484 --- /dev/null +++ b/backend/src/services/certificate-authority/azure-ad-cs/azure-ad-cs-certificate-authority-schemas.ts @@ -0,0 +1,29 @@ +import { z } from "zod"; + +import { CaType } from "../certificate-authority-enums"; +import { + BaseCertificateAuthoritySchema, + GenericCreateCertificateAuthorityFieldsSchema, + GenericUpdateCertificateAuthorityFieldsSchema +} from "../certificate-authority-schemas"; + +export const AzureAdCsCertificateAuthorityConfigurationSchema = z.object({ + azureAdcsConnectionId: z.string().uuid().trim().describe("Azure ADCS Connection ID") +}); + +export const AzureAdCsCertificateAuthoritySchema = BaseCertificateAuthoritySchema.extend({ + type: z.literal(CaType.AZURE_AD_CS), + configuration: AzureAdCsCertificateAuthorityConfigurationSchema +}); + +export const CreateAzureAdCsCertificateAuthoritySchema = GenericCreateCertificateAuthorityFieldsSchema( + CaType.AZURE_AD_CS +).extend({ + configuration: AzureAdCsCertificateAuthorityConfigurationSchema +}); + +export const UpdateAzureAdCsCertificateAuthoritySchema = GenericUpdateCertificateAuthorityFieldsSchema( + CaType.AZURE_AD_CS +).extend({ + configuration: AzureAdCsCertificateAuthorityConfigurationSchema.optional() +}); diff --git a/backend/src/services/certificate-authority/azure-ad-cs/azure-ad-cs-certificate-authority-types.ts b/backend/src/services/certificate-authority/azure-ad-cs/azure-ad-cs-certificate-authority-types.ts new file mode 100644 index 000000000..1c4b3699d --- /dev/null +++ b/backend/src/services/certificate-authority/azure-ad-cs/azure-ad-cs-certificate-authority-types.ts @@ -0,0 +1,13 @@ +import { z } from "zod"; + +import { + AzureAdCsCertificateAuthoritySchema, + CreateAzureAdCsCertificateAuthoritySchema, + UpdateAzureAdCsCertificateAuthoritySchema +} from "./azure-ad-cs-certificate-authority-schemas"; + +export type TAzureAdCsCertificateAuthority = z.infer; + +export type TCreateAzureAdCsCertificateAuthorityDTO = z.infer; + +export type TUpdateAzureAdCsCertificateAuthorityDTO = z.infer; diff --git a/backend/src/services/certificate-authority/certificate-authority-enums.ts b/backend/src/services/certificate-authority/certificate-authority-enums.ts index 8de80495e..f9b769a86 100644 --- a/backend/src/services/certificate-authority/certificate-authority-enums.ts +++ b/backend/src/services/certificate-authority/certificate-authority-enums.ts @@ -1,6 +1,7 @@ export enum CaType { INTERNAL = "internal", - ACME = "acme" + ACME = "acme", + AZURE_AD_CS = "azure-ad-cs" } export enum InternalCaType { @@ -17,3 +18,9 @@ export enum CaStatus { export enum CaRenewalType { EXISTING = "existing" } + +export enum CaCapability { + ISSUE_CERTIFICATES = "issue-certificates", + REVOKE_CERTIFICATES = "revoke-certificates", + RENEW_CERTIFICATES = "renew-certificates" +} diff --git a/backend/src/services/certificate-authority/certificate-authority-maps.ts b/backend/src/services/certificate-authority/certificate-authority-maps.ts index d13f65138..ef844a1ed 100644 --- a/backend/src/services/certificate-authority/certificate-authority-maps.ts +++ b/backend/src/services/certificate-authority/certificate-authority-maps.ts @@ -1,6 +1,29 @@ -import { CaType } from "./certificate-authority-enums"; +import { CaCapability, CaType } from "./certificate-authority-enums"; export const CERTIFICATE_AUTHORITIES_TYPE_MAP: Record = { [CaType.INTERNAL]: "Internal", - [CaType.ACME]: "ACME" + [CaType.ACME]: "ACME", + [CaType.AZURE_AD_CS]: "Azure AD Certificate Service" +}; + +export const CERTIFICATE_AUTHORITIES_CAPABILITIES_MAP: Record = { + [CaType.INTERNAL]: [ + CaCapability.ISSUE_CERTIFICATES, + CaCapability.REVOKE_CERTIFICATES, + CaCapability.RENEW_CERTIFICATES + ], + [CaType.ACME]: [CaCapability.ISSUE_CERTIFICATES, CaCapability.REVOKE_CERTIFICATES, CaCapability.RENEW_CERTIFICATES], + [CaType.AZURE_AD_CS]: [ + CaCapability.ISSUE_CERTIFICATES, + CaCapability.RENEW_CERTIFICATES + // Note: REVOKE_CERTIFICATES intentionally omitted - not supported by ADCS connector + ] +}; + +/** + * Check if a certificate authority type supports a specific capability + */ +export const caSupportsCapability = (caType: CaType, capability: CaCapability): boolean => { + const capabilities = CERTIFICATE_AUTHORITIES_CAPABILITIES_MAP[caType] || []; + return capabilities.includes(capability); }; diff --git a/backend/src/services/certificate-authority/certificate-authority-queue.ts b/backend/src/services/certificate-authority/certificate-authority-queue.ts index afe17ec5f..0e015da03 100644 --- a/backend/src/services/certificate-authority/certificate-authority-queue.ts +++ b/backend/src/services/certificate-authority/certificate-authority-queue.ts @@ -21,6 +21,7 @@ import { TCertificateSecretDALFactory } from "../certificate/certificate-secret- import { TPkiSubscriberDALFactory } from "../pki-subscriber/pki-subscriber-dal"; import { SubscriberOperationStatus } from "../pki-subscriber/pki-subscriber-types"; import { AcmeCertificateAuthorityFns } from "./acme/acme-certificate-authority-fns"; +import { AzureAdCsCertificateAuthorityFns } from "./azure-ad-cs/azure-ad-cs-certificate-authority-fns"; import { TCertificateAuthorityDALFactory } from "./certificate-authority-dal"; import { CaType } from "./certificate-authority-enums"; import { keyAlgorithmToAlgCfg } from "./certificate-authority-fns"; @@ -33,7 +34,7 @@ import { type TCertificateAuthorityQueueFactoryDep = { certificateAuthorityDAL: TCertificateAuthorityDALFactory; - appConnectionDAL: Pick; + appConnectionDAL: Pick; appConnectionService: Pick; externalCertificateAuthorityDAL: Pick; keyStore: Pick; @@ -82,6 +83,19 @@ export const certificateAuthorityQueueFactory = ({ projectDAL }); + const azureAdCsFns = AzureAdCsCertificateAuthorityFns({ + appConnectionDAL, + appConnectionService, + certificateAuthorityDAL, + externalCertificateAuthorityDAL, + certificateDAL, + certificateBodyDAL, + certificateSecretDAL, + kmsService, + pkiSubscriberDAL, + projectDAL + }); + // TODO 1: auto-periodic rotation // TODO 2: manual rotation @@ -158,6 +172,13 @@ export const certificateAuthorityQueueFactory = ({ lastOperationMessage: "Certificate ordered successfully", lastOperationAt: new Date() }); + } else if (caType === CaType.AZURE_AD_CS) { + await azureAdCsFns.orderSubscriberCertificate(subscriberId); + await pkiSubscriberDAL.updateById(subscriberId, { + lastOperationStatus: SubscriberOperationStatus.SUCCESS, + lastOperationMessage: "Certificate ordered successfully", + lastOperationAt: new Date() + }); } } catch (e: unknown) { if (e instanceof Error) { diff --git a/backend/src/services/certificate-authority/certificate-authority-service.ts b/backend/src/services/certificate-authority/certificate-authority-service.ts index fa0fe017f..6cf55fc52 100644 --- a/backend/src/services/certificate-authority/certificate-authority-service.ts +++ b/backend/src/services/certificate-authority/certificate-authority-service.ts @@ -22,6 +22,14 @@ import { TCreateAcmeCertificateAuthorityDTO, TUpdateAcmeCertificateAuthorityDTO } from "./acme/acme-certificate-authority-types"; +import { + AzureAdCsCertificateAuthorityFns, + castDbEntryToAzureAdCsCertificateAuthority +} from "./azure-ad-cs/azure-ad-cs-certificate-authority-fns"; +import { + TCreateAzureAdCsCertificateAuthorityDTO, + TUpdateAzureAdCsCertificateAuthorityDTO +} from "./azure-ad-cs/azure-ad-cs-certificate-authority-types"; import { TCertificateAuthorityDALFactory } from "./certificate-authority-dal"; import { CaType } from "./certificate-authority-enums"; import { @@ -34,7 +42,7 @@ import { TInternalCertificateAuthorityServiceFactory } from "./internal/internal import { TCreateInternalCertificateAuthorityDTO } from "./internal/internal-certificate-authority-types"; type TCertificateAuthorityServiceFactoryDep = { - appConnectionDAL: Pick; + appConnectionDAL: Pick; appConnectionService: Pick; certificateAuthorityDAL: Pick< TCertificateAuthorityDALFactory, @@ -91,6 +99,19 @@ export const certificateAuthorityServiceFactory = ({ projectDAL }); + const azureAdCsFns = AzureAdCsCertificateAuthorityFns({ + appConnectionDAL, + appConnectionService, + certificateAuthorityDAL, + externalCertificateAuthorityDAL, + certificateDAL, + certificateBodyDAL, + certificateSecretDAL, + kmsService, + pkiSubscriberDAL, + projectDAL + }); + const createCertificateAuthority = async ( { type, projectId, name, enableDirectIssuance, configuration, status }: TCreateCertificateAuthorityDTO, actor: OrgServiceActor @@ -146,6 +167,17 @@ export const certificateAuthorityServiceFactory = ({ }); } + if (type === CaType.AZURE_AD_CS) { + return azureAdCsFns.createCertificateAuthority({ + name, + projectId, + configuration: configuration as TCreateAzureAdCsCertificateAuthorityDTO["configuration"], + enableDirectIssuance, + status, + actor + }); + } + throw new BadRequestError({ message: "Invalid certificate authority type" }); }; @@ -205,6 +237,10 @@ export const certificateAuthorityServiceFactory = ({ return castDbEntryToAcmeCertificateAuthority(certificateAuthority); } + if (type === CaType.AZURE_AD_CS) { + return castDbEntryToAzureAdCsCertificateAuthority(certificateAuthority); + } + throw new BadRequestError({ message: "Invalid certificate authority type" }); }; @@ -249,6 +285,10 @@ export const certificateAuthorityServiceFactory = ({ return acmeFns.listCertificateAuthorities({ projectId }); } + if (type === CaType.AZURE_AD_CS) { + return azureAdCsFns.listCertificateAuthorities({ projectId }); + } + throw new BadRequestError({ message: "Invalid certificate authority type" }); }; @@ -323,6 +363,17 @@ export const certificateAuthorityServiceFactory = ({ }); } + if (type === CaType.AZURE_AD_CS) { + return azureAdCsFns.updateCertificateAuthority({ + id: certificateAuthority.id, + configuration: configuration as TUpdateAzureAdCsCertificateAuthorityDTO["configuration"], + enableDirectIssuance, + actor, + status, + name + }); + } + throw new BadRequestError({ message: "Invalid certificate authority type" }); }; @@ -384,14 +435,54 @@ export const certificateAuthorityServiceFactory = ({ return castDbEntryToAcmeCertificateAuthority(certificateAuthority); } + if (type === CaType.AZURE_AD_CS) { + return castDbEntryToAzureAdCsCertificateAuthority(certificateAuthority); + } + throw new BadRequestError({ message: "Invalid certificate authority type" }); }; + const getAzureAdcsTemplates = async ({ + caId, + projectId, + actor, + actorId, + actorAuthMethod, + actorOrgId + }: { + caId: string; + projectId: string; + actor: OrgServiceActor["type"]; + actorId: string; + actorAuthMethod: OrgServiceActor["authMethod"]; + actorOrgId?: string; + }) => { + const { permission } = await permissionService.getProjectPermission({ + actor, + actorId, + projectId, + actorAuthMethod, + actorOrgId, + actionProjectType: ActionProjectType.CertificateManager + }); + + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Read, + ProjectPermissionSub.CertificateAuthorities + ); + + return azureAdCsFns.getTemplates({ + caId, + projectId + }); + }; + return { createCertificateAuthority, findCertificateAuthorityByNameAndProjectId, listCertificateAuthoritiesByProjectId, updateCertificateAuthority, - deleteCertificateAuthority + deleteCertificateAuthority, + getAzureAdcsTemplates }; }; diff --git a/backend/src/services/certificate-authority/certificate-authority-types.ts b/backend/src/services/certificate-authority/certificate-authority-types.ts index d76330bd8..13b5cec40 100644 --- a/backend/src/services/certificate-authority/certificate-authority-types.ts +++ b/backend/src/services/certificate-authority/certificate-authority-types.ts @@ -1,13 +1,23 @@ import { TAcmeCertificateAuthority, TAcmeCertificateAuthorityInput } from "./acme/acme-certificate-authority-types"; +import { + TAzureAdCsCertificateAuthority, + TCreateAzureAdCsCertificateAuthorityDTO +} from "./azure-ad-cs/azure-ad-cs-certificate-authority-types"; import { CaType } from "./certificate-authority-enums"; import { TInternalCertificateAuthority, TInternalCertificateAuthorityInput } from "./internal/internal-certificate-authority-types"; -export type TCertificateAuthority = TInternalCertificateAuthority | TAcmeCertificateAuthority; +export type TCertificateAuthority = + | TInternalCertificateAuthority + | TAcmeCertificateAuthority + | TAzureAdCsCertificateAuthority; -export type TCertificateAuthorityInput = TInternalCertificateAuthorityInput | TAcmeCertificateAuthorityInput; +export type TCertificateAuthorityInput = + | TInternalCertificateAuthorityInput + | TAcmeCertificateAuthorityInput + | TCreateAzureAdCsCertificateAuthorityDTO; export type TCreateCertificateAuthorityDTO = Omit; diff --git a/backend/src/services/certificate-authority/internal/internal-certificate-authority-fns.ts b/backend/src/services/certificate-authority/internal/internal-certificate-authority-fns.ts index 5671435f6..9f1a5e5c2 100644 --- a/backend/src/services/certificate-authority/internal/internal-certificate-authority-fns.ts +++ b/backend/src/services/certificate-authority/internal/internal-certificate-authority-fns.ts @@ -36,12 +36,18 @@ import { validateAndMapAltNameType } from "../certificate-authority-validators"; import { TIssueCertWithTemplateDTO } from "./internal-certificate-authority-types"; type TInternalCertificateAuthorityFnsDeps = { - certificateAuthorityDAL: Pick; + certificateAuthorityDAL: Pick< + TCertificateAuthorityDALFactory, + "findByIdWithAssociatedCa" | "findById" | "create" | "transaction" | "updateById" | "findWithAssociatedCa" + >; certificateAuthorityCertDAL: Pick; certificateAuthoritySecretDAL: Pick; certificateAuthorityCrlDAL: Pick; projectDAL: Pick; - kmsService: Pick; + kmsService: Pick< + TKmsServiceFactory, + "decryptWithKmsKey" | "encryptWithKmsKey" | "generateKmsKey" | "createCipherPairWithDataKey" + >; certificateDAL: Pick; certificateBodyDAL: Pick; certificateSecretDAL: Pick; diff --git a/backend/src/services/certificate/certificate-service.ts b/backend/src/services/certificate/certificate-service.ts index de6da16ee..7adc60d52 100644 --- a/backend/src/services/certificate/certificate-service.ts +++ b/backend/src/services/certificate/certificate-service.ts @@ -14,6 +14,8 @@ import { TCertificateBodyDALFactory } from "@app/services/certificate/certificat import { TCertificateDALFactory } from "@app/services/certificate/certificate-dal"; import { TCertificateAuthorityCertDALFactory } from "@app/services/certificate-authority/certificate-authority-cert-dal"; import { TCertificateAuthorityDALFactory } from "@app/services/certificate-authority/certificate-authority-dal"; +import { CaCapability, CaType } from "@app/services/certificate-authority/certificate-authority-enums"; +import { caSupportsCapability } from "@app/services/certificate-authority/certificate-authority-maps"; import { TCertificateAuthoritySecretDALFactory } from "@app/services/certificate-authority/certificate-authority-secret-dal"; import { TKmsServiceFactory } from "@app/services/kms/kms-service"; import { TPkiCollectionDALFactory } from "@app/services/pki-collection/pki-collection-dal"; @@ -184,9 +186,11 @@ export const certificateServiceFactory = ({ const ca = await certificateAuthorityDAL.findByIdWithAssociatedCa(cert.caId); - if (ca.externalCa?.id) { + // Check if the CA type supports revocation + const caType = (ca.externalCa?.type as CaType) ?? CaType.INTERNAL; + if (!caSupportsCapability(caType, CaCapability.REVOKE_CERTIFICATES)) { throw new BadRequestError({ - message: "Cannot revoke external certificates" + message: "Certificate revocation is not supported by this certificate authority type" }); } @@ -218,18 +222,37 @@ export const certificateServiceFactory = ({ } ); - // rebuild CRL (TODO: move to interval-based cron job) - await rebuildCaCrl({ - caId: ca.id, - certificateAuthorityDAL, - certificateAuthorityCrlDAL, - certificateAuthoritySecretDAL, - projectDAL, - certificateDAL, - kmsService - }); + // Note: External CA revocation handling would go here for supported CA types + // Currently, only internal CAs and ACME CAs support revocation - return { revokedAt, cert, ca: expandInternalCa(ca) }; + // rebuild CRL (TODO: move to interval-based cron job) + // Only rebuild CRL for internal CAs - external CAs manage their own CRLs + if (!ca.externalCa?.id) { + await rebuildCaCrl({ + caId: ca.id, + certificateAuthorityDAL, + certificateAuthorityCrlDAL, + certificateAuthoritySecretDAL, + projectDAL, + certificateDAL, + kmsService + }); + } + + // Return appropriate CA format based on CA type + const caResult = ca.externalCa?.id + ? { + id: ca.id, + name: ca.name, + projectId: ca.projectId, + status: ca.status, + enableDirectIssuance: ca.enableDirectIssuance, + type: ca.externalCa.type, + externalCa: ca.externalCa + } + : expandInternalCa(ca); + + return { revokedAt, cert, ca: caResult }; }; /** diff --git a/backend/src/services/org-membership/org-membership-dal.ts b/backend/src/services/org-membership/org-membership-dal.ts index 8f2ca01f0..7cef4a6cc 100644 --- a/backend/src/services/org-membership/org-membership-dal.ts +++ b/backend/src/services/org-membership/org-membership-dal.ts @@ -124,12 +124,12 @@ export const orgMembershipDALFactory = (db: TDbClient) => { void qb .whereNull(`${TableName.OrgMembership}.lastInvitedAt`) .whereBetween(`${TableName.OrgMembership}.createdAt`, [twelveMonthsAgo, oneWeekAgo]); - }) - .orWhere((qb) => { // lastInvitedAt is older than 1 week ago AND createdAt is younger than 1 month ago - void qb - .where(`${TableName.OrgMembership}.lastInvitedAt`, "<", oneWeekAgo) - .where(`${TableName.OrgMembership}.createdAt`, ">", oneMonthAgo); + void qb.orWhere((qbInner) => { + void qbInner + .where(`${TableName.OrgMembership}.lastInvitedAt`, "<", oneWeekAgo) + .where(`${TableName.OrgMembership}.createdAt`, ">", oneMonthAgo); + }); }); return memberships; diff --git a/backend/src/services/pki-subscriber/pki-subscriber-schema.ts b/backend/src/services/pki-subscriber/pki-subscriber-schema.ts index 337f81d8c..7b22ab645 100644 --- a/backend/src/services/pki-subscriber/pki-subscriber-schema.ts +++ b/backend/src/services/pki-subscriber/pki-subscriber-schema.ts @@ -18,7 +18,8 @@ export const sanitizedPkiSubscriber = PkiSubscribersSchema.pick({ lastOperationAt: true, enableAutoRenewal: true, autoRenewalPeriodInDays: true, - lastAutoRenewAt: true + lastAutoRenewAt: true, + properties: true }).extend({ supportsImmediateCertIssuance: z.boolean().optional() }); diff --git a/backend/src/services/pki-subscriber/pki-subscriber-service.ts b/backend/src/services/pki-subscriber/pki-subscriber-service.ts index a3e6ec78c..391b5b2a8 100644 --- a/backend/src/services/pki-subscriber/pki-subscriber-service.ts +++ b/backend/src/services/pki-subscriber/pki-subscriber-service.ts @@ -109,6 +109,7 @@ export const pkiSubscriberServiceFactory = ({ extendedKeyUsages, enableAutoRenewal, autoRenewalPeriodInDays, + properties, projectId, actorId, actorAuthMethod, @@ -157,7 +158,8 @@ export const pkiSubscriberServiceFactory = ({ keyUsages, extendedKeyUsages, enableAutoRenewal, - autoRenewalPeriodInDays + autoRenewalPeriodInDays, + properties }); return newSubscriber; @@ -221,6 +223,7 @@ export const pkiSubscriberServiceFactory = ({ extendedKeyUsages, enableAutoRenewal, autoRenewalPeriodInDays, + properties, actorId, actorAuthMethod, actor, @@ -275,7 +278,8 @@ export const pkiSubscriberServiceFactory = ({ keyUsages, extendedKeyUsages, enableAutoRenewal, - autoRenewalPeriodInDays + autoRenewalPeriodInDays, + properties }); return updatedSubscriber; @@ -360,7 +364,7 @@ export const pkiSubscriberServiceFactory = ({ throw new BadRequestError({ message: "CA is disabled" }); } - if (ca.externalCa?.id && ca.externalCa.type === CaType.ACME) { + if (ca.externalCa?.id && (ca.externalCa.type === CaType.ACME || ca.externalCa.type === CaType.AZURE_AD_CS)) { await certificateAuthorityQueue.orderCertificateForSubscriber({ subscriberId: subscriber.id, caType: ca.externalCa.type diff --git a/backend/src/services/pki-subscriber/pki-subscriber-types.ts b/backend/src/services/pki-subscriber/pki-subscriber-types.ts index 6881eea74..60b6f51b8 100644 --- a/backend/src/services/pki-subscriber/pki-subscriber-types.ts +++ b/backend/src/services/pki-subscriber/pki-subscriber-types.ts @@ -18,6 +18,7 @@ export type TCreatePkiSubscriberDTO = { extendedKeyUsages: CertExtendedKeyUsage[]; enableAutoRenewal?: boolean; autoRenewalPeriodInDays?: number; + properties?: TPkiSubscriberProperties; } & TProjectPermission; export type TGetPkiSubscriberDTO = { @@ -36,6 +37,7 @@ export type TUpdatePkiSubscriberDTO = { extendedKeyUsages?: CertExtendedKeyUsage[]; enableAutoRenewal?: boolean; autoRenewalPeriodInDays?: number; + properties?: TPkiSubscriberProperties; } & TProjectPermission; export type TDeletePkiSubscriberDTO = { @@ -69,3 +71,13 @@ export enum SubscriberOperationStatus { SUCCESS = "success", FAILED = "failed" } + +export type TPkiSubscriberProperties = { + azureTemplateType?: string; + organization?: string; + organizationalUnit?: string; + country?: string; + state?: string; + locality?: string; + emailAddress?: string; +}; diff --git a/backend/src/services/secret-sync/checkly/checkly-sync-fns.ts b/backend/src/services/secret-sync/checkly/checkly-sync-fns.ts index eded130bb..822773948 100644 --- a/backend/src/services/secret-sync/checkly/checkly-sync-fns.ts +++ b/backend/src/services/secret-sync/checkly/checkly-sync-fns.ts @@ -23,56 +23,120 @@ export const ChecklySyncFns = { const config = secretSync.destinationConfig; - const variables = await ChecklyPublicAPI.getVariables(secretSync.connection, config.accountId); + if (config.groupId) { + // Handle group environment variables + const groupVars = await ChecklyPublicAPI.getCheckGroupEnvironmentVariables( + secretSync.connection, + config.accountId, + config.groupId + ); - const checklySecrets = Object.fromEntries(variables!.map((variable) => [variable.key, variable])); + const checklyGroupSecrets = Object.fromEntries(groupVars.map((variable) => [variable.key, variable])); - for await (const key of Object.keys(secretMap)) { - try { + // Prepare all variables to update at once + const updatedVariables = { ...checklyGroupSecrets }; + + for (const key of Object.keys(secretMap)) { const entry = secretMap[key]; - // If value is empty, we skip the upsert - checkly does not allow empty values + // If value is empty, we skip adding it - checkly does not allow empty values if (entry.value.trim() === "") { - // Delete the secret from Checkly if its empty + // Delete the secret from the group if it's empty if (!disableSecretDeletion) { - await ChecklyPublicAPI.deleteVariable(secretSync.connection, config.accountId, { - key - }); + delete updatedVariables[key]; } continue; // Skip empty values } - await ChecklyPublicAPI.upsertVariable(secretSync.connection, config.accountId, { + // Add or update the variable + updatedVariables[key] = { key, value: entry.value, - secret: true, locked: true - }); + }; + } + + // Remove secrets that are not in the secretMap if deletion is enabled + if (!disableSecretDeletion) { + for (const key of Object.keys(checklyGroupSecrets)) { + // eslint-disable-next-line no-continue + if (!matchesSchema(key, environment?.slug || "", keySchema)) continue; + + if (!secretMap[key]) { + delete updatedVariables[key]; + } + } + } + + // Update all group environment variables at once + try { + await ChecklyPublicAPI.updateCheckGroupEnvironmentVariables( + secretSync.connection, + config.accountId, + config.groupId, + Object.values(updatedVariables) + ); } catch (error) { + if (error instanceof SecretSyncError) throw error; + throw new SecretSyncError({ error, - secretKey: key + secretKey: "group_update" }); } - } + } else { + // Handle global variables (existing logic) + const variables = await ChecklyPublicAPI.getVariables(secretSync.connection, config.accountId); - if (disableSecretDeletion) return; + const checklySecrets = Object.fromEntries(variables!.map((variable) => [variable.key, variable])); - for await (const key of Object.keys(checklySecrets)) { - try { - // eslint-disable-next-line no-continue - if (!matchesSchema(key, environment?.slug || "", keySchema)) continue; + for await (const key of Object.keys(secretMap)) { + try { + const entry = secretMap[key]; - if (!secretMap[key]) { - await ChecklyPublicAPI.deleteVariable(secretSync.connection, config.accountId, { - key + // If value is empty, we skip the upsert - checkly does not allow empty values + if (entry.value.trim() === "") { + // Delete the secret from Checkly if its empty + if (!disableSecretDeletion) { + await ChecklyPublicAPI.deleteVariable(secretSync.connection, config.accountId, { + key + }); + } + continue; // Skip empty values + } + + await ChecklyPublicAPI.upsertVariable(secretSync.connection, config.accountId, { + key, + value: entry.value, + secret: true, + locked: true + }); + } catch (error) { + throw new SecretSyncError({ + error, + secretKey: key + }); + } + } + + if (disableSecretDeletion) return; + + for await (const key of Object.keys(checklySecrets)) { + try { + // eslint-disable-next-line no-continue + if (!matchesSchema(key, environment?.slug || "", keySchema)) continue; + + if (!secretMap[key]) { + await ChecklyPublicAPI.deleteVariable(secretSync.connection, config.accountId, { + key + }); + } + } catch (error) { + throw new SecretSyncError({ + error, + secretKey: key }); } - } catch (error) { - throw new SecretSyncError({ - error, - secretKey: key - }); } } }, @@ -80,23 +144,54 @@ export const ChecklySyncFns = { async removeSecrets(secretSync: TChecklySyncWithCredentials, secretMap: TSecretMap) { const config = secretSync.destinationConfig; - const variables = await ChecklyPublicAPI.getVariables(secretSync.connection, config.accountId); + if (config.groupId) { + // Handle group environment variables + const groupVars = await ChecklyPublicAPI.getCheckGroupEnvironmentVariables( + secretSync.connection, + config.accountId, + config.groupId + ); - const checklySecrets = Object.fromEntries(variables!.map((variable) => [variable.key, variable])); + const checklyGroupSecrets = Object.fromEntries(groupVars.map((variable) => [variable.key, variable])); + + // Filter out the secrets to remove + const remainingVariables = Object.keys(checklyGroupSecrets) + .filter((key) => !(key in secretMap)) + .map((key) => checklyGroupSecrets[key]); - for await (const secret of Object.keys(checklySecrets)) { try { - if (secret in secretMap) { - await ChecklyPublicAPI.deleteVariable(secretSync.connection, config.accountId, { - key: secret - }); - } + await ChecklyPublicAPI.updateCheckGroupEnvironmentVariables( + secretSync.connection, + config.accountId, + config.groupId, + remainingVariables + ); } catch (error) { throw new SecretSyncError({ error, - secretKey: secret + secretKey: "group_remove" }); } + } else { + // Handle global variables (existing logic) + const variables = await ChecklyPublicAPI.getVariables(secretSync.connection, config.accountId); + + const checklySecrets = Object.fromEntries(variables!.map((variable) => [variable.key, variable])); + + for await (const secret of Object.keys(checklySecrets)) { + try { + if (secret in secretMap) { + await ChecklyPublicAPI.deleteVariable(secretSync.connection, config.accountId, { + key: secret + }); + } + } catch (error) { + throw new SecretSyncError({ + error, + secretKey: secret + }); + } + } } } }; diff --git a/backend/src/services/secret-sync/checkly/checkly-sync-schemas.ts b/backend/src/services/secret-sync/checkly/checkly-sync-schemas.ts index 04f444357..fc511b2d7 100644 --- a/backend/src/services/secret-sync/checkly/checkly-sync-schemas.ts +++ b/backend/src/services/secret-sync/checkly/checkly-sync-schemas.ts @@ -11,7 +11,17 @@ import { TSyncOptionsConfig } from "@app/services/secret-sync/secret-sync-types" const ChecklySyncDestinationConfigSchema = z.object({ accountId: z.string().min(1, "Account ID is required").max(255, "Account ID must be less than 255 characters"), - accountName: z.string().min(1, "Account Name is required").max(255, "Account ID must be less than 255 characters") + accountName: z + .string() + .min(1, "Account Name is required") + .max(255, "Account ID must be less than 255 characters") + .optional(), + groupId: z.string().min(1, "Group ID is required").max(255, "Group ID must be less than 255 characters").optional(), + groupName: z + .string() + .min(1, "Group Name is required") + .max(255, "Group Name must be less than 255 characters") + .optional() }); const ChecklySyncOptionsConfig: TSyncOptionsConfig = { canImportSecrets: false }; diff --git a/docs/api-reference/endpoints/app-connections/azure-adcs/available.mdx b/docs/api-reference/endpoints/app-connections/azure-adcs/available.mdx new file mode 100644 index 000000000..6b23a07e9 --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/azure-adcs/available.mdx @@ -0,0 +1,4 @@ +--- +title: "Available" +openapi: "GET /api/v1/app-connections/azure-adcs/available" +--- diff --git a/docs/api-reference/endpoints/app-connections/azure-adcs/create.mdx b/docs/api-reference/endpoints/app-connections/azure-adcs/create.mdx new file mode 100644 index 000000000..132ee409c --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/azure-adcs/create.mdx @@ -0,0 +1,10 @@ +--- +title: "Create" +openapi: "POST /api/v1/app-connections/azure-adcs" +--- + + + Azure ADCS Connections must be created through the Infisical UI. + Check out the configuration docs for [Azure ADCS Connections](/integrations/app-connections/azure-adcs) for a step-by-step + guide. + \ No newline at end of file diff --git a/docs/api-reference/endpoints/app-connections/azure-adcs/delete.mdx b/docs/api-reference/endpoints/app-connections/azure-adcs/delete.mdx new file mode 100644 index 000000000..4d7037959 --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/azure-adcs/delete.mdx @@ -0,0 +1,4 @@ +--- +title: "Delete" +openapi: "DELETE /api/v1/app-connections/azure-adcs/{connectionId}" +--- diff --git a/docs/api-reference/endpoints/app-connections/azure-adcs/get-by-id.mdx b/docs/api-reference/endpoints/app-connections/azure-adcs/get-by-id.mdx new file mode 100644 index 000000000..46a57d890 --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/azure-adcs/get-by-id.mdx @@ -0,0 +1,4 @@ +--- +title: "Get by ID" +openapi: "GET /api/v1/app-connections/azure-adcs/{connectionId}" +--- diff --git a/docs/api-reference/endpoints/app-connections/azure-adcs/get-by-name.mdx b/docs/api-reference/endpoints/app-connections/azure-adcs/get-by-name.mdx new file mode 100644 index 000000000..179ef9076 --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/azure-adcs/get-by-name.mdx @@ -0,0 +1,4 @@ +--- +title: "Get by Name" +openapi: "GET /api/v1/app-connections/azure-adcs/connection-name/{connectionName}" +--- diff --git a/docs/api-reference/endpoints/app-connections/azure-adcs/list.mdx b/docs/api-reference/endpoints/app-connections/azure-adcs/list.mdx new file mode 100644 index 000000000..c0f679ee1 --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/azure-adcs/list.mdx @@ -0,0 +1,4 @@ +--- +title: "List" +openapi: "GET /api/v1/app-connections/azure-adcs" +--- diff --git a/docs/api-reference/endpoints/app-connections/azure-adcs/update.mdx b/docs/api-reference/endpoints/app-connections/azure-adcs/update.mdx new file mode 100644 index 000000000..4c4dfc91d --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/azure-adcs/update.mdx @@ -0,0 +1,10 @@ +--- +title: "Update" +openapi: "PATCH /api/v1/app-connections/azure-adcs/{connectionId}" +--- + + + Azure ADCS Connections must be updated through the Infisical UI. + Check out the configuration docs for [Azure ADCS Connections](/integrations/app-connections/azure-adcs) for a step-by-step + guide. + diff --git a/docs/docs.json b/docs/docs.json index 3b116f4db..6eadf70b8 100644 --- a/docs/docs.json +++ b/docs/docs.json @@ -106,6 +106,7 @@ "integrations/app-connections/auth0", "integrations/app-connections/aws", "integrations/app-connections/azure-app-configuration", + "integrations/app-connections/azure-adcs", "integrations/app-connections/azure-client-secrets", "integrations/app-connections/azure-devops", "integrations/app-connections/azure-key-vault", @@ -342,10 +343,7 @@ }, { "group": "Architecture", - "pages": [ - "internals/architecture/components", - "internals/architecture/cloud" - ] + "pages": ["internals/architecture/components", "internals/architecture/cloud"] }, "internals/security", "internals/service-tokens" @@ -564,10 +562,7 @@ "integrations/cloud/gcp-secret-manager", { "group": "Cloudflare", - "pages": [ - "integrations/cloud/cloudflare-pages", - "integrations/cloud/cloudflare-workers" - ] + "pages": ["integrations/cloud/cloudflare-pages", "integrations/cloud/cloudflare-workers"] }, "integrations/cloud/terraform-cloud", "integrations/cloud/databricks", @@ -661,9 +656,7 @@ "documentation/platform/secret-scanning/overview", { "group": "Concepts", - "pages": [ - "documentation/platform/secret-scanning/concepts/secret-scanning" - ] + "pages": ["documentation/platform/secret-scanning/concepts/secret-scanning"] } ] }, @@ -690,6 +683,7 @@ "documentation/platform/pki/subscribers", "documentation/platform/pki/certificates", "documentation/platform/pki/acme-ca", + "documentation/platform/pki/azure-adcs", "documentation/platform/pki/est", "documentation/platform/pki/alerting", { @@ -712,18 +706,13 @@ "documentation/platform/ssh/overview", { "group": "Concepts", - "pages": [ - "documentation/platform/ssh/concepts/ssh-certificates" - ] + "pages": ["documentation/platform/ssh/concepts/ssh-certificates"] } ] }, { "group": "Platform Reference", - "pages": [ - "documentation/platform/ssh/usage", - "documentation/platform/ssh/host-groups" - ] + "pages": ["documentation/platform/ssh/usage", "documentation/platform/ssh/host-groups"] } ] }, @@ -770,11 +759,7 @@ "cli/commands/reset", { "group": "infisical scan", - "pages": [ - "cli/commands/scan", - "cli/commands/scan-git-changes", - "cli/commands/scan-install" - ] + "pages": ["cli/commands/scan", "cli/commands/scan-git-changes", "cli/commands/scan-install"] } ] }, @@ -1108,9 +1093,7 @@ "pages": [ { "group": "Kubernetes", - "pages": [ - "api-reference/endpoints/dynamic-secrets/kubernetes/create-lease" - ] + "pages": ["api-reference/endpoints/dynamic-secrets/kubernetes/create-lease"] }, "api-reference/endpoints/dynamic-secrets/create", "api-reference/endpoints/dynamic-secrets/update", @@ -1396,6 +1379,18 @@ "api-reference/endpoints/app-connections/aws/delete" ] }, + { + "group": "Azure ADCS", + "pages": [ + "api-reference/endpoints/app-connections/azure-adcs/list", + "api-reference/endpoints/app-connections/azure-adcs/available", + "api-reference/endpoints/app-connections/azure-adcs/get-by-id", + "api-reference/endpoints/app-connections/azure-adcs/get-by-name", + "api-reference/endpoints/app-connections/azure-adcs/create", + "api-reference/endpoints/app-connections/azure-adcs/update", + "api-reference/endpoints/app-connections/azure-adcs/delete" + ] + }, { "group": "Azure App Configuration", "pages": [ @@ -2453,7 +2448,7 @@ "sdks/languages/node", "sdks/languages/python", "sdks/languages/java", - "sdks/languages/csharp", + "sdks/languages/dotnet", "sdks/languages/cpp", "sdks/languages/rust", "sdks/languages/go", @@ -2681,5 +2676,11 @@ "koala": { "publicApiKey": "pk_b50d7184e0e39ddd5cdb43cf6abeadd9b97d" } - } + }, + "redirects": [ + { + "source": "/sdks/languages/csharp", + "destination": "/sdks/languages/dotnet" + } + ] } diff --git a/docs/documentation/platform/access-controls/access-requests.mdx b/docs/documentation/platform/access-controls/access-requests.mdx index cc0add8d8..cf2643db8 100644 --- a/docs/documentation/platform/access-controls/access-requests.mdx +++ b/docs/documentation/platform/access-controls/access-requests.mdx @@ -25,6 +25,11 @@ This functionality works in the following way: {/* ![Access Request Review](/images/platform/access-controls/review-access-request.png) */} ![Access Request Bypass](/images/platform/access-controls/access-request-bypass.png) + + Optionally, approvers can edit the duration of an access request to reduce how long access will be granted by clicking the **Edit** icon next to the duration. + ![Edit Access Request](/images/platform/access-controls/edit-access-request.png) + + If the access request matches with a policy that allows break-glass approval bypasses, the requester may bypass the policy and get access to the resource diff --git a/docs/documentation/platform/pki/azure-adcs.mdx b/docs/documentation/platform/pki/azure-adcs.mdx new file mode 100644 index 000000000..b0cea565e --- /dev/null +++ b/docs/documentation/platform/pki/azure-adcs.mdx @@ -0,0 +1,206 @@ +--- +title: "Certificates with Azure ADCS" +description: "Learn how to issue and manage certificates using Microsoft Active Directory Certificate Services (ADCS) with Infisical." +--- + +Issue and manage certificates using Microsoft Active Directory Certificate Services (ADCS) for enterprise-grade certificate management integrated with your existing Windows infrastructure. + +## Prerequisites + +Before setting up ADCS integration, ensure you have: + +- Microsoft Active Directory Certificate Services (ADCS) server running and accessible +- Domain administrator account with certificate management permissions +- ADCS web enrollment enabled on your server +- Network connectivity from Infisical to the ADCS server +- **IP whitelisting**: Your ADCS server must allow connections from Infisical's IP addresses + - For Infisical Cloud instances, see [Networking Configuration](/documentation/setup/networking) for the list of IPs to whitelist + - For self-hosted instances, whitelist your Infisical server's IP address +- Azure ADCS app connection configured (see [Azure ADCS Connection](/integrations/app-connections/azure-adcs)) + +## Complete Workflow: From Setup to Certificate Issuance + +This section walks you through the complete end-to-end process of setting up Azure ADCS integration and issuing your first certificate. + + + + In your Infisical project, go to your **Certificate Project** → **Certificate Authority** to access the external CAs page. + + ![External CA Page](/images/platform/pki/azure-adcs/azure-adcs-external-ca-page.png) + + + + Click **Create CA** and configure: + - **Type**: Choose **Azure AD Certificate Service** + - **Name**: Friendly name for this CA (e.g., "Production ADCS CA") + - **App Connection**: Choose your ADCS connection from the dropdown + + ![External CA Form](/images/platform/pki/azure-adcs/azure-adcs-external-ca-form.png) + + + + Once created, your Azure ADCS Certificate Authority will appear in the list and be ready for use. + + ![External CA Created](/images/platform/pki/azure-adcs/azure-adcs-external-ca-created.png) + + + + Go to **Subscribers** to access the subscribers page. + + ![Subscribers Page](/images/platform/pki/azure-adcs/azure-adcs-subscribers-page.png) + + + + Click **Add Subscriber** and configure: + - **Name**: Unique subscriber name (e.g., "web-server-certs") + - **Certificate Authority**: Select your ADCS CA + - **Common Name**: Certificate CN (e.g., "api.example.com") + - **Certificate Template**: Select from dynamically loaded ADCS templates + - **Subject Alternative Names**: DNS names, IP addresses, or email addresses + - **TTL**: Certificate validity period (e.g., "1y" for 1 year) + - **Additional Subject Fields**: Organization, OU, locality, state, country, email (if required by template) + + ![Subscribers Form](/images/platform/pki/azure-adcs/azure-adcs-subscribers-form.png) + + + + Your subscriber is now created and ready to issue certificates. + + ![Subscriber Created](/images/platform/pki/azure-adcs/azure-adcs-subscribers-created.png) + + + + Click into your subscriber and click **Order Certificate** to generate a new certificate using your ADCS template. + + ![Issue New Certificate](/images/platform/pki/azure-adcs/azure-adcs-subscriber-issue-new-certificate.png) + + + + Your certificate has been successfully issued by the ADCS server and is ready for use. + + ![Certificate Created](/images/platform/pki/azure-adcs/azure-adcs-certificate-created.png) + + + + Navigate to **Certificates** to view detailed information about all issued certificates, including expiration dates, serial numbers, and certificate chains. + + ![Certificates Page](/images/platform/pki/azure-adcs/azure-adcs-certificates-page.png) + + + +## Certificate Templates + +Infisical automatically retrieves available certificate templates from your ADCS server, ensuring you can only select templates that are properly configured and accessible. The system dynamically discovers templates during the certificate authority setup and certificate issuance process. + +### Common Template Types + +ADCS templates you might see include: +- **Web Server**: For SSL/TLS certificates with server authentication +- **Computer**: For machine authentication certificates +- **User**: For client authentication certificates +- **Basic EFS**: For Encrypting File System certificates +- **EFS Recovery Agent**: For EFS data recovery +- **Administrator**: For administrative certificates +- **Subordinate Certification Authority**: For issuing CA certificates + +### Template Requirements + +Ensure your ADCS templates are configured with: +- **Enroll permissions** for your connection account +- **Auto-enroll permissions** if using automated workflows +- **Subject name requirements** matching your certificate requests +- **Key usage extensions** appropriate for your use case + + +**Dynamic Template Discovery**: Infisical queries your ADCS server in real-time to populate available templates. Only templates you have permission to use will be displayed during certificate issuance. + + +## Certificate Issuance Limitations + +### Immediate Issuance Only + + +**Manual Approval Not Supported**: Infisical currently supports only **immediate certificate issuance**. Certificates that require manual approval or are held by ADCS policies cannot be issued through Infisical yet. + + +For successful certificate issuance, ensure your ADCS templates and policies are configured to: +- **Auto-approve** certificate requests without manual intervention +- **Not require** administrator approval for the templates you plan to use +- **Allow** the connection account to request and receive certificates immediately + +### What Happens with Manual Approval + +If a certificate request requires manual approval: +1. The request will be submitted to ADCS successfully +2. Infisical will attempt to retrieve the certificate with exponential backoff (up to 5 retries over ~1 minute) +3. If the certificate is not approved within this timeframe, the request will **fail** +4. **No background polling**: Currently, Infisical does not check for certificates that might be approved hours or days later + + +**Future Enhancement**: Background polling for delayed certificate approvals is planned for future releases. + + +### Certificate Revocation + + +Certificate revocation is **not supported** by the Azure ADCS connector due to security and complexity considerations. + + +## Advanced Configuration + +### Custom Validity Periods + +Enable custom certificate validity periods on your ADCS server: + +```cmd +# Run on ADCS server as Administrator +certutil -setreg policy\EditFlags +EDITF_ATTRIBUTEENDDATE +net stop certsvc +net start certsvc +``` + +This allows Infisical to control certificate expiration dates directly. + +## Troubleshooting + +### Common Issues + +**Certificate Request Denied** +- Verify ADCS template permissions for your connection account +- Check template subject name requirements +- Ensure template allows the requested key algorithm and size + +**Revocation Service Unavailable** +- Verify IIS is running and the revocation endpoint is accessible +- Check IIS application pool permissions +- Test endpoint connectivity from Infisical + +**Template Not Found** +- Verify template exists on ADCS server and is published +- Check that your connection account has enrollment permissions for the template +- Ensure the template is properly configured and available in the ADCS web enrollment interface +- Templates are dynamically loaded - refresh the PKI Subscriber form if templates don't appear + +**Certificate Request Pending/Timeout** +- Check if your ADCS template requires manual approval - Infisical only supports immediate issuance +- Verify the certificate template is configured for auto-approval +- Ensure your connection account has sufficient permissions to request certificates without approval +- Review ADCS server policies that might be holding the certificate request + +**Network Connectivity Issues** +- Verify your ADCS server's firewall allows connections from Infisical +- For Infisical Cloud: Ensure Infisical's IP addresses are whitelisted (see [Networking Configuration](/documentation/setup/networking)) +- For self-hosted: Whitelist your Infisical server's IP address on the ADCS server +- Test HTTPS connectivity to the ADCS web enrollment endpoint +- Check for any network security appliances blocking the connection + +**Authentication Failures** +- Verify ADCS connection credentials +- Check domain account permissions +- Ensure network connectivity to ADCS server + +**SSL/TLS Certificate Errors** +- For ADCS servers with self-signed or private certificates: disable "Reject Unauthorized" in the SSL tab of your Azure ADCS app connection, or provide the certificate in PEM format +- Common SSL errors: `UNABLE_TO_VERIFY_LEAF_SIGNATURE`, `SELF_SIGNED_CERT_IN_CHAIN`, `CERT_HAS_EXPIRED` +- The SSL configuration applies to all HTTPS communications between Infisical and your ADCS server +- Only HTTPS URLs are supported - HTTP connections are not allowed for security reasons diff --git a/docs/images/app-connections/azure-adcs/azure-adcs-app-connection-created.png b/docs/images/app-connections/azure-adcs/azure-adcs-app-connection-created.png new file mode 100644 index 000000000..dff826130 Binary files /dev/null and b/docs/images/app-connections/azure-adcs/azure-adcs-app-connection-created.png differ diff --git a/docs/images/app-connections/azure-adcs/azure-adcs-app-connection-form-ssl.png b/docs/images/app-connections/azure-adcs/azure-adcs-app-connection-form-ssl.png new file mode 100644 index 000000000..c919a1a87 Binary files /dev/null and b/docs/images/app-connections/azure-adcs/azure-adcs-app-connection-form-ssl.png differ diff --git a/docs/images/app-connections/azure-adcs/azure-adcs-app-connection-form.png b/docs/images/app-connections/azure-adcs/azure-adcs-app-connection-form.png new file mode 100644 index 000000000..002836a58 Binary files /dev/null and b/docs/images/app-connections/azure-adcs/azure-adcs-app-connection-form.png differ diff --git a/docs/images/app-connections/azure-adcs/azure-adcs-select-connection.png b/docs/images/app-connections/azure-adcs/azure-adcs-select-connection.png new file mode 100644 index 000000000..16321c3dd Binary files /dev/null and b/docs/images/app-connections/azure-adcs/azure-adcs-select-connection.png differ diff --git a/docs/images/platform/access-controls/edit-access-request.png b/docs/images/platform/access-controls/edit-access-request.png new file mode 100644 index 000000000..c5db2f5f9 Binary files /dev/null and b/docs/images/platform/access-controls/edit-access-request.png differ diff --git a/docs/images/platform/pki/azure-adcs/azure-adcs-certificate-created.png b/docs/images/platform/pki/azure-adcs/azure-adcs-certificate-created.png new file mode 100644 index 000000000..e82b597eb Binary files /dev/null and b/docs/images/platform/pki/azure-adcs/azure-adcs-certificate-created.png differ diff --git a/docs/images/platform/pki/azure-adcs/azure-adcs-certificates-page.png b/docs/images/platform/pki/azure-adcs/azure-adcs-certificates-page.png new file mode 100644 index 000000000..b5666eaf0 Binary files /dev/null and b/docs/images/platform/pki/azure-adcs/azure-adcs-certificates-page.png differ diff --git a/docs/images/platform/pki/azure-adcs/azure-adcs-external-ca-created.png b/docs/images/platform/pki/azure-adcs/azure-adcs-external-ca-created.png new file mode 100644 index 000000000..3326f6bac Binary files /dev/null and b/docs/images/platform/pki/azure-adcs/azure-adcs-external-ca-created.png differ diff --git a/docs/images/platform/pki/azure-adcs/azure-adcs-external-ca-form.png b/docs/images/platform/pki/azure-adcs/azure-adcs-external-ca-form.png new file mode 100644 index 000000000..3c5ccf079 Binary files /dev/null and b/docs/images/platform/pki/azure-adcs/azure-adcs-external-ca-form.png differ diff --git a/docs/images/platform/pki/azure-adcs/azure-adcs-external-ca-page.png b/docs/images/platform/pki/azure-adcs/azure-adcs-external-ca-page.png new file mode 100644 index 000000000..685b40492 Binary files /dev/null and b/docs/images/platform/pki/azure-adcs/azure-adcs-external-ca-page.png differ diff --git a/docs/images/platform/pki/azure-adcs/azure-adcs-subscriber-issue-new-certificate.png b/docs/images/platform/pki/azure-adcs/azure-adcs-subscriber-issue-new-certificate.png new file mode 100644 index 000000000..8a007adcd Binary files /dev/null and b/docs/images/platform/pki/azure-adcs/azure-adcs-subscriber-issue-new-certificate.png differ diff --git a/docs/images/platform/pki/azure-adcs/azure-adcs-subscribers-created.png b/docs/images/platform/pki/azure-adcs/azure-adcs-subscribers-created.png new file mode 100644 index 000000000..61bb140b8 Binary files /dev/null and b/docs/images/platform/pki/azure-adcs/azure-adcs-subscribers-created.png differ diff --git a/docs/images/platform/pki/azure-adcs/azure-adcs-subscribers-form.png b/docs/images/platform/pki/azure-adcs/azure-adcs-subscribers-form.png new file mode 100644 index 000000000..7e80c3763 Binary files /dev/null and b/docs/images/platform/pki/azure-adcs/azure-adcs-subscribers-form.png differ diff --git a/docs/images/platform/pki/azure-adcs/azure-adcs-subscribers-page.png b/docs/images/platform/pki/azure-adcs/azure-adcs-subscribers-page.png new file mode 100644 index 000000000..ce7cad5c8 Binary files /dev/null and b/docs/images/platform/pki/azure-adcs/azure-adcs-subscribers-page.png differ diff --git a/docs/images/secret-syncs/checkly/checkly-sync-destination.png b/docs/images/secret-syncs/checkly/checkly-sync-destination.png index bb3df8d94..522418a3e 100644 Binary files a/docs/images/secret-syncs/checkly/checkly-sync-destination.png and b/docs/images/secret-syncs/checkly/checkly-sync-destination.png differ diff --git a/docs/integrations/app-connections/azure-adcs.mdx b/docs/integrations/app-connections/azure-adcs.mdx new file mode 100644 index 000000000..7bc174135 --- /dev/null +++ b/docs/integrations/app-connections/azure-adcs.mdx @@ -0,0 +1,70 @@ +--- +title: "Azure ADCS Connection" +description: "Learn how to configure an Azure ADCS Connection for Infisical certificate management." +--- + +Connect Infisical to Microsoft Active Directory Certificate Services (ADCS) for automated certificate issuance and management. + +## Prerequisites + +- Microsoft Active Directory Certificate Services (ADCS) server running and accessible +- Domain administrator account with certificate management permissions +- Network connectivity from Infisical to the ADCS server +- ADCS web enrollment enabled on your server + +## Connection Setup + + + + Navigate to the **App Connections** tab on the **Organization Settings** page. + ![App Connections Tab](/images/app-connections/general/add-connection.png) + + + Select the **Azure ADCS Connection** option from the connection options modal. + ![Select Azure ADCS Connection](/images/app-connections/azure-adcs/azure-adcs-select-connection.png) + + + **Configuration Tab:** + - **Name**: Friendly name for this ADCS connection (e.g., "Production ADCS") + - **ADCS URL**: Your ADCS HTTPS URL (e.g., `https://adcs.yourdomain.com/certsrv`) - only HTTPS is supported + - **Username**: Domain administrator username (format: `DOMAIN\username` or `username@domain.com`) + - **Password**: Password for the domain administrator account + + **SSL Tab:** + - **SSL Certificate**: Optional PEM certificate for custom CA certificates or self-signed certificates + - **Reject Unauthorized**: Whether to reject connections with invalid SSL certificates (it's recommended to keep it enabled) + + + **Getting the SSL Certificate**: If your ADCS server uses a self-signed certificate or a certificate from a custom CA, you can obtain the certificate using OpenSSL or PowerShell: + + **Using OpenSSL:** + ```bash + openssl s_client -connect your-adcs-server:443 -showcerts + ``` + + **Using PowerShell (from inside your ADCS Server):** + ```powershell + Import-Module WebAdministration -ErrorAction SilentlyContinue + $binding = Get-WebBinding -Protocol https -Port 443 + if ($binding -and $binding.certificateHash) { + $cert = Get-ChildItem -Path "Cert:\LocalMachine\My\$($binding.certificateHash)" + if ($cert) { + $certBase64 = [System.Convert]::ToBase64String($cert.RawData) + $certPem = "-----BEGIN CERTIFICATE-----`n" + ($certBase64 -replace '(.{64})', '$1`n') + "`n-----END CERTIFICATE-----" + Write-Output $certPem + } + } + ``` + + Copy the certificate content (including `-----BEGIN CERTIFICATE-----` and `-----END CERTIFICATE-----` lines) and paste it into the SSL Certificate field. + + + And click **Connect to ADCS** to establish the connection. + ![Connect to ADCS](/images/app-connections/azure-adcs/azure-adcs-app-connection-form.png) + ![Connect to ADCS - SSL](/images/app-connections/azure-adcs/azure-adcs-app-connection-form-ssl.png) + + + Your **Azure ADCS Connection** is now available for use in your Infisical projects. ![Azure ADCS Connection Created](/images/app-connections/azure-adcs/azure-adcs-app-connection-created.png) + + + diff --git a/docs/integrations/platforms/ansible.mdx b/docs/integrations/platforms/ansible.mdx index 5c4f34784..85f63079f 100644 --- a/docs/integrations/platforms/ansible.mdx +++ b/docs/integrations/platforms/ansible.mdx @@ -34,8 +34,54 @@ $ pip install infisicalsdk You can either call modules by their Fully Qualified Collection Name (FQCN), such as `infisical.vault.read_secrets`, or you can call modules by their short name if you list the `infisical.vault` collection in the playbook's collections keyword: +### Authentication -```bash +The Infisical Ansible Collection supports [Universal Auth](/documentation/platform/identities/universal-auth) and [OIDC](/documentation/platform/identities/oidc-auth/general) for authenticating against Infisical. + + + + + Using Universal Auth for authentication is the most straight-forward way to get started with using the Ansible collection. + + To use Universal Auth, you need to provide the Client ID and Client Secret of your Infisical Machine Identity. + + ```yaml + lookup('infisical.vault.read_secrets', auth_method="universal-auth", universal_auth_client_id='', universal_auth_client_secret='' ...rest) + ``` + + You can also provide the `auth_method`, `universal_auth_client_id`, and `universal_auth_client_secret` parameters through environment variables: + + | Parameter Name | Environment Variable Name | + | ------------------------------ | ---------------------------------------- | + | `auth_method` | `INFISICAL_AUTH_METHOD` | + | `universal_auth_client_id` | `INFISICAL_UNIVERSAL_AUTH_CLIENT_ID` | + | `universal_auth_client_secret` | `INFISICAL_UNIVERSAL_AUTH_CLIENT_SECRET` | + + + + To use OIDC Auth, you'll need to provide the ID of your machine identity, and the OIDC JWT to be used for authentication. + + + Please note that in order to use OIDC Auth, you must have `1.0.10` or newer of the `infisicalsdk` package installed. + + + ```yaml + lookup('infisical.vault.read_secrets', auth_method="oidc-auth", identity_id='', jwt='' ...rest) + ``` + You can also provide the `auth_method`, `identity_id`, and `jwt` parameters through environment variables: + + | Parameter Name | Environment Variable Name | + | --------------- | ------------------------- | + | auth_method | `INFISICAL_AUTH_METHOD` | + | identity_id | `INFISICAL_IDENTITY_ID` | + | jwt | `INFISICAL_JWT` | + + + + +### Examples + +```yaml --- vars: read_all_secrets_within_scope: "{{ lookup('infisical.vault.read_secrets', universal_auth_client_id='<>', universal_auth_client_secret='<>', project_id='<>', path='/', env_slug='dev', url='https://spotify.infisical.com') }}" diff --git a/docs/integrations/platforms/docker.mdx b/docs/integrations/platforms/docker.mdx index 8e429461a..7b46e8495 100644 --- a/docs/integrations/platforms/docker.mdx +++ b/docs/integrations/platforms/docker.mdx @@ -6,32 +6,10 @@ description: "Learn how to use Infisical to inject environment variables into a This approach allows you to inject secrets from Infisical directly into your application. This is achieved by installing the Infisical CLI into your docker image and modifying your start command to execute with Infisical. -## Add the Infisical CLI to your Dockerfile +## Install the Infisical CLI to your Dockerfile - - - ```dockerfile - RUN apk add --no-cache bash curl && curl -1sLf \ - 'https://dl.cloudsmith.io/public/infisical/infisical-cli/setup.alpine.sh' | bash \ - && apk add infisical - ``` +To install the CLI, follow the instructions for your chosen distribution [here](/cli/overview). - - - ```dockerfile - RUN curl -1sLf \ - 'https://dl.cloudsmith.io/public/infisical/infisical-cli/setup.rpm.sh' | sh \ - && yum install -y infisical - ``` - - - ```dockerfile - RUN apt-get update && apt-get install -y bash curl && curl -1sLf \ - 'https://dl.cloudsmith.io/public/infisical/infisical-cli/setup.deb.sh' | bash \ - && apt-get update && apt-get install -y infisical - ``` - - #### We recommend you to set the version of the CLI to a specific version. This will help keep your CLI version consistent across reinstalls. [View versions](https://cloudsmith.io/~infisical/repos/infisical-cli/packages/) diff --git a/docs/integrations/secret-syncs/checkly.mdx b/docs/integrations/secret-syncs/checkly.mdx index 4599fe634..07382fb35 100644 --- a/docs/integrations/secret-syncs/checkly.mdx +++ b/docs/integrations/secret-syncs/checkly.mdx @@ -37,6 +37,7 @@ description: "Learn how to configure a Checkly Sync for Infisical." - **Checkly Connection**: The Checkly Connection to authenticate with. - **Account**: The Checkly account to sync secrets to. + - **Group**: The Checkly check group to sync secrets to (Optional). Configure the **Sync Options** to specify how secrets should be synced, then click **Next**. diff --git a/docs/sdks/languages/cpp.mdx b/docs/sdks/languages/cpp.mdx index 2e22def9c..27f1aad3e 100644 --- a/docs/sdks/languages/cpp.mdx +++ b/docs/sdks/languages/cpp.mdx @@ -1,6 +1,315 @@ --- title: "Infisical C++ SDK" sidebarTitle: "C++" -url: "https://github.com/Infisical/infisical-cpp-sdk/?tab=readme-ov-file#infisical-c-sdk" icon: "/images/sdks/languages/cpp.svg" ---- \ No newline at end of file +--- + +If you're working with C++, the official Infisical C++ SDK package is the easiest way to fetch and work with secrets for your application. + +## Compatible with C++ 17 and later +The Infisical C++ SDK is compatible with C++ 17 capable compilers. This implies GCC 8 or newer, and clang 3.8 or newer. Earlier versions of C++ are unsupported. + +## Dependencies +- `cURL`: Used internally for crafting HTTP requests. + +## CMake Installation + +```bash +cmake_minimum_required(VERSION 3.14) +project(InfisicalTest) + +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(CMAKE_PREFIX_PATH ${CMAKE_BINARY_DIR}) + +find_package(OpenSSL REQUIRED) + + +include(FetchContent) + + +FetchContent_Declare( + infisical + GIT_REPOSITORY https://github.com/Infisical/infisical-cpp-sdk.git + GIT_TAG 1.0.0 # Replace with the desired version +) + +FetchContent_MakeAvailable(infisical) +FetchContent_GetProperties(infisical) + + +# Example usage. This will differ based on your project structure. +add_executable(my_app src/main.cpp) +target_link_libraries(my_app PRIVATE infisical OpenSSL::SSL OpenSSL::Crypto) +target_include_directories(my_app PRIVATE ${infisical_SOURCE_DIR}/include) +``` + +## Manual Installation +If you're unable to use the recommended CMake installation approach, you can choose to manually build the library and use it in your project. + +```bash +mkdir build +cd build +cmake .. +make +``` + +## Quick-Start Example + +Below you'll find an example that uses the Infisical SDK to fetch a secret with the key `API_KEY` using [Machine Identity Universal Auth](https://infisical.com/docs/documentation/platform/identities/universal-auth) + +More examples can be found in the [/examples](https://github.com/Infisical/infisical-cpp-sdk/tree/main/examples) folder. + +```cpp +#include +#include + +int main() { + + try { + Infisical::InfisicalClient client( + Infisical::ConfigBuilder() + .withHostUrl("https://app.infisical.com") // Optionally change this to your custom Infisical instance URL. + .withAuthentication( + Infisical::AuthenticationBuilder() + .withUniversalAuth("", "") + .build()) + .build()); + + const auto getSecretOptions = Infisical::Input::GetSecretOptionsBuilder() + .withEnvironment("") // dev, staging, prod, etc + .withProjectId("") + .withSecretKey("API_KEY") + .build(); + + const auto apiKeySecret = client.secrets().getSecret(getSecretOptions); + + printf("Secret retrieved, [key=%s] [value=%s]\n", apiKeySecret.getSecretKey().c_str(), apiKeySecret.getSecretValue().c_str()); + } catch (const Infisical::InfisicalError &e) { + std::cerr << "Error: " << e.what() << std::endl; + return 1; + } + + return 0; +} +``` + +## JSON Serialization +The SDK uses [nlohmann/json](https://github.com/nlohmann/json) internally to serialize/deserialize JSON data. This SDK makes no assumptions about which JSON library you use in your project, and you aren't constrained to `nlohmann/json` in any way. Data returned by the SDK is returned as a class, which exposes Getter methods for getting fields such as the secret value or secret key. + + +## Documentation +The Infisical C++ SDK follows a builder pattern for all types of input. Below is a detailed documentation of our currently support methods. + +Everything related to the Infisical SDK lives inside the `Infisical` namespace. + +### InfisicalClient Class +`InfisicalClient(Config &config)` + +```cpp + Infisical::InfisicalClient client( + Infisical::ConfigBuilder() + .withHostUrl("https://app.infisical.com") + .withAuthentication( + Infisical::AuthenticationBuilder() + .withUniversalAuth(clientId, clientSecret) + .build()) + .build()); +``` + +Config is created through the `ConfigBuilder` class. See below for more details + +### Config Class + +`Config` defines the configuration of the Infisical Client itself, such as authentication. + +```cpp +Infisical::Config config = Infisical::ConfigBuilder() + .withHostUrl("https://app.infisical.com") + .withAuthentication( + Infisical::AuthenticationBuilder() + .withUniversalAuth(clientId, clientSecret) + .build()) + .build(); + +Infisical::InfisicalClient client(config); +``` + +- `withHostUrl(string)` _(optional)_: Specify a custom Infisical host URL, pointing to your Infisical instance. Defaults to `https://app.infisical.com` +- `withAuthentication(Infisical::Authentication)`: Configure the authentication that will be used by the SDK. See [Authentication Class](#authentication-class) for more details. +- `build()`: Returns the `Config` object with the options you configured. + +### Authentication Class +```cpp +Infisical::Authentication auth = Infisical::AuthenticationBuilder() + .withUniversalAuth(clientId, clientSecret) + .build(); + +Infisical::Config config = Infisical::ConfigBuilder() + .withAuthentication(std::move(auth)) // Or use inline declaration + .build(); +``` + +- `withUniversalAuth(string, string)`: Specify the Universal Auth Client ID and Client Secret that will be used for authentication. +- `build()`: Returns the `Authentication` object with the options you specified. + +### TSecret Class +The `TSecret` class is the class that's returned by all secret methods (get/list/delete/update/create). It can come in the form of a `std::vector` or a single instance. + +**Available getter methods:** +- `getId(): std::string`: Returns the ID of the secret. +- `getWorkspace(): std::string`: Returns the project ID of the secret. +- `getEnvironment(): std::string`: Returns the environment slug of the secret. +- `getVersion(): unsigned int`: Gets the version of the secret. By default this will always be the latest version unless specified otherwise with `withVersion()` +- `getType(): std::string`: Returns the type of the secret. Can only be `shared` or `personal`. Shared secrets are available to everyone with access to the secret. Personal secrets are personal overwrites of the secret, mainly intended for local development purposes. +- `getSecretKey(): std::string`: Returns the secret key. +- `getSecretValue(): std::string` Returns the secret value. +- `getRotationId(): std::string`: If the secret is a rotation secret, this will return the rotation ID of the secret. If it's a regular secret, this will return an empty string. +- `getSecretPath(): std::string`: Returns the secret path of the secret. +- `getSkipMultilineEncoding(): bool`: Returns whether or not skip multiline encoding is enabled for the secret or not. +`getIsRotatedSecret(): bool`: Returns wether or not the secret is a rotated secret. If `true`, then `getRotationId()` returns the ID of the rotation. + + + +### Secrets + +#### Create Secret +```cpp +const auto createSecretOptions = Infisical::Input::CreateSecretOptionsBuilder() + .withEnvironment("") + .withProjectId("") + .withSecretKey("SECRET_KEY_TO_CREATE") + .withSecretValue("VALUE_TO_CREATE") + .withSecretComment("Secret comment to attach") // Optional + .withSecretPath("/path/where/to/create/secret") // Optional, defaults to / + .withTagIds({"tag-id-1", "tag-id-2"}) // Optional + .build(); + +const auto secret = client.secrets().createSecret(createSecretOptions); +``` + +**Parameters**: +- `withEnvironment(string)`: Specify the slug of the environment to create the secret in. +- `withProjectId(string)`: Specify the ID of the project to create the secret in. +- `withSecretPath(string)`: Specify the secret path to create the secret in. Defaults to `/` +- `withSecretKey(string)`: The secret key to be created. +- `withSecretValue(string)`: The value of the secret to create. +- `withSecretComment(string)` _(optional)_: Optionally add a comment to the secret. +- `withTagIds(std::vector>)` _(optional)_: A list of ID's of tags to attach to the secret. +- `build()`: Returns the `CreateSecretOptions` class that can be passed into the `createSecret()` method. + +**Returns**: +- Returns the created secret as a `TSecret` class. Read more in the [TSecret Class](#tsecret-class) documentation. + +#### Update Secret + +```cpp + const auto updateSecretOptions = Infisical::Input::UpdateSecretOptionsBuilder() + .withEnvironment("") + .withProjectId("") + .withSecretKey("") + .withNewSecretKey("") // Optional + .withSecretValue("") // Optional + .withSecretComment("Updated comment") // Optional + .withSecretReminderNote("Updated reminder note") // Optional + .withSecretReminderRepeatDays(1) // Optional + .withType("shared") // Optional + .withTagIds({"tag-id-3", "tag-id-4"}) // Optional + .build(); + +const auto updatedSecret = client.secrets().updateSecret(updateSecretOptions); +``` + +**Parameters**: +- `withEnvironment(string)`: Specify the slug of the environment where the secret lives in. +- `withProjectId(string)`: Specify the ID of the project where the secret to update lives in. +- `withSecretPath(string)`: Specify the secret path of the secret to update. Defaults to `/`. +- `withType("shared" | "personal")`: _(optional)_: The type of secret to update. Defaults to `shared`. +- `withSecretKey(string)`: The key of the secret you wish to update. +- `withNewSecretKey(string)` _(optional)_: The new key of the secret you wish to update. +- `withSecretValue(string)` _(optional)_: The new value of the secret. +- `withSecretReminderNote(string)` _(optional)_: Update the secret reminder note attached to the secret. +- `withSecretReminderRepeatDays(unsigned int)` _(optional)_: Update the secret reminder repeat days attached to the secret. +- `withTagIds(std::vector>)` _(optional)_: A list of ID's of tags to attach to the secret. +- `build()`: Returns the `UpdateSecretOptions` class that can be passed into the `updateSecret()` method. + +**Returns**: +- Returns the updated secret as a `TSecret` class. Read more in the [TSecret Class](#tsecret-class) documentation. + +#### Get Secret +```cpp +const auto getSecretOptions = Infisical::Input::GetSecretOptionsBuilder() + .withEnvironment("") + .withProjectId("") + .withSecretKey("") + .withType("shared") + .withVersion(2) + .withExpandSecretReferences(true) + .build(); + +const auto secret = client.secrets().getSecret(getSecretOptions); +``` +**Parameters**: +- `withEnvironment(string)`: Specify the slug of the environment where the secret lives in. +- `withProjectId(string)`: Specify the ID of the project where the secret lives in. +- `withSecretPath(string)`: Specify the secret path of the secret to get. Defaults to `/` +- `withType("shared" | "personal")`: _(optional)_: The type of secret to get. Defaults to `shared`. +- `withSecretKey(string)`: The key of the secret to get. +- `withExpandSecretReferences(bool)` _(optional)_: Whether or not to expand secret references automatically. Defaults to `true`. +- `withVersion(unsigned int)` _(optional)_: Optionally fetch a specific version of the secret. If not defined, the latest version of the secret is returned. +- `build()`: Returns the `GetSecretOptions` class that can be passed into the `getSecret()` method. + +**Returns**: +- Returns the secret as a `TSecret` class. Read more in the [TSecret Class](#tsecret-class) documentation. + +#### Delete Secret + +```cpp +const auto deleteSecretOptions = Infisical::Input::DeleteSecretOptionsBuilder() + .withEnvironment("") + .withProjectId("") + .withSecretKey("") + .withType("shared") + .withSecretPath("") + .build(); + +const auto deletedSecret = client.secrets().deleteSecret(deleteSecretOptions); +``` + +**Parameters**: +- `withEnvironment(string)`: Specify the slug of the environment where the secret to delete lives in. +- `withProjectId(string)`: Specify the ID of the project where the secret to delete lives in. +- `withSecretPath(string)`: Specify the secret path of the secret to delete. Defaults to `/` +- `withType("shared" | "personal")`: _(optional)_: The type of secret to delete. Defaults to `shared`. +- `withSecretKey(string)`: The key of the secret to delete. +- `build()` Returns the `DeleteSecretOptions` class that can be passed into the `deleteSecret()` method. + +**Returns**: +- Returns the deleted secret as a `TSecret` class. Read more in the [TSecret Class](#tsecret-class) documentation. + + +#### List Secrets +```cpp +const auto listSecretsOptions = Infisical::Input::ListSecretOptionsBuilder() + .withProjectId(projectId) + .withEnvironment(environment) + .withSecretPath("/") + .withRecursive(false) + .withAddSecretsToEnvironmentVariables(false) + .build(); + +const auto secrets = client.secrets().listSecrets(listSecretsOptions); + +``` + +**Parameters**: +- `withEnvironment(string)`: Specify the slug of the environment to list secrets from. +- `withProjectId(string)`: Specify the ID of the project to fetch secrets from. +- `withSecretPath(string)`: Specify the secret path to fetch secrets from. Defaults to `/` +- `withExpandSecretReferences(bool)` _(optional)_: Whether or not to expand secret references automatically. Defaults to `true`. +- `withRecursive(bool)` _(optional)_: Wether or not to recursively fetch secrets from sub-folders. If set to true, all secrets from the secret path specified with `withSecretPath()` and downwards will be fetched. +- `withAddSecretsToEnvironmentVariables(bool)` _(optional)_: If set to true, the fetched secrets will be automatically set as environment variables, making them accessible with `std::getenv` or equivalent by secret key. +- `build()`: Returns the `ListSecretsOptions` class that can be passed into the `listSecrets()` method. + +**Returns**: +- Returns the listed secrets as `std::vector`. Read more in the [TSecret Class](#tsecret-class) documentation. diff --git a/docs/sdks/languages/csharp.mdx b/docs/sdks/languages/csharp.mdx deleted file mode 100644 index 524778a4a..000000000 --- a/docs/sdks/languages/csharp.mdx +++ /dev/null @@ -1,594 +0,0 @@ ---- -title: "Infisical .NET SDK" -sidebarTitle: ".NET" -url: "https://github.com/Infisical/infisical-dotnet-sdk?tab=readme-ov-file#infisical-net-sdk" -icon: "/images/sdks/languages/dotnet.svg" ---- -{/* -If you're working with C#, the official [Infisical C# SDK](https://github.com/Infisical/sdk/tree/main/languages/csharp) package is the easiest way to fetch and work with secrets for your application. - -- [Nuget Package](https://www.nuget.org/packages/Infisical.Sdk) -- [Github Repository](https://github.com/Infisical/sdk/tree/main/languages/csharp) - - - **Deprecation Notice** - - All versions prior to **2.3.9** should be considered deprecated and are no longer supported by Infisical. Please update to version **2.3.9** or newer. All changes are fully backwards compatible with older versions. - - -## Basic Usage - -```cs -using Infisical.Sdk; - -namespace Example -{ - class Program - { - static void Main(string[] args) - { - - ClientSettings settings = new ClientSettings - { - Auth = new AuthenticationOptions - { - UniversalAuth = new UniversalAuthMethod - { - ClientId = "your-client-id", - ClientSecret = "your-client-secret" - } - } - }; - - - var infisicalClient = new InfisicalClient(settings); - - var getSecretOptions = new GetSecretOptions - { - SecretName = "TEST", - ProjectId = "PROJECT_ID", - Environment = "dev", - }; - var secret = infisicalClient.GetSecret(getSecretOptions); - - - Console.WriteLine($"The value of secret '{secret.SecretKey}', is: {secret.SecretValue}"); - } - } -} -``` - -This example demonstrates how to use the Infisical C# SDK in a C# application. The application retrieves a secret named `TEST` from the `dev` environment of the `PROJECT_ID` project. - - - We do not recommend hardcoding your [Machine Identity Tokens](/platform/identities/overview). Setting it as an environment variable would be best. - - -# Installation - -```console -$ dotnet add package Infisical.Sdk -``` -# Configuration - -Import the SDK and create a client instance with your [Machine Identity](/platform/identities/universal-auth). - -```cs -using Infisical.Sdk; - -namespace Example -{ - class Program - { - static void Main(string[] args) - { - ClientSettings settings = new ClientSettings - { - Auth = new AuthenticationOptions - { - UniversalAuth = new UniversalAuthMethod - { - ClientId = "your-client-id", - ClientSecret = "your-client-secret" - } - } - }; - - - var infisicalClient = new InfisicalClient(settings); // <-- Your SDK client is now ready to use - } - } -} -``` - -### ClientSettings methods - - - - - Your machine identity client ID. - - - Your machine identity client secret. - - - - An access token obtained from the machine identity login endpoint. - - - - Time-to-live (in seconds) for refreshing cached secrets. - If manually set to 0, caching will be disabled, this is not recommended. - - - - Your self-hosted absolute site URL including the protocol (e.g. `https://app.infisical.com`) - - - - Optionally provide a path to a custom SSL certificate file. This can be substituted by setting the `INFISICAL_SSL_CERTIFICATE` environment variable to the contents of the certificate. - - - - The authentication object to use for the client. This is required unless you're using environment variables. - - - - - -### Authentication - -The SDK supports a variety of authentication methods. The most common authentication method is Universal Auth, which uses a client ID and client secret to authenticate. - -#### Universal Auth - -**Using environment variables** -- `INFISICAL_UNIVERSAL_AUTH_CLIENT_ID` - Your machine identity client ID. -- `INFISICAL_UNIVERSAL_AUTH_CLIENT_SECRET` - Your machine identity client secret. - -**Using the SDK directly** -```csharp - ClientSettings settings = new ClientSettings - { - Auth = new AuthenticationOptions - { - UniversalAuth = new UniversalAuthMethod - { - ClientId = "your-client-id", - ClientSecret = "your-client-secret" - } - } - }; - - var infisicalClient = new InfisicalClient(settings); -``` - -#### GCP ID Token Auth - - Please note that this authentication method will only work if you're running your application on Google Cloud Platform. - Please [read more](/documentation/platform/identities/gcp-auth) about this authentication method. - - -**Using environment variables** -- `INFISICAL_GCP_AUTH_IDENTITY_ID` - Your Infisical Machine Identity ID. - -**Using the SDK directly** -```csharp - ClientSettings settings = new ClientSettings - { - Auth = new AuthenticationOptions - { - GcpIdToken = new GcpIdTokenAuthMethod - { - IdentityId = "your-machine-identity-id", - } - } - }; - - - var infisicalClient = new InfisicalClient(settings); -``` - -#### GCP IAM Auth - -**Using environment variables** -- `INFISICAL_GCP_IAM_AUTH_IDENTITY_ID` - Your Infisical Machine Identity ID. -- `INFISICAL_GCP_IAM_SERVICE_ACCOUNT_KEY_FILE_PATH` - The path to your GCP service account key file. - -**Using the SDK directly** -```csharp - ClientSettings settings = new ClientSettings - { - Auth = new AuthenticationOptions - { - GcpIam = new GcpIamAuthMethod - { - IdentityId = "your-machine-identity-id", - ServiceAccountKeyFilePath = "./path/to/your/service-account-key.json" - } - } - }; - - - var infisicalClient = new InfisicalClient(settings); -``` - -#### AWS IAM Auth - - Please note that this authentication method will only work if you're running your application on AWS. - Please [read more](/documentation/platform/identities/aws-auth) about this authentication method. - - -**Using environment variables** -- `INFISICAL_AWS_IAM_AUTH_IDENTITY_ID` - Your Infisical Machine Identity ID. - -**Using the SDK directly** -```csharp - ClientSettings settings = new ClientSettings - { - Auth = new AuthenticationOptions - { - AwsIam = new AwsIamAuthMethod - { - IdentityId = "your-machine-identity-id", - } - } - }; - - - var infisicalClient = new InfisicalClient(settings); -``` - - -#### Azure Auth - - Please note that this authentication method will only work if you're running your application on Azure. - Please [read more](/documentation/platform/identities/azure-auth) about this authentication method. - - -**Using environment variables** -- `INFISICAL_AZURE_AUTH_IDENTITY_ID` - Your Infisical Machine Identity ID. - -**Using the SDK directly** -```csharp - ClientSettings settings = new ClientSettings - { - Auth = new AuthenticationOptions - { - Azure = new AzureAuthMethod - { - IdentityId = "YOUR_IDENTITY_ID", - } - } - }; - - var infisicalClient = new InfisicalClient(settings); -``` - -#### Kubernetes Auth - - Please note that this authentication method will only work if you're running your application on Kubernetes. - Please [read more](/documentation/platform/identities/kubernetes-auth) about this authentication method. - - -**Using environment variables** -- `INFISICAL_KUBERNETES_IDENTITY_ID` - Your Infisical Machine Identity ID. -- `INFISICAL_KUBERNETES_SERVICE_ACCOUNT_TOKEN_PATH_ENV_NAME` - The environment variable name that contains the path to the service account token. This is optional and will default to `/var/run/secrets/kubernetes.io/serviceaccount/token`. - -**Using the SDK directly** -```csharp - ClientSettings settings = new ClientSettings - { - Auth = new AuthenticationOptions - { - Kubernetes = new KubernetesAuthMethod - { - ServiceAccountTokenPath = "/var/run/secrets/kubernetes.io/serviceaccount/token", // Optional - IdentityId = "YOUR_IDENTITY_ID", - } - } - }; - - var infisicalClient = new InfisicalClient(settings); -``` - - - -### Caching - -To reduce the number of API requests, the SDK temporarily stores secrets it retrieves. By default, a secret remains cached for 5 minutes after it's first fetched. Each time it's fetched again, this 5-minute timer resets. You can adjust this caching duration by setting the "cacheTTL" option when creating the client. - -## Working with Secrets - -### client.ListSecrets(options) - -```cs -var options = new ListSecretsOptions -{ - ProjectId = "PROJECT_ID", - Environment = "dev", - Path = "/foo/bar", - AttachToProcessEnv = false, -}; - -var secrets = infisical.ListSecrets(options); -``` - -Retrieve all secrets within the Infisical project and environment that client is connected to - -#### Parameters - - - - - The slug name (dev, prod, etc) of the environment from where secrets should be fetched from. - - - - The project ID where the secret lives in. - - - - The path from where secrets should be fetched from. - - - - Whether or not to set the fetched secrets to the process environment. If true, you can access the secrets like so `System.getenv("SECRET_NAME")`. - - - - Whether or not to include imported secrets from the current path. Read about [secret import](/documentation/platform/secret-reference) - - - - Whether or not to fetch secrets recursively from the specified path. Please note that there's a 20-depth limit for recursive fetching. - - - - Whether or not to expand secret references in the fetched secrets. Read about [secret reference](/documentation/platform/secret-reference) - - - - - -### client.GetSecret(options) - -```cs -var options = new GetSecretOptions - { - SecretName = "AAAA", - ProjectId = "659c781eb2d4fe3e307b77bd", - Environment = "dev", - }; -var secret = infisical.GetSecret(options); -``` - -Retrieve a secret from Infisical. - -By default, `GetSecret()` fetches and returns a shared secret. - -#### Parameters - - - - - The key of the secret to retrieve. - - - The project ID where the secret lives in. - - - The slug name (dev, prod, etc) of the environment from where secrets should be fetched from. - - - The path from where secret should be fetched from. - - - The type of the secret. Valid options are "shared" or "personal". If not specified, the default value is "shared". - - - Whether or not to include imported secrets from the current path. Read about [secret import](/documentation/platform/secret-reference) - - - Whether or not to expand secret references in the fetched secrets. Read about [secret reference](/documentation/platform/secret-reference) - - - - - -### client.CreateSecret(options) - -```cs -var options = new CreateSecretOptions { - Environment = "dev", - ProjectId = "PROJECT_ID", - - SecretName = "NEW_SECRET", - SecretValue = "NEW_SECRET_VALUE", - SecretComment = "This is a new secret", -}; - -var newSecret = infisical.CreateSecret(options); -``` - -Create a new secret in Infisical. - -#### Parameters - - - - - The key of the secret to create. - - - The value of the secret. - - - The project ID where the secret lives in. - - - The slug name (dev, prod, etc) of the environment from where secrets should be fetched from. - - - The path from where secret should be created. - - - The type of the secret. Valid options are "shared" or "personal". If not specified, the default value is "shared". - - - - -### client.UpdateSecret(options) - -```cs -var options = new UpdateSecretOptions { - Environment = "dev", - ProjectId = "PROJECT_ID", - - SecretName = "SECRET_TO_UPDATE", - SecretValue = "NEW VALUE" -}; - -var updatedSecret = infisical.UpdateSecret(options); -``` - -Update an existing secret in Infisical. - -#### Parameters - - - - - The key of the secret to update. - - - The new value of the secret. - - - The project ID where the secret lives in. - - - The slug name (dev, prod, etc) of the environment from where secrets should be fetched from. - - - The path from where secret should be updated. - - - The type of the secret. Valid options are "shared" or "personal". If not specified, the default value is "shared". - - - - -### client.DeleteSecret(options) - -```cs -var options = new DeleteSecretOptions -{ - Environment = "dev", - ProjectId = "PROJECT_ID", - SecretName = "NEW_SECRET", -}; - -var deletedSecret = infisical.DeleteSecret(options); -``` - -Delete a secret in Infisical. - -#### Parameters - - - - - The key of the secret to update. - - - The project ID where the secret lives in. - - - The slug name (dev, prod, etc) of the environment from where secrets should be fetched from. - - - The path from where secret should be deleted. - - - The type of the secret. Valid options are "shared" or "personal". If not specified, the default value is "shared". - - - - -## Cryptography - -### Create a symmetric key - -Create a base64-encoded, 256-bit symmetric key to be used for encryption/decryption. - -```cs -var key = infisical.CreateSymmetricKey(); -``` - -#### Returns (string) -`key` (string): A base64-encoded, 256-bit symmetric key, that can be used for encryption/decryption purposes. - -### Encrypt symmetric -```cs -var options = new EncryptSymmetricOptions -{ - Plaintext = "Infisical is awesome!", - Key = key, -}; - -var encryptedData = infisical.EncryptSymmetric(options); -``` - -#### Parameters - - - - - The plaintext you want to encrypt. - - - The symmetric key to use for encryption. - - - - -#### Returns (object) -`Tag` (string): A base64-encoded, 128-bit authentication tag. -`Iv` (string): A base64-encoded, 96-bit initialization vector. -`CipherText` (string): A base64-encoded, encrypted ciphertext. - -### Decrypt symmetric -```cs -var decryptOptions = new DecryptSymmetricOptions -{ - Key = key, - Ciphertext = encryptedData.Ciphertext, - Iv = encryptedData.Iv, - Tag = encryptedData.Tag, -}; - -var decryptedPlaintext = infisical.DecryptSymmetric(decryptOptions); -``` - -#### Parameters - - - - The ciphertext you want to decrypt. - - - The symmetric key to use for encryption. - - - The initialization vector to use for decryption. - - - The authentication tag to use for decryption. - - - - -#### Returns (string) -`Plaintext` (string): The decrypted plaintext. - */} diff --git a/docs/sdks/languages/dotnet.mdx b/docs/sdks/languages/dotnet.mdx new file mode 100644 index 000000000..7b3d12885 --- /dev/null +++ b/docs/sdks/languages/dotnet.mdx @@ -0,0 +1,358 @@ +--- +title: "Infisical .NET SDK" +sidebarTitle: ".NET" +icon: "/images/sdks/languages/dotnet.svg" +--- + +If you're working with .NET, the official Infisical .NET SDK package is the easiest way to fetch and work with secrets for your application. + +## Installation + +```bash +dotnet add package Infisical.Sdk +``` + +## Getting Started (.NET) + +```csharp +namespace Example; + +using Infisical.Sdk; +using Infisical.Sdk.Model; + +public class Program { + public static void Main(string[] args) { + + var settings = new InfisicalSdkSettingsBuilder() + .WithHostUri("http://localhost:8080") // Optional. Will default to https://app.infisical.com + .Build(); + + var infisicalClient = new InfisicalClient(settings); + + var _ = infisicalClient.Auth().UniversalAuth().LoginAsync("", "").Result; + + var options = new ListSecretsOptions + { + SetSecretsAsEnvironmentVariables = true, + EnvironmentSlug = "", + SecretPath = "/", + ProjectId = "", + }; + + var secrets = infisicalClient.Secrets().ListAsync(options).Result; + + if (secrets == null) + { + throw new Exception("Failed to fetch secrets, returned null response"); + } + + foreach (var secret in secrets) + { + Console.WriteLine($"{secret.SecretKey}: {secret.SecretValue}"); + } + } +} +``` + +## Getting Started (Visual Basic) +```vb +Imports Infisical.Sdk +Imports Infisical.Sdk.Model + +Module Program + Sub Main(args As String()) + Dim settings = New InfisicalSdkSettingsBuilder() _ + .WithHostUri("https://app.infisical.com") _ + .Build() + + Dim infisicalClient As New InfisicalClient(settings) + + Dim authResult = infisicalClient.Auth().UniversalAuth() _ + .LoginAsync("", "machine-identity-universal-auth-client-secret").Result + + Dim options As New ListSecretsOptions With { + .SetSecretsAsEnvironmentVariables = True, + .EnvironmentSlug = "", + .SecretPath = "/", + .ProjectId = "" + } + + Dim secrets = infisicalClient.Secrets().ListAsync(options).Result + + For Each secret In secrets + Console.WriteLine(secret.SecretKey) + if Environment.GetEnvironmentVariable(secret.SecretKey) IsNot Nothing Then + Console.WriteLine("{0} found on environment variables", secret.SecretKey) + End If + Next + + End Sub +End Module +``` + +## Core Methods + +The SDK methods are organized into the following high-level categories: + +1. `Auth()`: Handles authentication methods. +2. `Secrets()`: Manages CRUD operations for secrets. +3. `Pki()`: Programmatically interact with the Infisical PKI. + * `Subscribers()`: Manage PKI Subscribers. + +### `Auth()` + +The `Auth()` component provides methods for authentication: + +### Universal Auth + +#### Authenticating +```cs +var _ = await sdk.Auth().UniversalAuth().LoginAsync( + "CLIENT_ID", + "CLIENT_SECRET" +); +``` + + +**Parameters:** +- `clientId` (string): The client ID of your Machine Identity. +- `clientSecret` (string): The client secret of your Machine Identity. + +### `Secrets()` + +The `Secrets()` sub-class handles operations related to the Infisical secrets management product. + +#### List Secrets + +```cs +Task ListAsync(ListSecretsOptions options); + +throws InfisicalException +``` + +```csharp +var options = new ListSecretsOptions + { + SetSecretsAsEnvironmentVariables = true, + EnvironmentSlug = "dev", + SecretPath = "/test", + Recursive = true, + ExpandSecretReferences = true, + ProjectId = projectId, + ViewSecretValue = true, + }; + +Secret[] secrets = await sdk.Secrets().ListAsync(options); +``` + +**ListSecretsOptions:** +- `ProjectId` (string): The ID of your project. +- `EnvironmentSlug` (string): The environment in which to list secrets (e.g., "dev"). +- `SecretPath` (string): The path to the secrets. +- `ExpandSecretReferences` (boolean): Whether to expand secret references. +- `Recursive` (boolean): Whether to list secrets recursively. +- `SetSecretsAsEnvironmentVariables` (boolean): Set the retrieved secrets as environment variables. + +**Returns:** +- `Task`: The response containing the list of secrets. + +#### Create Secret + +```cs +public Task CreateAsync(CreateSecretOptions options); + +throws InfisicalException +``` + +```cs + +var options = new CreateSecretOptions +{ + SecretName = "SECRET_NAME", + SecretValue = "SECRET_VALUE", + EnvironmentSlug = "", + SecretPath = "/", + ProjectId = "", + Metadata = new SecretMetadata[] { + new SecretMetadata { + Key = "metadata-key", + Value = "metadata-value" + } + } +}; + +Task newSecret = await sdk.Secrets().CreateAsync(options); +``` + +**Parameters:** +- `SecretName` (string): The name of the secret to create +- `SecretValue` (string): The value of the secret. +- `ProjectId` (string): The ID of your project. +- `EnvironmentSlug` (string): The environment in which to create the secret. +- `SecretPath` (string, optional): The path to the secret. +- `Metadata` (object, optional): Attach metadata to the secret. +- `SecretComment` (string, optional): Attach a secret comment to the secret. +- `SecretReminderNote` (string, optional): Attach a secret reminder note to the secret. +- `SecretReminderRepeatDays` (int, optional): Set the reminder repeat days on the secret. +- `SkipMultilineEncoding` (bool, optional): Whether or not to skip multiline encoding for the secret's value. Defaults to `false`. + +**Returns:** +- `Task`: The created secret. + +#### Update Secret + +```cs +public Task UpdateAsync(UpdateSecretOptions options); + +throws InfisicalException +``` + + +```cs + +var updateSecretOptions = new UpdateSecretOptions +{ + SecretName = "EXISTING_SECRET_NAME", + EnvironmentSlug = "", + SecretPath = "/", + NewSecretName = "NEW_SECRET_NAME", + NewSecretValue = "new-secret-value", + ProjectId = "", +}; + + +Task updatedSecret = await sdk.Secrets().UpdateAsync(updateSecretOptions); +``` + +**Parameters:** +- `SecretName` (string): The name of the secret to update.` +- `ProjectId` (string): The ID of your project. +- `EnvironmentSlug` (string): The environment in which to update the secret. +- `SecretPath` (string): The path to the secret. +- `NewSecretValue` (string, optional): The new value of the secret. +- `NewSecretName` (string, optional): A new name for the secret. +- `NewMetadata` (object, optional): New metadata to attach to the secret. + +**Returns:** +- `Task`: The updated secret. + +#### Get Secret by Name + +```cs +public Task GetAsync(GetSecretOptions options); + +throws InfisicalException +``` + +```cs + +var getSecretOptions = new GetSecretOptions +{ + SecretName = "SECRET_NAME", + EnvironmentSlug = "", + SecretPath = "/", + ProjectId = "", +}; + +Secret secret = await sdk.Secrets().GetAsync(getSecretOptions); +``` + +**Parameters:** +- `SecretName` (string): The name of the secret to get` +- `ProjectId` (string): The ID of your project. +- `EnvironmentSlug` (string): The environment in which to retrieve the secret. +- `SecretPath` (string): The path to the secret. +- `ExpandSecretReferences` (boolean, optional): Whether to expand secret references. +- `Type` (SecretType, optional): The type of secret to fetch. Defaults to `Shared`. + + +**Returns:** +- `Task`: The fetched secret. + +#### Delete Secret by Name + +```cs +public Secret DeleteAsync(DeleteSecretOptions options); + +throws InfisicalException +``` + +```cs + +var options = new DeleteSecretOptions +{ + SecretName = "SECRET_TO_DELETE", + EnvironmentSlug = "", + SecretPath = "/", + ProjectId = "", +}; + + +Secret deletedSecret = await sdk.Secrets().DeleteAsync(options); +``` + +**Parameters:** +- `SecretName` (string): The name of the secret to delete. +- `ProjectId` (string): The ID of your project. +- `EnvironmentSlug` (string): The environment in which to delete the secret. +- `SecretPath` (string, optional): The path to the secret. + +**Returns:** +- `Task`: The deleted secret. + + +### `Pki().Subscribers()` + +The `Pki().Subscribers()` sub-class is used to programmatically interact with the Infisical PKI product line. Currently only issuing new certificates and retrieving the latest certificate bundle from a subscriber is supported. More widespread support for the PKI product is coming to the .NET SDK in the near future. + + +#### Issue a new certificate + +```cs +public async Task IssueCertificateAsync(IssueCertificateOptions options); + +throws InfisicalException +``` + +```cs + +var options = new IssueCertificateOptions +{ + SubscriberName = "", + ProjectId = "", +}; + +SubscriberIssuedCertificate newCertificate = await sdk.Pki().Subscribers().IssueCertificateAsync(options); +``` + +**Parameters:** +- `SubscriberName` (string): The name of the subscriber to create a certificate for. +- `ProjectId` (string): The ID of PKI project. + +**Returns:** +- `Task`: The newly issued certificate along with it's credentials for the specified subscriber. + +#### Retrieve latest certificate bundle + +```cs +public async Task RetrieveLatestCertificateBundleAsync(RetrieveLatestCertificateBundleOptions options) + +throws InfisicalException +``` + +```cs +var options = new RetrieveLatestCertificateBundleOptions +{ + SubscriberName = "", + ProjectId = "", +}; + +CertificateBundle latestCertificate = await sdk.Pki().Subscribers().RetrieveLatestCertificateBundleAsync(options); +``` + +**Parameters:** +- `SubscriberName` (string): The name of the subscriber to retrieve the latest certificate bundle for +- `ProjectId` (string): The ID of PKI project. + +**Returns:** +- `Task`: The latest certificate bundle for the specified subscriber. \ No newline at end of file diff --git a/docs/sdks/languages/go.mdx b/docs/sdks/languages/go.mdx index 312f7e307..01250c452 100644 --- a/docs/sdks/languages/go.mdx +++ b/docs/sdks/languages/go.mdx @@ -4,7 +4,7 @@ sidebarTitle: "Go" icon: "/images/sdks/languages/go.svg" --- -If you're working with Go Lang, the official [Infisical Go SDK](https://github.com/infisical/go-sdk) package is the easiest way to fetch and work with secrets for your application. +If you're working with Go, the official Infisical Go SDK package is the easiest way to fetch and work with secrets for your application. - [Package](https://pkg.go.dev/github.com/infisical/go-sdk) - [Github Repository](https://github.com/infisical/go-sdk) diff --git a/docs/sdks/languages/java.mdx b/docs/sdks/languages/java.mdx index 359afb987..7ead4d3dd 100644 --- a/docs/sdks/languages/java.mdx +++ b/docs/sdks/languages/java.mdx @@ -1,575 +1,487 @@ --- title: "Infisical Java SDK" sidebarTitle: "Java" -url: "https://github.com/Infisical/java-sdk?tab=readme-ov-file#infisical-java-sdk" icon: "/images/sdks/languages/java.svg" --- -{ -/* -If you're working with Java, the official [Infisical Java SDK](https://github.com/Infisical/sdk/tree/main/languages/java) package is the easiest way to fetch and work with secrets for your application. +If you're working with Java, the official Infisical Java SDK package is the easiest way to fetch and work with secrets for your application. -- [Maven Package](https://github.com/Infisical/sdk/packages/2019741) -- [Github Repository](https://github.com/Infisical/sdk/tree/main/languages/java) +## Installation -## Basic Usage +Replace `{version}` with the version of the SDK you wish to use. This documentation covers version >=3.0.0. + +### Maven + +```xml + + com.infisical + sdk + {version} + +``` + +### Gradle +```gradle + implementation group: 'com.infisical', name: 'sdk', version: '{version}' +``` + + +### Others +For other build tools, please check our [package snippets](https://central.sonatype.com/artifact/com.infisical/sdk), and select the build tool you're using for your project. + + + +## Getting Started ```java -package com.example.app; +package com.example.example; -import com.infisical.sdk.InfisicalClient; -import com.infisical.sdk.schema.*; +import com.infisical.sdk.InfisicalSdk; +import com.infisical.sdk.SdkConfig; public class Example { - public static void main(String[] args) { - - // Create the authentication settings for the client - ClientSettings settings = new ClientSettings(); - AuthenticationOptions authOptions = new AuthenticationOptions(); - UniversalAuthMethod authMethod = new UniversalAuthMethod(); - authMethod.setClientID("YOUR_IDENTITY_ID"); - authMethod.setClientSecret("YOUR_CLIENT_SECRET"); + public static void main(String[] args) { + var sdk = new InfisicalSdk( + new SdkConfig.Builder() + // Optional, will default to https://app.infisical.com + .withSiteUrl("https://your-infisical-instance.com") + .build() + ); - authOptions.setUniversalAuth(authMethod); - settings.setAuth(authOptions); + sdk.Auth().UniversalAuthLogin( + "CLIENT_ID", + "CLIENT_SECRET" + ); - // Create a new Infisical Client - InfisicalClient client = new InfisicalClient(settings); + var secret = sdk.Secrets().GetSecret( + "", + "", + "", + "", + null, // Expand Secret References (boolean, optional) + null, // Include Imports (boolean, optional) + null // Secret Type (shared/personal, defaults to shared, optional) + ); - // Create the options for fetching the secret - GetSecretOptions options = new GetSecretOptions(); - options.setSecretName("TEST"); - options.setEnvironment("dev"); - options.setProjectID("PROJECT_ID"); - // Fetch the sercret with the provided options - GetSecretResponseSecret secret = client.getSecret(options); - - // Print the value - System.out.println(secret.getSecretValue()); - - // Important to avoid memory leaks! - // If you intend to use the client throughout your entire application, you can omit this line. - client.close(); - } + System.out.println(secret); + } } ``` -This example demonstrates how to use the Infisical Java SDK in a Java application. The application retrieves a secret named `TEST` from the `dev` environment of the `PROJECT_ID` project. +## Core Methods - - We do not recommend hardcoding your [Machine Identity Tokens](/platform/identities/overview). Setting it as an environment variable would be best. - +The SDK methods are organized into the following high-level categories: -# Installation +1. `Auth()`: Handles authentication methods. +2. `Secrets()`: Manages CRUD operations for secrets. -The Infisical Java SDK is hosted on the GitHub Packages Apache Maven registry. Because of this you need to configure your environment properly so it's able to pull dependencies from the GitHub registry. Please check [this guide from GitHub](https://docs.github.com/en/packages/working-with-a-github-packages-registry/working-with-the-apache-maven-registry) on how to achieve this. +### `Auth` -Our package is [located here](https://github.com/Infisical/sdk/packages/2019741). Please follow the installation guide on the page. +The `Auth` component provides methods for authentication: -# Configuration +### Universal Auth -Import the SDK and create a client instance with your [Machine Identity](/platform/identities/universal-auth). +#### Authenticating ```java -import com.infisical.sdk.InfisicalClient; -import com.infisical.sdk.schema.*; - -public class App { - public static void main(String[] args) { - // Create the authentication settings for the client - ClientSettings settings = new ClientSettings(); - AuthenticationOptions authOptions = new AuthenticationOptions(); - UniversalAuthMethod authMethod = new UniversalAuthMethod(); - - authMethod.setClientID("YOUR_IDENTITY_ID"); - authMethod.setClientSecret("YOUR_CLIENT_SECRET"); - - authOptions.setUniversalAuth(authMethod); - settings.setAuth(authOptions); - - // Create a new Infisical Client - InfisicalClient client = new InfisicalClient(settings); // Your client! - } -} +public void UniversalAuthLogin( + String clientId, + String clientSecret +) +throws InfisicalException ``` -### ClientSettings methods - - - - - Your machine identity client ID. - - **This field is deprecated and will be removed in future versions.** Please use the `setAuth()` method on the client settings instead. - - - Your machine identity client secret. - - **This field is deprecated and will be removed in future versions.** Please use the `setAuth()` method on the client settings instead. - - - - An access token obtained from the machine identity login endpoint. - - **This field is deprecated and will be removed in future versions.** Please use the `setAuth()` method on the client settings instead. - - - - Time-to-live (in seconds) for refreshing cached secrets. - If manually set to 0, caching will be disabled, this is not recommended. - - - - Your self-hosted absolute site URL including the protocol (e.g. `https://app.infisical.com`) - - - - Optionally provide a path to a custom SSL certificate file. This can be substituted by setting the `INFISICAL_SSL_CERTIFICATE` environment variable to the contents of the certificate. - - - - The authentication object to use for the client. This is required unless you're using environment variables. - - - - - -### Authentication - -The SDK supports a variety of authentication methods. The most common authentication method is Universal Auth, which uses a client ID and client secret to authenticate. - -#### Universal Auth - -**Using environment variables** -- `INFISICAL_UNIVERSAL_AUTH_CLIENT_ID` - Your machine identity client ID. -- `INFISICAL_UNIVERSAL_AUTH_CLIENT_SECRET` - Your machine identity client secret. - -**Using the SDK directly** ```java - ClientSettings settings = new ClientSettings(); - AuthenticationOptions authOptions = new AuthenticationOptions(); - UniversalAuthMethod authMethod = new UniversalAuthMethod(); - - authMethod.setClientID("YOUR_IDENTITY_ID"); - authMethod.setClientSecret("YOUR_CLIENT_SECRET"); - - authOptions.setUniversalAuth(authMethod); - settings.setAuth(authOptions); - - InfisicalClient client = new InfisicalClient(settings); -``` - -#### GCP ID Token Auth - - Please note that this authentication method will only work if you're running your application on Google Cloud Platform. - Please [read more](/documentation/platform/identities/gcp-auth) about this authentication method. - - -**Using environment variables** -- `INFISICAL_GCP_AUTH_IDENTITY_ID` - Your Infisical Machine Identity ID. - -**Using the SDK directly** -```java - ClientSettings settings = new ClientSettings(); - AuthenticationOptions authOptions = new AuthenticationOptions(); - GCPIDTokenAuthMethod authMethod = new GCPIDTokenAuthMethod(); - - authMethod.setIdentityID("YOUR_MACHINE_IDENTITY_ID"); - - authOptions.setGcpIDToken(authMethod); - settings.setAuth(authOptions); - - InfisicalClient client = new InfisicalClient(settings); -``` - -#### GCP IAM Auth - -**Using environment variables** -- `INFISICAL_GCP_IAM_AUTH_IDENTITY_ID` - Your Infisical Machine Identity ID. -- `INFISICAL_GCP_IAM_SERVICE_ACCOUNT_KEY_FILE_PATH` - The path to your GCP service account key file. - -**Using the SDK directly** -```java - ClientSettings settings = new ClientSettings(); - AuthenticationOptions authOptions = new AuthenticationOptions(); - GCPIamAuthMethod authMethod = new GCPIamAuthMethod(); - - authMethod.setIdentityID("YOUR_MACHINE_IDENTITY_ID"); - authMethod.setServiceAccountKeyFilePath("./path/to/your/service-account-key.json"); - - authOptions.setGcpIam(authMethod); - settings.setAuth(authOptions); - - InfisicalClient client = new InfisicalClient(settings); -``` - -#### AWS IAM Auth - - Please note that this authentication method will only work if you're running your application on AWS. - Please [read more](/documentation/platform/identities/aws-auth) about this authentication method. - - -**Using environment variables** -- `INFISICAL_AWS_IAM_AUTH_IDENTITY_ID` - Your Infisical Machine Identity ID. - -**Using the SDK directly** -```java - ClientSettings settings = new ClientSettings(); - AuthenticationOptions authOptions = new AuthenticationOptions(); - AWSIamAuthMethod authMethod = new AWSIamAuthMethod(); - - authMethod.setIdentityID("YOUR_MACHINE_IDENTITY_ID"); - - authOptions.setAwsIam(authMethod); - settings.setAuth(authOptions); - - InfisicalClient client = new InfisicalClient(settings); -``` - -#### Azure Auth - - Please note that this authentication method will only work if you're running your application on Azure. - Please [read more](/documentation/platform/identities/azure-auth) about this authentication method. - - -**Using environment variables** -- `INFISICAL_AZURE_AUTH_IDENTITY_ID` - Your Infisical Machine Identity ID. - -**Using the SDK directly** -```java - ClientSettings settings = new ClientSettings(); - AuthenticationOptions authOptions = new AuthenticationOptions(); - AzureAuthMethod authMethod = new AzureAuthMethod(); - - authMethod.setIdentityID("YOUR_IDENTITY_ID"); - - authOptions.setAzure(authMethod); - settings.setAuth(authOptions); - - InfisicalClient client = new InfisicalClient(settings); -``` - -#### Kubernetes Auth - - Please note that this authentication method will only work if you're running your application on Kubernetes. - Please [read more](/documentation/platform/identities/kubernetes-auth) about this authentication method. - - -**Using environment variables** -- `INFISICAL_KUBERNETES_IDENTITY_ID` - Your Infisical Machine Identity ID. -- `INFISICAL_KUBERNETES_SERVICE_ACCOUNT_TOKEN_PATH_ENV_NAME` - The environment variable name that contains the path to the service account token. This is optional and will default to `/var/run/secrets/kubernetes.io/serviceaccount/token`. - -**Using the SDK directly** -```java - ClientSettings settings = new ClientSettings(); - AuthenticationOptions authOptions = new AuthenticationOptions(); - KubernetesAuthMethod authMethod = new KubernetesAuthMethod(); - - authMethod.setIdentityID("YOUR_IDENTITY_ID"); - authMethod.setServiceAccountTokenPath("/var/run/secrets/kubernetes.io/serviceaccount/token"); // Optional - - authOptions.setKubernetes(authMethod); - settings.setAuth(authOptions); - - InfisicalClient client = new InfisicalClient(settings); +sdk.Auth().UniversalAuthLogin( + "CLIENT_ID", + "CLIENT_SECRET" +); ``` -### Caching +**Parameters:** +- `clientId` (string): The client ID of your Machine Identity. +- `clientSecret` (string): The client secret of your Machine Identity. -To reduce the number of API requests, the SDK temporarily stores secrets it retrieves. By default, a secret remains cached for 5 minutes after it's first fetched. Each time it's fetched again, this 5-minute timer resets. You can adjust this caching duration by setting the "cacheTTL" option when creating the client. - -## Working with Secrets - -### client.listSecrets(options) +### LDAP Auth ```java -ListSecretsOptions options = new ListSecretsOptions(); -options.setEnvironment("dev"); -options.setProjectID("PROJECT_ID"); -options.setPath("/foo/bar"); -options.setIncludeImports(false); -options.setRecursive(false); -options.setExpandSecretReferences(true); - -SecretElement[] secrets = client.listSecrets(options); +public void LdapAuthLogin( + LdapAuthLoginInput input +) +throws InfisicalException ``` -Retrieve all secrets within the Infisical project and environment that client is connected to - -#### Methods - - - - - The slug name (dev, prod, etc) of the environment from where secrets should be fetched from. - - - - The project ID where the secret lives in. - - - - The path from where secrets should be fetched from. - - - - Whether or not to set the fetched secrets to the process environment. If true, you can access the secrets like so `System.getenv("SECRET_NAME")`. - - - - Whether or not to include imported secrets from the current path. Read about [secret import](/documentation/platform/secret-reference) - - - - Whether or not to fetch secrets recursively from the specified path. Please note that there's a 20-depth limit for recursive fetching. - - - - Whether or not to expand secret references in the fetched secrets. Read about [secret reference](/documentation/platform/secret-reference) - - - - - -### client.getSecret(options) - ```java -GetSecretOptions options = new GetSecretOptions(); -options.setSecretName("TEST"); -options.setEnvironment("dev"); -options.setProjectID("PROJECT_ID"); +var input = LdapAuthLoginInput + .builder() + .identityId("") + .username("") + .password("") + .build(); -GetSecretResponseSecret secret = client.getSecret(options); - -String secretValue = secret.getSecretValue(); +sdk.Auth().LdapAuthLogin(input); ``` -Retrieve a secret from Infisical. +**Parameters:** +- `input` (LdapAuthLoginInput): The input for authenticating with LDAP. + - `identityId` (String): The ID of the machine identity to authenticate with. + - `username` (String): The LDAP username. + - `password` (String): The LDAP password. -By default, `getSecret()` fetches and returns a shared secret. +### Access Token Auth -#### Methods - - - - - The key of the secret to retrieve. - - - The project ID where the secret lives in. - - - The slug name (dev, prod, etc) of the environment from where secrets should be fetched from. - - - The path from where secret should be fetched from. - - - The type of the secret. Valid options are "shared" or "personal". If not specified, the default value is "shared". - - - Whether or not to include imported secrets from the current path. Read about [secret import](/documentation/platform/secret-reference) - - - Whether or not to expand secret references in the fetched secrets. Read about [secret reference](/documentation/platform/secret-reference) - - - - -### client.createSecret(options) +#### Authenticating ```java -CreateSecretOptions createOptions = new CreateSecretOptions(); -createOptions.setSecretName("NEW_SECRET"); -createOptions.setEnvironment("dev"); -createOptions.setProjectID("PROJECT_ID"); -createOptions.setSecretValue("SOME SECRET VALUE"); -createOptions.setPath("/"); // Default -createOptions.setType("shared"); // Default - -CreateSecretResponseSecret newSecret = client.createSecret(createOptions); +public void SetAccessToken( + String accessToken +) +throws InfisicalException ``` -Create a new secret in Infisical. - -#### Methods - - - - - The key of the secret to create. - - - The value of the secret. - - - The project ID where the secret lives in. - - - The slug name (dev, prod, etc) of the environment from where secrets should be fetched from. - - - The path from where secret should be created. - - - The type of the secret. Valid options are "shared" or "personal". If not specified, the default value is "shared". - - - - -### client.updateSecret(options) - ```java -UpdateSecretOptions options = new UpdateSecretOptions(); - -options.setSecretName("SECRET_TO_UPDATE"); -options.setSecretValue("NEW SECRET VALUE"); -options.setEnvironment("dev"); -options.setProjectID("PROJECT_ID"); -options.setPath("/"); // Default -options.setType("shared"); // Default - -UpdateSecretResponseSecret updatedSecret = client.updateSecret(options); +sdk.Auth().SetAccessToken("ACCESS_TOKEN"); ``` -Update an existing secret in Infisical. +**Parameters:** +- `accessToken` (string): The access token you want to use for authentication. -#### Methods +### `Secrets` - - - - The key of the secret to update. - - - The new value of the secret. - - - The project ID where the secret lives in. - - - The slug name (dev, prod, etc) of the environment from where secrets should be fetched from. - - - The path from where secret should be updated. - - - The type of the secret. Valid options are "shared" or "personal". If not specified, the default value is "shared". - - - +This sub-class handles operations related to secrets: -### client.deleteSecret(options) +#### List Secrets ```java -DeleteSecretOptions options = new DeleteSecretOptions(); +public List ListSecrets( + String projectId, + String environmentSlug, + String secretPath, + Boolean expandSecretReferences, + Boolean recursive, + Boolean includeImports, + Boolean setSecretsOnSystemProperties +) -options.setSecretName("SECRET_TO_DELETE"); -options.setEnvironment("dev"); -options.setProjectID("PROJECT_ID"); -options.setPath("/"); // Default -options.setType("shared"); // Default - -DeleteSecretResponseSecret deletedSecret = client.deleteSecret(options); +throws InfisicalException ``` -Delete a secret in Infisical. - -#### Methods - - - - - The key of the secret to update. - - - The project ID where the secret lives in. - - - The slug name (dev, prod, etc) of the environment from where secrets should be fetched from. - - - The path from where secret should be deleted. - - - The type of the secret. Valid options are "shared" or "personal". If not specified, the default value is "shared". - - - - -## Cryptography - -### Create a symmetric key - -Create a base64-encoded, 256-bit symmetric key to be used for encryption/decryption. - ```java -String key = client.createSymmetricKey(); +List secrets = sdk.Secrets().ListSecrets( + "", + "", // dev, prod, staging, etc. + "/secret/path", // `/` is the root folder + false, // Should expand secret references + false, // Should get secrets recursively from sub folders + false, // Should include imports + false // Should set the fetched secrets as key/value pairs on the system properties. Makes the secrets accessible as System.getProperty("") +); ``` -#### Returns (string) -`key` (string): A base64-encoded, 256-bit symmetric key, that can be used for encryption/decryption purposes. +**Parameters:** +- `projectId` (string): The ID of your project. +- `environmentSlug` (string): The environment in which to list secrets (e.g., "dev"). +- `secretPath` (string): The path to the secrets. +- `expandSecretReferences` (boolean): Whether to expand secret references. +- `recursive` (boolean): Whether to list secrets recursively. +- `includeImports` (boolean): Whether to include imported secrets. +- `setSecretsOnSystemProperties` (boolean): Set the retrieved secrets as key/value pairs on the system properties, making them accessible through `System.getProperty("")` + +**Returns:** +- `List`: The response containing the list of secrets. + +#### Create Secret + -### Encrypt symmetric ```java -EncryptSymmetricOptions options = new EncryptSymmetricOptions(); -options.setKey(key); -options.setPlaintext("Infisical is awesome!"); - -EncryptSymmetricResponse encryptedData = client.encryptSymmetric(options); +public Secret CreateSecret( + String secretName, + String secretValue, + String projectId, + String environmentSlug, + String secretPath +) +throws InfisicalException ``` -#### Methods - - - - - The plaintext you want to encrypt. - - - The symmetric key to use for encryption. - - - - -#### Returns (object) -`tag (getTag())` (string): A base64-encoded, 128-bit authentication tag. -`iv (getIv())` (string): A base64-encoded, 96-bit initialization vector. -`ciphertext (getCipherText())` (string): A base64-encoded, encrypted ciphertext. - - -### Decrypt symmetric ```java -DecryptSymmetricOptions decryptOptions = new DecryptSymmetricOptions(); -decryptOptions.setKey(key); -decryptOptions.setCiphertext(encryptedData.getCiphertext()); -decryptOptions.setIv(encryptedData.getIv()); -decryptOptions.setTag(encryptedData.getTag()); - -String decryptedString = client.decryptSymmetric(decryptOptions); +Secret newSecret = sdk.Secrets().CreateSecret( + "NEW_SECRET_NAME", + "secret-value", + "", + "", // dev, prod, staging, etc. + "/secret/path", // `/` is the root folder +); ``` -#### Methods - - - - The ciphertext you want to decrypt. - - - The symmetric key to use for encryption. - - - The initialization vector to use for decryption. - - - The authentication tag to use for decryption. - - - +**Parameters:** +- `secretName` (string): The name of the secret to create +- `secretValue` (string): The value of the secret. +- `projectId` (string): The ID of your project. +- `environmentSlug` (string): The environment in which to create the secret. +- `secretPath` (string, optional): The path to the secret. -#### Returns (string) -`Plaintext` (string): The decrypted plaintext. -*/} \ No newline at end of file +**Returns:** +- `Secret`: The created secret. + +#### Update Secret + +```java +public Secret UpdateSecret( + String secretName, + String projectId, + String environmentSlug, + String secretPath, + String newSecretValue, + String newSecretName + ) + +throws InfisicalException +``` + + +```java +Secret updatedSecret = sdk.Secrets().UpdateSecret( + "SECRET_NAME", + "", + "", // dev, prod, staging, etc. + "/secret/path", // `/` is the root folder + "NEW_SECRET_VALUE", // nullable + "NEW_SECRET_NAME" // nullable +); +``` + +**Parameters:** +- `secretName` (string): The name of the secret to update. +- `projectId` (string): The ID of your project. +- `environmentSlug` (string): The environment in which to update the secret. +- `secretPath` (string): The path to the secret. +- `newSecretValue` (string, nullable): The new value of the secret. +- `newSecretName` (string, nullable): A new name for the secret. + +**Returns:** +- `Secret`: The updated secret. + +#### Get Secret by Name + +```java +public Secret GetSecret( + String secretName, + String projectId, + String environmentSlug, + String secretPath, + Boolean expandSecretReferences, + Boolean includeImports, + String secretType + ) +throws InfisicalException +``` + +```java +Secret secret = sdk.Secrets().GetSecret( + "SECRET_NAME", + "", + "", // dev, prod, staging, etc. + "/secret/path", // `/` is the root folder + false, // Should expand secret references + false, // Should get secrets recursively from sub folders + false, // Should include imports + "shared" // Optional Secret Type (defaults to "shared") +); +``` + +**Parameters:** +- `secretName` (string): The name of the secret to get` +- `projectId` (string): The ID of your project. +- `environmentSlug` (string): The environment in which to retrieve the secret. +- `secretPath` (string): The path to the secret. +- `expandSecretReferences` (boolean, optional): Whether to expand secret references. +- `includeImports` (boolean, optional): Whether to include imported secrets. +- `secretType` (personal | shared, optional): The type of secret to fetch. + + +**Returns:** +- `Secret`: The fetched secret. + +#### Delete Secret by Name + +```java +public Secret DeleteSecret( + String secretName, + String projectId, + String environmentSlug, + String secretPath + ) +throws InfisicalException +``` + +```java +Secret deletedSecret = sdk.Secrets().DeleteSecret( + "SECRET_NAME", + "", + "", // dev, prod, staging, etc. + "/secret/path", // `/` is the root folder +); +``` + +**Parameters:** +- `secretName` (string): The name of the secret to delete. +- `projectId` (string): The ID of your project. +- `environmentSlug` (string): The environment in which to delete the secret. +- `secretPath` (string, optional): The path to the secret. + +**Returns:** +- `Secret`: The deleted secret. + + +### `Folders` + +#### Get Folder By Name + +```java +public Folder Get( + String folderId +); +throws InfisicalException +``` + +```java +Folder folder = sdk.Folders().Get(""); +``` + +**Parameters:** +- `folderId` (String): The ID of the folder to retrieve. + +**Returns:** +- `Folder`: The retrieved folder. + +#### List Folders + +```java +public List List( + ListFoldersInput input +) +throws InfisicalException +``` + +```java +ListFoldersInput input = ListFoldersInput + .builder() + .projectId("") + .environmentSlug("") + .folderPath("/") + .recursive(false) + .build(); + +List folders = sdk.Folders().List(input); +``` + + +**Parameters:** +- `input` (ListFoldersInput): The input for listing folders. + - `projectId` (String): The ID of the project to list folders from. + - `environmentSlug` (String): The slug of the environment to list folders from. + - `folderPath` (String): The path to list folders from. Defaults to `/`. + - `recursive` (Boolean): Whether or not to list sub-folders recursively from the specified folder path and downwards. Defaults to `false`. + +**Returns:** +- `List`: The retrieved folders. + +#### Create Folder + +```java +public Folder Create( + CreateFolderInput input +) +throws InfisicalException +``` + +```java +var input = CreateFolderInput + .builder() + .projectId("") + .environmentSlug("") + .folderName("") + .folderPath("/") + .description("Optional folder description") + .build(); + +Folder createdFolder = sdk.Folders().Create(input); +``` + +**Parameters:** +- `input` (CreateFolderInput): The input for creating a folder. + - `projectId` (String): The ID of the project to create the folder in. + - `environmentSlug` (String): The slug of the environment to create the folder in. + - `folderPath` (String): The path to create the folder in. Defaults to `/`. + - `folderName` (String): The name of the folder to create. + - `description` (String): The description of the folder to create. This is optional. + +**Returns:** +- `Folder`: The created folder. + +#### Update Folder + +```java +public Folder Update( + UpdateFolderInput input +) +throws InfisicalException +``` + +```java +var input = UpdateFolderInput + .builder() + .projectId("") + .environmentSlug("") + .folderId("") + .newFolderName("") + .folderPath("/") + .build(); + +Folder updatedFolder = sdk.Folders().Update(input); +``` + +**Parameters:** +- `input` (UpdateFolderInput): The input for updating a folder. + - `projectId` (String): The ID of the project where the folder exists. + - `environmentSlug` (String): The slug of the environment where the folder exists. + - `folderPath` (String): The path of the folder to update. + - `folderId` (String): The ID of the folder to update. + - `newFolderName` (String): The new folder name. + +**Returns:** +- `Folder`: The updated folder. + +#### Delete Folder + +```java +public Folder Delete( + DeleteFolderInput input +) +throws InfisicalException +``` + +```java +var input = DeleteFolderInput + .builder() + .folderId("") + .environmentSlug("") + .projectId("") + .build(); + +Folder deletedFolder = sdk.Folders().Delete(input); +``` + + +**Parameters:** +- `input` (DeleteFolderInput): The input for deleting a folder. + - `projectId` (String): The ID of the project where the folder exists. + - `environmentSlug` (String): The slug of the environment where the folder exists. + - `folderId` (String): The ID of the folder to delete. + +**Returns:** +- `Folder`: The deleted folder. \ No newline at end of file diff --git a/docs/sdks/languages/node.mdx b/docs/sdks/languages/node.mdx index 69bb36e97..e28e9c9f2 100644 --- a/docs/sdks/languages/node.mdx +++ b/docs/sdks/languages/node.mdx @@ -1,558 +1,559 @@ --- title: "Infisical Node.js SDK" sidebarTitle: "Node.js" -url: "https://github.com/Infisical/node-sdk-v2?tab=readme-ov-file#infisical-nodejs-sdk" icon: "/images/sdks/languages/node.svg" --- -{/* -If you're working with Node.js, the official [Infisical Node SDK](https://github.com/Infisical/sdk/tree/main/languages/node) package is the easiest way to fetch and work with secrets for your application. +If you're working with Node.js, the official Infisical Node.js SDK package is the easiest way to fetch and work with secrets for your application. -- [NPM Package](https://www.npmjs.com/package/@infisical/sdk) -- [Github Repository](https://github.com/Infisical/sdk/tree/main/languages/node) +## Deprecation Notice -## Basic Usage - -```js -import express from "express"; - -import { InfisicalClient } from "@infisical/sdk"; - -const app = express(); - -const PORT = 3000; - -const client = new InfisicalClient({ - siteUrl: "https://app.infisical.com", // Optional, defaults to https://app.infisical.com - auth: { - universalAuth: { - clientId: "YOUR_CLIENT_ID", - clientSecret: "YOUR_CLIENT_SECRET" - } - } -}); - -app.get("/", async (req, res) => { - // Access the secret - - const name = await client.getSecret({ - environment: "dev", - projectId: "PROJECT_ID", - path: "/", - type: "shared", - secretName: "NAME" - }); - - res.send(`Hello! My name is: ${name.secretValue}`); -}); - -app.listen(PORT, async () => { - // initialize client - - console.log(`App listening on port ${PORT}`); -}); -``` - -This example demonstrates how to use the Infisical Node SDK with an Express application. The application retrieves a secret named "NAME" and responds to requests with a greeting that includes the secret value. - - - We do not recommend hardcoding your [Machine Identity Tokens](/documentation/platform/identities/overview). Setting it as an environment variable - would be best. - +Please be aware that all versions prior to `4.0.0` are officially fully unsupported. +Please upgrade to version 4.0.0 or newer in order to receive the latest updates. ## Installation -Run `npm` to add `@infisical/sdk` to your project. - -```console -$ npm install @infisical/sdk +```bash +npm install @infisical/sdk ``` -## Configuration +## Getting Started -Import the SDK and create a client instance with your [Machine Identity](/documentation/platform/identities/overview). +```typescript +import { InfisicalSDK } from '@infisical/sdk' - - - ```js - import { InfisicalClient, LogLevel } from "@infisical/sdk"; +const client = new InfisicalSDK({ + siteUrl: "your-infisical-instance.com" // Optional, defaults to https://app.infisical.com +}); - const client = new InfisicalClient({ - auth: { - universalAuth: { - clientId: "YOUR_CLIENT_ID", - clientSecret: "YOUR_CLIENT_SECRET" - } - }, - logLevel: LogLevel.Error - }); - ``` +// Authenticate with Infisical +await client.auth().universalAuth.login({ + clientId: "", + clientSecret: "" +}); - - - ```js - const { InfisicalClient } = require("@infisical/sdk"); +const allSecrets = await client.secrets().listSecrets({ + environment: "dev", // stg, dev, prod, or custom environment slugs + projectId: "" +}); - const client = new InfisicalClient({ - auth: { - universalAuth: { - clientId: "YOUR_CLIENT_ID", - clientSecret: "YOUR_CLIENT_SECRET" - } - }, - }); - ``` +console.log("Fetched secrets", allSecrets) +``` - - +## Core Methods -### Parameters +The SDK methods are organized into the following high-level categories: - - - - Your machine identity client ID. - - **This field is deprecated and will be removed in future versions.** Please use the `auth.universalAuth.clientId` field instead. - - - Your machine identity client secret. - - **This field is deprecated and will be removed in future versions.** Please use the `auth.universalAuth.clientSecret` field instead. - +1. `auth`: Handles authentication methods. +2. `secrets`: Manages CRUD operations for secrets. +3. `dynamicSecrets`: Manages dynamic secrets and leases. +4. `projects`: Creates and manages projects. +5. `environments`: Creates and manages environments. +6. `folders`: Creates and manages folders. - - An access token obtained from the machine identity login endpoint. - - **This field is deprecated and will be removed in future versions.** Please use the `auth.accessToken` field instead. - +### `auth` - - Time-to-live (in seconds) for refreshing cached secrets. - If manually set to 0, caching will be disabled, this is not recommended. - - - - Your self-hosted absolute site URL including the protocol (e.g. `https://app.infisical.com`) - - - The level of logs you wish to log The logs are derived from Rust, as we have written our base SDK in Rust. - - - - Optionally provide a path to a custom SSL certificate file. This can be substituted by setting the `INFISICAL_SSL_CERTIFICATE` environment variable to the contents of the certificate. - - - - The authentication object to use for the client. This is required unless you're using environment variables. - - - - - - -### Authentication - -The SDK supports a variety of authentication methods. The most common authentication method is Universal Auth, which uses a client ID and client secret to authenticate. +The `Auth` component provides methods for authentication: #### Universal Auth -**Using environment variables** -- `INFISICAL_UNIVERSAL_AUTH_CLIENT_ID` - Your machine identity client ID. -- `INFISICAL_UNIVERSAL_AUTH_CLIENT_SECRET` - Your machine identity client secret. - -**Using the SDK directly** -```js -const client = new InfisicalClient({ - auth: { - universalAuth: { - clientId: "YOUR_CLIENT_ID", - clientSecret: "YOUR_CLIENT_SECRET" - } - } +#### Authenticating +```typescript +await client.auth().universalAuth.login({ + clientId: "", + clientSecret: "" }); ``` -#### GCP ID Token Auth - - Please note that this authentication method will only work if you're running your application on Google Cloud Platform. - Please [read more](/documentation/platform/identities/gcp-auth) about this authentication method. - +**Parameters:** +- `options` (object): + - `clientId` (string): The client ID of your Machine Identity. + - `clientSecret` (string): The client secret of your Machine Identity. -**Using environment variables** -- `INFISICAL_GCP_AUTH_IDENTITY_ID` - Your Infisical Machine Identity ID. - -**Using the SDK directly** -```js -const client = new InfisicalClient({ - auth: { - gcpIdToken: { - identityId: "YOUR_IDENTITY_ID" - } - } -}); +#### Renewing +You can renew the authentication token that is currently set by using the `renew()` method. +```typescript +await client.auth().universalAuth.renew(); ``` -#### GCP IAM Auth +#### Manually set access token +By default, when you run a successful `.login()` method call, the access token returned will be auto set for the client instance. However, if you wish to set the access token manually, you may use this method. -**Using environment variables** -- `INFISICAL_GCP_IAM_AUTH_IDENTITY_ID` - Your Infisical Machine Identity ID. -- `INFISICAL_GCP_IAM_SERVICE_ACCOUNT_KEY_FILE_PATH` - The path to your GCP service account key file. - -**Using the SDK directly** -```js -const client = new InfisicalClient({ - auth: { - gcpIam: { - identityId: "YOUR_IDENTITY_ID", - serviceAccountKeyFilePath: "./path/to/your/service-account-key.json" - } - } -}); +```typescript +client.auth().accessToken("") ``` -#### AWS IAM Auth - - Please note that this authentication method will only work if you're running your application on AWS. - Please [read more](/documentation/platform/identities/aws-auth) about this authentication method. - - -**Using environment variables** -- `INFISICAL_AWS_IAM_AUTH_IDENTITY_ID` - Your Infisical Machine Identity ID. - -**Using the SDK directly** -```js -const client = new InfisicalClient({ - auth: { - awsIam: { - identityId: "YOUR_IDENTITY_ID" - } - } -}); -``` - -#### Azure Auth - - Please note that this authentication method will only work if you're running your application on Azure. - Please [read more](/documentation/platform/identities/azure-auth) about this authentication method. - - -**Using environment variables** -- `INFISICAL_AZURE_AUTH_IDENTITY_ID` - Your Infisical Machine Identity ID. - -**Using the SDK directly** -```js -const client = new InfisicalClient({ - auth: { - azure: { - identityId: "YOUR_IDENTITY_ID" - } - } -}); -``` +**Parameters:** +- `accessToken` (string): The access token to be used for authentication. _This should not include "Bearer"._ -#### Kubernetes Auth - - Please note that this authentication method will only work if you're running your application on Kubernetes. - Please [read more](/documentation/platform/identities/kubernetes-auth) about this authentication method. - +#### AWS IAM -**Using environment variables** -- `INFISICAL_KUBERNETES_IDENTITY_ID` - Your Infisical Machine Identity ID. -- `INFISICAL_KUBERNETES_SERVICE_ACCOUNT_TOKEN_PATH_ENV_NAME` - The environment variable name that contains the path to the service account token. This is optional and will default to `/var/run/secrets/kubernetes.io/serviceaccount/token`. + + AWS IAM auth only works when the SDK is being used from within an AWS service, such as Lambda, EC2, etc. + -**Using the SDK directly** -```js -const client = new InfisicalClient({ - auth: { - kubernetes: { - identityId: "YOUR_IDENTITY_ID", - serviceAccountTokenPathEnvName: "/var/run/secrets/kubernetes.io/serviceaccount/token" // Optional - } - } -}); -``` - -### Caching - -To reduce the number of API requests, the SDK temporarily stores secrets it retrieves. By default, a secret remains cached for 5 minutes after it's first fetched. Each time it's fetched again, this 5-minute timer resets. You can adjust this caching duration by setting the "cacheTtl" option when creating the client. - -## Working with Secrets - -### client.listSecrets(options) - -```js -const secrets = await client.listSecrets({ - environment: "dev", - projectId: "PROJECT_ID", - path: "/foo/bar/", - includeImports: false -}); -``` - -Retrieve all secrets within the Infisical project and environment that client is connected to - -#### Parameters - - - - - The slug name (dev, prod, etc) of the environment from where secrets should be fetched from. - - - The project ID where the secret lives in. - - - - The path from where secrets should be fetched from. - - - - Whether or not to set the fetched secrets to the process environment. If true, you can access the secrets like so `process.env["SECRET_NAME"]`. - - - - Whether or not to fetch secrets recursively from the specified path. Please note that there's a 20-depth limit for recursive fetching. - - - - Whether or not to expand secret references in the fetched secrets. Read about [secret reference](/documentation/platform/secret-reference) - - - - Whether or not to include imported secrets from the current path. Read about [secret import](/documentation/platform/secret-reference) - - - - - -### client.getSecret(options) - -```js -const secret = await client.getSecret({ - environment: "dev", - projectId: "PROJECT_ID", - secretName: "API_KEY", - path: "/", - type: "shared" -}); -``` - -Retrieve a secret from Infisical. - -By default, `getSecret()` fetches and returns a shared secret. - -#### Parameters - - - - - The key of the secret to retrieve. - - - The project ID where the secret lives in. - - - The slug name (dev, prod, etc) of the environment from where secrets should be fetched from. - - - The path from where secret should be fetched from. - - - The type of the secret. Valid options are "shared" or "personal". If not specified, the default value is "shared". - - - Whether or not to include imported secrets from the current path. Read about [secret import](/documentation/platform/secret-reference) - - - Whether or not to expand secret references in the fetched secrets. Read about [secret reference](/documentation/platform/secret-reference) - - - - -### client.createSecret(options) - -```js -const newApiKey = await client.createSecret({ - projectId: "PROJECT_ID", - environment: "dev", - secretName: "API_KEY", - secretValue: "SECRET VALUE", - path: "/", - type: "shared" -}); -``` - -Create a new secret in Infisical. - - - - - The key of the secret to create. - - - The value of the secret. - - - The project ID where the secret lives in. - - - The slug name (dev, prod, etc) of the environment from where secrets should be fetched from. - - - The path from where secret should be created. - - - The type of the secret. Valid options are "shared" or "personal". If not specified, the default value is "shared". - - - - -### client.updateSecret(options) - -```js -const updatedApiKey = await client.updateSecret({ - secretName: "API_KEY", - secretValue: "NEW SECRET VALUE", - projectId: "PROJECT_ID", - environment: "dev", - path: "/", - type: "shared" -}); -``` - -Update an existing secret in Infisical. - -#### Parameters - - - - - The key of the secret to update. - - - The new value of the secret. - - - The project ID where the secret lives in. - - - The slug name (dev, prod, etc) of the environment from where secrets should be fetched from. - - - The path from where secret should be updated. - - - The type of the secret. Valid options are "shared" or "personal". If not specified, the default value is "shared". - - - - -### client.deleteSecret(options) - -```js -const deletedSecret = await client.deleteSecret({ - secretName: "API_KEY", - - environment: "dev", - projectId: "PROJECT_ID", - path: "/", - - type: "shared" -}); -``` - -Delete a secret in Infisical. - - - - - The key of the secret to update. - - - The project ID where the secret lives in. - - - The slug name (dev, prod, etc) of the environment from where secrets should be fetched from. - - - The path from where secret should be deleted. - - - The type of the secret. Valid options are "shared" or "personal". If not specified, the default value is "shared". - - - - -## Cryptography - -### Create a symmetric key - -Create a base64-encoded, 256-bit symmetric key to be used for encryption/decryption. - -```js -const key = client.createSymmetricKey(); -``` - -#### Returns (string) -`key` (string): A base64-encoded, 256-bit symmetric key, that can be used for encryption/decryption purposes. - -### Encrypt symmetric -```js -const { iv, tag, ciphertext } = await client.encryptSymmetric({ - key: key, - plaintext: "Infisical is awesome!", +#### Authenticating +```typescript +await client.auth().awsIamAuth.login({ + identityId: "" }) ``` -#### Parameters +**Parameters:** +- `options` (object): + - `identityId` (string): The ID of your identity - - - - The plaintext you want to encrypt. - - - The symmetric key to use for encryption. - - - +#### Renewing +You can renew the authentication token that is currently set by using the `renew()` method. +```typescript +await client.auth().awsIamAuth.renew(); +``` -#### Returns (object) -`tag` (string): A base64-encoded, 128-bit authentication tag. -`iv` (string): A base64-encoded, 96-bit initialization vector. -`ciphertext` (string): A base64-encoded, encrypted ciphertext. -### Decrypt symmetric -```js -const decryptedString = await client.decryptSymmetric({ - key: key, - iv: iv, - tag: tag, - ciphertext: ciphertext, +### `secrets` + +This sub-class handles operations related to secrets: + +#### List Secrets + +```typescript +const allSecrets = await client.secrets().listSecrets({ + environment: "dev", + projectId: "", + expandSecretReferences: true, + viewSecretValue: true, + includeImports: false, + recursive: false, + secretPath: "/foo/bar", }); ``` -#### Parameters - - - - The ciphertext you want to decrypt. - - - The symmetric key to use for encryption. - - - The initialization vector to use for decryption. - - - The authentication tag to use for decryption. - - - +**Parameters:** +- `projectId` (string): The ID of your project. +- `environment` (string): The environment in which to list secrets (e.g., "dev"). +- `secretPath` (str): The path to the secrets. +- `expandSecretReferences` (bool, optional): Whether to expand secret references. +- `viewSecretValue` (bool, optional): Whether or not to reveal the secret value of the secrets. If set to `false`, the `secretValue` is masked with ``. Defaults to `true`. +- `recursive` (bool, optional): Whether to list secrets recursively. +- `includeImports` (bool, optional): Whether to include imported secrets. +- `tagFilters` (string[], optional): Tags to filter secrets. -#### Returns (string) -`plaintext` (string): The decrypted plaintext. +**Returns:** +- `ListSecretsResponse`: The response containing the list of secrets. -*/} \ No newline at end of file +#### List secrets with imports + +The `listSecretsWithImports` method makes it easier to get all your secrets at once. The imported secrets will automatically be added to the secrets returned. The secrets in the selected environment will take precedence over the imported secrets. This means if you have secrets with conflicting names, the secret from the environment the import was imported into, will take precedence. + +```typescript +const allSecrets = await client.secrets().listSecretsWithImports({ + environment: "dev", + projectId: "", + expandSecretReferences: true, + viewSecretValue: true, + recursive: false, + secretPath: "/foo/bar" +}); +``` + +**Parameters:** +- `projectId` (string): The ID of your project. +- `environment` (string): The environment in which to list secrets (e.g., "dev"). +- `secretPath` (str): The path to the secrets. +- `expandSecretReferences` (bool, optional): Whether to expand secret references. +- `viewSecretValue` (bool, optional): Whether or not to reveal the secret value of the secrets. If set to `false`, the `secretValue` is masked with ``. Defaults to `true`. +- `recursive` (bool, optional): Whether to list secrets recursively. +- `tagFilters` (string[], optional): Tags to filter secrets. + +**Returns:** +- `Secret[]`: Returns the list of secrets objects, with imports. + + + +#### Create Secret + +```typescript + const newSecret = await client.secrets().createSecret("SECRET_NAME", { + environment: "dev", + projectId: "", + secretValue: "SECRET_VALUE", + secretComment: "This is a new secret", // Optional + secretPath: "/foo/bar", // Optional + secretReminderNote: "This is a reminder note", // Optional + secretReminderRepeatDays: 7, // Optional + skipMultilineEncoding: false, // Optional + tagIds: ["tagId1", "tagId2"], // Optional + type: "personal" // Optional + }); +``` + +**Parameters:** +- `secretName` (string): The name of the secret to create +- `options` (object): + - `projectId` (string): The ID of your project. + - `environment` (str): The environment in which to create the secret. + - `secretValue` (str): The value of the secret. + - `secretPath` (string, optional): The path to the secret. + - `secretComment` (str, optional): A comment associated with the secret. + - `skipMultilineEncoding` (bool, optional): Whether to skip encoding for multiline secrets. + - `secretReminderNote` (string, optional): A note for the secret reminder. + - `secretReminderRepeatDays` (number, optional): Number of days after which to repeat secret reminders. + - `tagIds` (string[], optional): Array of tags to assign to the new secret. + - `type` (personal | shared, optional): Which type of secret to create. + +**Returns:** +- `CreateSecretResponse`: The response after creating the secret. + +#### Update Secret + +```typescript +const updatedSecret = await client.secrets().updateSecret("SECRET_TO_UPDATE", { + environment: "dev", + projectId: "", + secretValue: "UPDATED_SECRET_VALUE", + newSecretName: "NEW_SECRET_NAME2", // Optional + secretComment: "This is an updated secret", // Optional + secretPath: "/foo/bar", // Optional + secretReminderNote: "This is an updated reminder note", // Optional + secretReminderRepeatDays: 14, // Optional + skipMultilineEncoding: false, // Optional + tagIds: ["tagId1", "tagId2"], // Optional + type: "personal", // Optional + metadata: { // Optional + extra: "metadata" + } +}); +``` + +**Parameters:** +- `secretName` (string): The name of the secret to update.` +- `options` (object): + - `environment` (str): The environment in which to update the secret. + - `projectId` (str): The ID of your project. + - `secretValue` (str, optional): The new value of the secret. + - `newSecretName` (str, optional): A new name for the secret. + - `secretComment` (str, optional): An updated comment associated with the secret. + - `secretPath` (str): The path to the secret. + - `secretReminderNote` (str, optional): An updated note for the secret reminder. + - `secretReminderRepeatDays` (number, optional): Updated number of days after which to repeat secret reminders. + - `skipMultilineEncoding` (bool, optional): Whether to skip encoding for multiline secrets. + - `tagIds` (string[], optional): Array of tags to assign to the secret. + - `type` (personal | shared, optional): Which type of secret to create. + - `metadata` (object, optional): Assign additional details to the secret, accessible through the API. + +**Returns:** +- `UpdateSecretResponse`: The response after updating the secret. + +#### Get Secret by Name + +```typescript + const singleSecret = await client.secrets().getSecret({ + environment: "dev", + projectId: "", + secretName: "DATABASE_URL", + expandSecretReferences: true, // Optional + viewSecretValue: true, // Optional + includeImports: true, // Optional + secretPath: "/foo/bar", // Optional + type: "shared", // Optional + version: 1 // Optional + }); +``` + +**Parameters:** +- `environment` (str): The environment in which to retrieve the secret. +- `projectId` (str): The ID of your project. +- `secretName` (str): The name of the secret. +- `secretPath` (str, optional): The path to the secret. +- `expandSecretReferences` (bool, optional): Whether to expand secret references. +- `viewSecretValue` (bool, optional): Whether or not to reveal the secret value of the secret. If set to `false`, the `secretValue` is masked with ``. Defaults to `true`. +- `includeImports` (bool): Whether to include imported secrets. +- `version` (str, optional): The version of the secret to retrieve. Fetches the latest by default. +- `type` (personal | shared, optional): The type of secret to fetch. + + +**Returns:** +- `Secret`: Returns the secret object. + +#### Delete Secret by Name + +```typescript +const deletedSecret = await client.secrets().deleteSecret("SECRET_TO_DELETE", { + environment: "dev", + projectId: "", + secretPath: "/foo/bar", // Optional + type: "personal" // Optional +}); +``` + +**Parameters:** +- `secretName` (string): The name of the secret to delete. +- `options` (object): + - `projectId` (str): The ID of your project. + - `environment` (str): The environment in which to delete the secret. + - `secret_path` (str, optional): The path to the secret. + - `type` (personal | shared, optional): The type of secret to delete. + +**Returns:** +- `DeleteSecretResponse`: The response after deleting the secret. + + + + +### `dynamicSecrets` + + +#### Create a new dynamic secret + +Creating a new dynamic secret can be done by using the `.dynamicSecrets().create({})` function. More details below. + + +The input for creating new dynamic secret varies greatly between secret types. +For a more in-depth description of each input type for each dynamic secret type, please refer to [our API documentation](https://infisical.com/docs/api-reference/endpoints/dynamic-secrets/create) + + +##### Example for creating a new Redis dynamic secret + +```typescript +import { InfisicalSDK, DynamicSecretProviders } from "@infisical/sdk"; + const client = new InfisicalSDK(); + + await client.auth().universalAuth.login({ + // For localhost + clientId: "CLIENT_ID", + clientSecret: "CLIENT_SECRET" + }); +const dynamicSecret = await client.dynamicSecrets().create({ + provider: { + type: DynamicSecretProviders.Redis, + inputs: { + host: "", + port: 6479, + username: "", + password: "", // Only required if your Redis instance uses authentication (recommended) + creationStatement: "ACL SETUSER {{username}} on >{{password}} ~* &* +@all", + revocationStatement: "ACL DELUSER {{username}}" + } + }, + defaultTTL: "1h", + maxTTL: "24h", + name: "dynamic-secret-name", + projectSlug: "project-slug", + environmentSlug: "dev" +}); +console.log(dynamicSecret); +``` + +**Returns:** +- `DynamicSecret`: The created dynamic secret. + + +#### Delete a dynamic secret + +Note: Deleting a dynamic secret will also delete it's associated leases. + +```typescript +const deletedDynamicSecret = await client.dynamicSecrets().delete("dynamic-secret-name", { + environmentSlug: "dev", + projectSlug: "project-slug" +}); +``` + +**Parameters:** +- `secretName` (string): The ID of the dynamic secret to delete +- `options` (object): + - `projectSlug` (str): The ID of your project. + - `environment` (str): The environment in which to delete the secret. + +**Returns:** +- `DynamicSecret`: The deleted dynamic secret. + +### `dynamicSecrets.leases` +In this section you'll learn how to work with dynamic secret leases + + +#### Create a new lease + +```typescript +const lease = await client.dynamicSecrets().leases.create({ + dynamicSecretName: "dynamic-secret-name", + environmentSlug: "dev", + projectSlug: "your-project-slug", + path: "/foo/bar", + ttl: "5m" // Optional +}); + +console.log(lease); +``` + +**Your dynamic secret credentials will be contained user `lease.data` in this example.** + +**Parameters:** +- `dynamicSecretName` (string): The name of the dynamic secret you wish to create a lease for. +- `projectSlug` (string): The slug of the project where the secret is located. +- `environmentSlug` (string): The environment where the dynamic secret is located. +- `path` (string, optional): The path of where the dynamic secret is located. +- `ttl` (string, optional): A [vercel/ms](https://github.com/vercel/ms) encoded string representation of how long the lease credentials should be valid for. This will default to the dynamic secret's default TTL if not specified. + +**Returns:** +- `CreateLeaseResponse`: The dynamic secret lease result. + + +#### Delete a lease +```typescript +const deletedLease = await client.dynamicSecrets().leases.delete(newLease.lease.id, { + environmentSlug: "dev", + projectSlug: "test-zb-3a", + path: "/foo/bar", + isForced: false // Whether or not to forcefully delete the lease. This can't guarantee that the lease will be deleted from the external provider. +}); +``` + +**Parameters:** +- `leaseId` (string): The ID of the lease you want to delete. +- options: + - `projectSlug` (string): The slug of the project where the secret is located. + - `environmentSlug` (string): The environment where the dynamic secret is located. + - `path` (string, optional): The path of where the dynamic secret is located. + - `isForced` (bool, optional): Whether or not to forcefully delete the lease. This can't guarantee that the lease will be deleted from the external provider, and is potentially unsafe for sensitive dynamic secrets. + +**Returns:** +- `DeleteLeaseResponse`: The deleted lease result. + +#### Renew a lease + +Please note that renewals must happen **before** the lease has fully expired. After renewing the lease, you won't be given new credentials. Instead the existing credentials will continue to live for the specified TTL + +```typescript +const renewedLease = await client.dynamicSecrets().leases.renew(newLease.lease.id, { + environmentSlug: "dev", + projectSlug: "project-slug", + path: "/foo/bar", // Optional + ttl: "10m" // Optional + }); +``` + +**Parameters:** +- `leaseId` (string): The ID of the lease you want to delete. +- `options` (object): + - `projectSlug` (string): The slug of the project where the secret is located. + - `environmentSlug` (string): The environment where the dynamic secret is located. + - `path` (string, optional): The path of where the dynamic secret is located. + - `ttl` (string, optional): A [vercel/ms](https://github.com/vercel/ms) encoded string representation of how long the lease credentials should be valid for. This will default to the dynamic secret's default TTL if not specified. + +**Returns:** +- `RenewLeaseResponse`: The renewed lease response _(doesn't contain new credentials)_. + +### `projects` + +#### Create a new project + +```typescript +const project = await client.projects().create({ + projectName: "", + type: "secret-manager", // cert-manager, secret-manager, kms, ssh + projectDescription: "", // Optional + slug: "", // Optional + template: "", // Optional + kmsKeyId: "kms-key-id" // Optional +}); +``` + +**Parameters:** +- `projectName` (string): The name of the project to create. +- `type` (string): The type of project to create. Valid options are `secret-manager`, `cert-manager`, `kms`, `ssh` +- `projectDescription` (string): An optional description of the project to create. +- `slug` (string): An optional slug for the project to create. If not provided, one will be generated automatically. +- `template` (string): Optionally provide a project template name to use for creating this project. +- `kmsKeyId` (string): The ID of the KMS key to use for the project. Will use the Infisical KMS by default. + +**Returns:** +- `Project`: The project that was created. + + +#### Invite members to a project + +When inviting members to projects, you must either specify the `emails` or `usernames`. If neither are specified, the SDK will throw an error. + +```typescript +const memberships = await client.projects().inviteMembers({ + projectId: project.id, + emails: ["test1@example.com", "test2@example.com"], // Optional + usernames: ["example-user3", "example-user4"] // Optional + roleSlugs: ["member"] // Optional +}); +``` + +**Parameters:** +- `projectId`: (string): The ID of the project to invite members to +- `emails`: (string[]): An array of emails of the users to invite to the project. +- `usernames`: (string[]) An array of usernames of the users to invite to the project. +- `roleSlugs`: (string[]): An array of role slugs to assign to the members. If not specified, this will default to `member`. + +**Returns:** +- `Membership[]`: An array of the created project memberships. + +### `environments` + +#### Create a new environment + +```typescript +const environment = await client.environments().create({ + name: "", + projectId: "", + slug: "", + position: 1 // Optional +}); +``` + +**Parameters:** +- `name` (string): The name of the environment to be created. +- `projectId` (string): The ID of the project to create the environment within. +- `slug`: (string): The slug of the environment to be created. +- `position` (number): An optional position of the environment to be created. The position is used in the Infisical UI to display environments in order. Environments with the lowest position come first. + +**Returns:** +- `Environment`: The environment that was created. + +#### Create a new folder + +```typescript +const folder = await client.folders().create({ + name: "", + path: "", + projectId: "", + environment: "", + description: "" // Optional +}); +``` + +**Parameters:** +- `name` (string): The name of the folder to create. +- `path` (string): The path where of where to create the folder. Defaults to `/`, which is the root folder. +- `projectId` (string): The ID of the project to create the folder within. +- `environment` (string): The slug of the environment to create the folder within. +- `description` (string): An optional folder description. + +**Returns:** +- `Folder`: The folder that was created. + +#### List folders + +```typescript +const folders = await client.folders().listFolders({ + environment: "dev", + projectId: "", + path: "/foo/bar", // Optional + recursive: false // Optional +}); +``` + +**Parameters:** +- `environment` (string): The slug of the environment to list folders within. +- `projectId` (string): The ID of the project to list folders within. +- `path` (string): The path to list folders within. Defaults to `/`, which is the root folder. +- `recursive` (boolean): An optional flag to list folders recursively. Defaults to `false`. + +**Returns:** +- `Folder[]`: An array of folders. \ No newline at end of file diff --git a/docs/sdks/languages/python.mdx b/docs/sdks/languages/python.mdx index f888c0819..066ecc060 100644 --- a/docs/sdks/languages/python.mdx +++ b/docs/sdks/languages/python.mdx @@ -1,533 +1,418 @@ --- title: "Infisical Python SDK" sidebarTitle: "Python" -url: "https://github.com/Infisical/python-sdk-official?tab=readme-ov-file#infisical-python-sdk" icon: "/images/sdks/languages/python.svg" --- -{/* If you're working with Python, the official [infisical-python](https://github.com/Infisical/sdk/edit/main/crates/infisical-py) package is the easiest way to fetch and work with secrets for your application. +If you're working with Python, the official Infisical Python SDK package is the easiest way to fetch and work with secrets for your application. -- [PyPi Package](https://pypi.org/project/infisical-python/) -- [Github Repository](https://github.com/Infisical/sdk/edit/main/crates/infisical-py) +### Migrating to version 1.0.3 or above -## Basic Usage +We have recently rolled out our first stable version of the SDK, version `1.0.3` and above. -```py -from flask import Flask -from infisical_client import ClientSettings, InfisicalClient, GetSecretOptions, AuthenticationOptions, UniversalAuthMethod +The 1.0.3 version comes with a few key changes that may change how you're using the SDK. +1. **Removal of `rest`**: The SDK no longer exposes the entire Infisical API. This was nessecary as we have moved away from using an OpenAPI generator approach. We aim to add support for more API resources in the near future. If you have any specific requests, please [open an issue](https://github.com/Infisical/python-sdk-official/issues). -app = Flask(__name__) +2. **New response types**: The 1.0.3 release uses return types that differ from the older versions. The new return types such as `BaseSecret`, are all exported from the Infisical SDK. -client = InfisicalClient(ClientSettings( - auth=AuthenticationOptions( - universal_auth=UniversalAuthMethod( - client_id="CLIENT_ID", - client_secret="CLIENT_SECRET", - ) - ) -)) +3. **Property renaming**: Some properties on the responses have been slightly renamed. An example of this would be that the `secret_key` property on the `get_secret_by_name()` method, that has been renamed to `secretKey`. -@app.route("/") -def hello_world(): - # access value +With this in mind, you're ready to upgrade your SDK version to `1.0.3` or above. - name = client.getSecret(options=GetSecretOptions( - environment="dev", - project_id="PROJECT_ID", - secret_name="NAME" - )) +You can refer to our [legacy documentation](https://github.com/Infisical/python-sdk-official/tree/9b0403938ee5ae599d42c5f1fdf9158671a15606?tab=readme-ov-file#infisical-python-sdk) if need be. - return f"Hello! My name is: {name.secret_value}" -``` +## Requirements -This example demonstrates how to use the Infisical Python SDK with a Flask application. The application retrieves a secret named "NAME" and responds to requests with a greeting that includes the secret value. - - - We do not recommend hardcoding your [Machine Identity Tokens](/platform/identities/overview). Setting it as an environment variable would be best. - +Python 3.7+ ## Installation -Run `pip` to add `infisical-python` to your project - -```console -$ pip install infisical-python +```bash +pip install infisicalsdk ``` -Note: You need Python 3.7+. +## Getting Started -## Configuration +```python +from infisical_sdk import InfisicalSDKClient -Import the SDK and create a client instance with your [Machine Identity](/api-reference/overview/authentication). +# Initialize the client +client = InfisicalSDKClient(host="https://app.infisical.com") -```py -from infisical_client import ClientSettings, InfisicalClient, AuthenticationOptions, UniversalAuthMethod +# Authenticate (example using Universal Auth) +client.auth.universal_auth.login( + client_id="", + client_secret="" +) -client = InfisicalClient(ClientSettings( - auth=AuthenticationOptions( - universal_auth=UniversalAuthMethod( - client_id="CLIENT_ID", - client_secret="CLIENT_SECRET", - ) - ) -)) +# Use the SDK to interact with Infisical +secrets = client.secrets.list_secrets(project_id="", environment_slug="dev", secret_path="/") ``` -#### Parameters +## InfisicalSDKClient Parameters - - - - Your Infisical Client ID. +The `InfisicalSDKClient` takes the following parameters, which are used as a global configuration for the lifetime of the SDK instance. - **This field is deprecated and will be removed in future versions.** Please use the `auth` field instead. - - - Your Infisical Client Secret. +- **host** (`str`, _Optional_): The host URL for your Infisical instance. Defaults to `https://app.infisical.com`. +- **token** (`str`, _Optional_): Specify an authentication token to use for all requests. If provided, you will not need to call any of the `auth` methods. Defaults to `None` +- **cache_ttl** (`int`, _Optional_): The SDK has built-in client-side caching for secrets, greatly improving response times. By default, secrets are cached for 1 minute (60 seconds). You can disable caching by setting `cache_ttl` to `None`, or adjust the duration in seconds as needed. - **This field is deprecated and will be removed in future versions.** Please use the `auth` field instead. - - - If you want to directly pass an access token obtained from the authentication endpoints, you can do so. +```python +client = InfisicalSDKClient( + host="https://app.infisical.com", # Defaults to https://app.infisical.com + token="", # If not set, use the client.auth() methods. + cache_ttl = 300 # `None` to disable caching +) +``` - **This field is deprecated and will be removed in future versions.** Please use the `auth` field instead. - +## Core Methods - - Time-to-live (in seconds) for refreshing cached secrets. - If manually set to 0, caching will be disabled, this is not recommended. - +The SDK methods are organized into the following high-level categories: - - Your self-hosted absolute site URL including the protocol (e.g. `https://app.infisical.com`) - +1. `auth`: Handles authentication methods. +2. `secrets`: Manages CRUD operations for secrets. +3. `kms`: Perform cryptographic operations with Infisical KMS. - - Optionally provide a path to a custom SSL certificate file. This can be substituted by setting the `INFISICAL_SSL_CERTIFICATE` environment variable to the contents of the certificate. - +### `auth` - - The authentication object to use for the client. This is required unless you're using environment variables. - - - - - -### Authentication - -The SDK supports a variety of authentication methods. The most common authentication method is Universal Auth, which uses a client ID and client secret to authenticate. +The `Auth` component provides methods for authentication: #### Universal Auth -**Using environment variables** -- `INFISICAL_UNIVERSAL_AUTH_CLIENT_ID` - Your machine identity client ID. -- `INFISICAL_UNIVERSAL_AUTH_CLIENT_SECRET` - Your machine identity client secret. - -**Using the SDK directly** -```python3 -from infisical_client import ClientSettings, InfisicalClient, AuthenticationOptions, UniversalAuthMethod - -client = InfisicalClient(ClientSettings( - auth=AuthenticationOptions( - universal_auth=UniversalAuthMethod( - client_id="CLIENT_ID", - client_secret="CLIENT_SECRET", - ) - ) -)) -``` - -#### GCP ID Token Auth - - Please note that this authentication method will only work if you're running your application on Google Cloud Platform. - Please [read more](/documentation/platform/identities/gcp-auth) about this authentication method. - - -**Using environment variables** -- `INFISICAL_GCP_AUTH_IDENTITY_ID` - Your Infisical Machine Identity ID. - -**Using the SDK directly** -```py -from infisical_client import ClientSettings, InfisicalClient, AuthenticationOptions, GCPIDTokenAuthMethod - -client = InfisicalClient(ClientSettings( - auth=AuthenticationOptions( - gcp_id_token=GCPIDTokenAuthMethod( - identity_id="MACHINE_IDENTITY_ID", - ) - ) -)) -``` - -#### GCP IAM Auth - -**Using environment variables** -- `INFISICAL_GCP_IAM_AUTH_IDENTITY_ID` - Your Infisical Machine Identity ID. -- `INFISICAL_GCP_IAM_SERVICE_ACCOUNT_KEY_FILE_PATH` - The path to your GCP service account key file. - -**Using the SDK directly** -```py -from infisical_client import ClientSettings, InfisicalClient, AuthenticationOptions, GCPIamAuthMethod - - -client = InfisicalClient(ClientSettings( - auth=AuthenticationOptions( - gcp_iam=GCPIamAuthMethod( - identity_id="MACHINE_IDENTITY_ID", - service_account_key_file_path="./path/to/service_account_key.json" - ) - ) -)) -``` - -#### AWS IAM Auth - - Please note that this authentication method will only work if you're running your application on AWS. - Please [read more](/documentation/platform/identities/aws-auth) about this authentication method. - - -**Using environment variables** -- `INFISICAL_AWS_IAM_AUTH_IDENTITY_ID` - Your Infisical Machine Identity ID. - -**Using the SDK directly** -```py -from infisical_client import ClientSettings, InfisicalClient, AuthenticationOptions, AWSIamAuthMethod - -client = InfisicalClient(ClientSettings( - auth=AuthenticationOptions( - aws_iam=AWSIamAuthMethod(identity_id="MACHINE_IDENTITY_ID") - ) -)) -``` - -#### Azure Auth - - Please note that this authentication method will only work if you're running your application on Azure. - Please [read more](/documentation/platform/identities/azure-auth) about this authentication method. - - -**Using environment variables** -- `INFISICAL_AZURE_AUTH_IDENTITY_ID` - Your Infisical Machine Identity ID. - -**Using the SDK directly** ```python -from infisical_client import InfisicalClient, ClientSettings, AuthenticationOptions, AzureAuthMethod - -kubernetes_client = InfisicalClient(ClientSettings( - auth=AuthenticationOptions( - azure=AzureAuthMethod( - identity_id="YOUR_IDENTITY_ID", - ) - ) -)) +response = client.auth.universal_auth.login( + client_id="", + client_secret="" +) ``` +#### AWS Auth -#### Kubernetes Auth - - Please note that this authentication method will only work if you're running your application on Kubernetes. - Please [read more](/documentation/platform/identities/kubernetes-auth) about this authentication method. - - -**Using environment variables** -- `INFISICAL_KUBERNETES_IDENTITY_ID` - Your Infisical Machine Identity ID. -- `INFISICAL_KUBERNETES_SERVICE_ACCOUNT_TOKEN_PATH_ENV_NAME` - The environment variable name that contains the path to the service account token. This is optional and will default to `/var/run/secrets/kubernetes.io/serviceaccount/token`. - -**Using the SDK directly** ```python -from infisical_client import InfisicalClient, ClientSettings, AuthenticationOptions, KubernetesAuthMethod - -kubernetes_client = InfisicalClient(ClientSettings( - auth=AuthenticationOptions( - kubernetes=KubernetesAuthMethod( - identity_id="YOUR_IDENTITY_ID", - service_account_token_path="/var/run/secrets/kubernetes.io/serviceaccount/token" # Optional - ) - ) -)) +response = client.auth.aws_auth.login(identity_id="") ``` -### Caching +#### OIDC Auth -To reduce the number of API requests, the SDK temporarily stores secrets it retrieves. By default, a secret remains cached for 5 minutes after it's first fetched. Each time it's fetched again, this 5-minute timer resets. You can adjust this caching duration by setting the "cache_ttl" option when creating the client. - -## Working with Secrets - -### client.listSecrets(options) - -```py -client.listSecrets(options=ListSecretsOptions( - environment="dev", - project_id="PROJECT_ID" -)) -``` - -Retrieve all secrets within the Infisical project and environment that client is connected to - -#### Parameters - - - - - The slug name (dev, prod, etc) of the environment from where secrets should be fetched from. - - - The project ID where the secret lives in. - - - - The path from where secrets should be fetched from. - - - - Whether or not to set the fetched secrets to the process environment. If true, you can access the secrets like so `process.env["SECRET_NAME"]`. - - - - Whether or not to fetch secrets recursively from the specified path. Please note that there's a 20-depth limit for recursive fetching. - - - - Whether or not to expand secret references in the fetched secrets. Read about [secret reference](/documentation/platform/secret-reference) - - - - Whether or not to include imported secrets from the current path. Read about [secret import](/documentation/platform/secret-reference) - - - - - -### client.getSecret(options) - -```py -secret = client.getSecret(options=GetSecretOptions( - environment="dev", - project_id="PROJECT_ID", - secret_name="API_KEY" -)) -value = secret.secret_value # get its value -``` - -By default, `getSecret()` fetches and returns a shared secret. If not found, it returns a personal secret. - -#### Parameters - - - - - The key of the secret to retrieve - - - Whether or not to include imported secrets from the current path. Read about [secret import](/documentation/platform/secret-reference) - - - The slug name (dev, prod, etc) of the environment from where secrets should be fetched from. - - - The project ID where the secret lives in. - - - The path from where secret should be fetched from. - - - The type of the secret. Valid options are "shared" or "personal". If not specified, the default value is "personal". - - - Whether or not to include imported secrets from the current path. Read about [secret import](/documentation/platform/secret-reference) - - - Whether or not to expand secret references in the fetched secrets. Read about [secret reference](/documentation/platform/secret-reference) - - - - -### client.createSecret(options) - -```py -api_key = client.createSecret(options=CreateSecretOptions( - secret_name="API_KEY", - secret_value="Some API Key", - environment="dev", - project_id="PROJECT_ID" -)) -``` - -Create a new secret in Infisical. - -#### Parameters - - - - - The key of the secret to create. - - - The value of the secret. - - - The project ID where the secret lives in. - - - The slug name (dev, prod, etc) of the environment from where secrets should be fetched from. - - - The path from where secret should be created. - - - The type of the secret. Valid options are "shared" or "personal". If not specified, the default value is "shared". - - - - -### client.updateSecret(options) - -```py -client.updateSecret(options=UpdateSecretOptions( - secret_name="API_KEY", - secret_value="NEW_VALUE", - environment="dev", - project_id="PROJECT_ID" -)) -``` - -Update an existing secret in Infisical. - -#### Parameters - - - - - The key of the secret to update. - - - The new value of the secret. - - - The project ID where the secret lives in. - - - The slug name (dev, prod, etc) of the environment from where secrets should be fetched from. - - - The path from where secret should be updated. - - - The type of the secret. Valid options are "shared" or "personal". If not specified, the default value is "shared". - - - - -### client.deleteSecret(options) - -```py -client.deleteSecret(options=DeleteSecretOptions( - environment="dev", - project_id="PROJECT_ID", - secret_name="API_KEY" -)) -``` - -Delete a secret in Infisical. - -#### Parameters - - - - - The key of the secret to update. - - - The project ID where the secret lives in. - - - The slug name (dev, prod, etc) of the environment from where secrets should be fetched from. - - - The path from where secret should be deleted. - - - The type of the secret. Valid options are "shared" or "personal". If not specified, the default value is "shared". - - - - -## Cryptography - -### Create a symmetric key - -Create a base64-encoded, 256-bit symmetric key to be used for encryption/decryption. - -```py -key = client.createSymmetricKey() -``` - -#### Returns (string) - -`key` (string): A base64-encoded, 256-bit symmetric key, that can be used for encryption/decryption purposes. - -### Encrypt symmetric - -```py -encryptOptions = EncryptSymmetricOptions( - key=key, - plaintext="Infisical is awesome!" +```python +response = client.auth.oidc_auth.login( + identity_id="", + jwt="" ) - -encryptedData = client.encryptSymmetric(encryptOptions) ``` -#### Parameters +**Parameters:** +- `identity_id` (str): The ID of the OIDC identity configuration in Infisical. +- `jwt` (str): The OIDC JWT token obtained from your identity provider. - - - - The plaintext you want to encrypt. - - - The symmetric key to use for encryption. - - - +This authentication method is useful when integrating with OIDC-compliant identity providers like Okta, Auth0, or any service that issues OIDC tokens. -#### Returns (object) +### `secrets` -`tag` (string): A base64-encoded, 128-bit authentication tag. `iv` (string): A base64-encoded, 96-bit initialization vector. `ciphertext` (string): A base64-encoded, encrypted ciphertext. +This sub-class handles operations related to secrets: -### Decrypt symmetric +#### List Secrets -```py -decryptOptions = DecryptSymmetricOptions( - ciphertext=encryptedData.ciphertext, - iv=encryptedData.iv, - tag=encryptedData.tag, - key=key +```python +secrets = client.secrets.list_secrets( + project_id="", + environment_slug="dev", + secret_path="/", + expand_secret_references=True, # Optional + view_secret_value=True, # Optional + recursive=False, # Optional + include_imports=True, # Optional + tag_filters=[] # Optional ) - -decryptedString = client.decryptSymmetric(decryptOptions) - - ``` -#### Parameters +**Parameters:** +- `project_id` (str): The ID of your project. +- `project_slug` (str): The slug of your project. +- `environment_slug` (str): The environment in which to list secrets (e.g., "dev"). +- `secret_path` (str): The path to the secrets. +- `expand_secret_references` (bool): Whether to expand secret references. +- `view_secret_value` (bool): Whether or not to include the secret value in the response. If set to false, the `secretValue` will be masked with ``. Defaults to true. +- `recursive` (bool): Whether to list secrets recursively. +- `include_imports` (bool): Whether to include imported secrets. +- `tag_filters` (List[str]): Tags to filter secrets. - - - - The ciphertext you want to decrypt. - - - The symmetric key to use for encryption. - - - The initialization vector to use for decryption. - - - The authentication tag to use for decryption. - - - +**Note:** Exactly one of `project_id` or `project_slug` is required. If both are provided, `project_id` takes precedence. -#### Returns (string) +**Returns:** +- `ListSecretsResponse`: The response containing the list of secrets. -`plaintext` (string): The decrypted plaintext. */} +#### Create Secret + +```python +new_secret = client.secrets.create_secret_by_name( + secret_name="NEW_SECRET", + project_id="", + secret_path="/", + environment_slug="dev", + secret_value="secret_value", + secret_comment="Optional comment", + skip_multiline_encoding=False, + secret_reminder_repeat_days=30, # Optional + secret_reminder_note="Remember to update this secret", # Optional + secret_metadata=[{"key": "metadata_key", "value": "metadata_value"}], # Optional + tags_ids=["tag_id_1", "tag_id_2"] # Optional +) +``` + +**Parameters:** +- `secret_name` (str): The name of the secret. +- `project_id` (str): The ID of your project. +- `project_slug` (str): The slug of your project. +- `secret_path` (str): The path to the secret. +- `environment_slug` (str): The environment in which to create the secret. +- `secret_value` (str): The value of the secret. +- `secret_comment` (str, optional): A comment associated with the secret. +- `skip_multiline_encoding` (bool, optional): Whether to skip encoding for multiline secrets. +- `secret_reminder_repeat_days` (Union[float, int], optional): Number of days after which to repeat secret reminders. +- `secret_reminder_note` (str, optional): A note for the secret reminder. +- `secret_metadata` (List[Dict[str, Any]], optional): Metadata associated with the secret. +- `tags_ids` (List[str], optional): IDs of tags to associate with the secret. + +**Note:** Exactly one of `project_id` or `project_slug` is required. If both are provided, `project_id` takes precedence. + +**Returns:** +- `BaseSecret`: The response after creating the secret. + +#### Update Secret + +```python +updated_secret = client.secrets.update_secret_by_name( + current_secret_name="EXISTING_SECRET", + project_id="", + project_slug="", + secret_path="/", + environment_slug="dev", + secret_value="new_secret_value", + secret_comment="Updated comment", # Optional + skip_multiline_encoding=False, + secret_reminder_repeat_days=30, # Optional + secret_reminder_note="Updated reminder note", # Optional + new_secret_name="NEW_NAME", # Optional + secret_metadata=[{"key": "metadata_key", "value": "metadata_value"}], # Optional + tags_ids=["tag_id_1", "tag_id_2"] # Optional +) +``` + +**Parameters:** +- `current_secret_name` (str): The current name of the secret. +- `project_id` (str): The ID of your project. +- `project_slug` (str): The slug of your project. +- `secret_path` (str): The path to the secret. +- `environment_slug` (str): The environment in which to update the secret. +- `secret_value` (str, optional): The new value of the secret. +- `secret_comment` (str, optional): An updated comment associated with the secret. +- `skip_multiline_encoding` (bool, optional): Whether to skip encoding for multiline secrets. +- `secret_reminder_repeat_days` (Union[float, int], optional): Updated number of days after which to repeat secret reminders. +- `secret_reminder_note` (str, optional): An updated note for the secret reminder. +- `new_secret_name` (str, optional): A new name for the secret. +- `secret_metadata` (List[Dict[str, Any]], optional): Metadata associated with the secret. +- `tags_ids` (List[str], optional): IDs of tags to associate with the secret. + +**Note:** Exactly one of `project_id` or `project_slug` is required. If both are provided, `project_id` takes precedence. + +**Returns:** +- `BaseSecret`: The response after updating the secret. + +#### Get Secret by Name + +```python +secret = client.secrets.get_secret_by_name( + secret_name="EXISTING_SECRET", + project_id="", + environment_slug="dev", + secret_path="/", + expand_secret_references=True, # Optional + view_secret_value=True, # Optional + include_imports=True, # Optional + version=None # Optional +) +``` + +**Parameters:** +- `secret_name` (str): The name of the secret. +- `project_id` (str): The ID of your project. +- `project_slug` (str): The slug of your project. +- `environment_slug` (str): The environment in which to retrieve the secret. +- `secret_path` (str): The path to the secret. +- `expand_secret_references` (bool): Whether to expand secret references. +- `view_secret_value` (bool): Whether or not to include the secret value in the response. If set to false, the `secretValue` will be masked with ``. Defaults to true. +- `include_imports` (bool): Whether to include imported secrets. +- `version` (str, optional): The version of the secret to retrieve. Fetches the latest by default. + +**Note:** Exactly one of `project_id` or `project_slug` is required. If both are provided, `project_id` takes precedence. + +**Returns:** +- `BaseSecret`: The response containing the secret. + +#### Delete Secret by Name + +```python +deleted_secret = client.secrets.delete_secret_by_name( + secret_name="EXISTING_SECRET", + project_id="", + environment_slug="dev", + secret_path="/" +) +``` + +**Parameters:** +- `secret_name` (str): The name of the secret to delete. +- `project_id` (str): The ID of your project. +- `project_slug` (str): The slug of your project. +- `environment_slug` (str): The environment in which to delete the secret. +- `secret_path` (str): The path to the secret. + +**Note:** Exactly one of `project_id` or `project_slug` is required. If both are provided, `project_id` takes precedence. + +**Returns:** +- `BaseSecret`: The response after deleting the secret. + +### `kms` + +This sub-class handles KMS related operations: + +#### List KMS Keys + +```python +kms_keys = client.kms.list_keys( + project_id="", + offset=0, # Optional + limit=100, # Optional + order_by=KmsKeysOrderBy.NAME, # Optional + order_direction=OrderDirection.ASC, # Optional + search=None # Optional +) +``` + +**Parameters:** +- `project_id` (str): The ID of your project. +- `offset` (int, optional): The offset to paginate from. +- `limit` (int, optional): The page size for paginating. +- `order_by` (KmsKeysOrderBy, optional): The key property to order the list response by. +- `order_direction` (OrderDirection, optional): The direction to order the list response in. +- `search` (str, optional): The text value to filter key names by. + +**Returns:** +- `ListKmsKeysResponse`: The response containing the list of KMS keys. + +#### Get KMS Key by ID + +```python +kms_key = client.kms.get_key_by_id( + key_id="" +) +``` + +**Parameters:** +- `key_id` (str): The ID of the key to retrieve. + +**Returns:** +- `KmsKey`: The specified key. + +#### Get KMS Key by Name + +```python +kms_key = client.kms.get_key_by_name( + key_name="my-key", + project_id="" +) +``` + +**Parameters:** +- `key_name` (str): The name of the key to retrieve. +- `project_id` (str): The ID of your project. + +**Returns:** +- `KmsKey`: The specified key. + +#### Create KMS Key + +```python +kms_key = client.kms.create_key( + name="my-key", + project_id="", + encryption_algorithm=SymmetricEncryption.AES_GCM_256, + description=None # Optional +) +``` + +**Parameters:** +- `name` (str): The name of the key (must be slug-friendly). +- `project_id` (str): The ID of your project. +- `encryption_algorithm` (SymmetricEncryption): The encryption algorithm this key should use. +- `description` (str, optional): A description of your key. + +**Returns:** +- `KmsKey`: The newly created key. + +#### Update KMS Key + +```python +updated_key = client.kms.update_key( + key_id="", + name="my-updated-key", # Optional + description="Updated description", # Optional + is_disabled=True # Optional +) +``` + +**Parameters:** +- `key_id` (str): The ID of the key to be updated. +- `name` (str, optional): The updated name of the key (must be slug-friendly). +- `description` (str): The updated description of the key. +- `is_disabled` (str): The flag to disable operations with this key. + +**Returns:** +- `KmsKey`: The updated key. + +#### Delete KMS Key + +```python +deleted_key = client.kms.delete_key( + key_id="" +) +``` + +**Parameters:** +- `key_id` (str): The ID of the key to be deleted. + +**Returns:** +- `KmsKey`: The deleted key. + +#### Encrypt Data with KMS Key + +```python +encrypted_data = client.kms.encrypt_data( + key_id="", + base64EncodedPlaintext="TXkgc2VjcmV0IG1lc3NhZ2U=" # must be base64 encoded +) +``` + +**Parameters:** +- `key_id` (str): The ID of the key to encrypt the data with. +- `base64EncodedPlaintext` (str): The plaintext data to encrypt (must be base64 encoded). + +**Returns:** +- `str`: The encrypted ciphertext. + +#### Decrypt Data with KMS Key + +```python +decrypted_data = client.kms.decrypt_data( + key_id="", + ciphertext="Aq96Ry7sMH3k/ogaIB5MiSfH+LblQRBu69lcJe0GfIvI48ZvbWY+9JulyoQYdjAx" +) +``` + +**Parameters:** +- `key_id` (str): The ID of the key to decrypt the data with. +- `ciphertext` (str): The ciphertext returned from the encrypt operation. + +**Returns:** +- `str`: The base64 encoded plaintext. \ No newline at end of file diff --git a/docs/sdks/languages/ruby.mdx b/docs/sdks/languages/ruby.mdx index 5780c48ec..3fb9cb3e1 100644 --- a/docs/sdks/languages/ruby.mdx +++ b/docs/sdks/languages/ruby.mdx @@ -4,9 +4,7 @@ sidebarTitle: "Ruby" icon: "/images/sdks/languages/ruby.svg" --- - - -If you're working with Ruby, the official [Infisical Ruby SDK](https://github.com/infisical/sdk) package is the easiest way to fetch and work with secrets for your application. +If you're working with Ruby, the official Infisical Ruby SDK package is the easiest way to fetch and work with secrets for your application. - [Ruby Package](https://rubygems.org/gems/infisical-sdk) - [Github Repository](https://github.com/infisical/sdk) diff --git a/docs/sdks/languages/rust.mdx b/docs/sdks/languages/rust.mdx index 3d2f16175..40ad78bfb 100644 --- a/docs/sdks/languages/rust.mdx +++ b/docs/sdks/languages/rust.mdx @@ -2,5 +2,494 @@ title: "Infisical Rust SDK" sidebarTitle: "Rust" icon: "/images/sdks/languages/rust.svg" -url: "https://github.com/Infisical/rust-sdk?tab=readme-ov-file#infisical--the-official-infisical-rust-sdk" ---- \ No newline at end of file +--- + +If you're working with Rust, the official Infisical Rust SDK package is the easiest way to fetch and work with secrets for your application. + +### Installation + +```bash +cargo add infisical +``` + +### Getting Started + +The easiest way to get started is to use the builder pattern for both the client and your requests. + +```rust +use infisical::{AuthMethod, Client, InfisicalError, encode_base64, decode_base64}; +use infisical::secrets::GetSecretRequest; + +async fn fetch_secret() -> Result<(), InfisicalError> { + // 1. Build the client. You can chain methods to configure it. + let mut client = Client::builder() + .base_url("https://app.infisical.com") // Optional: defaults to https://app.infisical.com + .build() + .await?; + + // 2. Set up your authentication method and log in. + let auth_method = AuthMethod::new_universal_auth("", ""); + client.login(auth_method).await?; + + // 3. Build a request to get a secret. + // Required parameters (name, project_id, environment) are passed to `builder()`. + let request = GetSecretRequest::builder("API_KEY", "", "dev") + .path("/") // Optional parameters are set with builder methods. + .build(); + + // 4. Make the API call. + let secret = client.secrets().get(request).await?; + + println!("Fetched secret key: {}", secret.secret_key); + // For security, avoid printing the secret value in production code! + // println!("Secret value: {}", secret.secret_value); + + Ok(()) +} +``` + +### Client Configuration + +The `Client::builder()` provides several configuration options: + +```rust +let mut client = Client::builder() + .base_url("https://app.infisical.com") // Optional: set custom Infisical instance URL + .build() + .await?; +``` + +**Parameters** +- `.base_url(url)`: Optional method to set the Infisical instance URL. Defaults to `https://app.infisical.com` for Infisical Cloud. Use `https://eu.infisical.com` for EU and `http://localhost:8080` for local development. + +### Core Methods + +The SDK methods are organized into the following high-level categories: + +- `Client::builder()`: The main entry point for creating a client. +- `client.login()`: Allows client to make authenticated requests to the API. +- `client.secrets()`: Provides access to all CRUD operations for secrets. +- `client.kms()`: Provides access to all KMS (Key Management Service) operations. + +### Helper Functions + +The SDK provides utility functions for common operations: + +```rust +use infisical::{encode_base64, decode_base64}; + +// Base64 encode a string +let encoded = encode_base64("sensitive data"); +println!("Encoded: {}", encoded); + +// Base64 decode a string +let decoded = decode_base64(&encoded)?; +println!("Decoded: {}", decoded); +``` + +**Available Functions** +- `encode_base64(data: &str) -> String`: Encodes a string as base64 +- `decode_base64(data: &str) -> Result`: Decodes a base64 string + +### `secrets` + +All secret operations are accessed via `client.secrets()`. Each operation has a dedicated request builder. + +#### Create Secret + +Create a new secret in your project. + +**Example** + +```rust +use infisical::secrets::CreateSecretRequest; + +let request = CreateSecretRequest::builder( + "API_KEY", + "your-secret-value", + "", + "dev" +) +.path("/") +.secret_comment("A comment for the new secret") +.build(); + +let created_secret = client.secrets().create(request).await?; +``` + +**Parameters** + +- `secret_name`, `secret_value`, `project_id`, `environment`: Required parameters passed to the `builder()` function. +- `.path(path)`: Optional method to set the secret's path (defaults to `/`). +- `.secret_comment(comment)`: Optional method to add a comment. +- `.skip_multiline_encoding(bool)`: Optional method to control multiline encoding (defaults to `false`). +- `.r#type(type)`: Optional method to set the secret type (`shared` or `personal`), defaults to `shared`. + +#### Get Secret + +Retrieve a specific secret by name. + +**Example** + +```rust +use infisical::secrets::GetSecretRequest; + +let request = GetSecretRequest::builder("API_KEY", "", "dev") + .path("/") + .build(); + +let secret = client.secrets().get(request).await?; +``` + +**Parameters** + +- `secret_name`, `project_id`, `environment`: Required parameters passed to the `builder()` function. +- `.path(path)`: Optional method to set the secret's path (defaults to `/`). +- `.expand_secret_references(bool)`: Optional method to control secret reference expansion (defaults to `true`). +- `.r#type(type)`: Optional method to set the secret type (`shared` or `personal`), defaults to `shared`. + +#### List Secrets + +List all secrets in a project and environment. + +**Example** + +```rust +use infisical::secrets::ListSecretsRequest; + +let request = ListSecretsRequest::builder("", "dev") + .path("/") + .recursive(true) + .build(); + +let secrets = client.secrets().list(request).await?; +``` + +**Parameters** + +- `project_id`, `environment`: Required parameters passed to the `builder()` function. +- `.path(path)`: Optional method to set the path from which to list secrets (defaults to `/`). +- `.expand_secret_references(bool)`: Optional method to control secret reference expansion (defaults to `true`). +- `.recursive(bool)`: Optional method to recursively list secrets from sub-folders (defaults to `false`). +- `.attach_to_process_env(bool)`: Optional method to attach fetched secrets to the current process's environment variables (defaults to `false`). + +#### Update Secret + +Update an existing secret. + +**Example** + +```rust +use infisical::secrets::UpdateSecretRequest; + +let request = UpdateSecretRequest::builder("API_KEY", "", "dev") + .secret_value("new-secret-value") // Set the new value + .build(); + +let updated_secret = client.secrets().update(request).await?; +``` + +**Parameters** + +- `secret_name`, `project_id`, `environment`: Required parameters passed to the `builder()` function. +- `.new_secret_name(name)`: Optional method to rename the secret. +- `.secret_value(value)`: Optional method to set a new value for the secret. +- `.path(path)`: Optional method to set the secret's path. +- `.secret_comment(comment)`: Optional method to add or change the comment. +- `.skip_multiline_encoding(bool)`: Optional method to control multiline encoding. +- `.r#type(type)`: Optional method to set the secret type (`shared` or `personal`). + +#### Delete Secret + +Delete a secret from your project. + +**Example** + +```rust +use infisical::secrets::DeleteSecretRequest; + +let request = DeleteSecretRequest::builder("API_KEY", "", "dev") + .path("/") + .build(); + +let deleted_secret = client.secrets().delete(request).await?; +``` + +**Parameters** + +- `secret_name`, `project_id`, `environment`: Required parameters passed to the `builder()` function. +- `.path(path)`: Optional method to set the secret's path (defaults to `/`). +- `.r#type(type)`: Optional method to set the secret type (`shared` or `personal`), defaults to `shared`. + +### `kms` + +All KMS (Key Management Service) operations are accessed via `client.kms()`. Each operation has a dedicated request builder. + +#### List KMS Keys + +List all KMS keys in a project. + +**Example** + +```rust +use infisical::kms::ListKmsKeysRequest; + +let request = ListKmsKeysRequest::builder("").build(); + +let keys = client.kms().list(request).await?; +``` + +**Parameters** + +- `project_id`: Required parameter passed to the `builder()` function. + +#### Get KMS Key + +Retrieve a specific KMS key by ID. + +**Example** + +```rust +use infisical::kms::GetKmsKeyRequest; + +let request = GetKmsKeyRequest::builder("").build(); + +let key = client.kms().get(request).await?; +``` + +**Parameters** + +- `key_id`: Required parameter passed to the `builder()` function. + +#### Get KMS Key by Name + +Retrieve a specific KMS key by name. + +**Example** + +```rust +use infisical::kms::GetKmsKeyByNameRequest; + +let request = GetKmsKeyByNameRequest::builder("").build(); + +let key = client.kms().get_by_name(request).await?; +``` + +**Parameters** + +- `key_name`: Required parameter passed to the `builder()` function. + +#### Create KMS Key + +Create a new KMS key in your project. + +**Example** + +```rust +use infisical::kms::{CreateKmsKeyRequest, EncryptionAlgorithm, KeyUsage}; + +let request = CreateKmsKeyRequest::builder("", "my-key") + .description("A key for encryption operations") + .key_usage(KeyUsage::EncryptDecrypt) + .encryption_algorithm(EncryptionAlgorithm::Aes256Gcm) + .build(); + +let created_key = client.kms().create(request).await?; +``` + +**Parameters** + +- `project_id`, `name`: Required parameters passed to the `builder()` function. +- `.description(description)`: Optional method to set the key description. +- `.key_usage(usage)`: Optional method to set the key usage using the `KeyUsage` enum (defaults to `KeyUsage::EncryptDecrypt`). +- `.encryption_algorithm(algorithm)`: Optional method to set the encryption algorithm using the `EncryptionAlgorithm` enum (defaults to `EncryptionAlgorithm::Aes256Gcm`). + +#### Update KMS Key + +Update an existing KMS key. + +**Example** + +```rust +use infisical::kms::UpdateKmsKeyRequest; + +let request = UpdateKmsKeyRequest::builder("") + .name("updated-key-name") + .description("Updated description") + .is_disabled(false) + .build(); + +let updated_key = client.kms().update(request).await?; +``` + +**Parameters** + +- `key_id`: Required parameter passed to the `builder()` function. +- `.name(name)`: Optional method to rename the key. +- `.description(description)`: Optional method to update the key description. +- `.is_disabled(disabled)`: Optional method to enable or disable the key. + +#### Delete KMS Key + +Delete a KMS key from your project. + +**Example** + +```rust +use infisical::kms::DeleteKmsKeyRequest; + +let request = DeleteKmsKeyRequest::builder("").build(); + +let deleted_key = client.kms().delete(request).await?; +``` + +**Parameters** + +- `key_id`: Required parameter passed to the `builder()` function. + +#### Encrypt Data + +Encrypt data using a KMS key. + +**Example** + +```rust +use infisical::kms::EncryptRequest; + +let request = EncryptRequest::builder("", "sensitive data").build(); + +let ciphertext = client.kms().encrypt(request).await?; +``` + +**Parameters** + +- `key_id`, `plaintext`: Required parameters passed to the `builder()` function. + +#### Decrypt Data + +Decrypt data using a KMS key. + +**Example** + +```rust +use infisical::kms::DecryptRequest; + +let request = DecryptRequest::builder("", "encrypted-data").build(); + +let plaintext = client.kms().decrypt(request).await?; +``` + +**Parameters** + +- `key_id`, `ciphertext`: Required parameters passed to the `builder()` function. + +#### Sign Data + +Sign data using a KMS key. + +**Example** + +```rust +use infisical::kms::{SigningAlgorithm, SignRequest}; + +let request = SignRequest::builder("", "data to sign") + .signing_algorithm(SigningAlgorithm::RsassaPkcs1V15Sha256) + .is_digest(false) + .build(); + +let signature = client.kms().sign(request).await?; +``` + +**Parameters** + +- `key_id`, `data`: Required parameters passed to the `builder()` function. +- `.signing_algorithm(algorithm)`: Optional method to set the signing algorithm using the `SigningAlgorithm` enum (defaults to `SigningAlgorithm::RsassaPkcs1V15Sha256`). +- `.is_digest(is_digest)`: Optional method to indicate if the data is a digest (defaults to `false`). + +#### Verify Signature + +Verify a signature using a KMS key. + +**Example** + +```rust +use infisical::kms::{SigningAlgorithm, VerifyRequest}; + +let request = VerifyRequest::builder("", "data to sign", "signature") + .signing_algorithm(SigningAlgorithm::RsassaPkcs1V15Sha256) + .is_digest(false) + .build(); + +let verification = client.kms().verify(request).await?; +``` + +**Parameters** + +- `key_id`, `data`, `signature`: Required parameters passed to the `builder()` function. +- `.signing_algorithm(algorithm)`: Optional method to set the signing algorithm using the `SigningAlgorithm` enum (defaults to `SigningAlgorithm::RsassaPkcs1V15Sha256`). +- `.is_digest(is_digest)`: Optional method to indicate if the data is a digest (defaults to `false`). + +#### Get Public Key + +Get the public key for a KMS key. + +**Example** + +```rust +let public_key = client.kms().get_public_key("").await?; +``` + +**Parameters** + +- `key_id`: The ID of the key to get the public key for. + +#### Get Signing Algorithms + +Get the available signing algorithms for a KMS key. + +**Example** + +```rust +let algorithms = client.kms().get_signing_algorithms("").await?; +``` + +**Parameters** + +- `key_id`: The ID of the key to get signing algorithms for. + +## Development and Testing + +### Environment Setup + +For development and testing, you'll need to set up environment variables. Create a `.env` file in your project root: + +```env +INFISICAL_CLIENT_ID=your_client_id_here +INFISICAL_CLIENT_SECRET=your_client_secret_here +INFISICAL_BASE_URL=http://localhost:8080 # Optional: for local development + +# Project IDs for different resources +INFISICAL_SECRETS_PROJECT_ID=your_project_id_here +INFISICAL_KMS_PROJECT_ID=your_project_id_here +``` + +### Getting Credentials + +To obtain the required credentials: + +1. **Client ID and Secret**: Create a Universal Auth machine identity in your Infisical project settings +2. **Project ID**: Found in your project settings or URL when viewing a project in the Infisical dashboard + +### Running Tests + +Tests that require authentication are marked with `#[ignore]` and need valid credentials: + +```bash +# Run ignored tests (requires .env file with valid credentials) +cargo test -- --ignored --nocapture + +# Run a specific test +cargo test test_kms_resource -- --ignored --nocapture +``` + +**Note**: Integration tests require a running Infisical instance and valid authentication credentials. \ No newline at end of file diff --git a/docs/sdks/overview.mdx b/docs/sdks/overview.mdx index 32192805d..0b6949108 100644 --- a/docs/sdks/overview.mdx +++ b/docs/sdks/overview.mdx @@ -12,22 +12,22 @@ From local development to production, Infisical SDKs provide the easiest way for - Fetch secrets on demand - + Manage secrets for your Node application on demand - + Manage secrets for your Python application on demand - + Manage secrets for your Java application on demand - + Manage secrets for your .NET application on demand - + Manage secrets for your C++ application on demand - + Manage secrets for your Rust application on demand diff --git a/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/ChecklySyncFields.tsx b/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/ChecklySyncFields.tsx index 50c645ee2..c55313898 100644 --- a/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/ChecklySyncFields.tsx +++ b/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/ChecklySyncFields.tsx @@ -5,14 +5,15 @@ import { SecretSyncConnectionField } from "@app/components/secret-syncs/forms/Se import { FilterableSelect, FormControl } from "@app/components/v2"; import { TChecklyAccount, - useChecklyConnectionListAccounts + useChecklyConnectionListAccounts, + useChecklyConnectionListGroups } from "@app/hooks/api/appConnections/checkly"; import { SecretSync } from "@app/hooks/api/secretSyncs"; import { TSecretSyncForm } from "../schemas"; export const ChecklySyncFields = () => { - const { control, setValue } = useFormContext< + const { control, setValue, watch } = useFormContext< TSecretSyncForm & { destination: SecretSync.Checkly } >(); @@ -25,12 +26,24 @@ export const ChecklySyncFields = () => { } ); + const accountId = watch("destinationConfig.accountId"); + + const { data: groups = [], isPending: isGroupsLoading } = useChecklyConnectionListGroups( + connectionId, + accountId, + { + enabled: Boolean(connectionId && accountId) + } + ); + return ( <> { setValue("destinationConfig.accountId", ""); setValue("destinationConfig.accountName", ""); + setValue("destinationConfig.groupId", undefined); + setValue("destinationConfig.groupName", undefined); }} /> { )} /> + + ( + + p.id === value) ?? null} + onChange={(option) => { + const v = option as SingleValue; + onChange(v?.id ?? null); + setValue("destinationConfig.groupName", v?.name ?? undefined); + }} + options={groups} + placeholder="Select a group..." + getOptionLabel={(option) => option.name} + getOptionValue={(option) => option.id} + /> + + )} + /> ); }; diff --git a/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/ChecklySyncReviewFields.tsx b/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/ChecklySyncReviewFields.tsx index d520699ce..0f86a0c06 100644 --- a/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/ChecklySyncReviewFields.tsx +++ b/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/ChecklySyncReviewFields.tsx @@ -6,7 +6,16 @@ import { SecretSync } from "@app/hooks/api/secretSyncs"; export const ChecklySyncReviewFields = () => { const { watch } = useFormContext(); - const accountName = watch("destinationConfig.accountName"); + const config = watch("destinationConfig"); - return {accountName}; + return ( + <> + + {config.accountName ?? config.accountId} + + {config.groupId && ( + {config.groupName ?? config.groupId} + )} + + ); }; diff --git a/frontend/src/components/secret-syncs/forms/schemas/checkly-sync-destination-schema.ts b/frontend/src/components/secret-syncs/forms/schemas/checkly-sync-destination-schema.ts index 9ada6e276..7684df981 100644 --- a/frontend/src/components/secret-syncs/forms/schemas/checkly-sync-destination-schema.ts +++ b/frontend/src/components/secret-syncs/forms/schemas/checkly-sync-destination-schema.ts @@ -8,7 +8,15 @@ export const ChecklySyncDestinationSchema = BaseSecretSyncSchema().merge( destination: z.literal(SecretSync.Checkly), destinationConfig: z.object({ accountId: z.string(), - accountName: z.string() + accountName: z.string(), + groupId: z + .string() + .nullish() + .transform((val) => val || undefined), + groupName: z + .string() + .nullish() + .transform((val) => val || undefined) }) }) ); diff --git a/frontend/src/components/v2/DatePicker/DatePicker.tsx b/frontend/src/components/v2/DatePicker/DatePicker.tsx index 115b0ad3e..9611e957f 100644 --- a/frontend/src/components/v2/DatePicker/DatePicker.tsx +++ b/frontend/src/components/v2/DatePicker/DatePicker.tsx @@ -98,7 +98,7 @@ export const DatePicker = ({ > {value ? formatDateTime({ timestamp: value, timezone, dateFormat }) - : "Pick a date and time"} + : `Select Date${hideTime ? "" : " and Time"}`} diff --git a/frontend/src/helpers/appConnections.ts b/frontend/src/helpers/appConnections.ts index 7ecc92811..88d9bb0d1 100644 --- a/frontend/src/helpers/appConnections.ts +++ b/frontend/src/helpers/appConnections.ts @@ -13,6 +13,7 @@ import { AppConnection } from "@app/hooks/api/appConnections/enums"; import { Auth0ConnectionMethod, AwsConnectionMethod, + AzureADCSConnectionMethod, AzureAppConfigurationConnectionMethod, AzureClientSecretsConnectionMethod, AzureDevOpsConnectionMethod, @@ -76,6 +77,7 @@ export const APP_CONNECTION_MAP: Record< image: "Microsoft Azure.png" }, [AppConnection.AzureDevOps]: { name: "Azure DevOps", image: "Microsoft Azure.png" }, + [AppConnection.AzureADCS]: { name: "Azure ADCS", image: "Microsoft Azure.png" }, [AppConnection.Databricks]: { name: "Databricks", image: "Databricks.png" }, [AppConnection.Humanitec]: { name: "Humanitec", image: "Humanitec.png" }, [AppConnection.TerraformCloud]: { name: "Terraform Cloud", image: "Terraform Cloud.png" }, @@ -151,6 +153,7 @@ export const getAppConnectionMethodDetails = (method: TAppConnection["method"]) case MsSqlConnectionMethod.UsernameAndPassword: case MySqlConnectionMethod.UsernameAndPassword: case OracleDBConnectionMethod.UsernameAndPassword: + case AzureADCSConnectionMethod.UsernamePassword: return { name: "Username & Password", icon: faLock }; case HCVaultConnectionMethod.AccessToken: case TeamCityConnectionMethod.AccessToken: diff --git a/frontend/src/hooks/api/appConnections/checkly/queries.tsx b/frontend/src/hooks/api/appConnections/checkly/queries.tsx index 452c21245..95156aaa4 100644 --- a/frontend/src/hooks/api/appConnections/checkly/queries.tsx +++ b/frontend/src/hooks/api/appConnections/checkly/queries.tsx @@ -8,7 +8,9 @@ import { TChecklyAccount } from "./types"; const checklyConnectionKeys = { all: [...appConnectionKeys.all, "checkly"] as const, listAccounts: (connectionId: string) => - [...checklyConnectionKeys.all, "workspace-scopes", connectionId] as const + [...checklyConnectionKeys.all, "workspace-scopes", connectionId] as const, + listGroups: (connectionId: string, accountId: string) => + [...checklyConnectionKeys.all, "groups", connectionId, accountId] as const }; export const useChecklyConnectionListAccounts = ( @@ -35,3 +37,29 @@ export const useChecklyConnectionListAccounts = ( ...options }); }; + +export const useChecklyConnectionListGroups = ( + connectionId: string, + accountId: string, + options?: Omit< + UseQueryOptions< + TChecklyAccount[], + unknown, + TChecklyAccount[], + ReturnType + >, + "queryKey" | "queryFn" + > +) => { + return useQuery({ + queryKey: checklyConnectionKeys.listGroups(connectionId, accountId), + queryFn: async () => { + const { data } = await apiRequest.get<{ groups: TChecklyAccount[] }>( + `/api/v1/app-connections/checkly/${connectionId}/accounts/${accountId}/groups` + ); + + return data.groups; + }, + ...options + }); +}; diff --git a/frontend/src/hooks/api/appConnections/enums.ts b/frontend/src/hooks/api/appConnections/enums.ts index eff3c9b62..7b041b797 100644 --- a/frontend/src/hooks/api/appConnections/enums.ts +++ b/frontend/src/hooks/api/appConnections/enums.ts @@ -7,6 +7,7 @@ export enum AppConnection { AzureAppConfiguration = "azure-app-configuration", AzureClientSecrets = "azure-client-secrets", AzureDevOps = "azure-devops", + AzureADCS = "azure-adcs", Databricks = "databricks", Humanitec = "humanitec", TerraformCloud = "terraform-cloud", diff --git a/frontend/src/hooks/api/appConnections/queries.tsx b/frontend/src/hooks/api/appConnections/queries.tsx index 0b55cb79b..dbccade88 100644 --- a/frontend/src/hooks/api/appConnections/queries.tsx +++ b/frontend/src/hooks/api/appConnections/queries.tsx @@ -54,13 +54,13 @@ export const useAppConnectionOptions = ( export const useGetAppConnectionOption = (app: T) => { const { data: options = [], isPending } = useAppConnectionOptions(); - return useMemo( - () => ({ - option: (options.find((opt) => opt.app === app) as TAppConnectionOptionMap[T]) ?? {}, + return useMemo(() => { + const foundOption = options.find((opt) => opt.app === app); + return { + option: (foundOption as TAppConnectionOptionMap[T]) ?? {}, isLoading: isPending - }), - [options, app, isPending] - ); + }; + }, [options, app, isPending]); }; export const useListAppConnections = ( diff --git a/frontend/src/hooks/api/appConnections/types/app-options.ts b/frontend/src/hooks/api/appConnections/types/app-options.ts index 4191cb890..67d8feb48 100644 --- a/frontend/src/hooks/api/appConnections/types/app-options.ts +++ b/frontend/src/hooks/api/appConnections/types/app-options.ts @@ -32,7 +32,7 @@ export type TAzureKeyVaultConnectionOption = TAppConnectionOptionBase & { }; export type TAzureAppConfigurationConnectionOption = TAppConnectionOptionBase & { - app: AppConnection.AzureKeyVault; + app: AppConnection.AzureAppConfiguration; oauthClientId?: string; }; @@ -164,9 +164,14 @@ export type TOktaConnectionOption = TAppConnectionOptionBase & { app: AppConnection.Okta; }; +export type TAzureAdCsConnectionOption = TAppConnectionOptionBase & { + app: AppConnection.AzureADCS; +}; + export type TAppConnectionOption = | TAwsConnectionOption | TGitHubConnectionOption + | TGitHubRadarConnectionOption | TGcpConnectionOption | TAzureAppConfigurationConnectionOption | TAzureKeyVaultConnectionOption @@ -184,6 +189,7 @@ export type TAppConnectionOption = | TWindmillConnectionOption | TAuth0ConnectionOption | THCVaultConnectionOption + | TLdapConnectionOption | TTeamCityConnectionOption | TOCIConnectionOption | TOnePassConnectionOption @@ -196,9 +202,11 @@ export type TAppConnectionOption = | TZabbixConnectionOption | TRailwayConnectionOption | TChecklyConnectionOption + | TSupabaseConnectionOption | TDigitalOceanConnectionOption | TNetlifyConnectionOption - | TOktaConnectionOption; + | TOktaConnectionOption + | TAzureAdCsConnectionOption; export type TAppConnectionOptionMap = { [AppConnection.AWS]: TAwsConnectionOption; @@ -238,4 +246,5 @@ export type TAppConnectionOptionMap = { [AppConnection.DigitalOcean]: TDigitalOceanConnectionOption; [AppConnection.Netlify]: TNetlifyConnectionOption; [AppConnection.Okta]: TOktaConnectionOption; + [AppConnection.AzureADCS]: TAzureAdCsConnectionOption; }; diff --git a/frontend/src/hooks/api/appConnections/types/azure-adcs-connection.ts b/frontend/src/hooks/api/appConnections/types/azure-adcs-connection.ts new file mode 100644 index 000000000..2da9d56b5 --- /dev/null +++ b/frontend/src/hooks/api/appConnections/types/azure-adcs-connection.ts @@ -0,0 +1,21 @@ +import { z } from "zod"; + +import { AppConnection } from "../enums"; +import { TRootAppConnection } from "./root-connection"; + +export enum AzureADCSConnectionMethod { + UsernamePassword = "username-password" +} + +export const CreateAzureADCSConnectionSchema = z.object({ + adcsUrl: z.string().url().min(1, "ADCS URL is required"), + username: z.string().min(1, "Username is required"), + password: z.string().min(1, "Password is required") +}); + +export type TCreateAzureADCSConnection = z.infer; + +export type TAzureADCSConnection = TRootAppConnection & { app: AppConnection.AzureADCS } & { + method: AzureADCSConnectionMethod.UsernamePassword; + credentials: TCreateAzureADCSConnection; +}; diff --git a/frontend/src/hooks/api/appConnections/types/index.ts b/frontend/src/hooks/api/appConnections/types/index.ts index f07dcd283..e63979a8e 100644 --- a/frontend/src/hooks/api/appConnections/types/index.ts +++ b/frontend/src/hooks/api/appConnections/types/index.ts @@ -3,6 +3,7 @@ import { TOnePassConnection } from "./1password-connection"; import { TAppConnectionOption } from "./app-options"; import { TAuth0Connection } from "./auth0-connection"; import { TAwsConnection } from "./aws-connection"; +import { TAzureADCSConnection } from "./azure-adcs-connection"; import { TAzureAppConfigurationConnection } from "./azure-app-configuration-connection"; import { TAzureClientSecretsConnection } from "./azure-client-secrets-connection"; import { TAzureDevOpsConnection } from "./azure-devops-connection"; @@ -41,6 +42,7 @@ import { TZabbixConnection } from "./zabbix-connection"; export * from "./1password-connection"; export * from "./auth0-connection"; export * from "./aws-connection"; +export * from "./azure-adcs-connection"; export * from "./azure-app-configuration-connection"; export * from "./azure-client-secrets-connection"; export * from "./azure-devops-connection"; @@ -83,6 +85,7 @@ export type TAppConnection = | TAzureAppConfigurationConnection | TAzureClientSecretsConnection | TAzureDevOpsConnection + | TAzureADCSConnection | TDatabricksConnection | THumanitecConnection | TTerraformCloudConnection @@ -156,6 +159,7 @@ export type TAppConnectionMap = { [AppConnection.AzureAppConfiguration]: TAzureAppConfigurationConnection; [AppConnection.AzureClientSecrets]: TAzureClientSecretsConnection; [AppConnection.AzureDevOps]: TAzureDevOpsConnection; + [AppConnection.AzureADCS]: TAzureADCSConnection; [AppConnection.Databricks]: TDatabricksConnection; [AppConnection.Humanitec]: THumanitecConnection; [AppConnection.TerraformCloud]: TTerraformCloudConnection; diff --git a/frontend/src/hooks/api/ca/constants.tsx b/frontend/src/hooks/api/ca/constants.tsx index fa59b3ae0..778656da4 100644 --- a/frontend/src/hooks/api/ca/constants.tsx +++ b/frontend/src/hooks/api/ca/constants.tsx @@ -1,7 +1,7 @@ import { AppConnection } from "../appConnections/enums"; import { SshCaStatus } from "../sshCa"; import { SshCertTemplateStatus } from "../sshCertificateTemplates"; -import { AcmeDnsProvider, CaStatus, InternalCaType } from "./enums"; +import { AcmeDnsProvider, CaCapability, CaStatus, CaType, InternalCaType } from "./enums"; export const caTypeToNameMap: { [K in InternalCaType]: string } = { [InternalCaType.ROOT]: "Root", @@ -24,6 +24,28 @@ export const ACME_DNS_PROVIDER_APP_CONNECTION_MAP: Record = { + [CaType.INTERNAL]: [ + CaCapability.ISSUE_CERTIFICATES, + CaCapability.REVOKE_CERTIFICATES, + CaCapability.RENEW_CERTIFICATES + ], + [CaType.ACME]: [ + CaCapability.ISSUE_CERTIFICATES, + CaCapability.REVOKE_CERTIFICATES, + CaCapability.RENEW_CERTIFICATES + ], + [CaType.AZURE_AD_CS]: [CaCapability.ISSUE_CERTIFICATES, CaCapability.RENEW_CERTIFICATES] +}; + +/** + * Check if a certificate authority type supports a specific capability + */ +export const caSupportsCapability = (caType: CaType, capability: CaCapability): boolean => { + const capabilities = CA_TYPE_CAPABILITIES_MAP[caType] || []; + return capabilities.includes(capability); +}; + export const getCaStatusBadgeVariant = (status: CaStatus | SshCaStatus | SshCertTemplateStatus) => { switch (status) { case CaStatus.ACTIVE: diff --git a/frontend/src/hooks/api/ca/enums.tsx b/frontend/src/hooks/api/ca/enums.tsx index b010faa07..a68f3e862 100644 --- a/frontend/src/hooks/api/ca/enums.tsx +++ b/frontend/src/hooks/api/ca/enums.tsx @@ -1,6 +1,7 @@ export enum CaType { INTERNAL = "internal", - ACME = "acme" + ACME = "acme", + AZURE_AD_CS = "azure-ad-cs" } export enum InternalCaType { @@ -22,3 +23,9 @@ export enum AcmeDnsProvider { ROUTE53 = "route53", Cloudflare = "cloudflare" } + +export enum CaCapability { + ISSUE_CERTIFICATES = "issue-certificates", + REVOKE_CERTIFICATES = "revoke-certificates", + RENEW_CERTIFICATES = "renew-certificates" +} diff --git a/frontend/src/hooks/api/ca/index.tsx b/frontend/src/hooks/api/ca/index.tsx index 82e9ea3be..36526ec18 100644 --- a/frontend/src/hooks/api/ca/index.tsx +++ b/frontend/src/hooks/api/ca/index.tsx @@ -9,6 +9,7 @@ export { useUpdateCa } from "./mutations"; export { + useGetAzureAdcsTemplates, useGetCa, useGetCaById, useGetCaCert, @@ -17,5 +18,6 @@ export { useGetCaCrls, useGetCaCsr, useListCasByProjectId, - useListCasByTypeAndProjectId + useListCasByTypeAndProjectId, + useListExternalCasByProjectId } from "./queries"; diff --git a/frontend/src/hooks/api/ca/mutations.tsx b/frontend/src/hooks/api/ca/mutations.tsx index c05760beb..f6204d4fc 100644 --- a/frontend/src/hooks/api/ca/mutations.tsx +++ b/frontend/src/hooks/api/ca/mutations.tsx @@ -39,6 +39,10 @@ export const useUpdateCa = () => { queryClient.invalidateQueries({ queryKey: caKeys.getCaByNameAndProjectId(caName, projectId) }); + // Invalidate external CAs list + queryClient.invalidateQueries({ + queryKey: caKeys.listExternalCasByProjectId(projectId) + }); } }); }; @@ -57,6 +61,10 @@ export const useCreateCa = () => { queryClient.invalidateQueries({ queryKey: caKeys.listCasByTypeAndProjectId(type, projectId) }); + // Invalidate external CAs list + queryClient.invalidateQueries({ + queryKey: caKeys.listExternalCasByProjectId(projectId) + }); } }); }; @@ -79,6 +87,10 @@ export const useDeleteCa = () => { queryClient.invalidateQueries({ queryKey: caKeys.listCasByTypeAndProjectId(type, projectId) }); + // Invalidate external CAs list + queryClient.invalidateQueries({ + queryKey: caKeys.listExternalCasByProjectId(projectId) + }); } }); }; diff --git a/frontend/src/hooks/api/ca/queries.tsx b/frontend/src/hooks/api/ca/queries.tsx index 4f10c6e84..68e8d2c12 100644 --- a/frontend/src/hooks/api/ca/queries.tsx +++ b/frontend/src/hooks/api/ca/queries.tsx @@ -4,20 +4,25 @@ import { apiRequest } from "@app/config/request"; import { TCertificateTemplate } from "../certificateTemplates/types"; import { CaType } from "./enums"; -import { TCertificateAuthority, TUnifiedCertificateAuthority } from "./types"; +import { TAzureAdCsTemplate, TCertificateAuthority, TUnifiedCertificateAuthority } from "./types"; export const caKeys = { getCaById: (caId: string) => [{ caId }, "ca"], getCaByNameAndProjectId: (caName: string, projectId: string) => [{ caName, projectId }, "ca"], listCasByTypeAndProjectId: (type: CaType, projectId: string) => [{ type, projectId }, "cas"], listCasByProjectId: (projectId: string) => [{ projectId }, "cas"], + listExternalCasByProjectId: (projectId: string) => [{ projectId }, "external-cas"], getCaCerts: (caId: string) => [{ caId }, "ca-cert"], getCaCrls: (caId: string) => [{ caId }, "ca-crls"], getCaCert: (caId: string) => [{ caId }, "ca-cert"], getCaCsr: (caId: string) => [{ caId }, "ca-csr"], getCaCrl: (caId: string) => [{ caId }, "ca-crl"], getCaCertTemplates: (caId: string) => [{ caId }, "ca-cert-templates"], - getCaEstConfig: (caId: string) => [{ caId }, "ca-est-config"] + getCaEstConfig: (caId: string) => [{ caId }, "ca-est-config"], + getAzureAdcsTemplates: (caId: string, projectId: string) => [ + { caId, projectId }, + "azure-adcs-templates" + ] }; export const useGetCa = ({ @@ -67,6 +72,34 @@ export const useListCasByProjectId = (projectId: string) => { }); }; +export const useListExternalCasByProjectId = (projectId: string) => { + return useQuery({ + queryKey: caKeys.listExternalCasByProjectId(projectId), + queryFn: async () => { + const [acmeResponse, azureAdCsResponse] = await Promise.allSettled([ + apiRequest.get( + `/api/v1/pki/ca/${CaType.ACME}?projectId=${projectId}` + ), + apiRequest.get( + `/api/v1/pki/ca/${CaType.AZURE_AD_CS}?projectId=${projectId}` + ) + ]); + + const allCas: TUnifiedCertificateAuthority[] = []; + + if (acmeResponse.status === "fulfilled") { + allCas.push(...acmeResponse.value.data); + } + + if (azureAdCsResponse.status === "fulfilled") { + allCas.push(...azureAdCsResponse.value.data); + } + + return allCas; + } + }); +}; + export const useGetCaById = (caId: string) => { return useQuery({ queryKey: caKeys.getCaById(caId), @@ -156,3 +189,22 @@ export const useGetCaCertTemplates = (caId: string) => { enabled: Boolean(caId) }); }; + +export const useGetAzureAdcsTemplates = ({ + caId, + projectId +}: { + caId: string; + projectId: string; +}) => { + return useQuery({ + queryKey: caKeys.getAzureAdcsTemplates(caId, projectId), + queryFn: async () => { + const { data } = await apiRequest.get<{ + templates: TAzureAdCsTemplate[]; + }>(`/api/v1/pki/ca/azure-ad-cs/${caId}/templates?projectId=${projectId}`); + return data; + }, + enabled: Boolean(caId && projectId) + }); +}; diff --git a/frontend/src/hooks/api/ca/types.ts b/frontend/src/hooks/api/ca/types.ts index f9f812cb2..8dd874a75 100644 --- a/frontend/src/hooks/api/ca/types.ts +++ b/frontend/src/hooks/api/ca/types.ts @@ -19,6 +19,19 @@ export type TAcmeCertificateAuthority = { }; }; +export type TAzureAdCsCertificateAuthority = { + id: string; + projectId: string; + type: CaType.AZURE_AD_CS; + status: CaStatus; + name: string; + enableDirectIssuance: boolean; + configuration: { + azureAdcsConnectionId: string; + templateName: string; + }; +}; + export type TInternalCertificateAuthority = { id: string; projectId: string; @@ -48,6 +61,7 @@ export type TInternalCertificateAuthority = { export type TUnifiedCertificateAuthority = | TAcmeCertificateAuthority + | TAzureAdCsCertificateAuthority | TInternalCertificateAuthority; export type TCreateCertificateAuthorityDTO = Omit; @@ -114,6 +128,12 @@ export type TSignIntermediateResponse = { serialNumber: string; }; +export type TAzureAdCsTemplate = { + id: string; + name: string; + description?: string; +}; + export type TImportCaCertificateDTO = { caId: string; projectSlug: string; diff --git a/frontend/src/hooks/api/pkiSubscriber/types.ts b/frontend/src/hooks/api/pkiSubscriber/types.ts index 628cfec0a..23ca6baf3 100644 --- a/frontend/src/hooks/api/pkiSubscriber/types.ts +++ b/frontend/src/hooks/api/pkiSubscriber/types.ts @@ -10,6 +10,16 @@ export enum SubscriberOperationStatus { FAILED = "failed" } +export type TPkiSubscriberProperties = { + azureTemplateType?: string; + organization?: string; + organizationalUnit?: string; + country?: string; + state?: string; + locality?: string; + emailAddress?: string; +}; + export type TPkiSubscriber = { id: string; projectId: string; @@ -27,6 +37,7 @@ export type TPkiSubscriber = { lastOperationStatus?: SubscriberOperationStatus; lastOperationMessage?: string; lastOperationAt?: string; + properties?: TPkiSubscriberProperties; }; export type TCreatePkiSubscriberDTO = { @@ -40,6 +51,7 @@ export type TCreatePkiSubscriberDTO = { extendedKeyUsages: CertExtendedKeyUsage[]; enableAutoRenewal?: boolean; autoRenewalPeriodInDays?: number; + properties?: TPkiSubscriberProperties; }; export type TUpdatePkiSubscriberDTO = { @@ -55,6 +67,7 @@ export type TUpdatePkiSubscriberDTO = { extendedKeyUsages?: CertExtendedKeyUsage[]; enableAutoRenewal?: boolean; autoRenewalPeriodInDays?: number; + properties?: TPkiSubscriberProperties; }; export type TDeletePkiSubscriberDTO = { diff --git a/frontend/src/hooks/api/secretSyncs/types/checkly-sync.ts b/frontend/src/hooks/api/secretSyncs/types/checkly-sync.ts index 40718d261..10a8601de 100644 --- a/frontend/src/hooks/api/secretSyncs/types/checkly-sync.ts +++ b/frontend/src/hooks/api/secretSyncs/types/checkly-sync.ts @@ -3,11 +3,17 @@ import { AppConnection } from "@app/hooks/api/appConnections/enums"; import { SecretSync } from "@app/hooks/api/secretSyncs"; import { TRootSecretSync } from "@app/hooks/api/secretSyncs/types/root-sync"; +export enum ChecklySyncScope { + Global = "global", + Group = "group" +} export type TChecklySync = TRootSecretSync & { destination: SecretSync.Checkly; destinationConfig: { accountId: string; accountName: string; + groupId?: string; + groupName?: string; }; connection: { app: AppConnection.Checkly; diff --git a/frontend/src/pages/cert-manager/CertificateAuthoritiesPage/components/ExternalCaModal.tsx b/frontend/src/pages/cert-manager/CertificateAuthoritiesPage/components/ExternalCaModal.tsx index e0bc547e0..8b0a88bd0 100644 --- a/frontend/src/pages/cert-manager/CertificateAuthoritiesPage/components/ExternalCaModal.tsx +++ b/frontend/src/pages/cert-manager/CertificateAuthoritiesPage/components/ExternalCaModal.tsx @@ -42,29 +42,45 @@ import { import { UsePopUpState } from "@app/hooks/usePopUp"; import { slugSchema } from "@app/lib/schemas"; -const schema = z - .object({ - type: z.nativeEnum(CaType), - name: slugSchema({ - field: "Name" - }), - enableDirectIssuance: z.boolean(), - status: z.nativeEnum(CaStatus), - configuration: z.object({ - dnsAppConnection: z.object({ - id: z.string(), - name: z.string() - }), - // currently specific to Route53 & Cloudflare but can be extended to others by differentiating via the provider property - dnsProviderConfig: z.object({ - provider: z.nativeEnum(AcmeDnsProvider), - hostedZoneId: z.string() - }), - directoryUrl: z.string(), - accountEmail: z.string() - }) +const baseSchema = z.object({ + type: z.nativeEnum(CaType), + name: slugSchema({ + field: "Name" + }), + enableDirectIssuance: z.boolean(), + status: z.nativeEnum(CaStatus) +}); + +const acmeConfigurationSchema = z.object({ + dnsAppConnection: z.object({ + id: z.string(), + name: z.string() + }), + dnsProviderConfig: z.object({ + provider: z.nativeEnum(AcmeDnsProvider), + hostedZoneId: z.string() + }), + directoryUrl: z.string(), + accountEmail: z.string() +}); + +const azureAdCsConfigurationSchema = z.object({ + azureAdcsConnection: z.object({ + id: z.string(), + name: z.string() }) - .required(); +}); + +const schema = z.discriminatedUnion("type", [ + baseSchema.extend({ + type: z.literal(CaType.ACME), + configuration: acmeConfigurationSchema + }), + baseSchema.extend({ + type: z.literal(CaType.AZURE_AD_CS), + configuration: azureAdCsConfigurationSchema + }) +]); export type FormData = z.infer; @@ -73,12 +89,15 @@ type Props = { handlePopUpToggle: (popUpName: keyof UsePopUpState<["ca"]>, state?: boolean) => void; }; -const caTypes = [{ label: "ACME", value: CaType.ACME }]; +const caTypes = [ + { label: "ACME", value: CaType.ACME }, + { label: "Azure AD Certificate Service", value: CaType.AZURE_AD_CS } +]; export const ExternalCaModal = ({ popUp, handlePopUpToggle }: Props) => { const { currentWorkspace } = useWorkspace(); - const { data: ca } = useGetCa({ + const { data: ca, isLoading: isCaLoading } = useGetCa({ caName: (popUp?.ca?.data as { name: string })?.name || "", projectId: currentWorkspace?.id || "", type: (popUp?.ca?.data as { type: CaType })?.type || "" @@ -94,29 +113,54 @@ export const ExternalCaModal = ({ popUp, handlePopUpToggle }: Props) => { formState: { isSubmitting }, watch } = useForm({ - resolver: zodResolver(schema), - defaultValues: { - type: CaType.ACME, - name: "", - status: CaStatus.ACTIVE, - enableDirectIssuance: true, - configuration: { - dnsAppConnection: { - id: "", - name: "" - }, - dnsProviderConfig: { - provider: AcmeDnsProvider.ROUTE53, - hostedZoneId: "" - }, - directoryUrl: "", - accountEmail: "" - } - } + resolver: zodResolver(schema) }); const caType = watch("type"); - const dnsProvider = watch("configuration.dnsProviderConfig.provider"); + const configuration = watch("configuration"); + const dnsProvider = + caType === CaType.ACME && configuration && "dnsProviderConfig" in configuration + ? configuration.dnsProviderConfig.provider + : undefined; + + useEffect(() => { + const initialType = (popUp?.ca?.data as { type: CaType })?.type; + if (!ca && popUp?.ca?.isOpen) { + if (initialType === CaType.AZURE_AD_CS) { + reset({ + type: CaType.AZURE_AD_CS, + name: "", + status: CaStatus.ACTIVE, + enableDirectIssuance: false, + configuration: { + azureAdcsConnection: { + id: "", + name: "" + } + } + }); + } else { + reset({ + type: CaType.ACME, + name: "", + status: CaStatus.ACTIVE, + enableDirectIssuance: true, + configuration: { + dnsAppConnection: { + id: "", + name: "" + }, + dnsProviderConfig: { + provider: AcmeDnsProvider.ROUTE53, + hostedZoneId: "" + }, + directoryUrl: "", + accountEmail: "" + } + }); + } + } + }, [popUp?.ca?.isOpen, popUp?.ca?.data, reset, ca]); const { data: availableRoute53Connections, isPending: isRoute53Pending } = useListAvailableAppConnections(AppConnection.AWS, { @@ -128,25 +172,44 @@ export const ExternalCaModal = ({ popUp, handlePopUpToggle }: Props) => { enabled: caType === CaType.ACME }); - const availableConnections: TAvailableAppConnection[] = useMemo( - () => [...(availableRoute53Connections || []), ...(availableCloudflareConnections || [])], - [availableRoute53Connections, availableCloudflareConnections] - ); + const { data: availableAzureConnections, isPending: isAzurePending } = + useListAvailableAppConnections(AppConnection.AzureADCS, { + enabled: caType === CaType.AZURE_AD_CS + }); - const isPending = isRoute53Pending || isCloudflarePending; + const availableConnections: TAvailableAppConnection[] = useMemo(() => { + if (caType === CaType.ACME) { + return [...(availableRoute53Connections || []), ...(availableCloudflareConnections || [])]; + } + if (caType === CaType.AZURE_AD_CS) { + return availableAzureConnections || []; + } + return []; + }, [ + caType, + availableRoute53Connections, + availableCloudflareConnections, + availableAzureConnections + ]); - const dnsAppConnection = watch("configuration.dnsAppConnection"); + const isPending = isRoute53Pending || isCloudflarePending || isAzurePending; + + const dnsAppConnection = + caType === CaType.ACME && configuration && "dnsAppConnection" in configuration + ? configuration.dnsAppConnection + : { id: "", name: "" }; const { data: cloudflareZones = [], isPending: isZonesPending } = useCloudflareConnectionListZones(dnsAppConnection.id, { enabled: dnsProvider === AcmeDnsProvider.Cloudflare && !!dnsAppConnection.id }); + // Populate form with CA data when editing useEffect(() => { - if (ca) { - if (ca.type !== CaType.INTERNAL && availableConnections?.length) { + if (ca && !isCaLoading) { + if (ca.type === CaType.ACME && availableConnections?.length) { const selectedConnection = availableConnections?.find( - (connection) => connection.id === ca?.configuration.dnsAppConnectionId + (connection) => connection.id === ca.configuration.dnsAppConnectionId ); reset({ @@ -167,32 +230,63 @@ export const ExternalCaModal = ({ popUp, handlePopUpToggle }: Props) => { accountEmail: ca.configuration.accountEmail } }); + } else if (ca.type === CaType.AZURE_AD_CS && availableConnections?.length) { + const selectedConnection = availableConnections?.find( + (connection) => connection.id === ca.configuration.azureAdcsConnectionId + ); + + reset({ + type: ca.type, + name: ca.name, + status: ca.status, + enableDirectIssuance: false, + configuration: { + azureAdcsConnection: { + id: ca.configuration.azureAdcsConnectionId, + name: selectedConnection?.name || "" + } + } + }); } } - }, [ca, availableConnections]); + }, [ca, availableConnections, reset, isCaLoading]); const onFormSubmit = async ({ type, name, enableDirectIssuance, status, - configuration + configuration: formConfiguration }: FormData) => { try { if (!currentWorkspace?.slug) return; - if (ca && type !== CaType.INTERNAL) { + let configPayload: any; + + if (type === CaType.ACME && "dnsAppConnection" in formConfiguration) { + configPayload = { + dnsProviderConfig: formConfiguration.dnsProviderConfig, + directoryUrl: formConfiguration.directoryUrl, + accountEmail: formConfiguration.accountEmail, + dnsAppConnectionId: formConfiguration.dnsAppConnection.id + }; + } else if (type === CaType.AZURE_AD_CS && "azureAdcsConnection" in formConfiguration) { + configPayload = { + azureAdcsConnectionId: formConfiguration.azureAdcsConnection.id + }; + } else { + throw new Error("Invalid certificate authority configuration"); + } + + if (ca) { await updateMutateAsync({ caName: ca.name, projectId: currentWorkspace.id, name, type, status, - enableDirectIssuance, - configuration: { - ...configuration, - dnsAppConnectionId: configuration.dnsAppConnection.id - } + enableDirectIssuance: type === CaType.AZURE_AD_CS ? false : enableDirectIssuance, + configuration: configPayload }); } else { await createMutateAsync({ @@ -200,11 +294,8 @@ export const ExternalCaModal = ({ popUp, handlePopUpToggle }: Props) => { name, type, status, - enableDirectIssuance, - configuration: { - ...configuration, - dnsAppConnectionId: configuration.dnsAppConnection.id - } + enableDirectIssuance: type === CaType.AZURE_AD_CS ? false : enableDirectIssuance, + configuration: configPayload }); } @@ -232,7 +323,7 @@ export const ExternalCaModal = ({ popUp, handlePopUpToggle }: Props) => { handlePopUpToggle("ca", isOpen); }} > - +
{ca && ( @@ -307,7 +398,11 @@ export const ExternalCaModal = ({ popUp, handlePopUpToggle }: Props) => { ( { /> )} - { - return ( - - field.onChange(value)} - isChecked={field.value} - > -

Enable Direct Issuance

-
+ {caType === CaType.AZURE_AD_CS && ( + ( + + { + onChange(newValue); + }} + isLoading={isPending} + options={availableConnections} + placeholder="Select connection..." + getOptionLabel={(option) => option.name} + getOptionValue={(option) => option.id} + /> - ); - }} - /> + )} + control={control} + name="configuration.azureAdcsConnection" + /> + )} + {caType === CaType.ACME && ( + { + return ( + + field.onChange(value)} + isChecked={field.value} + > +

Enable Direct Issuance

+
+
+ ); + }} + /> + )}