Added Microsoft ADCS connector

This commit is contained in:
Carlos Monastyrski
2025-08-27 02:45:46 -03:00
parent 5c21ac3182
commit 4e2592960d
79 changed files with 3500 additions and 514 deletions
+69 -261
View File
@@ -63,6 +63,7 @@
"argon2": "^0.31.2", "argon2": "^0.31.2",
"aws-sdk": "^2.1553.0", "aws-sdk": "^2.1553.0",
"axios": "^1.11.0", "axios": "^1.11.0",
"axios-ntlm": "^1.4.4",
"axios-retry": "^4.0.0", "axios-retry": "^4.0.0",
"bcrypt": "^5.1.1", "bcrypt": "^5.1.1",
"botbuilder": "^4.23.2", "botbuilder": "^4.23.2",
@@ -78,6 +79,7 @@
"googleapis": "^137.1.0", "googleapis": "^137.1.0",
"handlebars": "^4.7.8", "handlebars": "^4.7.8",
"hdb": "^0.19.10", "hdb": "^0.19.10",
"httpntlm": "^1.8.13",
"ioredis": "^5.3.2", "ioredis": "^5.3.2",
"isomorphic-dompurify": "^2.22.0", "isomorphic-dompurify": "^2.22.0",
"jmespath": "^0.16.0", "jmespath": "^0.16.0",
@@ -12956,216 +12958,6 @@
"dev": true, "dev": true,
"license": "MIT" "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": { "node_modules/@swc/counter": {
"version": "0.1.3", "version": "0.1.3",
"resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz", "resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz",
@@ -13183,14 +12975,6 @@
"tslib": "^2.8.0" "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": { "node_modules/@techteamer/ocsp": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/@techteamer/ocsp/-/ocsp-1.0.1.tgz", "resolved": "https://registry.npmjs.org/@techteamer/ocsp/-/ocsp-1.0.1.tgz",
@@ -15195,6 +14979,18 @@
"proxy-from-env": "^1.1.0" "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": { "node_modules/axios-retry": {
"version": "4.0.0", "version": "4.0.0",
"resolved": "https://registry.npmjs.org/axios-retry/-/axios-retry-4.0.0.tgz", "resolved": "https://registry.npmjs.org/axios-retry/-/axios-retry-4.0.0.tgz",
@@ -16954,6 +16750,16 @@
"resolved": "https://registry.npmjs.org/deprecation/-/deprecation-2.3.1.tgz", "resolved": "https://registry.npmjs.org/deprecation/-/deprecation-2.3.1.tgz",
"integrity": "sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ==" "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": { "node_modules/destroy": {
"version": "1.2.0", "version": "1.2.0",
"resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz",
@@ -16981,6 +16787,12 @@
"node": ">=8" "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": { "node_modules/diff": {
"version": "4.0.2", "version": "4.0.2",
"resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz",
@@ -19029,49 +18841,6 @@
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" "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": { "node_modules/generate-function": {
"version": "2.3.1", "version": "2.3.1",
"resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.3.1.tgz", "resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.3.1.tgz",
@@ -19855,6 +19624,39 @@
"node": ">=0.10" "node": ">=0.10"
} }
}, },
"node_modules/httpntlm": {
"version": "1.8.13",
"resolved": "https://registry.npmjs.org/httpntlm/-/httpntlm-1.8.13.tgz",
"integrity": "sha512-2F2FDPiWT4rewPzNMg3uPhNkP3NExENlUGADRUDPQvuftuUTGW98nLZtGemCIW3G40VhWZYgkIDcQFAwZ3mf2Q==",
"funding": [
{
"type": "paypal",
"url": "https://www.paypal.com/donate/?hosted_button_id=2CKNJLZJBW8ZC"
},
{
"type": "buymeacoffee",
"url": "https://www.buymeacoffee.com/samdecrock"
}
],
"dependencies": {
"des.js": "^1.0.1",
"httpreq": ">=0.4.22",
"js-md4": "^0.3.2",
"underscore": "~1.12.1"
},
"engines": {
"node": ">=10.4.0"
}
},
"node_modules/httpreq": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/httpreq/-/httpreq-1.1.1.tgz",
"integrity": "sha512-uhSZLPPD2VXXOSN8Cni3kIsoFHaU2pT/nySEU/fHr/ePbqHYr0jeiQRmUKLEirC09SFPsdMoA7LU7UXMd/w0Kw==",
"license": "MIT",
"engines": {
"node": ">= 6.15.1"
}
},
"node_modules/https-proxy-agent": { "node_modules/https-proxy-agent": {
"version": "5.0.1", "version": "5.0.1",
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz",
@@ -30579,6 +30381,12 @@
"integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==", "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==",
"dev": true "dev": true
}, },
"node_modules/underscore": {
"version": "1.12.1",
"resolved": "https://registry.npmjs.org/underscore/-/underscore-1.12.1.tgz",
"integrity": "sha512-hEQt0+ZLDVUMhebKxL4x1BTtDY7bavVofhZ9KZ4aI26X9SRaE+Y3m83XUL1UP2jn8ynjndwCCpEHdUG+9pP1Tw==",
"license": "MIT"
},
"node_modules/undici": { "node_modules/undici": {
"version": "6.19.8", "version": "6.19.8",
"resolved": "https://registry.npmjs.org/undici/-/undici-6.19.8.tgz", "resolved": "https://registry.npmjs.org/undici/-/undici-6.19.8.tgz",
+2
View File
@@ -183,6 +183,7 @@
"argon2": "^0.31.2", "argon2": "^0.31.2",
"aws-sdk": "^2.1553.0", "aws-sdk": "^2.1553.0",
"axios": "^1.11.0", "axios": "^1.11.0",
"axios-ntlm": "^1.4.4",
"axios-retry": "^4.0.0", "axios-retry": "^4.0.0",
"bcrypt": "^5.1.1", "bcrypt": "^5.1.1",
"botbuilder": "^4.23.2", "botbuilder": "^4.23.2",
@@ -198,6 +199,7 @@
"googleapis": "^137.1.0", "googleapis": "^137.1.0",
"handlebars": "^4.7.8", "handlebars": "^4.7.8",
"hdb": "^0.19.10", "hdb": "^0.19.10",
"httpntlm": "^1.8.13",
"ioredis": "^5.3.2", "ioredis": "^5.3.2",
"isomorphic-dompurify": "^2.22.0", "isomorphic-dompurify": "^2.22.0",
"jmespath": "^0.16.0", "jmespath": "^0.16.0",
@@ -0,0 +1,23 @@
import { Knex } from "knex";
import { TableName } from "../schemas";
export async function up(knex: Knex): Promise<void> {
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<void> {
const hasPropertiesCol = await knex.schema.hasColumn(TableName.PkiSubscriber, "properties");
if (hasPropertiesCol) {
await knex.schema.alterTable(TableName.PkiSubscriber, (t) => {
t.dropColumn("properties");
});
}
}
+3 -1
View File
@@ -25,7 +25,9 @@ export const PkiSubscribersSchema = z.object({
lastAutoRenewAt: z.date().nullable().optional(), lastAutoRenewAt: z.date().nullable().optional(),
lastOperationStatus: z.string().nullable().optional(), lastOperationStatus: z.string().nullable().optional(),
lastOperationMessage: z.string().nullable().optional(), lastOperationMessage: z.string().nullable().optional(),
lastOperationAt: z.date().nullable().optional() lastOperationAt: z.date().nullable().optional(),
azureAuthMethod: z.string().nullable().optional(),
properties: z.unknown().nullable().optional()
}); });
export type TPkiSubscribers = z.infer<typeof PkiSubscribersSchema>; export type TPkiSubscribers = z.infer<typeof PkiSubscribersSchema>;
+5
View File
@@ -2312,6 +2312,11 @@ export const AppConnections = {
OKTA: { OKTA: {
instanceUrl: "The URL used to access your Okta organization.", instanceUrl: "The URL used to access your Okta organization.",
apiToken: "The API token used to authenticate with Okta." apiToken: "The API token used to authenticate with Okta."
},
AZURE_ADCS: {
adcsUrl: "The URL of the Azure ADCS instance to connect with.",
username: "The username used to access Azure ADCS.",
password: "The password used to access Azure ADCS."
} }
} }
}; };
+1
View File
@@ -948,6 +948,7 @@ export const registerRoutes = async (
certificateAuthorityCrlDAL, certificateAuthorityCrlDAL,
certificateAuthoritySecretDAL, certificateAuthoritySecretDAL,
projectDAL, projectDAL,
appConnectionDAL,
kmsService, kmsService,
permissionService, permissionService,
pkiCollectionDAL, pkiCollectionDAL,
@@ -15,6 +15,10 @@ import {
} from "@app/services/app-connection/1password"; } from "@app/services/app-connection/1password";
import { Auth0ConnectionListItemSchema, SanitizedAuth0ConnectionSchema } from "@app/services/app-connection/auth0"; import { Auth0ConnectionListItemSchema, SanitizedAuth0ConnectionSchema } from "@app/services/app-connection/auth0";
import { AwsConnectionListItemSchema, SanitizedAwsConnectionSchema } from "@app/services/app-connection/aws"; import { AwsConnectionListItemSchema, SanitizedAwsConnectionSchema } from "@app/services/app-connection/aws";
import {
AzureADCSConnectionListItemSchema,
SanitizedAzureADCSConnectionSchema
} from "@app/services/app-connection/azure-adcs/azure-adcs-connection-schemas";
import { import {
AzureAppConfigurationConnectionListItemSchema, AzureAppConfigurationConnectionListItemSchema,
SanitizedAzureAppConfigurationConnectionSchema SanitizedAzureAppConfigurationConnectionSchema
@@ -150,7 +154,8 @@ const SanitizedAppConnectionSchema = z.union([
...SanitizedSupabaseConnectionSchema.options, ...SanitizedSupabaseConnectionSchema.options,
...SanitizedDigitalOceanConnectionSchema.options, ...SanitizedDigitalOceanConnectionSchema.options,
...SanitizedNetlifyConnectionSchema.options, ...SanitizedNetlifyConnectionSchema.options,
...SanitizedOktaConnectionSchema.options ...SanitizedOktaConnectionSchema.options,
...SanitizedAzureADCSConnectionSchema.options
]); ]);
const AppConnectionOptionsSchema = z.discriminatedUnion("app", [ const AppConnectionOptionsSchema = z.discriminatedUnion("app", [
@@ -190,7 +195,8 @@ const AppConnectionOptionsSchema = z.discriminatedUnion("app", [
SupabaseConnectionListItemSchema, SupabaseConnectionListItemSchema,
DigitalOceanConnectionListItemSchema, DigitalOceanConnectionListItemSchema,
NetlifyConnectionListItemSchema, NetlifyConnectionListItemSchema,
OktaConnectionListItemSchema OktaConnectionListItemSchema,
AzureADCSConnectionListItemSchema
]); ]);
export const registerAppConnectionRouter = async (server: FastifyZodProvider) => { export const registerAppConnectionRouter = async (server: FastifyZodProvider) => {
@@ -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
});
};
@@ -5,6 +5,7 @@ import { AppConnection } from "@app/services/app-connection/app-connection-enums
import { registerOnePassConnectionRouter } from "./1password-connection-router"; import { registerOnePassConnectionRouter } from "./1password-connection-router";
import { registerAuth0ConnectionRouter } from "./auth0-connection-router"; import { registerAuth0ConnectionRouter } from "./auth0-connection-router";
import { registerAwsConnectionRouter } from "./aws-connection-router"; import { registerAwsConnectionRouter } from "./aws-connection-router";
import { registerAzureADCSConnectionRouter } from "./azure-adcs-connection-router";
import { registerAzureAppConfigurationConnectionRouter } from "./azure-app-configuration-connection-router"; import { registerAzureAppConfigurationConnectionRouter } from "./azure-app-configuration-connection-router";
import { registerAzureClientSecretsConnectionRouter } from "./azure-client-secrets-connection-router"; import { registerAzureClientSecretsConnectionRouter } from "./azure-client-secrets-connection-router";
import { registerAzureDevOpsConnectionRouter } from "./azure-devops-connection-router"; import { registerAzureDevOpsConnectionRouter } from "./azure-devops-connection-router";
@@ -50,6 +51,7 @@ export const APP_CONNECTION_REGISTER_ROUTER_MAP: Record<AppConnection, (server:
[AppConnection.AzureAppConfiguration]: registerAzureAppConfigurationConnectionRouter, [AppConnection.AzureAppConfiguration]: registerAzureAppConfigurationConnectionRouter,
[AppConnection.AzureClientSecrets]: registerAzureClientSecretsConnectionRouter, [AppConnection.AzureClientSecrets]: registerAzureClientSecretsConnectionRouter,
[AppConnection.AzureDevOps]: registerAzureDevOpsConnectionRouter, [AppConnection.AzureDevOps]: registerAzureDevOpsConnectionRouter,
[AppConnection.AzureADCS]: registerAzureADCSConnectionRouter,
[AppConnection.Databricks]: registerDatabricksConnectionRouter, [AppConnection.Databricks]: registerDatabricksConnectionRouter,
[AppConnection.Humanitec]: registerHumanitecConnectionRouter, [AppConnection.Humanitec]: registerHumanitecConnectionRouter,
[AppConnection.TerraformCloud]: registerTerraformCloudConnectionRouter, [AppConnection.TerraformCloud]: registerTerraformCloudConnectionRouter,
@@ -0,0 +1,65 @@
import { z } from "zod";
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 {
AzureAdCsCertificateAuthoritySchema,
CreateAzureAdCsCertificateAuthoritySchema,
UpdateAzureAdCsCertificateAuthoritySchema
} 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 { registerCertificateAuthorityEndpoints } from "./certificate-authority-endpoints";
export const registerAzureAdCsCertificateAuthorityRouter = async (server: FastifyZodProvider) => {
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
});
return { templates };
}
});
};
@@ -1,6 +1,7 @@
import { CaType } from "@app/services/certificate-authority/certificate-authority-enums"; import { CaType } from "@app/services/certificate-authority/certificate-authority-enums";
import { registerAcmeCertificateAuthorityRouter } from "./acme-certificate-authority-router"; import { registerAcmeCertificateAuthorityRouter } from "./acme-certificate-authority-router";
import { registerAzureAdCsCertificateAuthorityRouter } from "./azure-ad-cs-certificate-authority-router";
import { registerInternalCertificateAuthorityRouter } from "./internal-certificate-authority-router"; import { registerInternalCertificateAuthorityRouter } from "./internal-certificate-authority-router";
export * 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<CaType, (server: FastifyZodProvider) => Promise<void>> = export const CERTIFICATE_AUTHORITY_REGISTER_ROUTER_MAP: Record<CaType, (server: FastifyZodProvider) => Promise<void>> =
{ {
[CaType.INTERNAL]: registerInternalCertificateAuthorityRouter, [CaType.INTERNAL]: registerInternalCertificateAuthorityRouter,
[CaType.ACME]: registerAcmeCertificateAuthorityRouter [CaType.ACME]: registerAcmeCertificateAuthorityRouter,
[CaType.AZURE_AD_CS]: registerAzureAdCsCertificateAuthorityRouter
}; };
@@ -112,7 +112,19 @@ export const registerPkiSubscriberRouter = async (server: FastifyZodProvider) =>
.transform((arr) => Array.from(new Set(arr))) .transform((arr) => Array.from(new Set(arr)))
.describe(PKI_SUBSCRIBERS.CREATE.extendedKeyUsages), .describe(PKI_SUBSCRIBERS.CREATE.extendedKeyUsages),
enableAutoRenewal: z.boolean().optional().describe(PKI_SUBSCRIBERS.CREATE.enableAutoRenewal), 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().optional().describe("Organization (O)"),
organizationalUnit: z.string().optional().describe("Organizational Unit (OU)"),
country: z.string().length(2).optional().describe("Country (C) - Two letter country code"),
state: z.string().optional().describe("State/Province (ST)"),
locality: z.string().optional().describe("Locality (L)"),
emailAddress: z.string().email().optional().describe("Email Address")
})
.optional()
.describe("Additional subscriber properties and subject fields")
}), }),
response: { response: {
200: sanitizedPkiSubscriber 200: sanitizedPkiSubscriber
@@ -199,7 +211,19 @@ export const registerPkiSubscriberRouter = async (server: FastifyZodProvider) =>
.optional() .optional()
.describe(PKI_SUBSCRIBERS.UPDATE.extendedKeyUsages), .describe(PKI_SUBSCRIBERS.UPDATE.extendedKeyUsages),
enableAutoRenewal: z.boolean().optional().describe(PKI_SUBSCRIBERS.UPDATE.enableAutoRenewal), 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().optional().describe("Organization (O)"),
organizationalUnit: z.string().optional().describe("Organizational Unit (OU)"),
country: z.string().length(2).optional().describe("Country (C) - Two letter country code"),
state: z.string().optional().describe("State/Province (ST)"),
locality: z.string().optional().describe("Locality (L)"),
emailAddress: z.string().email().optional().describe("Email Address")
})
.optional()
.describe("Additional subscriber properties and subject fields")
}), }),
response: { response: {
200: sanitizedPkiSubscriber 200: sanitizedPkiSubscriber
@@ -6,12 +6,14 @@ import { readLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type"; import { AuthMode } from "@app/services/auth/auth-type";
import { AcmeCertificateAuthoritySchema } from "@app/services/certificate-authority/acme/acme-certificate-authority-schemas"; 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 { CaType } from "@app/services/certificate-authority/certificate-authority-enums";
import { InternalCertificateAuthoritySchema } from "@app/services/certificate-authority/internal/internal-certificate-authority-schemas"; import { InternalCertificateAuthoritySchema } from "@app/services/certificate-authority/internal/internal-certificate-authority-schemas";
const CertificateAuthoritySchema = z.discriminatedUnion("type", [ const CertificateAuthoritySchema = z.discriminatedUnion("type", [
InternalCertificateAuthoritySchema, InternalCertificateAuthoritySchema,
AcmeCertificateAuthoritySchema AcmeCertificateAuthoritySchema,
AzureAdCsCertificateAuthoritySchema
]); ]);
export const registerCaRouter = async (server: FastifyZodProvider) => { export const registerCaRouter = async (server: FastifyZodProvider) => {
@@ -52,19 +54,31 @@ export const registerCaRouter = async (server: FastifyZodProvider) => {
req.permission 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({ await server.services.auditLog.createAuditLog({
...req.auditLogInfo, ...req.auditLogInfo,
projectId: req.query.projectId, projectId: req.query.projectId,
event: { event: {
type: EventType.GET_CAS, type: EventType.GET_CAS,
metadata: { 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 { return {
certificateAuthorities: [...(internalCas ?? []), ...(acmeCas ?? [])] certificateAuthorities: [...(internalCas ?? []), ...(acmeCas ?? []), ...(azureAdCsCas ?? [])]
}; };
} }
}); });
@@ -8,6 +8,7 @@ export enum AppConnection {
AzureAppConfiguration = "azure-app-configuration", AzureAppConfiguration = "azure-app-configuration",
AzureClientSecrets = "azure-client-secrets", AzureClientSecrets = "azure-client-secrets",
AzureDevOps = "azure-devops", AzureDevOps = "azure-devops",
AzureADCS = "azure-adcs",
Humanitec = "humanitec", Humanitec = "humanitec",
TerraformCloud = "terraform-cloud", TerraformCloud = "terraform-cloud",
Vercel = "vercel", Vercel = "vercel",
@@ -31,6 +31,11 @@ import {
} from "./app-connection-types"; } from "./app-connection-types";
import { Auth0ConnectionMethod, getAuth0ConnectionListItem, validateAuth0ConnectionCredentials } from "./auth0"; import { Auth0ConnectionMethod, getAuth0ConnectionListItem, validateAuth0ConnectionCredentials } from "./auth0";
import { AwsConnectionMethod, getAwsConnectionListItem, validateAwsConnectionCredentials } from "./aws"; import { AwsConnectionMethod, getAwsConnectionListItem, validateAwsConnectionCredentials } from "./aws";
import { AzureADCSConnectionMethod } from "./azure-adcs";
import {
getAzureADCSConnectionListItem,
validateAzureADCSConnectionCredentials
} from "./azure-adcs/azure-adcs-connection-fns";
import { import {
AzureAppConfigurationConnectionMethod, AzureAppConfigurationConnectionMethod,
getAzureAppConfigurationConnectionListItem, getAzureAppConfigurationConnectionListItem,
@@ -136,6 +141,7 @@ export const listAppConnectionOptions = () => {
getAzureKeyVaultConnectionListItem(), getAzureKeyVaultConnectionListItem(),
getAzureAppConfigurationConnectionListItem(), getAzureAppConfigurationConnectionListItem(),
getAzureDevopsConnectionListItem(), getAzureDevopsConnectionListItem(),
getAzureADCSConnectionListItem(),
getDatabricksConnectionListItem(), getDatabricksConnectionListItem(),
getHumanitecConnectionListItem(), getHumanitecConnectionListItem(),
getTerraformCloudConnectionListItem(), getTerraformCloudConnectionListItem(),
@@ -227,6 +233,7 @@ export const validateAppConnectionCredentials = async (
[AppConnection.AzureClientSecrets]: [AppConnection.AzureClientSecrets]:
validateAzureClientSecretsConnectionCredentials as TAppConnectionCredentialsValidator, validateAzureClientSecretsConnectionCredentials as TAppConnectionCredentialsValidator,
[AppConnection.AzureDevOps]: validateAzureDevOpsConnectionCredentials as TAppConnectionCredentialsValidator, [AppConnection.AzureDevOps]: validateAzureDevOpsConnectionCredentials as TAppConnectionCredentialsValidator,
[AppConnection.AzureADCS]: validateAzureADCSConnectionCredentials as TAppConnectionCredentialsValidator,
[AppConnection.Humanitec]: validateHumanitecConnectionCredentials as TAppConnectionCredentialsValidator, [AppConnection.Humanitec]: validateHumanitecConnectionCredentials as TAppConnectionCredentialsValidator,
[AppConnection.Postgres]: validateSqlConnectionCredentials as TAppConnectionCredentialsValidator, [AppConnection.Postgres]: validateSqlConnectionCredentials as TAppConnectionCredentialsValidator,
[AppConnection.MsSql]: validateSqlConnectionCredentials as TAppConnectionCredentialsValidator, [AppConnection.MsSql]: validateSqlConnectionCredentials as TAppConnectionCredentialsValidator,
@@ -300,6 +307,7 @@ export const getAppConnectionMethodName = (method: TAppConnection["method"]) =>
case MsSqlConnectionMethod.UsernameAndPassword: case MsSqlConnectionMethod.UsernameAndPassword:
case MySqlConnectionMethod.UsernameAndPassword: case MySqlConnectionMethod.UsernameAndPassword:
case OracleDBConnectionMethod.UsernameAndPassword: case OracleDBConnectionMethod.UsernameAndPassword:
case AzureADCSConnectionMethod.UsernamePassword:
return "Username & Password"; return "Username & Password";
case WindmillConnectionMethod.AccessToken: case WindmillConnectionMethod.AccessToken:
case HCVaultConnectionMethod.AccessToken: case HCVaultConnectionMethod.AccessToken:
@@ -357,6 +365,7 @@ export const TRANSITION_CONNECTION_CREDENTIALS_TO_PLATFORM: Record<
[AppConnection.AzureKeyVault]: platformManagedCredentialsNotSupported, [AppConnection.AzureKeyVault]: platformManagedCredentialsNotSupported,
[AppConnection.AzureAppConfiguration]: platformManagedCredentialsNotSupported, [AppConnection.AzureAppConfiguration]: platformManagedCredentialsNotSupported,
[AppConnection.AzureDevOps]: platformManagedCredentialsNotSupported, [AppConnection.AzureDevOps]: platformManagedCredentialsNotSupported,
[AppConnection.AzureADCS]: platformManagedCredentialsNotSupported,
[AppConnection.Humanitec]: platformManagedCredentialsNotSupported, [AppConnection.Humanitec]: platformManagedCredentialsNotSupported,
[AppConnection.Postgres]: transferSqlConnectionCredentialsToPlatform as TAppConnectionTransitionCredentialsToPlatform, [AppConnection.Postgres]: transferSqlConnectionCredentialsToPlatform as TAppConnectionTransitionCredentialsToPlatform,
[AppConnection.MsSql]: transferSqlConnectionCredentialsToPlatform as TAppConnectionTransitionCredentialsToPlatform, [AppConnection.MsSql]: transferSqlConnectionCredentialsToPlatform as TAppConnectionTransitionCredentialsToPlatform,
@@ -9,6 +9,7 @@ export const APP_CONNECTION_NAME_MAP: Record<AppConnection, string> = {
[AppConnection.AzureAppConfiguration]: "Azure App Configuration", [AppConnection.AzureAppConfiguration]: "Azure App Configuration",
[AppConnection.AzureClientSecrets]: "Azure Client Secrets", [AppConnection.AzureClientSecrets]: "Azure Client Secrets",
[AppConnection.AzureDevOps]: "Azure DevOps", [AppConnection.AzureDevOps]: "Azure DevOps",
[AppConnection.AzureADCS]: "Azure ADCS",
[AppConnection.Databricks]: "Databricks", [AppConnection.Databricks]: "Databricks",
[AppConnection.Humanitec]: "Humanitec", [AppConnection.Humanitec]: "Humanitec",
[AppConnection.TerraformCloud]: "Terraform Cloud", [AppConnection.TerraformCloud]: "Terraform Cloud",
@@ -49,6 +50,7 @@ export const APP_CONNECTION_PLAN_MAP: Record<AppConnection, AppConnectionPlanTyp
[AppConnection.AzureAppConfiguration]: AppConnectionPlanType.Regular, [AppConnection.AzureAppConfiguration]: AppConnectionPlanType.Regular,
[AppConnection.AzureClientSecrets]: AppConnectionPlanType.Regular, [AppConnection.AzureClientSecrets]: AppConnectionPlanType.Regular,
[AppConnection.AzureDevOps]: AppConnectionPlanType.Regular, [AppConnection.AzureDevOps]: AppConnectionPlanType.Regular,
[AppConnection.AzureADCS]: AppConnectionPlanType.Regular,
[AppConnection.Databricks]: AppConnectionPlanType.Regular, [AppConnection.Databricks]: AppConnectionPlanType.Regular,
[AppConnection.Humanitec]: AppConnectionPlanType.Regular, [AppConnection.Humanitec]: AppConnectionPlanType.Regular,
[AppConnection.TerraformCloud]: AppConnectionPlanType.Regular, [AppConnection.TerraformCloud]: AppConnectionPlanType.Regular,
@@ -45,6 +45,7 @@ import {
import { ValidateAuth0ConnectionCredentialsSchema } from "./auth0"; import { ValidateAuth0ConnectionCredentialsSchema } from "./auth0";
import { ValidateAwsConnectionCredentialsSchema } from "./aws"; import { ValidateAwsConnectionCredentialsSchema } from "./aws";
import { awsConnectionService } from "./aws/aws-connection-service"; import { awsConnectionService } from "./aws/aws-connection-service";
import { ValidateAzureADCSConnectionCredentialsSchema } from "./azure-adcs/azure-adcs-connection-schemas";
import { ValidateAzureAppConfigurationConnectionCredentialsSchema } from "./azure-app-configuration"; import { ValidateAzureAppConfigurationConnectionCredentialsSchema } from "./azure-app-configuration";
import { ValidateAzureClientSecretsConnectionCredentialsSchema } from "./azure-client-secrets"; import { ValidateAzureClientSecretsConnectionCredentialsSchema } from "./azure-client-secrets";
import { azureClientSecretsConnectionService } from "./azure-client-secrets/azure-client-secrets-service"; import { azureClientSecretsConnectionService } from "./azure-client-secrets/azure-client-secrets-service";
@@ -122,6 +123,7 @@ const VALIDATE_APP_CONNECTION_CREDENTIALS_MAP: Record<AppConnection, TValidateAp
[AppConnection.AzureKeyVault]: ValidateAzureKeyVaultConnectionCredentialsSchema, [AppConnection.AzureKeyVault]: ValidateAzureKeyVaultConnectionCredentialsSchema,
[AppConnection.AzureAppConfiguration]: ValidateAzureAppConfigurationConnectionCredentialsSchema, [AppConnection.AzureAppConfiguration]: ValidateAzureAppConfigurationConnectionCredentialsSchema,
[AppConnection.AzureDevOps]: ValidateAzureDevOpsConnectionCredentialsSchema, [AppConnection.AzureDevOps]: ValidateAzureDevOpsConnectionCredentialsSchema,
[AppConnection.AzureADCS]: ValidateAzureADCSConnectionCredentialsSchema,
[AppConnection.Databricks]: ValidateDatabricksConnectionCredentialsSchema, [AppConnection.Databricks]: ValidateDatabricksConnectionCredentialsSchema,
[AppConnection.Humanitec]: ValidateHumanitecConnectionCredentialsSchema, [AppConnection.Humanitec]: ValidateHumanitecConnectionCredentialsSchema,
[AppConnection.TerraformCloud]: ValidateTerraformCloudConnectionCredentialsSchema, [AppConnection.TerraformCloud]: ValidateTerraformCloudConnectionCredentialsSchema,
@@ -33,6 +33,12 @@ import {
TAwsConnectionInput, TAwsConnectionInput,
TValidateAwsConnectionCredentialsSchema TValidateAwsConnectionCredentialsSchema
} from "./aws"; } from "./aws";
import {
TAzureADCSConnection,
TAzureADCSConnectionConfig,
TAzureADCSConnectionInput,
TValidateAzureADCSConnectionCredentialsSchema
} from "./azure-adcs/azure-adcs-connection-types";
import { import {
TAzureAppConfigurationConnection, TAzureAppConfigurationConnection,
TAzureAppConfigurationConnectionConfig, TAzureAppConfigurationConnectionConfig,
@@ -223,6 +229,7 @@ export type TAppConnection = { id: string } & (
| TAzureKeyVaultConnection | TAzureKeyVaultConnection
| TAzureAppConfigurationConnection | TAzureAppConfigurationConnection
| TAzureDevOpsConnection | TAzureDevOpsConnection
| TAzureADCSConnection
| TDatabricksConnection | TDatabricksConnection
| THumanitecConnection | THumanitecConnection
| TTerraformCloudConnection | TTerraformCloudConnection
@@ -267,6 +274,7 @@ export type TAppConnectionInput = { id: string } & (
| TAzureKeyVaultConnectionInput | TAzureKeyVaultConnectionInput
| TAzureAppConfigurationConnectionInput | TAzureAppConfigurationConnectionInput
| TAzureDevOpsConnectionInput | TAzureDevOpsConnectionInput
| TAzureADCSConnectionInput
| TDatabricksConnectionInput | TDatabricksConnectionInput
| THumanitecConnectionInput | THumanitecConnectionInput
| TTerraformCloudConnectionInput | TTerraformCloudConnectionInput
@@ -322,6 +330,7 @@ export type TAppConnectionConfig =
| TAzureKeyVaultConnectionConfig | TAzureKeyVaultConnectionConfig
| TAzureAppConfigurationConnectionConfig | TAzureAppConfigurationConnectionConfig
| TAzureDevOpsConnectionConfig | TAzureDevOpsConnectionConfig
| TAzureADCSConnectionConfig
| TAzureClientSecretsConnectionConfig | TAzureClientSecretsConnectionConfig
| TDatabricksConnectionConfig | TDatabricksConnectionConfig
| THumanitecConnectionConfig | THumanitecConnectionConfig
@@ -359,6 +368,7 @@ export type TValidateAppConnectionCredentialsSchema =
| TValidateAzureAppConfigurationConnectionCredentialsSchema | TValidateAzureAppConfigurationConnectionCredentialsSchema
| TValidateAzureClientSecretsConnectionCredentialsSchema | TValidateAzureClientSecretsConnectionCredentialsSchema
| TValidateAzureDevOpsConnectionCredentialsSchema | TValidateAzureDevOpsConnectionCredentialsSchema
| TValidateAzureADCSConnectionCredentialsSchema
| TValidateDatabricksConnectionCredentialsSchema | TValidateDatabricksConnectionCredentialsSchema
| TValidateHumanitecConnectionCredentialsSchema | TValidateHumanitecConnectionCredentialsSchema
| TValidatePostgresConnectionCredentialsSchema | TValidatePostgresConnectionCredentialsSchema
@@ -0,0 +1,3 @@
export enum AzureADCSConnectionMethod {
UsernamePassword = "username-password"
}
@@ -0,0 +1,354 @@
/* eslint-disable no-case-declarations, @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-var-requires, no-await-in-loop, no-continue */
import { BadRequestError, NotFoundError } from "@app/lib/errors";
import { decryptAppConnectionCredentials } from "@app/services/app-connection/app-connection-fns";
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
import { TAppConnectionDALFactory } from "../app-connection-dal";
import { AppConnection } from "../app-connection-enums";
import { AzureADCSConnectionMethod } from "./azure-adcs-connection-enums";
import { TAzureADCSConnectionConfig } from "./azure-adcs-connection-types";
const httpntlm = require("httpntlm");
// Type definitions for httpntlm module
interface HttpNtlmRequestOptions {
url: string;
username: string;
password: string;
domain: string;
workstation?: string;
method?: string;
body?: string;
headers?: Record<string, string>;
rejectUnauthorized?: boolean;
}
interface HttpNtlmResponse {
statusCode: number;
body: string;
headers: Record<string, string>;
}
// 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: [email protected]
const [user, domainPart] = inputUsername.split("@");
if (!user || !domainPart) {
throw new BadRequestError({
message: "Invalid UPN format. Expected format: [email protected]"
});
}
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 ntlmRequest = (options: HttpNtlmRequestOptions): Promise<HttpNtlmResponse> => {
return new Promise((resolve, reject) => {
const method = options.method || "GET";
if (method.toLowerCase() === "get") {
httpntlm.get(options, (err: Error | null, res: HttpNtlmResponse) => {
if (err) reject(err);
else resolve(res);
});
} else if (method.toLowerCase() === "post") {
httpntlm.post(options, (err: Error | null, res: HttpNtlmResponse) => {
if (err) reject(err);
else resolve(res);
});
} else {
reject(new Error(`Unsupported HTTP method: ${method}`));
}
});
};
// Test ADCS connectivity and authentication using NTLM
const testAdcsConnection = async (
credentials: ParsedCredentials,
password: string,
baseUrl: string
): Promise<boolean> => {
// 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 response = await ntlmRequest({
url: testUrl,
username: credentials.username,
password,
domain: credentials.domain,
workstation: "",
rejectUnauthorized: false
});
// Check if we got a successful response
if (response.statusCode === 200) {
const responseText = response.body;
// 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;
}
}
// Handle authentication failures
if (response.statusCode === 401) {
throw new BadRequestError({
message: "Authentication failed. Please verify your username, password, and domain are correct."
});
}
if (response.statusCode === 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")) {
throw new BadRequestError({
message: "Connection timeout. Please verify the server is accessible and not blocked by firewall."
});
}
}
// 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) => {
const parsedCredentials = parseCredentials(username);
const normalizedUrl = normalizeAdcsUrl(baseUrl);
return {
get: (endpoint: string, additionalOptions: Partial<HttpNtlmRequestOptions> = {}) => {
return ntlmRequest({
url: `${normalizedUrl}${endpoint}`,
username: parsedCredentials.username,
password,
domain: parsedCredentials.domain,
workstation: "",
rejectUnauthorized: false,
...additionalOptions
});
},
post: (endpoint: string, body: string, additionalOptions: Partial<HttpNtlmRequestOptions> = {}) => {
return ntlmRequest({
method: "POST",
url: `${normalizedUrl}${endpoint}`,
username: parsedCredentials.username,
password,
domain: parsedCredentials.domain,
workstation: "",
rejectUnauthorized: false,
body,
headers: {
"Content-Type": "application/x-www-form-urlencoded",
...additionalOptions.headers
},
...additionalOptions
});
},
baseUrl: normalizedUrl,
credentials: parsedCredentials
};
};
export const getAzureADCSConnectionCredentials = async (
connectionId: string,
appConnectionDAL: Pick<TAppConnectionDALFactory, "findById">,
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">
) => {
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 };
return {
username: credentials.username,
password: credentials.password,
adcsUrl: credentials.adcsUrl
};
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);
// Test the connection using NTLM
await testAdcsConnection(parsedCredentials, credentials.password, normalizedUrl);
// If we get here, authentication was successful
return {
username: credentials.username,
password: credentials.password,
adcsUrl: credentials.adcsUrl
};
} 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")
) {
errorMessage = "SSL/TLS certificate error. The server certificate may be self-signed or invalid.";
}
}
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) => {
return createNtlmClient(username, password, baseUrl);
};
@@ -0,0 +1,77 @@
import z from "zod";
import { AppConnections } from "@app/lib/api-docs";
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
import {
BaseAppConnectionSchema,
GenericCreateAppConnectionFieldsSchema,
GenericUpdateAppConnectionFieldsSchema
} from "@app/services/app-connection/app-connection-schemas";
import { AzureADCSConnectionMethod } from "./azure-adcs-connection-enums";
export const AzureADCSConnectionAccessTokenCredentialsSchema = z.object({
adcsUrl: z
.string()
.trim()
.min(1, "ADCS URL required")
.max(255)
.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)
});
const BaseAzureADCSConnectionSchema = BaseAppConnectionSchema.extend({ app: z.literal(AppConnection.AzureADCS) });
export const AzureADCSConnectionSchema = BaseAzureADCSConnectionSchema.extend({
method: z.literal(AzureADCSConnectionMethod.UsernamePassword),
credentials: AzureADCSConnectionAccessTokenCredentialsSchema
});
export const SanitizedAzureADCSConnectionSchema = z.discriminatedUnion("method", [
BaseAzureADCSConnectionSchema.extend({
method: z.literal(AzureADCSConnectionMethod.UsernamePassword),
credentials: AzureADCSConnectionAccessTokenCredentialsSchema.pick({
username: true
})
})
]);
export const ValidateAzureADCSConnectionCredentialsSchema = z.discriminatedUnion("method", [
z.object({
method: z
.literal(AzureADCSConnectionMethod.UsernamePassword)
.describe(AppConnections.CREATE(AppConnection.AzureADCS).method),
credentials: AzureADCSConnectionAccessTokenCredentialsSchema.describe(
AppConnections.CREATE(AppConnection.AzureADCS).credentials
)
})
]);
export const CreateAzureADCSConnectionSchema = ValidateAzureADCSConnectionCredentialsSchema.and(
GenericCreateAppConnectionFieldsSchema(AppConnection.AzureADCS)
);
export const UpdateAzureADCSConnectionSchema = z
.object({
credentials: AzureADCSConnectionAccessTokenCredentialsSchema.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()
});
@@ -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<typeof AzureADCSConnectionSchema>;
export type TAzureADCSConnectionInput = z.infer<typeof CreateAzureADCSConnectionSchema> & {
app: AppConnection.AzureADCS;
};
export type TValidateAzureADCSConnectionCredentialsSchema = typeof ValidateAzureADCSConnectionCredentialsSchema;
export type TAzureADCSConnectionConfig = DiscriminativePick<
TAzureADCSConnectionInput,
"method" | "app" | "credentials"
>;
@@ -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";
@@ -0,0 +1,12 @@
export enum AzureAdCsTemplateType {
WEB_SERVER = "WebServer",
COMPUTER = "Computer",
USER = "User",
DOMAIN_CONTROLLER = "DomainController",
SUBORDINATE_CA = "SubordinateCA"
}
export enum AzureAdCsAuthMethod {
CLIENT_CERTIFICATE = "client-certificate",
KERBEROS = "kerberos"
}
@@ -0,0 +1,35 @@
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 AzureAdCsCertificateAuthorityCredentialsSchema = z.object({
clientId: z.string(),
clientSecret: z.string().optional(),
certificateThumbprint: z.string().optional()
});
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()
});
@@ -0,0 +1,15 @@
import { z } from "zod";
import {
AzureAdCsCertificateAuthoritySchema,
CreateAzureAdCsCertificateAuthoritySchema,
UpdateAzureAdCsCertificateAuthoritySchema
} from "./azure-ad-cs-certificate-authority-schemas";
export type TAzureAdCsCertificateAuthority = z.infer<typeof AzureAdCsCertificateAuthoritySchema>;
export type TAzureAdCsCertificateAuthorityInput = z.infer<typeof CreateAzureAdCsCertificateAuthoritySchema>;
export type TCreateAzureAdCsCertificateAuthorityDTO = z.infer<typeof CreateAzureAdCsCertificateAuthoritySchema>;
export type TUpdateAzureAdCsCertificateAuthorityDTO = z.infer<typeof UpdateAzureAdCsCertificateAuthoritySchema>;
@@ -1,6 +1,7 @@
export enum CaType { export enum CaType {
INTERNAL = "internal", INTERNAL = "internal",
ACME = "acme" ACME = "acme",
AZURE_AD_CS = "azure-ad-cs"
} }
export enum InternalCaType { export enum InternalCaType {
@@ -17,3 +18,9 @@ export enum CaStatus {
export enum CaRenewalType { export enum CaRenewalType {
EXISTING = "existing" EXISTING = "existing"
} }
export enum CaCapability {
ISSUE_CERTIFICATES = "issue-certificates",
REVOKE_CERTIFICATES = "revoke-certificates",
RENEW_CERTIFICATES = "renew-certificates"
}
@@ -1,6 +1,33 @@
import { CaType } from "./certificate-authority-enums"; import { CaCapability, CaType } from "./certificate-authority-enums";
export const CERTIFICATE_AUTHORITIES_TYPE_MAP: Record<CaType, string> = { export const CERTIFICATE_AUTHORITIES_TYPE_MAP: Record<CaType, string> = {
[CaType.INTERNAL]: "Internal", [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, CaCapability[]> = {
[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);
}; };
@@ -21,6 +21,7 @@ import { TCertificateSecretDALFactory } from "../certificate/certificate-secret-
import { TPkiSubscriberDALFactory } from "../pki-subscriber/pki-subscriber-dal"; import { TPkiSubscriberDALFactory } from "../pki-subscriber/pki-subscriber-dal";
import { SubscriberOperationStatus } from "../pki-subscriber/pki-subscriber-types"; import { SubscriberOperationStatus } from "../pki-subscriber/pki-subscriber-types";
import { AcmeCertificateAuthorityFns } from "./acme/acme-certificate-authority-fns"; 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 { TCertificateAuthorityDALFactory } from "./certificate-authority-dal";
import { CaType } from "./certificate-authority-enums"; import { CaType } from "./certificate-authority-enums";
import { keyAlgorithmToAlgCfg } from "./certificate-authority-fns"; import { keyAlgorithmToAlgCfg } from "./certificate-authority-fns";
@@ -33,7 +34,7 @@ import {
type TCertificateAuthorityQueueFactoryDep = { type TCertificateAuthorityQueueFactoryDep = {
certificateAuthorityDAL: TCertificateAuthorityDALFactory; certificateAuthorityDAL: TCertificateAuthorityDALFactory;
appConnectionDAL: Pick<TAppConnectionDALFactory, "findById" | "update">; appConnectionDAL: Pick<TAppConnectionDALFactory, "findById" | "update" | "updateById">;
appConnectionService: Pick<TAppConnectionServiceFactory, "connectAppConnectionById">; appConnectionService: Pick<TAppConnectionServiceFactory, "connectAppConnectionById">;
externalCertificateAuthorityDAL: Pick<TExternalCertificateAuthorityDALFactory, "create" | "update">; externalCertificateAuthorityDAL: Pick<TExternalCertificateAuthorityDALFactory, "create" | "update">;
keyStore: Pick<TKeyStoreFactory, "acquireLock" | "setItemWithExpiry" | "getItem">; keyStore: Pick<TKeyStoreFactory, "acquireLock" | "setItemWithExpiry" | "getItem">;
@@ -82,6 +83,19 @@ export const certificateAuthorityQueueFactory = ({
projectDAL projectDAL
}); });
const azureAdCsFns = AzureAdCsCertificateAuthorityFns({
appConnectionDAL,
appConnectionService,
certificateAuthorityDAL,
externalCertificateAuthorityDAL,
certificateDAL,
certificateBodyDAL,
certificateSecretDAL,
kmsService,
pkiSubscriberDAL,
projectDAL
});
// TODO 1: auto-periodic rotation // TODO 1: auto-periodic rotation
// TODO 2: manual rotation // TODO 2: manual rotation
@@ -158,6 +172,13 @@ export const certificateAuthorityQueueFactory = ({
lastOperationMessage: "Certificate ordered successfully", lastOperationMessage: "Certificate ordered successfully",
lastOperationAt: new Date() 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) { } catch (e: unknown) {
if (e instanceof Error) { if (e instanceof Error) {
@@ -2,7 +2,11 @@ import { ForbiddenError } from "@casl/ability";
import { ActionProjectType, TableName } from "@app/db/schemas"; import { ActionProjectType, TableName } from "@app/db/schemas";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types";
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; import {
ProjectPermissionActions,
ProjectPermissionCertificateActions,
ProjectPermissionSub
} from "@app/ee/services/permission/project-permission";
import { BadRequestError, NotFoundError } from "@app/lib/errors"; import { BadRequestError, NotFoundError } from "@app/lib/errors";
import { OrgServiceActor } from "@app/lib/types"; import { OrgServiceActor } from "@app/lib/types";
@@ -22,6 +26,14 @@ import {
TCreateAcmeCertificateAuthorityDTO, TCreateAcmeCertificateAuthorityDTO,
TUpdateAcmeCertificateAuthorityDTO TUpdateAcmeCertificateAuthorityDTO
} from "./acme/acme-certificate-authority-types"; } 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 { TCertificateAuthorityDALFactory } from "./certificate-authority-dal";
import { CaType } from "./certificate-authority-enums"; import { CaType } from "./certificate-authority-enums";
import { import {
@@ -34,7 +46,7 @@ import { TInternalCertificateAuthorityServiceFactory } from "./internal/internal
import { TCreateInternalCertificateAuthorityDTO } from "./internal/internal-certificate-authority-types"; import { TCreateInternalCertificateAuthorityDTO } from "./internal/internal-certificate-authority-types";
type TCertificateAuthorityServiceFactoryDep = { type TCertificateAuthorityServiceFactoryDep = {
appConnectionDAL: Pick<TAppConnectionDALFactory, "findById" | "update">; appConnectionDAL: Pick<TAppConnectionDALFactory, "findById" | "update" | "updateById">;
appConnectionService: Pick<TAppConnectionServiceFactory, "connectAppConnectionById">; appConnectionService: Pick<TAppConnectionServiceFactory, "connectAppConnectionById">;
certificateAuthorityDAL: Pick< certificateAuthorityDAL: Pick<
TCertificateAuthorityDALFactory, TCertificateAuthorityDALFactory,
@@ -91,6 +103,19 @@ export const certificateAuthorityServiceFactory = ({
projectDAL projectDAL
}); });
const azureAdCsFns = AzureAdCsCertificateAuthorityFns({
appConnectionDAL,
appConnectionService,
certificateAuthorityDAL,
externalCertificateAuthorityDAL,
certificateDAL,
certificateBodyDAL,
certificateSecretDAL,
kmsService,
pkiSubscriberDAL,
projectDAL
});
const createCertificateAuthority = async ( const createCertificateAuthority = async (
{ type, projectId, name, enableDirectIssuance, configuration, status }: TCreateCertificateAuthorityDTO, { type, projectId, name, enableDirectIssuance, configuration, status }: TCreateCertificateAuthorityDTO,
actor: OrgServiceActor actor: OrgServiceActor
@@ -146,6 +171,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" }); throw new BadRequestError({ message: "Invalid certificate authority type" });
}; };
@@ -205,6 +241,10 @@ export const certificateAuthorityServiceFactory = ({
return castDbEntryToAcmeCertificateAuthority(certificateAuthority); return castDbEntryToAcmeCertificateAuthority(certificateAuthority);
} }
if (type === CaType.AZURE_AD_CS) {
return castDbEntryToAzureAdCsCertificateAuthority(certificateAuthority);
}
throw new BadRequestError({ message: "Invalid certificate authority type" }); throw new BadRequestError({ message: "Invalid certificate authority type" });
}; };
@@ -249,6 +289,10 @@ export const certificateAuthorityServiceFactory = ({
return acmeFns.listCertificateAuthorities({ projectId }); return acmeFns.listCertificateAuthorities({ projectId });
} }
if (type === CaType.AZURE_AD_CS) {
return azureAdCsFns.listCertificateAuthorities({ projectId });
}
throw new BadRequestError({ message: "Invalid certificate authority type" }); throw new BadRequestError({ message: "Invalid certificate authority type" });
}; };
@@ -323,6 +367,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" }); throw new BadRequestError({ message: "Invalid certificate authority type" });
}; };
@@ -384,14 +439,98 @@ export const certificateAuthorityServiceFactory = ({
return castDbEntryToAcmeCertificateAuthority(certificateAuthority); return castDbEntryToAcmeCertificateAuthority(certificateAuthority);
} }
if (type === CaType.AZURE_AD_CS) {
return castDbEntryToAzureAdCsCertificateAuthority(certificateAuthority);
}
throw new BadRequestError({ message: "Invalid certificate authority type" }); throw new BadRequestError({ message: "Invalid certificate authority type" });
}; };
const orderSubscriberCertificate = async (subscriberId: string, actor: OrgServiceActor) => {
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);
const { permission } = await permissionService.getProjectPermission({
actor: actor.type,
actorId: actor.id,
projectId: ca.projectId,
actorAuthMethod: actor.authMethod,
actorOrgId: actor.orgId,
actionProjectType: ActionProjectType.CertificateManager
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionCertificateActions.Create,
ProjectPermissionSub.Certificates
);
if (!ca.externalCa && !ca.internalCa) {
throw new BadRequestError({ message: "Certificate authority configuration not found" });
}
if (ca.externalCa?.type === CaType.ACME) {
return acmeFns.orderSubscriberCertificate(subscriberId);
}
if (ca.externalCa?.type === CaType.AZURE_AD_CS) {
return azureAdCsFns.orderSubscriberCertificate(subscriberId);
}
if (ca.internalCa) {
// Handle internal CA certificate ordering - this would need to be implemented
throw new BadRequestError({ message: "Internal CA certificate ordering not yet supported" });
}
throw new BadRequestError({ message: "Unsupported 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(
ProjectPermissionCertificateActions.Read,
ProjectPermissionSub.Certificates
);
return azureAdCsFns.getTemplates({
caId,
projectId
});
};
return { return {
createCertificateAuthority, createCertificateAuthority,
findCertificateAuthorityByNameAndProjectId, findCertificateAuthorityByNameAndProjectId,
listCertificateAuthoritiesByProjectId, listCertificateAuthoritiesByProjectId,
updateCertificateAuthority, updateCertificateAuthority,
deleteCertificateAuthority deleteCertificateAuthority,
orderSubscriberCertificate,
getAzureAdcsTemplates
}; };
}; };
@@ -1,13 +1,23 @@
import { TAcmeCertificateAuthority, TAcmeCertificateAuthorityInput } from "./acme/acme-certificate-authority-types"; import { TAcmeCertificateAuthority, TAcmeCertificateAuthorityInput } from "./acme/acme-certificate-authority-types";
import {
TAzureAdCsCertificateAuthority,
TAzureAdCsCertificateAuthorityInput
} from "./azure-ad-cs/azure-ad-cs-certificate-authority-types";
import { CaType } from "./certificate-authority-enums"; import { CaType } from "./certificate-authority-enums";
import { import {
TInternalCertificateAuthority, TInternalCertificateAuthority,
TInternalCertificateAuthorityInput TInternalCertificateAuthorityInput
} from "./internal/internal-certificate-authority-types"; } 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
| TAzureAdCsCertificateAuthorityInput;
export type TCreateCertificateAuthorityDTO = Omit<TCertificateAuthority, "id">; export type TCreateCertificateAuthorityDTO = Omit<TCertificateAuthority, "id">;
@@ -8,6 +8,8 @@ import { getConfig } from "@app/lib/config/env";
import { crypto } from "@app/lib/crypto/cryptography"; import { crypto } from "@app/lib/crypto/cryptography";
import { BadRequestError } from "@app/lib/errors"; import { BadRequestError } from "@app/lib/errors";
import { ms } from "@app/lib/ms"; import { ms } from "@app/lib/ms";
import { TAppConnectionDALFactory } from "@app/services/app-connection/app-connection-dal";
import { TAppConnectionServiceFactory } from "@app/services/app-connection/app-connection-service";
import { TCertificateBodyDALFactory } from "@app/services/certificate/certificate-body-dal"; import { TCertificateBodyDALFactory } from "@app/services/certificate/certificate-body-dal";
import { TCertificateDALFactory } from "@app/services/certificate/certificate-dal"; import { TCertificateDALFactory } from "@app/services/certificate/certificate-dal";
import { TCertificateSecretDALFactory } from "@app/services/certificate/certificate-secret-dal"; import { TCertificateSecretDALFactory } from "@app/services/certificate/certificate-secret-dal";
@@ -19,9 +21,12 @@ import {
TAltNameMapping TAltNameMapping
} from "@app/services/certificate/certificate-types"; } from "@app/services/certificate/certificate-types";
import { TKmsServiceFactory } from "@app/services/kms/kms-service"; import { TKmsServiceFactory } from "@app/services/kms/kms-service";
import { TPkiSubscriberDALFactory } from "@app/services/pki-subscriber/pki-subscriber-dal";
import { TPkiSubscriberProperties } from "@app/services/pki-subscriber/pki-subscriber-types";
import { TProjectDALFactory } from "@app/services/project/project-dal"; import { TProjectDALFactory } from "@app/services/project/project-dal";
import { getProjectKmsCertificateKeyId } from "@app/services/project/project-fns"; import { getProjectKmsCertificateKeyId } from "@app/services/project/project-fns";
import { AzureAdCsCertificateAuthorityFns } from "../azure-ad-cs/azure-ad-cs-certificate-authority-fns";
import { TCertificateAuthorityCertDALFactory } from "../certificate-authority-cert-dal"; import { TCertificateAuthorityCertDALFactory } from "../certificate-authority-cert-dal";
import { TCertificateAuthorityDALFactory } from "../certificate-authority-dal"; import { TCertificateAuthorityDALFactory } from "../certificate-authority-dal";
import { CaStatus } from "../certificate-authority-enums"; import { CaStatus } from "../certificate-authority-enums";
@@ -33,18 +38,102 @@ import {
} from "../certificate-authority-fns"; } from "../certificate-authority-fns";
import { TCertificateAuthoritySecretDALFactory } from "../certificate-authority-secret-dal"; import { TCertificateAuthoritySecretDALFactory } from "../certificate-authority-secret-dal";
import { validateAndMapAltNameType } from "../certificate-authority-validators"; import { validateAndMapAltNameType } from "../certificate-authority-validators";
import { TExternalCertificateAuthorityDALFactory } from "../external-certificate-authority-dal";
import { TIssueCertWithTemplateDTO } from "./internal-certificate-authority-types"; import { TIssueCertWithTemplateDTO } from "./internal-certificate-authority-types";
type TInternalCertificateAuthorityFnsDeps = { type TInternalCertificateAuthorityFnsDeps = {
certificateAuthorityDAL: Pick<TCertificateAuthorityDALFactory, "findByIdWithAssociatedCa" | "findById">; certificateAuthorityDAL: Pick<
TCertificateAuthorityDALFactory,
"findByIdWithAssociatedCa" | "findById" | "create" | "transaction" | "updateById" | "findWithAssociatedCa"
>;
certificateAuthorityCertDAL: Pick<TCertificateAuthorityCertDALFactory, "findById">; certificateAuthorityCertDAL: Pick<TCertificateAuthorityCertDALFactory, "findById">;
certificateAuthoritySecretDAL: Pick<TCertificateAuthoritySecretDALFactory, "findOne">; certificateAuthoritySecretDAL: Pick<TCertificateAuthoritySecretDALFactory, "findOne">;
certificateAuthorityCrlDAL: Pick<TCertificateAuthorityCrlDALFactory, "findOne">; certificateAuthorityCrlDAL: Pick<TCertificateAuthorityCrlDALFactory, "findOne">;
projectDAL: Pick<TProjectDALFactory, "findById" | "transaction" | "findOne" | "updateById">; projectDAL: Pick<TProjectDALFactory, "findById" | "transaction" | "findOne" | "updateById">;
kmsService: Pick<TKmsServiceFactory, "decryptWithKmsKey" | "encryptWithKmsKey" | "generateKmsKey">; kmsService: Pick<
TKmsServiceFactory,
"decryptWithKmsKey" | "encryptWithKmsKey" | "generateKmsKey" | "createCipherPairWithDataKey"
>;
certificateDAL: Pick<TCertificateDALFactory, "create" | "transaction">; certificateDAL: Pick<TCertificateDALFactory, "create" | "transaction">;
certificateBodyDAL: Pick<TCertificateBodyDALFactory, "create">; certificateBodyDAL: Pick<TCertificateBodyDALFactory, "create">;
certificateSecretDAL: Pick<TCertificateSecretDALFactory, "create">; certificateSecretDAL: Pick<TCertificateSecretDALFactory, "create">;
appConnectionDAL?: Pick<TAppConnectionDALFactory, "findById" | "updateById">;
appConnectionService?: Pick<TAppConnectionServiceFactory, "connectAppConnectionById">;
externalCertificateAuthorityDAL?: Pick<TExternalCertificateAuthorityDALFactory, "create" | "update">;
pkiSubscriberDAL?: Pick<TPkiSubscriberDALFactory, "findById">;
};
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 sanitizedCN = commonName.trim().replace(/[,=+<>#;\\]/g, ""); // Remove problematic characters
if (!sanitizedCN) {
throw new BadRequestError({ message: "Common Name contains only invalid characters" });
}
let subject = `CN=${sanitizedCN}`;
// Helper function to validate and sanitize DN component values
const sanitizeComponent = (value: string | undefined): string | null => {
if (!value || typeof value !== "string") return null;
const trimmed = value.trim();
if (!trimmed) return null;
// Remove problematic characters for DN components
const sanitized = trimmed.replace(/[,=+<>#;\\"/\r\n\t]/g, "").trim();
// Additional validation to prevent empty components
if (sanitized.length === 0) return null;
// Ensure the component doesn't start or end with spaces or problematic chars
const finalSanitized = sanitized.replace(/^[\s-_.]+|[\s-_.]+$/g, "");
return finalSanitized.length > 0 ? finalSanitized : null;
};
// Build DN components in proper X.500 ordering
const emailAddress = sanitizeComponent(properties?.emailAddress);
if (emailAddress) {
// Enhanced email validation for DN usage
const emailRegex = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/;
if (emailRegex.test(emailAddress) && emailAddress.length > 5 && emailAddress.length < 64) {
subject += `,E=${emailAddress}`;
}
}
const organizationalUnit = sanitizeComponent(properties?.organizationalUnit);
if (organizationalUnit && organizationalUnit.length <= 64) {
subject += `,OU=${organizationalUnit}`;
}
const organization = sanitizeComponent(properties?.organization);
if (organization && organization.length <= 64) {
subject += `,O=${organization}`;
}
const locality = sanitizeComponent(properties?.locality);
if (locality && locality.length <= 64) {
subject += `,L=${locality}`;
}
const state = sanitizeComponent(properties?.state);
if (state && state.length <= 64) {
subject += `,ST=${state}`;
}
const country = sanitizeComponent(properties?.country);
if (country) {
// Country code must be exactly 2 uppercase letters
const countryCode = country.toUpperCase().replace(/[^A-Z]/g, "");
if (countryCode.length === 2) {
subject += `,C=${countryCode}`;
}
}
return subject;
}; };
export const InternalCertificateAuthorityFns = ({ export const InternalCertificateAuthorityFns = ({
@@ -56,7 +145,11 @@ export const InternalCertificateAuthorityFns = ({
certificateAuthorityCrlDAL, certificateAuthorityCrlDAL,
certificateDAL, certificateDAL,
certificateBodyDAL, certificateBodyDAL,
certificateSecretDAL certificateSecretDAL,
appConnectionDAL,
appConnectionService,
externalCertificateAuthorityDAL,
pkiSubscriberDAL
}: TInternalCertificateAuthorityFnsDeps) => { }: TInternalCertificateAuthorityFnsDeps) => {
const issueCertificate = async ( const issueCertificate = async (
subscriber: TPkiSubscribers, subscriber: TPkiSubscribers,
@@ -102,7 +195,7 @@ export const InternalCertificateAuthorityFns = ({
const leafKeys = await crypto.nativeCrypto.subtle.generateKey(alg, true, ["sign", "verify"]); const leafKeys = await crypto.nativeCrypto.subtle.generateKey(alg, true, ["sign", "verify"]);
const csrObj = await x509.Pkcs10CertificateRequestGenerator.create({ const csrObj = await x509.Pkcs10CertificateRequestGenerator.create({
name: `CN=${subscriber.commonName}`, name: buildSubjectDN(subscriber.commonName, subscriber.properties as TPkiSubscriberProperties | undefined),
keys: leafKeys, keys: leafKeys,
signingAlgorithm: alg, signingAlgorithm: alg,
extensions: [ extensions: [
@@ -518,8 +611,30 @@ export const InternalCertificateAuthorityFns = ({
}; };
}; };
const issueCertificateWithAzureAdCs = async (subscriberId: string) => {
if (!appConnectionDAL || !appConnectionService || !externalCertificateAuthorityDAL || !pkiSubscriberDAL) {
throw new BadRequestError({ message: "Azure AD CS dependencies not available" });
}
const azureAdCsFns = AzureAdCsCertificateAuthorityFns({
appConnectionDAL,
appConnectionService,
certificateAuthorityDAL,
externalCertificateAuthorityDAL,
certificateDAL,
certificateBodyDAL,
certificateSecretDAL,
kmsService,
projectDAL,
pkiSubscriberDAL
});
return azureAdCsFns.orderSubscriberCertificate(subscriberId);
};
return { return {
issueCertificate, issueCertificate,
issueCertificateWithTemplate issueCertificateWithTemplate,
issueCertificateWithAzureAdCs
}; };
}; };
@@ -10,10 +10,13 @@ import {
} from "@app/ee/services/permission/project-permission"; } from "@app/ee/services/permission/project-permission";
import { crypto } from "@app/lib/crypto/cryptography"; import { crypto } from "@app/lib/crypto/cryptography";
import { BadRequestError, NotFoundError } from "@app/lib/errors"; import { BadRequestError, NotFoundError } from "@app/lib/errors";
import { TAppConnectionDALFactory } from "@app/services/app-connection/app-connection-dal";
import { TCertificateBodyDALFactory } from "@app/services/certificate/certificate-body-dal"; import { TCertificateBodyDALFactory } from "@app/services/certificate/certificate-body-dal";
import { TCertificateDALFactory } from "@app/services/certificate/certificate-dal"; import { TCertificateDALFactory } from "@app/services/certificate/certificate-dal";
import { TCertificateAuthorityCertDALFactory } from "@app/services/certificate-authority/certificate-authority-cert-dal"; import { TCertificateAuthorityCertDALFactory } from "@app/services/certificate-authority/certificate-authority-cert-dal";
import { TCertificateAuthorityDALFactory } from "@app/services/certificate-authority/certificate-authority-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 { TCertificateAuthoritySecretDALFactory } from "@app/services/certificate-authority/certificate-authority-secret-dal";
import { TKmsServiceFactory } from "@app/services/kms/kms-service"; import { TKmsServiceFactory } from "@app/services/kms/kms-service";
import { TPkiCollectionDALFactory } from "@app/services/pki-collection/pki-collection-dal"; import { TPkiCollectionDALFactory } from "@app/services/pki-collection/pki-collection-dal";
@@ -49,6 +52,7 @@ type TCertificateServiceFactoryDep = {
pkiCollectionDAL: Pick<TPkiCollectionDALFactory, "findById">; pkiCollectionDAL: Pick<TPkiCollectionDALFactory, "findById">;
pkiCollectionItemDAL: Pick<TPkiCollectionItemDALFactory, "create">; pkiCollectionItemDAL: Pick<TPkiCollectionItemDALFactory, "create">;
projectDAL: Pick<TProjectDALFactory, "findProjectBySlug" | "findOne" | "updateById" | "findById" | "transaction">; projectDAL: Pick<TProjectDALFactory, "findProjectBySlug" | "findOne" | "updateById" | "findById" | "transaction">;
appConnectionDAL: Pick<TAppConnectionDALFactory, "findById">;
kmsService: Pick<TKmsServiceFactory, "generateKmsKey" | "encryptWithKmsKey" | "decryptWithKmsKey">; kmsService: Pick<TKmsServiceFactory, "generateKmsKey" | "encryptWithKmsKey" | "decryptWithKmsKey">;
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">; permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
}; };
@@ -66,6 +70,7 @@ export const certificateServiceFactory = ({
pkiCollectionDAL, pkiCollectionDAL,
pkiCollectionItemDAL, pkiCollectionItemDAL,
projectDAL, projectDAL,
appConnectionDAL,
kmsService, kmsService,
permissionService permissionService
}: TCertificateServiceFactoryDep) => { }: TCertificateServiceFactoryDep) => {
@@ -184,9 +189,11 @@ export const certificateServiceFactory = ({
const ca = await certificateAuthorityDAL.findByIdWithAssociatedCa(cert.caId); 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({ throw new BadRequestError({
message: "Cannot revoke external certificates" message: "Certificate revocation is not supported by this certificate authority type"
}); });
} }
@@ -218,7 +225,12 @@ export const certificateServiceFactory = ({
} }
); );
// Note: External CA revocation handling would go here for supported CA types
// Currently, only internal CAs and ACME CAs support revocation
// rebuild CRL (TODO: move to interval-based cron job) // 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({ await rebuildCaCrl({
caId: ca.id, caId: ca.id,
certificateAuthorityDAL, certificateAuthorityDAL,
@@ -228,8 +240,22 @@ export const certificateServiceFactory = ({
certificateDAL, certificateDAL,
kmsService kmsService
}); });
}
return { revokedAt, cert, ca: expandInternalCa(ca) }; // 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 };
}; };
/** /**
@@ -18,7 +18,8 @@ export const sanitizedPkiSubscriber = PkiSubscribersSchema.pick({
lastOperationAt: true, lastOperationAt: true,
enableAutoRenewal: true, enableAutoRenewal: true,
autoRenewalPeriodInDays: true, autoRenewalPeriodInDays: true,
lastAutoRenewAt: true lastAutoRenewAt: true,
properties: true
}).extend({ }).extend({
supportsImmediateCertIssuance: z.boolean().optional() supportsImmediateCertIssuance: z.boolean().optional()
}); });
@@ -109,6 +109,7 @@ export const pkiSubscriberServiceFactory = ({
extendedKeyUsages, extendedKeyUsages,
enableAutoRenewal, enableAutoRenewal,
autoRenewalPeriodInDays, autoRenewalPeriodInDays,
properties,
projectId, projectId,
actorId, actorId,
actorAuthMethod, actorAuthMethod,
@@ -157,7 +158,8 @@ export const pkiSubscriberServiceFactory = ({
keyUsages, keyUsages,
extendedKeyUsages, extendedKeyUsages,
enableAutoRenewal, enableAutoRenewal,
autoRenewalPeriodInDays autoRenewalPeriodInDays,
properties
}); });
return newSubscriber; return newSubscriber;
@@ -221,6 +223,7 @@ export const pkiSubscriberServiceFactory = ({
extendedKeyUsages, extendedKeyUsages,
enableAutoRenewal, enableAutoRenewal,
autoRenewalPeriodInDays, autoRenewalPeriodInDays,
properties,
actorId, actorId,
actorAuthMethod, actorAuthMethod,
actor, actor,
@@ -275,7 +278,8 @@ export const pkiSubscriberServiceFactory = ({
keyUsages, keyUsages,
extendedKeyUsages, extendedKeyUsages,
enableAutoRenewal, enableAutoRenewal,
autoRenewalPeriodInDays autoRenewalPeriodInDays,
properties
}); });
return updatedSubscriber; return updatedSubscriber;
@@ -360,7 +364,7 @@ export const pkiSubscriberServiceFactory = ({
throw new BadRequestError({ message: "CA is disabled" }); 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({ await certificateAuthorityQueue.orderCertificateForSubscriber({
subscriberId: subscriber.id, subscriberId: subscriber.id,
caType: ca.externalCa.type caType: ca.externalCa.type
@@ -18,6 +18,7 @@ export type TCreatePkiSubscriberDTO = {
extendedKeyUsages: CertExtendedKeyUsage[]; extendedKeyUsages: CertExtendedKeyUsage[];
enableAutoRenewal?: boolean; enableAutoRenewal?: boolean;
autoRenewalPeriodInDays?: number; autoRenewalPeriodInDays?: number;
properties?: TPkiSubscriberProperties;
} & TProjectPermission; } & TProjectPermission;
export type TGetPkiSubscriberDTO = { export type TGetPkiSubscriberDTO = {
@@ -36,6 +37,7 @@ export type TUpdatePkiSubscriberDTO = {
extendedKeyUsages?: CertExtendedKeyUsage[]; extendedKeyUsages?: CertExtendedKeyUsage[];
enableAutoRenewal?: boolean; enableAutoRenewal?: boolean;
autoRenewalPeriodInDays?: number; autoRenewalPeriodInDays?: number;
properties?: TPkiSubscriberProperties;
} & TProjectPermission; } & TProjectPermission;
export type TDeletePkiSubscriberDTO = { export type TDeletePkiSubscriberDTO = {
@@ -69,3 +71,13 @@ export enum SubscriberOperationStatus {
SUCCESS = "success", SUCCESS = "success",
FAILED = "failed" FAILED = "failed"
} }
export type TPkiSubscriberProperties = {
azureTemplateType?: string;
organization?: string;
organizationalUnit?: string;
country?: string;
state?: string;
locality?: string;
emailAddress?: string;
}
@@ -0,0 +1,4 @@
---
title: "Available"
openapi: "GET /api/v1/app-connections/azure-adcs/available"
---
@@ -0,0 +1,10 @@
---
title: "Create"
openapi: "POST /api/v1/app-connections/azure-adcs"
---
<Note>
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.
</Note>
@@ -0,0 +1,4 @@
---
title: "Delete"
openapi: "DELETE /api/v1/app-connections/azure-adcs/{connectionId}"
---
@@ -0,0 +1,4 @@
---
title: "Get by ID"
openapi: "GET /api/v1/app-connections/azure-adcs/{connectionId}"
---
@@ -0,0 +1,4 @@
---
title: "Get by Name"
openapi: "GET /api/v1/app-connections/azure-adcs/connection-name/{connectionName}"
---
@@ -0,0 +1,4 @@
---
title: "List"
openapi: "GET /api/v1/app-connections/azure-adcs"
---
@@ -0,0 +1,10 @@
---
title: "Update"
openapi: "PATCH /api/v1/app-connections/azure-adcs/{connectionId}"
---
<Note>
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.
</Note>
+14
View File
@@ -106,6 +106,7 @@
"integrations/app-connections/auth0", "integrations/app-connections/auth0",
"integrations/app-connections/aws", "integrations/app-connections/aws",
"integrations/app-connections/azure-app-configuration", "integrations/app-connections/azure-app-configuration",
"integrations/app-connections/azure-adcs",
"integrations/app-connections/azure-client-secrets", "integrations/app-connections/azure-client-secrets",
"integrations/app-connections/azure-devops", "integrations/app-connections/azure-devops",
"integrations/app-connections/azure-key-vault", "integrations/app-connections/azure-key-vault",
@@ -690,6 +691,7 @@
"documentation/platform/pki/subscribers", "documentation/platform/pki/subscribers",
"documentation/platform/pki/certificates", "documentation/platform/pki/certificates",
"documentation/platform/pki/acme-ca", "documentation/platform/pki/acme-ca",
"documentation/platform/pki/azure-adcs",
"documentation/platform/pki/est", "documentation/platform/pki/est",
"documentation/platform/pki/alerting", "documentation/platform/pki/alerting",
{ {
@@ -1396,6 +1398,18 @@
"api-reference/endpoints/app-connections/aws/delete" "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", "group": "Azure App Configuration",
"pages": [ "pages": [
@@ -0,0 +1,159 @@
---
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
- 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.
<Steps>
<Step title="Navigate to External Certificate Authorities">
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)
</Step>
<Step title="Create New Azure ADCS Certificate Service CA">
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)
</Step>
<Step title="Certificate Authority Created">
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)
</Step>
<Step title="Navigate to Subscribers">
Go to **Subscribers** to access the subscribers page.
![Subscribers Page](/images/platform/pki/azure-adcs/azure-adcs-subscribers-page.png)
</Step>
<Step title="Create New Subscriber">
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)
</Step>
<Step title="Subscriber Created">
Your subscriber is now created and ready to issue certificates.
![Subscriber Created](/images/platform/pki/azure-adcs/azure-adcs-subscribers-created.png)
</Step>
<Step title="Issue New Certificate">
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)
</Step>
<Step title="Certificate Created">
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)
</Step>
<Step title="View Certificate Details">
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)
</Step>
</Steps>
## 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
<Info>
**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.
</Info>
### Certificate Revocation
<Warning>
Certificate revocation is **not supported** by the Azure ADCS connector due to security and complexity considerations.
</Warning>
## 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
**Authentication Failures**
- Verify ADCS connection credentials
- Check domain account permissions
- Ensure network connectivity to ADCS server
Binary file not shown.

After

Width:  |  Height:  |  Size: 600 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 421 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 477 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 796 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 765 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 803 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 501 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 801 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 778 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 740 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 497 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 735 KiB

@@ -0,0 +1,42 @@
---
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
<Steps>
<Step title="Navigate to App Connections">
Navigate to the **App Connections** tab on the **Organization Settings** page.
![App Connections Tab](/images/app-connections/general/add-connection.png)
</Step>
<Step title="Add Connection">
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)
</Step>
<Step title="Configure Connection Details">
Fill in the following information:
- **Name**: Friendly name for this ADCS connection (e.g., "Production ADCS")
- **ADCS URL**: Your ADCS web enrollment URL (e.g., `https://adcs.yourdomain.com/certsrv`)
- **Username**: Domain administrator username (format: `DOMAIN\username` or `[email protected]`)
- **Password**: Password for the domain administrator account
And click **Connect to ADCS** to establish the connection.
![Connect to ADCS](/images/app-connections/azure-adcs/azure-adcs-app-connection-form.png)
</Step>
<Step title="Connection Created">
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)
</Step>
</Steps>
+3
View File
@@ -13,6 +13,7 @@ import { AppConnection } from "@app/hooks/api/appConnections/enums";
import { import {
Auth0ConnectionMethod, Auth0ConnectionMethod,
AwsConnectionMethod, AwsConnectionMethod,
AzureADCSConnectionMethod,
AzureAppConfigurationConnectionMethod, AzureAppConfigurationConnectionMethod,
AzureClientSecretsConnectionMethod, AzureClientSecretsConnectionMethod,
AzureDevOpsConnectionMethod, AzureDevOpsConnectionMethod,
@@ -76,6 +77,7 @@ export const APP_CONNECTION_MAP: Record<
image: "Microsoft Azure.png" image: "Microsoft Azure.png"
}, },
[AppConnection.AzureDevOps]: { name: "Azure DevOps", 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.Databricks]: { name: "Databricks", image: "Databricks.png" },
[AppConnection.Humanitec]: { name: "Humanitec", image: "Humanitec.png" }, [AppConnection.Humanitec]: { name: "Humanitec", image: "Humanitec.png" },
[AppConnection.TerraformCloud]: { name: "Terraform Cloud", image: "Terraform Cloud.png" }, [AppConnection.TerraformCloud]: { name: "Terraform Cloud", image: "Terraform Cloud.png" },
@@ -151,6 +153,7 @@ export const getAppConnectionMethodDetails = (method: TAppConnection["method"])
case MsSqlConnectionMethod.UsernameAndPassword: case MsSqlConnectionMethod.UsernameAndPassword:
case MySqlConnectionMethod.UsernameAndPassword: case MySqlConnectionMethod.UsernameAndPassword:
case OracleDBConnectionMethod.UsernameAndPassword: case OracleDBConnectionMethod.UsernameAndPassword:
case AzureADCSConnectionMethod.UsernamePassword:
return { name: "Username & Password", icon: faLock }; return { name: "Username & Password", icon: faLock };
case HCVaultConnectionMethod.AccessToken: case HCVaultConnectionMethod.AccessToken:
case TeamCityConnectionMethod.AccessToken: case TeamCityConnectionMethod.AccessToken:
@@ -7,6 +7,7 @@ export enum AppConnection {
AzureAppConfiguration = "azure-app-configuration", AzureAppConfiguration = "azure-app-configuration",
AzureClientSecrets = "azure-client-secrets", AzureClientSecrets = "azure-client-secrets",
AzureDevOps = "azure-devops", AzureDevOps = "azure-devops",
AzureADCS = "azure-adcs",
Databricks = "databricks", Databricks = "databricks",
Humanitec = "humanitec", Humanitec = "humanitec",
TerraformCloud = "terraform-cloud", TerraformCloud = "terraform-cloud",
@@ -54,13 +54,13 @@ export const useAppConnectionOptions = (
export const useGetAppConnectionOption = <T extends AppConnection>(app: T) => { export const useGetAppConnectionOption = <T extends AppConnection>(app: T) => {
const { data: options = [], isPending } = useAppConnectionOptions(); const { data: options = [], isPending } = useAppConnectionOptions();
return useMemo( return useMemo(() => {
() => ({ const foundOption = options.find((opt) => opt.app === app);
option: (options.find((opt) => opt.app === app) as TAppConnectionOptionMap[T]) ?? {}, return {
option: (foundOption as TAppConnectionOptionMap[T]) ?? {},
isLoading: isPending isLoading: isPending
}), };
[options, app, isPending] }, [options, app, isPending]);
);
}; };
export const useListAppConnections = ( export const useListAppConnections = (
@@ -32,7 +32,7 @@ export type TAzureKeyVaultConnectionOption = TAppConnectionOptionBase & {
}; };
export type TAzureAppConfigurationConnectionOption = TAppConnectionOptionBase & { export type TAzureAppConfigurationConnectionOption = TAppConnectionOptionBase & {
app: AppConnection.AzureKeyVault; app: AppConnection.AzureAppConfiguration;
oauthClientId?: string; oauthClientId?: string;
}; };
@@ -164,9 +164,14 @@ export type TOktaConnectionOption = TAppConnectionOptionBase & {
app: AppConnection.Okta; app: AppConnection.Okta;
}; };
export type TAzureAdCsConnectionOption = TAppConnectionOptionBase & {
app: AppConnection.AzureADCS;
};
export type TAppConnectionOption = export type TAppConnectionOption =
| TAwsConnectionOption | TAwsConnectionOption
| TGitHubConnectionOption | TGitHubConnectionOption
| TGitHubRadarConnectionOption
| TGcpConnectionOption | TGcpConnectionOption
| TAzureAppConfigurationConnectionOption | TAzureAppConfigurationConnectionOption
| TAzureKeyVaultConnectionOption | TAzureKeyVaultConnectionOption
@@ -184,6 +189,7 @@ export type TAppConnectionOption =
| TWindmillConnectionOption | TWindmillConnectionOption
| TAuth0ConnectionOption | TAuth0ConnectionOption
| THCVaultConnectionOption | THCVaultConnectionOption
| TLdapConnectionOption
| TTeamCityConnectionOption | TTeamCityConnectionOption
| TOCIConnectionOption | TOCIConnectionOption
| TOnePassConnectionOption | TOnePassConnectionOption
@@ -196,6 +202,7 @@ export type TAppConnectionOption =
| TZabbixConnectionOption | TZabbixConnectionOption
| TRailwayConnectionOption | TRailwayConnectionOption
| TChecklyConnectionOption | TChecklyConnectionOption
| TSupabaseConnectionOption
| TDigitalOceanConnectionOption | TDigitalOceanConnectionOption
| TNetlifyConnectionOption | TNetlifyConnectionOption
| TOktaConnectionOption; | TOktaConnectionOption;
@@ -238,4 +245,5 @@ export type TAppConnectionOptionMap = {
[AppConnection.DigitalOcean]: TDigitalOceanConnectionOption; [AppConnection.DigitalOcean]: TDigitalOceanConnectionOption;
[AppConnection.Netlify]: TNetlifyConnectionOption; [AppConnection.Netlify]: TNetlifyConnectionOption;
[AppConnection.Okta]: TOktaConnectionOption; [AppConnection.Okta]: TOktaConnectionOption;
[AppConnection.AzureADCS]: TAzureAdCsConnectionOption;
}; };
@@ -0,0 +1,25 @@
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<typeof CreateAzureADCSConnectionSchema>;
export type TAzureADCSConnection = TRootAppConnection & { app: AppConnection.AzureADCS } & {
method: AzureADCSConnectionMethod.UsernamePassword;
credentials: {
username: string;
password: string;
adcsUrl: string;
};
};
@@ -3,6 +3,7 @@ import { TOnePassConnection } from "./1password-connection";
import { TAppConnectionOption } from "./app-options"; import { TAppConnectionOption } from "./app-options";
import { TAuth0Connection } from "./auth0-connection"; import { TAuth0Connection } from "./auth0-connection";
import { TAwsConnection } from "./aws-connection"; import { TAwsConnection } from "./aws-connection";
import { TAzureADCSConnection } from "./azure-adcs-connection";
import { TAzureAppConfigurationConnection } from "./azure-app-configuration-connection"; import { TAzureAppConfigurationConnection } from "./azure-app-configuration-connection";
import { TAzureClientSecretsConnection } from "./azure-client-secrets-connection"; import { TAzureClientSecretsConnection } from "./azure-client-secrets-connection";
import { TAzureDevOpsConnection } from "./azure-devops-connection"; import { TAzureDevOpsConnection } from "./azure-devops-connection";
@@ -41,6 +42,7 @@ import { TZabbixConnection } from "./zabbix-connection";
export * from "./1password-connection"; export * from "./1password-connection";
export * from "./auth0-connection"; export * from "./auth0-connection";
export * from "./aws-connection"; export * from "./aws-connection";
export * from "./azure-adcs-connection";
export * from "./azure-app-configuration-connection"; export * from "./azure-app-configuration-connection";
export * from "./azure-client-secrets-connection"; export * from "./azure-client-secrets-connection";
export * from "./azure-devops-connection"; export * from "./azure-devops-connection";
@@ -83,6 +85,7 @@ export type TAppConnection =
| TAzureAppConfigurationConnection | TAzureAppConfigurationConnection
| TAzureClientSecretsConnection | TAzureClientSecretsConnection
| TAzureDevOpsConnection | TAzureDevOpsConnection
| TAzureADCSConnection
| TDatabricksConnection | TDatabricksConnection
| THumanitecConnection | THumanitecConnection
| TTerraformCloudConnection | TTerraformCloudConnection
@@ -156,6 +159,7 @@ export type TAppConnectionMap = {
[AppConnection.AzureAppConfiguration]: TAzureAppConfigurationConnection; [AppConnection.AzureAppConfiguration]: TAzureAppConfigurationConnection;
[AppConnection.AzureClientSecrets]: TAzureClientSecretsConnection; [AppConnection.AzureClientSecrets]: TAzureClientSecretsConnection;
[AppConnection.AzureDevOps]: TAzureDevOpsConnection; [AppConnection.AzureDevOps]: TAzureDevOpsConnection;
[AppConnection.AzureADCS]: TAzureADCSConnection;
[AppConnection.Databricks]: TDatabricksConnection; [AppConnection.Databricks]: TDatabricksConnection;
[AppConnection.Humanitec]: THumanitecConnection; [AppConnection.Humanitec]: THumanitecConnection;
[AppConnection.TerraformCloud]: TTerraformCloudConnection; [AppConnection.TerraformCloud]: TTerraformCloudConnection;
+48 -1
View File
@@ -1,7 +1,15 @@
import { AppConnection } from "../appConnections/enums"; import { AppConnection } from "../appConnections/enums";
import { SshCaStatus } from "../sshCa"; import { SshCaStatus } from "../sshCa";
import { SshCertTemplateStatus } from "../sshCertificateTemplates"; import { SshCertTemplateStatus } from "../sshCertificateTemplates";
import { AcmeDnsProvider, CaStatus, InternalCaType } from "./enums"; import {
AcmeDnsProvider,
AzureAdCsAuthMethod,
AzureAdCsTemplateType,
CaCapability,
CaStatus,
CaType,
InternalCaType
} from "./enums";
export const caTypeToNameMap: { [K in InternalCaType]: string } = { export const caTypeToNameMap: { [K in InternalCaType]: string } = {
[InternalCaType.ROOT]: "Root", [InternalCaType.ROOT]: "Root",
@@ -24,6 +32,45 @@ export const ACME_DNS_PROVIDER_APP_CONNECTION_MAP: Record<AcmeDnsProvider, AppCo
[AcmeDnsProvider.Cloudflare]: AppConnection.Cloudflare [AcmeDnsProvider.Cloudflare]: AppConnection.Cloudflare
}; };
export const AZURE_AD_CS_TEMPLATE_NAME_MAP: Record<AzureAdCsTemplateType, string> = {
[AzureAdCsTemplateType.WEB_SERVER]: "Web Server",
[AzureAdCsTemplateType.COMPUTER]: "Computer",
[AzureAdCsTemplateType.USER]: "User",
[AzureAdCsTemplateType.DOMAIN_CONTROLLER]: "Domain Controller",
[AzureAdCsTemplateType.SUBORDINATE_CA]: "Subordinate CA"
};
export const AZURE_AD_CS_AUTH_METHOD_NAME_MAP: Record<AzureAdCsAuthMethod, string> = {
[AzureAdCsAuthMethod.CLIENT_CERTIFICATE]: "Client Certificate",
[AzureAdCsAuthMethod.KERBEROS]: "Kerberos"
};
export const CA_TYPE_CAPABILITIES_MAP: Record<CaType, CaCapability[]> = {
[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 = CA_TYPE_CAPABILITIES_MAP[caType] || [];
return capabilities.includes(capability);
};
export const getCaStatusBadgeVariant = (status: CaStatus | SshCaStatus | SshCertTemplateStatus) => { export const getCaStatusBadgeVariant = (status: CaStatus | SshCaStatus | SshCertTemplateStatus) => {
switch (status) { switch (status) {
case CaStatus.ACTIVE: case CaStatus.ACTIVE:
+21 -1
View File
@@ -1,6 +1,7 @@
export enum CaType { export enum CaType {
INTERNAL = "internal", INTERNAL = "internal",
ACME = "acme" ACME = "acme",
AZURE_AD_CS = "azure-ad-cs"
} }
export enum InternalCaType { export enum InternalCaType {
@@ -22,3 +23,22 @@ export enum AcmeDnsProvider {
ROUTE53 = "route53", ROUTE53 = "route53",
Cloudflare = "cloudflare" Cloudflare = "cloudflare"
} }
export enum AzureAdCsTemplateType {
WEB_SERVER = "WebServer",
COMPUTER = "Computer",
USER = "User",
DOMAIN_CONTROLLER = "DomainController",
SUBORDINATE_CA = "SubordinateCA"
}
export enum AzureAdCsAuthMethod {
CLIENT_CERTIFICATE = "client-certificate",
KERBEROS = "kerberos"
}
export enum CaCapability {
ISSUE_CERTIFICATES = "issue-certificates",
REVOKE_CERTIFICATES = "revoke-certificates",
RENEW_CERTIFICATES = "renew-certificates"
}
+12 -2
View File
@@ -1,4 +1,12 @@
export { AcmeDnsProvider, CaRenewalType, CaStatus, CaType, InternalCaType } from "./enums"; export {
AcmeDnsProvider,
AzureAdCsAuthMethod,
AzureAdCsTemplateType,
CaRenewalType,
CaStatus,
CaType,
InternalCaType
} from "./enums";
export { export {
useCreateCa, useCreateCa,
useCreateCertificate, useCreateCertificate,
@@ -9,6 +17,7 @@ export {
useUpdateCa useUpdateCa
} from "./mutations"; } from "./mutations";
export { export {
useGetAzureAdcsTemplates,
useGetCa, useGetCa,
useGetCaById, useGetCaById,
useGetCaCert, useGetCaCert,
@@ -17,5 +26,6 @@ export {
useGetCaCrls, useGetCaCrls,
useGetCaCsr, useGetCaCsr,
useListCasByProjectId, useListCasByProjectId,
useListCasByTypeAndProjectId useListCasByTypeAndProjectId,
useListExternalCasByProjectId
} from "./queries"; } from "./queries";
+12
View File
@@ -39,6 +39,10 @@ export const useUpdateCa = () => {
queryClient.invalidateQueries({ queryClient.invalidateQueries({
queryKey: caKeys.getCaByNameAndProjectId(caName, projectId) queryKey: caKeys.getCaByNameAndProjectId(caName, projectId)
}); });
// Invalidate external CAs list
queryClient.invalidateQueries({
queryKey: [`external-cas-${projectId}`]
});
} }
}); });
}; };
@@ -57,6 +61,10 @@ export const useCreateCa = () => {
queryClient.invalidateQueries({ queryClient.invalidateQueries({
queryKey: caKeys.listCasByTypeAndProjectId(type, projectId) queryKey: caKeys.listCasByTypeAndProjectId(type, projectId)
}); });
// Invalidate external CAs list
queryClient.invalidateQueries({
queryKey: [`external-cas-${projectId}`]
});
} }
}); });
}; };
@@ -79,6 +87,10 @@ export const useDeleteCa = () => {
queryClient.invalidateQueries({ queryClient.invalidateQueries({
queryKey: caKeys.listCasByTypeAndProjectId(type, projectId) queryKey: caKeys.listCasByTypeAndProjectId(type, projectId)
}); });
// Invalidate external CAs list
queryClient.invalidateQueries({
queryKey: [`external-cas-${projectId}`]
});
} }
}); });
}; };
+52 -1
View File
@@ -17,7 +17,11 @@ export const caKeys = {
getCaCsr: (caId: string) => [{ caId }, "ca-csr"], getCaCsr: (caId: string) => [{ caId }, "ca-csr"],
getCaCrl: (caId: string) => [{ caId }, "ca-crl"], getCaCrl: (caId: string) => [{ caId }, "ca-crl"],
getCaCertTemplates: (caId: string) => [{ caId }, "ca-cert-templates"], 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 = ({ export const useGetCa = ({
@@ -67,6 +71,34 @@ export const useListCasByProjectId = (projectId: string) => {
}); });
}; };
export const useListExternalCasByProjectId = (projectId: string) => {
return useQuery({
queryKey: [`external-cas-${projectId}`],
queryFn: async () => {
const [acmeResponse, azureAdCsResponse] = await Promise.allSettled([
apiRequest.get<TUnifiedCertificateAuthority[]>(
`/api/v1/pki/ca/${CaType.ACME}?projectId=${projectId}`
),
apiRequest.get<TUnifiedCertificateAuthority[]>(
`/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) => { export const useGetCaById = (caId: string) => {
return useQuery({ return useQuery({
queryKey: caKeys.getCaById(caId), queryKey: caKeys.getCaById(caId),
@@ -156,3 +188,22 @@ export const useGetCaCertTemplates = (caId: string) => {
enabled: Boolean(caId) 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: { id: string; name: string; description?: string }[];
}>(`/api/v1/pki/ca/azure-ad-cs/${caId}/templates?projectId=${projectId}`);
return data;
},
enabled: Boolean(caId && projectId)
});
};
+24 -1
View File
@@ -1,5 +1,13 @@
import { CertExtendedKeyUsage, CertKeyAlgorithm, CertKeyUsage } from "../certificates/enums"; import { CertExtendedKeyUsage, CertKeyAlgorithm, CertKeyUsage } from "../certificates/enums";
import { AcmeDnsProvider, CaRenewalType, CaStatus, CaType, InternalCaType } from "./enums"; import {
AcmeDnsProvider,
AzureAdCsAuthMethod,
AzureAdCsTemplateType,
CaRenewalType,
CaStatus,
CaType,
InternalCaType
} from "./enums";
export type TAcmeCertificateAuthority = { export type TAcmeCertificateAuthority = {
id: string; id: string;
@@ -19,6 +27,20 @@ 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: AzureAdCsTemplateType;
authMethod: AzureAdCsAuthMethod;
};
};
export type TInternalCertificateAuthority = { export type TInternalCertificateAuthority = {
id: string; id: string;
projectId: string; projectId: string;
@@ -48,6 +70,7 @@ export type TInternalCertificateAuthority = {
export type TUnifiedCertificateAuthority = export type TUnifiedCertificateAuthority =
| TAcmeCertificateAuthority | TAcmeCertificateAuthority
| TAzureAdCsCertificateAuthority
| TInternalCertificateAuthority; | TInternalCertificateAuthority;
export type TCreateCertificateAuthorityDTO = Omit<TUnifiedCertificateAuthority, "id">; export type TCreateCertificateAuthorityDTO = Omit<TUnifiedCertificateAuthority, "id">;
@@ -10,6 +10,16 @@ export enum SubscriberOperationStatus {
FAILED = "failed" FAILED = "failed"
} }
export type TPkiSubscriberProperties = {
azureTemplateType?: string;
organization?: string;
organizationalUnit?: string;
country?: string;
state?: string;
locality?: string;
emailAddress?: string;
};
export type TPkiSubscriber = { export type TPkiSubscriber = {
id: string; id: string;
projectId: string; projectId: string;
@@ -27,6 +37,7 @@ export type TPkiSubscriber = {
lastOperationStatus?: SubscriberOperationStatus; lastOperationStatus?: SubscriberOperationStatus;
lastOperationMessage?: string; lastOperationMessage?: string;
lastOperationAt?: string; lastOperationAt?: string;
properties?: TPkiSubscriberProperties;
}; };
export type TCreatePkiSubscriberDTO = { export type TCreatePkiSubscriberDTO = {
@@ -40,6 +51,7 @@ export type TCreatePkiSubscriberDTO = {
extendedKeyUsages: CertExtendedKeyUsage[]; extendedKeyUsages: CertExtendedKeyUsage[];
enableAutoRenewal?: boolean; enableAutoRenewal?: boolean;
autoRenewalPeriodInDays?: number; autoRenewalPeriodInDays?: number;
properties?: TPkiSubscriberProperties;
}; };
export type TUpdatePkiSubscriberDTO = { export type TUpdatePkiSubscriberDTO = {
@@ -55,6 +67,7 @@ export type TUpdatePkiSubscriberDTO = {
extendedKeyUsages?: CertExtendedKeyUsage[]; extendedKeyUsages?: CertExtendedKeyUsage[];
enableAutoRenewal?: boolean; enableAutoRenewal?: boolean;
autoRenewalPeriodInDays?: number; autoRenewalPeriodInDays?: number;
properties?: TPkiSubscriberProperties;
}; };
export type TDeletePkiSubscriberDTO = { export type TDeletePkiSubscriberDTO = {
@@ -42,29 +42,45 @@ import {
import { UsePopUpState } from "@app/hooks/usePopUp"; import { UsePopUpState } from "@app/hooks/usePopUp";
import { slugSchema } from "@app/lib/schemas"; import { slugSchema } from "@app/lib/schemas";
const schema = z const baseSchema = z.object({
.object({
type: z.nativeEnum(CaType), type: z.nativeEnum(CaType),
name: slugSchema({ name: slugSchema({
field: "Name" field: "Name"
}), }),
enableDirectIssuance: z.boolean(), enableDirectIssuance: z.boolean(),
status: z.nativeEnum(CaStatus), status: z.nativeEnum(CaStatus)
configuration: z.object({ });
const acmeConfigurationSchema = z.object({
dnsAppConnection: z.object({ dnsAppConnection: z.object({
id: z.string(), id: z.string(),
name: 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({ dnsProviderConfig: z.object({
provider: z.nativeEnum(AcmeDnsProvider), provider: z.nativeEnum(AcmeDnsProvider),
hostedZoneId: z.string() hostedZoneId: z.string()
}), }),
directoryUrl: z.string(), directoryUrl: z.string(),
accountEmail: z.string() accountEmail: z.string()
});
const azureAdCsConfigurationSchema = z.object({
azureAdcsConnection: z.object({
id: z.string(),
name: z.string()
}) })
});
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
}) })
.required(); ]);
export type FormData = z.infer<typeof schema>; export type FormData = z.infer<typeof schema>;
@@ -73,12 +89,15 @@ type Props = {
handlePopUpToggle: (popUpName: keyof UsePopUpState<["ca"]>, state?: boolean) => void; 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) => { export const ExternalCaModal = ({ popUp, handlePopUpToggle }: Props) => {
const { currentWorkspace } = useWorkspace(); const { currentWorkspace } = useWorkspace();
const { data: ca } = useGetCa({ const { data: ca, isLoading: isCaLoading } = useGetCa({
caName: (popUp?.ca?.data as { name: string })?.name || "", caName: (popUp?.ca?.data as { name: string })?.name || "",
projectId: currentWorkspace?.id || "", projectId: currentWorkspace?.id || "",
type: (popUp?.ca?.data as { type: CaType })?.type || "" type: (popUp?.ca?.data as { type: CaType })?.type || ""
@@ -94,8 +113,34 @@ export const ExternalCaModal = ({ popUp, handlePopUpToggle }: Props) => {
formState: { isSubmitting }, formState: { isSubmitting },
watch watch
} = useForm<FormData>({ } = useForm<FormData>({
resolver: zodResolver(schema), resolver: zodResolver(schema)
defaultValues: { });
const caType = watch("type");
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, type: CaType.ACME,
name: "", name: "",
status: CaStatus.ACTIVE, status: CaStatus.ACTIVE,
@@ -112,11 +157,10 @@ export const ExternalCaModal = ({ popUp, handlePopUpToggle }: Props) => {
directoryUrl: "", directoryUrl: "",
accountEmail: "" accountEmail: ""
} }
}
}); });
}
const caType = watch("type"); }
const dnsProvider = watch("configuration.dnsProviderConfig.provider"); }, [popUp?.ca?.isOpen, popUp?.ca?.data, reset, ca]);
const { data: availableRoute53Connections, isPending: isRoute53Pending } = const { data: availableRoute53Connections, isPending: isRoute53Pending } =
useListAvailableAppConnections(AppConnection.AWS, { useListAvailableAppConnections(AppConnection.AWS, {
@@ -128,25 +172,44 @@ export const ExternalCaModal = ({ popUp, handlePopUpToggle }: Props) => {
enabled: caType === CaType.ACME enabled: caType === CaType.ACME
}); });
const availableConnections: TAvailableAppConnection[] = useMemo( const { data: availableAzureConnections, isPending: isAzurePending } =
() => [...(availableRoute53Connections || []), ...(availableCloudflareConnections || [])], useListAvailableAppConnections(AppConnection.AzureADCS, {
[availableRoute53Connections, availableCloudflareConnections] 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 } = const { data: cloudflareZones = [], isPending: isZonesPending } =
useCloudflareConnectionListZones(dnsAppConnection.id, { useCloudflareConnectionListZones(dnsAppConnection.id, {
enabled: dnsProvider === AcmeDnsProvider.Cloudflare && !!dnsAppConnection.id enabled: dnsProvider === AcmeDnsProvider.Cloudflare && !!dnsAppConnection.id
}); });
// Populate form with CA data when editing
useEffect(() => { useEffect(() => {
if (ca) { if (ca && !isCaLoading) {
if (ca.type !== CaType.INTERNAL && availableConnections?.length) { if (ca.type === CaType.ACME && availableConnections?.length) {
const selectedConnection = availableConnections?.find( const selectedConnection = availableConnections?.find(
(connection) => connection.id === ca?.configuration.dnsAppConnectionId (connection) => connection.id === ca.configuration.dnsAppConnectionId
); );
reset({ reset({
@@ -167,32 +230,61 @@ export const ExternalCaModal = ({ popUp, handlePopUpToggle }: Props) => {
accountEmail: ca.configuration.accountEmail 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 ({ const onFormSubmit = async ({
type, type,
name, name,
enableDirectIssuance, enableDirectIssuance,
status, status,
configuration configuration: formConfiguration
}: FormData) => { }: FormData) => {
try { try {
if (!currentWorkspace?.slug) return; 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
};
}
if (ca) {
await updateMutateAsync({ await updateMutateAsync({
caName: ca.name, caName: ca.name,
projectId: currentWorkspace.id, projectId: currentWorkspace.id,
name, name,
type, type,
status, status,
enableDirectIssuance, enableDirectIssuance: type === CaType.AZURE_AD_CS ? false : enableDirectIssuance,
configuration: { configuration: configPayload
...configuration,
dnsAppConnectionId: configuration.dnsAppConnection.id
}
}); });
} else { } else {
await createMutateAsync({ await createMutateAsync({
@@ -200,11 +292,8 @@ export const ExternalCaModal = ({ popUp, handlePopUpToggle }: Props) => {
name, name,
type, type,
status, status,
enableDirectIssuance, enableDirectIssuance: type === CaType.AZURE_AD_CS ? false : enableDirectIssuance,
configuration: { configuration: configPayload
...configuration,
dnsAppConnectionId: configuration.dnsAppConnection.id
}
}); });
} }
@@ -232,7 +321,7 @@ export const ExternalCaModal = ({ popUp, handlePopUpToggle }: Props) => {
handlePopUpToggle("ca", isOpen); handlePopUpToggle("ca", isOpen);
}} }}
> >
<ModalContent title={`${ca ? "View" : "Create"} External CA`}> <ModalContent title={`${ca ? "Edit" : "Create"} External CA`}>
<form onSubmit={handleSubmit(onFormSubmit)}> <form onSubmit={handleSubmit(onFormSubmit)}>
{ca && ( {ca && (
<FormControl label="CA ID"> <FormControl label="CA ID">
@@ -307,7 +396,11 @@ export const ExternalCaModal = ({ popUp, handlePopUpToggle }: Props) => {
<Controller <Controller
render={({ field: { value, onChange }, fieldState: { error } }) => ( render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl <FormControl
tooltipText={`${ACME_DNS_PROVIDER_NAME_MAP[dnsProvider]} uses the ${APP_CONNECTION_MAP[ACME_DNS_PROVIDER_APP_CONNECTION_MAP[dnsProvider]].name} App Connection. You can create one in the Organization Settings page.`} tooltipText={
dnsProvider
? `${ACME_DNS_PROVIDER_NAME_MAP[dnsProvider]} uses the ${APP_CONNECTION_MAP[ACME_DNS_PROVIDER_APP_CONNECTION_MAP[dnsProvider]].name} App Connection. You can create one in the Organization Settings page.`
: "Select a DNS provider first"
}
isError={Boolean(error)} isError={Boolean(error)}
errorText={error?.message} errorText={error?.message}
label="DNS App Connection" label="DNS App Connection"
@@ -406,6 +499,34 @@ export const ExternalCaModal = ({ popUp, handlePopUpToggle }: Props) => {
/> />
</> </>
)} )}
{caType === CaType.AZURE_AD_CS && (
<Controller
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
tooltipText="Azure ADCS App Connection contains the Windows domain credentials and ADCS server URL for certificate requests."
isError={Boolean(error)}
errorText={error?.message}
label="Azure ADCS Connection"
>
<FilterableSelect
menuPlacement="top"
value={value}
onChange={(newValue) => {
onChange(newValue);
}}
isLoading={isPending}
options={availableConnections}
placeholder="Select connection..."
getOptionLabel={(option) => option.name}
getOptionValue={(option) => option.id}
/>
</FormControl>
)}
control={control}
name="configuration.azureAdcsConnection"
/>
)}
{caType === CaType.ACME && (
<Controller <Controller
control={control} control={control}
name="enableDirectIssuance" name="enableDirectIssuance"
@@ -423,6 +544,7 @@ export const ExternalCaModal = ({ popUp, handlePopUpToggle }: Props) => {
); );
}} }}
/> />
)}
<div className="flex items-center"> <div className="flex items-center">
<Button <Button
className="mr-4" className="mr-4"
@@ -27,7 +27,7 @@ import {
Tr Tr
} from "@app/components/v2"; } from "@app/components/v2";
import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context"; import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context";
import { CaStatus, CaType, useListCasByTypeAndProjectId } from "@app/hooks/api"; import { CaStatus, CaType, useListExternalCasByProjectId } from "@app/hooks/api";
import { caStatusToNameMap, getCaStatusBadgeVariant } from "@app/hooks/api/ca/constants"; import { caStatusToNameMap, getCaStatusBadgeVariant } from "@app/hooks/api/ca/constants";
import { UsePopUpState } from "@app/hooks/usePopUp"; import { UsePopUpState } from "@app/hooks/usePopUp";
@@ -45,7 +45,7 @@ type Props = {
export const ExternalCaTable = ({ handlePopUpOpen }: Props) => { export const ExternalCaTable = ({ handlePopUpOpen }: Props) => {
const { currentWorkspace } = useWorkspace(); const { currentWorkspace } = useWorkspace();
const { data, isPending } = useListCasByTypeAndProjectId(CaType.ACME, currentWorkspace.id); const { data, isPending } = useListExternalCasByProjectId(currentWorkspace.id);
return ( return (
<div> <div>
@@ -1,4 +1,4 @@
import { useState } from "react"; import { useMemo, useState } from "react";
import { import {
faBan, faBan,
faCertificate, faCertificate,
@@ -36,6 +36,9 @@ import {
useWorkspace useWorkspace
} from "@app/context"; } from "@app/context";
import { useListWorkspaceCertificates } from "@app/hooks/api"; import { useListWorkspaceCertificates } from "@app/hooks/api";
import { caSupportsCapability } from "@app/hooks/api/ca/constants";
import { CaCapability, CaType } from "@app/hooks/api/ca/enums";
import { useListCasByProjectId } from "@app/hooks/api/ca/queries";
import { CertStatus } from "@app/hooks/api/certificates/enums"; import { CertStatus } from "@app/hooks/api/certificates/enums";
import { UsePopUpState } from "@app/hooks/usePopUp"; import { UsePopUpState } from "@app/hooks/usePopUp";
@@ -66,6 +69,20 @@ export const CertificatesTable = ({ handlePopUpOpen }: Props) => {
limit: perPage limit: perPage
}); });
// Fetch CA data to determine capabilities
const { data: caData } = useListCasByProjectId(currentWorkspace?.id ?? "");
// Create mapping from caId to CA type for capability checking
const caCapabilityMap = useMemo(() => {
if (!caData) return {};
const map: Record<string, CaType> = {};
caData.forEach((ca) => {
map[ca.id] = ca.type;
});
return map;
}, [caData]);
return ( return (
<TableContainer> <TableContainer>
<Table> <Table>
@@ -155,6 +172,18 @@ export const CertificatesTable = ({ handlePopUpOpen }: Props) => {
</DropdownMenuItem> </DropdownMenuItem>
)} )}
</ProjectPermissionCan> </ProjectPermissionCan>
{/* Only show revoke button if CA supports revocation */}
{(() => {
const caType = caCapabilityMap[certificate.caId];
const supportsRevocation =
caType &&
caSupportsCapability(caType, CaCapability.REVOKE_CERTIFICATES);
if (!supportsRevocation) {
return null;
}
return (
<ProjectPermissionCan <ProjectPermissionCan
I={ProjectPermissionCertificateActions.Delete} I={ProjectPermissionCertificateActions.Delete}
a={ProjectPermissionSub.Certificates} a={ProjectPermissionSub.Certificates}
@@ -162,7 +191,8 @@ export const CertificatesTable = ({ handlePopUpOpen }: Props) => {
{(isAllowed) => ( {(isAllowed) => (
<DropdownMenuItem <DropdownMenuItem
className={twMerge( className={twMerge(
!isAllowed && "pointer-events-none cursor-not-allowed opacity-50" !isAllowed &&
"pointer-events-none cursor-not-allowed opacity-50"
)} )}
onClick={async () => onClick={async () =>
handlePopUpOpen("revokeCertificate", { handlePopUpOpen("revokeCertificate", {
@@ -176,6 +206,8 @@ export const CertificatesTable = ({ handlePopUpOpen }: Props) => {
</DropdownMenuItem> </DropdownMenuItem>
)} )}
</ProjectPermissionCan> </ProjectPermissionCan>
);
})()}
<ProjectPermissionCan <ProjectPermissionCan
I={ProjectPermissionCertificateActions.Delete} I={ProjectPermissionCertificateActions.Delete}
a={ProjectPermissionSub.Certificates} a={ProjectPermissionSub.Certificates}
@@ -1,4 +1,4 @@
import { useState } from "react"; import { useMemo, useState } from "react";
import { subject } from "@casl/ability"; import { subject } from "@casl/ability";
import { faCertificate, faEllipsis, faTrash } from "@fortawesome/free-solid-svg-icons"; import { faCertificate, faEllipsis, faTrash } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
@@ -31,6 +31,9 @@ import {
useWorkspace useWorkspace
} from "@app/context"; } from "@app/context";
import { useGetPkiSubscriberCertificates } from "@app/hooks/api"; import { useGetPkiSubscriberCertificates } from "@app/hooks/api";
import { caSupportsCapability } from "@app/hooks/api/ca/constants";
import { CaCapability, CaType } from "@app/hooks/api/ca/enums";
import { useListCasByProjectId } from "@app/hooks/api/ca/queries";
import { CertStatus } from "@app/hooks/api/certificates/enums"; import { CertStatus } from "@app/hooks/api/certificates/enums";
import { UsePopUpState } from "@app/hooks/usePopUp"; import { UsePopUpState } from "@app/hooks/usePopUp";
@@ -60,6 +63,20 @@ export const PkiSubscriberCertificatesTable = ({ subscriberName, handlePopUpOpen
} }
); );
// Fetch CA data to determine capabilities
const { data: caData } = useListCasByProjectId(currentWorkspace?.id ?? "");
// Create mapping from caId to CA type for capability checking
const caCapabilityMap = useMemo(() => {
if (!caData) return {};
const map: Record<string, CaType> = {};
caData.forEach((ca) => {
map[ca.id] = ca.type;
});
return map;
}, [caData]);
const getCertStatusBadge = (status: string, notAfter: string) => { const getCertStatusBadge = (status: string, notAfter: string) => {
if (status === CertStatus.REVOKED) { if (status === CertStatus.REVOKED) {
return <Badge variant="danger">Revoked</Badge>; return <Badge variant="danger">Revoked</Badge>;
@@ -121,6 +138,17 @@ export const PkiSubscriberCertificatesTable = ({ subscriberName, handlePopUpOpen
: "-"} : "-"}
</Td> </Td>
<Td className="flex justify-end"> <Td className="flex justify-end">
{(() => {
const caType = caCapabilityMap[certificate.caId];
const supportsRevocation =
caType && caSupportsCapability(caType, CaCapability.REVOKE_CERTIFICATES);
// Don't show dropdown for CAs that don't support revocation
if (!supportsRevocation) {
return null;
}
return (
<DropdownMenu> <DropdownMenu>
<DropdownMenuTrigger asChild className="rounded-lg"> <DropdownMenuTrigger asChild className="rounded-lg">
<div className="hover:text-primary-400 data-[state=open]:text-primary-400"> <div className="hover:text-primary-400 data-[state=open]:text-primary-400">
@@ -137,7 +165,8 @@ export const PkiSubscriberCertificatesTable = ({ subscriberName, handlePopUpOpen
{(isAllowed) => ( {(isAllowed) => (
<DropdownMenuItem <DropdownMenuItem
className={twMerge( className={twMerge(
!isAllowed && "pointer-events-none cursor-not-allowed opacity-50" !isAllowed &&
"pointer-events-none cursor-not-allowed opacity-50"
)} )}
onClick={() => onClick={() =>
handlePopUpOpen && handlePopUpOpen &&
@@ -154,6 +183,8 @@ export const PkiSubscriberCertificatesTable = ({ subscriberName, handlePopUpOpen
</ProjectPermissionCan> </ProjectPermissionCan>
</DropdownMenuContent> </DropdownMenuContent>
</DropdownMenu> </DropdownMenu>
);
})()}
</Td> </Td>
</Tr> </Tr>
); );
@@ -26,6 +26,7 @@ import { useWorkspace } from "@app/context";
import { import {
CaType, CaType,
useCreatePkiSubscriber, useCreatePkiSubscriber,
useGetAzureAdcsTemplates,
useGetPkiSubscriber, useGetPkiSubscriber,
useListCasByProjectId, useListCasByProjectId,
useListWorkspacePkiSubscribers, useListWorkspacePkiSubscribers,
@@ -77,7 +78,15 @@ const schema = z
}), }),
enableAutoRenewal: z.boolean().optional().default(false), enableAutoRenewal: z.boolean().optional().default(false),
renewalBefore: z.number().min(1).optional(), renewalBefore: z.number().min(1).optional(),
renewalUnit: z.nativeEnum(TimeUnit).optional() renewalUnit: z.nativeEnum(TimeUnit).optional(),
// Properties for Azure ADCS and additional subject fields
azureTemplateType: z.string().optional(),
organization: z.string().optional(),
organizationalUnit: z.string().optional(),
country: z.string().length(2).optional().or(z.literal("")),
state: z.string().optional(),
locality: z.string().optional(),
emailAddress: z.string().email().optional().or(z.literal(""))
}) })
.required(); .required();
@@ -121,7 +130,14 @@ export const PkiSubscriberModal = ({ popUp, handlePopUpToggle }: Props) => {
extendedKeyUsages: {}, extendedKeyUsages: {},
enableAutoRenewal: false, enableAutoRenewal: false,
renewalBefore: 7, renewalBefore: 7,
renewalUnit: TimeUnit.DAY renewalUnit: TimeUnit.DAY,
azureTemplateType: "",
organization: "",
organizationalUnit: "",
country: "",
state: "",
locality: "",
emailAddress: ""
} }
}); });
@@ -129,46 +145,54 @@ export const PkiSubscriberModal = ({ popUp, handlePopUpToggle }: Props) => {
const selectedCa = cas?.find((ca) => ca.id === selectedCaId); const selectedCa = cas?.find((ca) => ca.id === selectedCaId);
const selectedAutoRenewalState = watch("enableAutoRenewal"); const selectedAutoRenewalState = watch("enableAutoRenewal");
// Fetch Azure ADCS templates when Azure CA is selected
const { data: azureTemplates } = useGetAzureAdcsTemplates({
caId: selectedCa?.type === CaType.AZURE_AD_CS ? selectedCaId : "",
projectId
});
console.log(pkiSubscriber);
// Initialize form with ALL subscriber data including template
useEffect(() => { useEffect(() => {
if (pkiSubscriber) { if (pkiSubscriber) {
reset({ // Set all values directly and immediately
name: pkiSubscriber.name, setValue("name", pkiSubscriber.name);
caId: pkiSubscriber.caId || "", setValue("caId", pkiSubscriber.caId || "");
commonName: pkiSubscriber.commonName, setValue("commonName", pkiSubscriber.commonName);
subjectAlternativeNames: pkiSubscriber.subjectAlternativeNames.join(", ") || "", setValue("subjectAlternativeNames", pkiSubscriber.subjectAlternativeNames.join(", "));
ttl: pkiSubscriber.ttl || "", setValue("ttl", pkiSubscriber.ttl || "");
keyUsages: Object.fromEntries((pkiSubscriber.keyUsages || []).map((name) => [name, true])), setValue(
extendedKeyUsages: Object.fromEntries( "keyUsages",
(pkiSubscriber.extendedKeyUsages || []).map((name) => [name, true]) Object.fromEntries((pkiSubscriber.keyUsages || []).map((name) => [name, true]))
), );
enableAutoRenewal: pkiSubscriber.enableAutoRenewal || false, setValue(
renewalBefore: pkiSubscriber.autoRenewalPeriodInDays || 7, "extendedKeyUsages",
renewalUnit: TimeUnit.DAY Object.fromEntries((pkiSubscriber.extendedKeyUsages || []).map((name) => [name, true]))
}); );
} else { setValue("enableAutoRenewal", pkiSubscriber.enableAutoRenewal || false);
reset({ setValue("renewalBefore", pkiSubscriber.autoRenewalPeriodInDays || 7);
name: "", setValue("renewalUnit", TimeUnit.DAY);
caId: "",
commonName: "", // Set Azure template immediately
subjectAlternativeNames: "", setValue("azureTemplateType", pkiSubscriber.properties?.azureTemplateType || "");
ttl: "",
keyUsages: { // Set all Additional Subject Fields immediately
[CertKeyUsage.DIGITAL_SIGNATURE]: true, setValue("organization", pkiSubscriber.properties?.organization || "");
[CertKeyUsage.KEY_ENCIPHERMENT]: true setValue("organizationalUnit", pkiSubscriber.properties?.organizationalUnit || "");
}, setValue("country", pkiSubscriber.properties?.country || "");
extendedKeyUsages: {}, setValue("state", pkiSubscriber.properties?.state || "");
enableAutoRenewal: false, setValue("locality", pkiSubscriber.properties?.locality || "");
renewalBefore: 7, setValue("emailAddress", pkiSubscriber.properties?.emailAddress || "");
renewalUnit: TimeUnit.DAY
});
} }
}, [pkiSubscriber, reset]); }, [pkiSubscriber, setValue]);
useEffect(() => { useEffect(() => {
if (cas?.length) { if (cas?.length && !pkiSubscriber) {
// Only auto-select first CA when creating new subscriber, not when updating
setValue("caId", cas[0].id); setValue("caId", cas[0].id);
} }
}, [cas, setValue]); }, [cas, setValue, pkiSubscriber]);
const onFormSubmit = async ({ const onFormSubmit = async ({
name, name,
@@ -180,7 +204,14 @@ export const PkiSubscriberModal = ({ popUp, handlePopUpToggle }: Props) => {
extendedKeyUsages, extendedKeyUsages,
enableAutoRenewal, enableAutoRenewal,
renewalBefore, renewalBefore,
renewalUnit renewalUnit,
azureTemplateType,
organization,
organizationalUnit,
country,
state,
locality,
emailAddress
}: FormData) => { }: FormData) => {
try { try {
if (!projectId) return; if (!projectId) return;
@@ -205,11 +236,26 @@ export const PkiSubscriberModal = ({ popUp, handlePopUpToggle }: Props) => {
return; return;
} }
const keyUsagesList = Object.entries(keyUsages) // Validate Azure template for Azure ADCS CA
if (selectedCa?.type === CaType.AZURE_AD_CS && !azureTemplateType) {
createNotification({
text: "Please select an Azure certificate template",
type: "error"
});
return;
}
const keyUsagesList =
selectedCa?.type === CaType.AZURE_AD_CS
? []
: Object.entries(keyUsages)
.filter(([, value]) => value) .filter(([, value]) => value)
.map(([key]) => key as CertKeyUsage); .map(([key]) => key as CertKeyUsage);
const extendedKeyUsagesList = Object.entries(extendedKeyUsages) const extendedKeyUsagesList =
selectedCa?.type === CaType.AZURE_AD_CS
? []
: Object.entries(extendedKeyUsages)
.filter(([, value]) => value) .filter(([, value]) => value)
.map(([key]) => key as CertExtendedKeyUsage); .map(([key]) => key as CertExtendedKeyUsage);
@@ -222,6 +268,17 @@ export const PkiSubscriberModal = ({ popUp, handlePopUpToggle }: Props) => {
? convertTimeUnitValueToDays(renewalUnit, renewalBefore) ? convertTimeUnitValueToDays(renewalUnit, renewalBefore)
: undefined; : undefined;
// Build properties object
const properties = {
...(selectedCa?.type === CaType.AZURE_AD_CS && azureTemplateType && { azureTemplateType }),
...(organization && { organization }),
...(organizationalUnit && { organizationalUnit }),
...(country && { country }),
...(state && { state }),
...(locality && { locality }),
...(emailAddress && { emailAddress })
};
if (pkiSubscriber) { if (pkiSubscriber) {
await updateMutateAsync({ await updateMutateAsync({
subscriberName: pkiSubscriber.name, subscriberName: pkiSubscriber.name,
@@ -234,7 +291,8 @@ export const PkiSubscriberModal = ({ popUp, handlePopUpToggle }: Props) => {
keyUsages: keyUsagesList, keyUsages: keyUsagesList,
extendedKeyUsages: extendedKeyUsagesList, extendedKeyUsages: extendedKeyUsagesList,
enableAutoRenewal, enableAutoRenewal,
autoRenewalPeriodInDays autoRenewalPeriodInDays,
properties: Object.keys(properties).length > 0 ? properties : undefined
}); });
} else { } else {
await createMutateAsync({ await createMutateAsync({
@@ -247,7 +305,8 @@ export const PkiSubscriberModal = ({ popUp, handlePopUpToggle }: Props) => {
keyUsages: keyUsagesList, keyUsages: keyUsagesList,
extendedKeyUsages: extendedKeyUsagesList, extendedKeyUsages: extendedKeyUsagesList,
enableAutoRenewal, enableAutoRenewal,
autoRenewalPeriodInDays autoRenewalPeriodInDays,
properties: Object.keys(properties).length > 0 ? properties : undefined
}); });
} }
@@ -276,17 +335,7 @@ export const PkiSubscriberModal = ({ popUp, handlePopUpToggle }: Props) => {
}} }}
> >
<ModalContent title={`${pkiSubscriber ? "Update" : "Add"} PKI Subscriber`}> <ModalContent title={`${pkiSubscriber ? "Update" : "Add"} PKI Subscriber`}>
<form <form key={pkiSubscriber?.id || "new"} onSubmit={handleSubmit(onFormSubmit)}>
onSubmit={handleSubmit(onFormSubmit, (fields) => {
setTabValue(
["name", "caId", "commonName", "subjectAlternativeNames", "ttl"].includes(
Object.keys(fields)[0]
)
? FormTab.Configuration
: FormTab.Advanced
);
})}
>
<Tabs value={tabValue} onValueChange={(value) => setTabValue(value as FormTab)}> <Tabs value={tabValue} onValueChange={(value) => setTabValue(value as FormTab)}>
<TabList> <TabList>
<Tab value={FormTab.Configuration}>Configuration</Tab> <Tab value={FormTab.Configuration}>Configuration</Tab>
@@ -323,8 +372,7 @@ export const PkiSubscriberModal = ({ popUp, handlePopUpToggle }: Props) => {
isRequired isRequired
> >
<Select <Select
defaultValue={field.value} value={field.value}
{...field}
onValueChange={(e) => onChange(e)} onValueChange={(e) => onChange(e)}
className="w-full" className="w-full"
> >
@@ -342,6 +390,34 @@ export const PkiSubscriberModal = ({ popUp, handlePopUpToggle }: Props) => {
</FormControl> </FormControl>
)} )}
/> />
{selectedCa?.type === CaType.AZURE_AD_CS && (
<Controller
control={control}
name="azureTemplateType"
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
<FormControl
label="Certificate Template"
errorText={error?.message}
isError={Boolean(error)}
isRequired
>
<Select
value={field.value}
onValueChange={(e) => onChange(e)}
className="w-full"
>
{(azureTemplates?.templates || []).map(
(template: { id: string; name: string }) => (
<SelectItem value={template.id} key={template.id}>
{template.name}
</SelectItem>
)
)}
</Select>
</FormControl>
)}
/>
)}
<Controller <Controller
control={control} control={control}
name="commonName" name="commonName"
@@ -369,6 +445,102 @@ export const PkiSubscriberModal = ({ popUp, handlePopUpToggle }: Props) => {
</FormControl> </FormControl>
)} )}
/> />
{/* Additional Subject Fields - Available for all CA types */}
<Accordion type="single" collapsible className="mb-4 w-full">
<AccordionItem value="subject-fields" className="data-[state=open]:border-none">
<AccordionTrigger className="h-fit flex-none pl-1 text-sm">
<div className="order-1 ml-3">Additional Subject Fields</div>
</AccordionTrigger>
<AccordionContent>
<div className="grid grid-cols-1 gap-4">
<Controller
control={control}
name="organization"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Organization (O)"
isError={Boolean(error)}
errorText={error?.message}
>
<Input {...field} placeholder="Example Corp" />
</FormControl>
)}
/>
<Controller
control={control}
name="organizationalUnit"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Organizational Unit (OU)"
isError={Boolean(error)}
errorText={error?.message}
>
<Input {...field} placeholder="IT Department" />
</FormControl>
)}
/>
<div className="grid grid-cols-2 gap-4">
<Controller
control={control}
name="country"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Country (C)"
isError={Boolean(error)}
errorText={error?.message}
>
<Input {...field} placeholder="US" maxLength={2} />
</FormControl>
)}
/>
<Controller
control={control}
name="state"
render={({ field, fieldState: { error } }) => (
<FormControl
label="State/Province (ST)"
isError={Boolean(error)}
errorText={error?.message}
>
<Input {...field} placeholder="California" />
</FormControl>
)}
/>
</div>
<div className="grid grid-cols-2 gap-4">
<Controller
control={control}
name="locality"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Locality (L)"
isError={Boolean(error)}
errorText={error?.message}
>
<Input {...field} placeholder="San Francisco" />
</FormControl>
)}
/>
<Controller
control={control}
name="emailAddress"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Email Address"
isError={Boolean(error)}
errorText={error?.message}
>
<Input {...field} type="email" placeholder="[email protected]" />
</FormControl>
)}
/>
</div>
</div>
</AccordionContent>
</AccordionItem>
</Accordion>
{selectedCa?.type !== CaType.ACME && ( {selectedCa?.type !== CaType.ACME && (
<Controller <Controller
control={control} control={control}
@@ -385,7 +557,7 @@ export const PkiSubscriberModal = ({ popUp, handlePopUpToggle }: Props) => {
)} )}
/> />
)} )}
{selectedCa?.type !== CaType.ACME && ( {selectedCa?.type !== CaType.ACME && selectedCa?.type !== CaType.AZURE_AD_CS && (
<Accordion type="single" collapsible className="w-full"> <Accordion type="single" collapsible className="w-full">
<AccordionItem value="key-usages" className="data-[state=open]:border-none"> <AccordionItem value="key-usages" className="data-[state=open]:border-none">
<AccordionTrigger className="h-fit flex-none pl-1 text-sm"> <AccordionTrigger className="h-fit flex-none pl-1 text-sm">
@@ -517,8 +689,7 @@ export const PkiSubscriberModal = ({ popUp, handlePopUpToggle }: Props) => {
isError={Boolean(error)} isError={Boolean(error)}
> >
<Select <Select
defaultValue={field.value} value={field.value}
{...field}
onValueChange={(e) => onChange(e)} onValueChange={(e) => onChange(e)}
className="w-48" className="w-48"
> >
@@ -12,6 +12,7 @@ import { AppConnectionHeader } from "../AppConnectionHeader";
import { OnePassConnectionForm } from "./1PasswordConnectionForm"; import { OnePassConnectionForm } from "./1PasswordConnectionForm";
import { Auth0ConnectionForm } from "./Auth0ConnectionForm"; import { Auth0ConnectionForm } from "./Auth0ConnectionForm";
import { AwsConnectionForm } from "./AwsConnectionForm"; import { AwsConnectionForm } from "./AwsConnectionForm";
import { AzureADCSConnectionForm } from "./AzureADCSConnectionForm";
import { AzureAppConfigurationConnectionForm } from "./AzureAppConfigurationConnectionForm"; import { AzureAppConfigurationConnectionForm } from "./AzureAppConfigurationConnectionForm";
import { AzureClientSecretsConnectionForm } from "./AzureClientSecretsConnectionForm"; import { AzureClientSecretsConnectionForm } from "./AzureClientSecretsConnectionForm";
import { AzureDevOpsConnectionForm } from "./AzureDevOpsConnectionForm"; import { AzureDevOpsConnectionForm } from "./AzureDevOpsConnectionForm";
@@ -96,6 +97,8 @@ const CreateForm = ({ app, onComplete }: CreateFormProps) => {
return <AzureKeyVaultConnectionForm onSubmit={onSubmit} />; return <AzureKeyVaultConnectionForm onSubmit={onSubmit} />;
case AppConnection.AzureAppConfiguration: case AppConnection.AzureAppConfiguration:
return <AzureAppConfigurationConnectionForm onSubmit={onSubmit} />; return <AzureAppConfigurationConnectionForm onSubmit={onSubmit} />;
case AppConnection.AzureADCS:
return <AzureADCSConnectionForm onSubmit={onSubmit} />;
case AppConnection.Databricks: case AppConnection.Databricks:
return <DatabricksConnectionForm onSubmit={onSubmit} />; return <DatabricksConnectionForm onSubmit={onSubmit} />;
case AppConnection.Humanitec: case AppConnection.Humanitec:
@@ -208,6 +211,8 @@ const UpdateForm = ({ appConnection, onComplete }: UpdateFormProps) => {
return ( return (
<AzureAppConfigurationConnectionForm appConnection={appConnection} onSubmit={onSubmit} /> <AzureAppConfigurationConnectionForm appConnection={appConnection} onSubmit={onSubmit} />
); );
case AppConnection.AzureADCS:
return <AzureADCSConnectionForm appConnection={appConnection} onSubmit={onSubmit} />;
case AppConnection.Databricks: case AppConnection.Databricks:
return <DatabricksConnectionForm onSubmit={onSubmit} appConnection={appConnection} />; return <DatabricksConnectionForm onSubmit={onSubmit} appConnection={appConnection} />;
case AppConnection.Humanitec: case AppConnection.Humanitec:
@@ -0,0 +1,169 @@
import { Controller, FormProvider, useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import {
Button,
FormControl,
Input,
ModalClose,
SecretInput,
Select,
SelectItem
} from "@app/components/v2";
import { APP_CONNECTION_MAP, getAppConnectionMethodDetails } from "@app/helpers/appConnections";
import { AzureADCSConnectionMethod, TAzureADCSConnection } from "@app/hooks/api/appConnections";
import { AppConnection } from "@app/hooks/api/appConnections/enums";
import {
genericAppConnectionFieldsSchema,
GenericAppConnectionsFields
} from "./GenericAppConnectionFields";
type Props = {
appConnection?: TAzureADCSConnection;
onSubmit: (formData: FormData) => Promise<void>;
};
const rootSchema = genericAppConnectionFieldsSchema.extend({
app: z.literal(AppConnection.AzureADCS)
});
const formSchema = z.discriminatedUnion("method", [
rootSchema.extend({
method: z.literal(AzureADCSConnectionMethod.UsernamePassword),
credentials: z.object({
adcsUrl: z.string().url().trim().min(1, "ADCS URL required"),
username: z.string().trim().min(1, "Username required"),
password: z.string().trim().min(1, "Password required")
})
})
]);
type FormData = z.infer<typeof formSchema>;
export const AzureADCSConnectionForm = ({ appConnection, onSubmit }: Props) => {
const isUpdate = Boolean(appConnection);
const form = useForm<FormData>({
resolver: zodResolver(formSchema),
defaultValues: appConnection ?? {
app: AppConnection.AzureADCS,
method: AzureADCSConnectionMethod.UsernamePassword,
name: "",
description: "",
credentials: {
adcsUrl: "",
username: "",
password: ""
}
}
});
const {
handleSubmit,
control,
formState: { isSubmitting, isDirty }
} = form;
return (
<FormProvider {...form}>
<form onSubmit={handleSubmit(onSubmit)}>
{!isUpdate && <GenericAppConnectionsFields />}
<Controller
name="method"
control={control}
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
tooltipText={`The method you would like to use to connect with ${
APP_CONNECTION_MAP[AppConnection.AzureADCS].name
}. This field cannot be changed after creation.`}
errorText={error?.message}
isError={Boolean(error?.message)}
label="Method"
>
<Select
isDisabled={isUpdate}
value={value}
onValueChange={(val) => onChange(val)}
className="w-full border border-mineshaft-500"
position="popper"
dropdownContainerClassName="max-w-none"
>
{Object.values(AzureADCSConnectionMethod).map((method) => {
return (
<SelectItem value={method} key={method}>
{getAppConnectionMethodDetails(method).name}
</SelectItem>
);
})}
</Select>
</FormControl>
)}
/>
<Controller
name="credentials.adcsUrl"
control={control}
render={({ field, fieldState: { error } }) => (
<FormControl
errorText={error?.message}
isError={Boolean(error?.message)}
label="ADCS URL"
>
<Input {...field} placeholder="https://your-adcs-server.com" />
</FormControl>
)}
/>
<div className="grid grid-cols-2 gap-2">
<Controller
name="credentials.username"
control={control}
render={({ field, fieldState: { error } }) => (
<FormControl
errorText={error?.message}
isError={Boolean(error?.message)}
label="Username"
>
<Input {...field} placeholder="domain\\username" />
</FormControl>
)}
/>
<Controller
name="credentials.password"
control={control}
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
errorText={error?.message}
isError={Boolean(error?.message)}
label="Password"
>
<SecretInput
containerClassName="text-gray-400 group-focus-within:!border-primary-400/50 border border-mineshaft-500 bg-mineshaft-900 px-2.5 py-1.5"
value={value}
onChange={(e) => onChange(e.target.value)}
/>
</FormControl>
)}
/>
</div>
<div className="mt-8 flex items-center">
<Button
className="mr-4"
size="sm"
type="submit"
colorSchema="secondary"
isLoading={isSubmitting}
isDisabled={isSubmitting || !isDirty}
>
{isUpdate ? "Update Credentials" : "Connect to Azure ADCS"}
</Button>
<ModalClose asChild>
<Button colorSchema="secondary" variant="plain">
Cancel
</Button>
</ModalClose>
</div>
</form>
</FormProvider>
);
};