Added Microsoft ADCS connector
330
backend/package-lock.json
generated
@@ -63,6 +63,7 @@
|
||||
"argon2": "^0.31.2",
|
||||
"aws-sdk": "^2.1553.0",
|
||||
"axios": "^1.11.0",
|
||||
"axios-ntlm": "^1.4.4",
|
||||
"axios-retry": "^4.0.0",
|
||||
"bcrypt": "^5.1.1",
|
||||
"botbuilder": "^4.23.2",
|
||||
@@ -78,6 +79,7 @@
|
||||
"googleapis": "^137.1.0",
|
||||
"handlebars": "^4.7.8",
|
||||
"hdb": "^0.19.10",
|
||||
"httpntlm": "^1.8.13",
|
||||
"ioredis": "^5.3.2",
|
||||
"isomorphic-dompurify": "^2.22.0",
|
||||
"jmespath": "^0.16.0",
|
||||
@@ -12956,216 +12958,6 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@swc/core": {
|
||||
"version": "1.3.107",
|
||||
"resolved": "https://registry.npmjs.org/@swc/core/-/core-1.3.107.tgz",
|
||||
"integrity": "sha512-zKhqDyFcTsyLIYK1iEmavljZnf4CCor5pF52UzLAz4B6Nu/4GLU+2LQVAf+oRHjusG39PTPjd2AlRT3f3QWfsQ==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@swc/counter": "^0.1.1",
|
||||
"@swc/types": "^0.1.5"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/swc"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@swc/core-darwin-arm64": "1.3.107",
|
||||
"@swc/core-darwin-x64": "1.3.107",
|
||||
"@swc/core-linux-arm-gnueabihf": "1.3.107",
|
||||
"@swc/core-linux-arm64-gnu": "1.3.107",
|
||||
"@swc/core-linux-arm64-musl": "1.3.107",
|
||||
"@swc/core-linux-x64-gnu": "1.3.107",
|
||||
"@swc/core-linux-x64-musl": "1.3.107",
|
||||
"@swc/core-win32-arm64-msvc": "1.3.107",
|
||||
"@swc/core-win32-ia32-msvc": "1.3.107",
|
||||
"@swc/core-win32-x64-msvc": "1.3.107"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@swc/helpers": "^0.5.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@swc/helpers": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@swc/core-darwin-arm64": {
|
||||
"version": "1.3.107",
|
||||
"resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.3.107.tgz",
|
||||
"integrity": "sha512-47tD/5vSXWxPd0j/ZllyQUg4bqalbQTsmqSw0J4dDdS82MWqCAwUErUrAZPRjBkjNQ6Kmrf5rpCWaGTtPw+ngw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/@swc/core-darwin-x64": {
|
||||
"version": "1.3.107",
|
||||
"resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.3.107.tgz",
|
||||
"integrity": "sha512-hwiLJ2ulNkBGAh1m1eTfeY1417OAYbRGcb/iGsJ+LuVLvKAhU/itzsl535CvcwAlt2LayeCFfcI8gdeOLeZa9A==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/@swc/core-linux-arm-gnueabihf": {
|
||||
"version": "1.3.107",
|
||||
"resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.3.107.tgz",
|
||||
"integrity": "sha512-I2wzcC0KXqh0OwymCmYwNRgZ9nxX7DWnOOStJXV3pS0uB83TXAkmqd7wvMBuIl9qu4Hfomi9aDM7IlEEn9tumQ==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/@swc/core-linux-arm64-gnu": {
|
||||
"version": "1.3.107",
|
||||
"resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.3.107.tgz",
|
||||
"integrity": "sha512-HWgnn7JORYlOYnGsdunpSF8A+BCZKPLzLtEUA27/M/ZuANcMZabKL9Zurt7XQXq888uJFAt98Gy+59PU90aHKg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/@swc/core-linux-arm64-musl": {
|
||||
"version": "1.3.107",
|
||||
"resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.3.107.tgz",
|
||||
"integrity": "sha512-vfPF74cWfAm8hyhS8yvYI94ucMHIo8xIYU+oFOW9uvDlGQRgnUf/6DEVbLyt/3yfX5723Ln57U8uiMALbX5Pyw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/@swc/core-linux-x64-gnu": {
|
||||
"version": "1.3.107",
|
||||
"resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.3.107.tgz",
|
||||
"integrity": "sha512-uBVNhIg0ip8rH9OnOsCARUFZ3Mq3tbPHxtmWk9uAa5u8jQwGWeBx5+nTHpDOVd3YxKb6+5xDEI/edeeLpha/9g==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/@swc/core-linux-x64-musl": {
|
||||
"version": "1.3.107",
|
||||
"resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.3.107.tgz",
|
||||
"integrity": "sha512-mvACkUvzSIB12q1H5JtabWATbk3AG+pQgXEN95AmEX2ZA5gbP9+B+mijsg7Sd/3tboHr7ZHLz/q3SHTvdFJrEw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/@swc/core-win32-arm64-msvc": {
|
||||
"version": "1.3.107",
|
||||
"resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.3.107.tgz",
|
||||
"integrity": "sha512-J3P14Ngy/1qtapzbguEH41kY109t6DFxfbK4Ntz9dOWNuVY3o9/RTB841ctnJk0ZHEG+BjfCJjsD2n8H5HcaOA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/@swc/core-win32-ia32-msvc": {
|
||||
"version": "1.3.107",
|
||||
"resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.3.107.tgz",
|
||||
"integrity": "sha512-ZBUtgyjTHlz8TPJh7kfwwwFma+ktr6OccB1oXC8fMSopD0AxVnQasgun3l3099wIsAB9eEsJDQ/3lDkOLs1gBA==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/@swc/core-win32-x64-msvc": {
|
||||
"version": "1.3.107",
|
||||
"resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.3.107.tgz",
|
||||
"integrity": "sha512-Eyzo2XRqWOxqhE1gk9h7LWmUf4Bp4Xn2Ttb0ayAXFp6YSTxQIThXcT9kipXZqcpxcmDwoq8iWbbf2P8XL743EA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/@swc/counter": {
|
||||
"version": "0.1.3",
|
||||
"resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz",
|
||||
@@ -13183,14 +12975,6 @@
|
||||
"tslib": "^2.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@swc/types": {
|
||||
"version": "0.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.5.tgz",
|
||||
"integrity": "sha512-myfUej5naTBWnqOCc/MdVOLVjXUXtIA+NpDrDBKJtLLg2shUjBu3cZmB/85RyitKc55+lUUyl7oRfLOvkr2hsw==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@techteamer/ocsp": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@techteamer/ocsp/-/ocsp-1.0.1.tgz",
|
||||
@@ -15195,6 +14979,18 @@
|
||||
"proxy-from-env": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/axios-ntlm": {
|
||||
"version": "1.4.4",
|
||||
"resolved": "https://registry.npmjs.org/axios-ntlm/-/axios-ntlm-1.4.4.tgz",
|
||||
"integrity": "sha512-kpCRdzMfL8gi0Z0o96P3QPAK4XuC8iciGgxGXe+PeQ4oyjI2LZN8WSOKbu0Y9Jo3T/A7pB81n6jYVPIpglEuRA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"axios": "^1.8.4",
|
||||
"des.js": "^1.1.0",
|
||||
"dev-null": "^0.1.1",
|
||||
"js-md4": "^0.3.2"
|
||||
}
|
||||
},
|
||||
"node_modules/axios-retry": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/axios-retry/-/axios-retry-4.0.0.tgz",
|
||||
@@ -16954,6 +16750,16 @@
|
||||
"resolved": "https://registry.npmjs.org/deprecation/-/deprecation-2.3.1.tgz",
|
||||
"integrity": "sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ=="
|
||||
},
|
||||
"node_modules/des.js": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/des.js/-/des.js-1.1.0.tgz",
|
||||
"integrity": "sha512-r17GxjhUCjSRy8aiJpr8/UadFIzMzJGexI3Nmz4ADi9LYSFx4gTBp80+NaX/YsXWWLhpZ7v/v/ubEc/bCNfKwg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"inherits": "^2.0.1",
|
||||
"minimalistic-assert": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/destroy": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz",
|
||||
@@ -16981,6 +16787,12 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/dev-null": {
|
||||
"version": "0.1.1",
|
||||
"resolved": "https://registry.npmjs.org/dev-null/-/dev-null-0.1.1.tgz",
|
||||
"integrity": "sha512-nMNZG0zfMgmdv8S5O0TM5cpwNbGKRGPCxVsr0SmA3NZZy9CYBbuNLL0PD3Acx9e5LIUgwONXtM9kM6RlawPxEQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/diff": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz",
|
||||
@@ -19029,49 +18841,6 @@
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
|
||||
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
|
||||
},
|
||||
"node_modules/gcp-metadata": {
|
||||
"version": "5.3.0",
|
||||
"resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-5.3.0.tgz",
|
||||
"integrity": "sha512-FNTkdNEnBdlqF2oatizolQqNANMrcqJt6AAYt99B3y1aLLC8Hc5IOBb+ZnnzllodEEf6xMBp6wRcBbc16fa65w==",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"gaxios": "^5.0.0",
|
||||
"json-bigint": "^1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/gcp-metadata/node_modules/gaxios": {
|
||||
"version": "5.1.3",
|
||||
"resolved": "https://registry.npmjs.org/gaxios/-/gaxios-5.1.3.tgz",
|
||||
"integrity": "sha512-95hVgBRgEIRQQQHIbnxBXeHbW4TqFk4ZDJW7wmVtvYar72FdhRIo1UGOLS2eRAKCPEdPBWu+M7+A33D9CdX9rA==",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"extend": "^3.0.2",
|
||||
"https-proxy-agent": "^5.0.0",
|
||||
"is-stream": "^2.0.0",
|
||||
"node-fetch": "^2.6.9"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/gcp-metadata/node_modules/is-stream": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz",
|
||||
"integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/generate-function": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.3.1.tgz",
|
||||
@@ -19855,6 +19624,39 @@
|
||||
"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": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz",
|
||||
@@ -30579,6 +30381,12 @@
|
||||
"integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==",
|
||||
"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": {
|
||||
"version": "6.19.8",
|
||||
"resolved": "https://registry.npmjs.org/undici/-/undici-6.19.8.tgz",
|
||||
|
||||
@@ -183,6 +183,7 @@
|
||||
"argon2": "^0.31.2",
|
||||
"aws-sdk": "^2.1553.0",
|
||||
"axios": "^1.11.0",
|
||||
"axios-ntlm": "^1.4.4",
|
||||
"axios-retry": "^4.0.0",
|
||||
"bcrypt": "^5.1.1",
|
||||
"botbuilder": "^4.23.2",
|
||||
@@ -198,6 +199,7 @@
|
||||
"googleapis": "^137.1.0",
|
||||
"handlebars": "^4.7.8",
|
||||
"hdb": "^0.19.10",
|
||||
"httpntlm": "^1.8.13",
|
||||
"ioredis": "^5.3.2",
|
||||
"isomorphic-dompurify": "^2.22.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");
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -25,7 +25,9 @@ export const PkiSubscribersSchema = z.object({
|
||||
lastAutoRenewAt: z.date().nullable().optional(),
|
||||
lastOperationStatus: z.string().nullable().optional(),
|
||||
lastOperationMessage: z.string().nullable().optional(),
|
||||
lastOperationAt: z.date().nullable().optional()
|
||||
lastOperationAt: z.date().nullable().optional(),
|
||||
azureAuthMethod: z.string().nullable().optional(),
|
||||
properties: z.unknown().nullable().optional()
|
||||
});
|
||||
|
||||
export type TPkiSubscribers = z.infer<typeof PkiSubscribersSchema>;
|
||||
|
||||
@@ -2312,6 +2312,11 @@ export const AppConnections = {
|
||||
OKTA: {
|
||||
instanceUrl: "The URL used to access your Okta organization.",
|
||||
apiToken: "The API token used to authenticate with Okta."
|
||||
},
|
||||
AZURE_ADCS: {
|
||||
adcsUrl: "The 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."
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -948,6 +948,7 @@ export const registerRoutes = async (
|
||||
certificateAuthorityCrlDAL,
|
||||
certificateAuthoritySecretDAL,
|
||||
projectDAL,
|
||||
appConnectionDAL,
|
||||
kmsService,
|
||||
permissionService,
|
||||
pkiCollectionDAL,
|
||||
|
||||
@@ -15,6 +15,10 @@ import {
|
||||
} from "@app/services/app-connection/1password";
|
||||
import { Auth0ConnectionListItemSchema, SanitizedAuth0ConnectionSchema } from "@app/services/app-connection/auth0";
|
||||
import { AwsConnectionListItemSchema, SanitizedAwsConnectionSchema } from "@app/services/app-connection/aws";
|
||||
import {
|
||||
AzureADCSConnectionListItemSchema,
|
||||
SanitizedAzureADCSConnectionSchema
|
||||
} from "@app/services/app-connection/azure-adcs/azure-adcs-connection-schemas";
|
||||
import {
|
||||
AzureAppConfigurationConnectionListItemSchema,
|
||||
SanitizedAzureAppConfigurationConnectionSchema
|
||||
@@ -150,7 +154,8 @@ const SanitizedAppConnectionSchema = z.union([
|
||||
...SanitizedSupabaseConnectionSchema.options,
|
||||
...SanitizedDigitalOceanConnectionSchema.options,
|
||||
...SanitizedNetlifyConnectionSchema.options,
|
||||
...SanitizedOktaConnectionSchema.options
|
||||
...SanitizedOktaConnectionSchema.options,
|
||||
...SanitizedAzureADCSConnectionSchema.options
|
||||
]);
|
||||
|
||||
const AppConnectionOptionsSchema = z.discriminatedUnion("app", [
|
||||
@@ -190,7 +195,8 @@ const AppConnectionOptionsSchema = z.discriminatedUnion("app", [
|
||||
SupabaseConnectionListItemSchema,
|
||||
DigitalOceanConnectionListItemSchema,
|
||||
NetlifyConnectionListItemSchema,
|
||||
OktaConnectionListItemSchema
|
||||
OktaConnectionListItemSchema,
|
||||
AzureADCSConnectionListItemSchema
|
||||
]);
|
||||
|
||||
export const registerAppConnectionRouter = async (server: FastifyZodProvider) => {
|
||||
|
||||
@@ -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 { registerAuth0ConnectionRouter } from "./auth0-connection-router";
|
||||
import { registerAwsConnectionRouter } from "./aws-connection-router";
|
||||
import { registerAzureADCSConnectionRouter } from "./azure-adcs-connection-router";
|
||||
import { registerAzureAppConfigurationConnectionRouter } from "./azure-app-configuration-connection-router";
|
||||
import { registerAzureClientSecretsConnectionRouter } from "./azure-client-secrets-connection-router";
|
||||
import { registerAzureDevOpsConnectionRouter } from "./azure-devops-connection-router";
|
||||
@@ -50,6 +51,7 @@ export const APP_CONNECTION_REGISTER_ROUTER_MAP: Record<AppConnection, (server:
|
||||
[AppConnection.AzureAppConfiguration]: registerAzureAppConfigurationConnectionRouter,
|
||||
[AppConnection.AzureClientSecrets]: registerAzureClientSecretsConnectionRouter,
|
||||
[AppConnection.AzureDevOps]: registerAzureDevOpsConnectionRouter,
|
||||
[AppConnection.AzureADCS]: registerAzureADCSConnectionRouter,
|
||||
[AppConnection.Databricks]: registerDatabricksConnectionRouter,
|
||||
[AppConnection.Humanitec]: registerHumanitecConnectionRouter,
|
||||
[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 { registerAcmeCertificateAuthorityRouter } from "./acme-certificate-authority-router";
|
||||
import { registerAzureAdCsCertificateAuthorityRouter } from "./azure-ad-cs-certificate-authority-router";
|
||||
import { registerInternalCertificateAuthorityRouter } from "./internal-certificate-authority-router";
|
||||
|
||||
export * from "./internal-certificate-authority-router";
|
||||
@@ -8,5 +9,6 @@ export * from "./internal-certificate-authority-router";
|
||||
export const CERTIFICATE_AUTHORITY_REGISTER_ROUTER_MAP: Record<CaType, (server: FastifyZodProvider) => Promise<void>> =
|
||||
{
|
||||
[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)))
|
||||
.describe(PKI_SUBSCRIBERS.CREATE.extendedKeyUsages),
|
||||
enableAutoRenewal: z.boolean().optional().describe(PKI_SUBSCRIBERS.CREATE.enableAutoRenewal),
|
||||
autoRenewalPeriodInDays: z.number().min(1).optional().describe(PKI_SUBSCRIBERS.CREATE.autoRenewalPeriodInDays)
|
||||
autoRenewalPeriodInDays: z.number().min(1).optional().describe(PKI_SUBSCRIBERS.CREATE.autoRenewalPeriodInDays),
|
||||
properties: z
|
||||
.object({
|
||||
azureTemplateType: z.string().optional().describe("Azure ADCS Certificate Template Type"),
|
||||
organization: z.string().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: {
|
||||
200: sanitizedPkiSubscriber
|
||||
@@ -199,7 +211,19 @@ export const registerPkiSubscriberRouter = async (server: FastifyZodProvider) =>
|
||||
.optional()
|
||||
.describe(PKI_SUBSCRIBERS.UPDATE.extendedKeyUsages),
|
||||
enableAutoRenewal: z.boolean().optional().describe(PKI_SUBSCRIBERS.UPDATE.enableAutoRenewal),
|
||||
autoRenewalPeriodInDays: z.number().min(1).optional().describe(PKI_SUBSCRIBERS.UPDATE.autoRenewalPeriodInDays)
|
||||
autoRenewalPeriodInDays: z.number().min(1).optional().describe(PKI_SUBSCRIBERS.UPDATE.autoRenewalPeriodInDays),
|
||||
properties: z
|
||||
.object({
|
||||
azureTemplateType: z.string().optional().describe("Azure ADCS Certificate Template Type"),
|
||||
organization: z.string().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: {
|
||||
200: sanitizedPkiSubscriber
|
||||
|
||||
@@ -6,12 +6,14 @@ import { readLimit } from "@app/server/config/rateLimiter";
|
||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||
import { AuthMode } from "@app/services/auth/auth-type";
|
||||
import { AcmeCertificateAuthoritySchema } from "@app/services/certificate-authority/acme/acme-certificate-authority-schemas";
|
||||
import { AzureAdCsCertificateAuthoritySchema } from "@app/services/certificate-authority/azure-ad-cs/azure-ad-cs-certificate-authority-schemas";
|
||||
import { CaType } from "@app/services/certificate-authority/certificate-authority-enums";
|
||||
import { InternalCertificateAuthoritySchema } from "@app/services/certificate-authority/internal/internal-certificate-authority-schemas";
|
||||
|
||||
const CertificateAuthoritySchema = z.discriminatedUnion("type", [
|
||||
InternalCertificateAuthoritySchema,
|
||||
AcmeCertificateAuthoritySchema
|
||||
AcmeCertificateAuthoritySchema,
|
||||
AzureAdCsCertificateAuthoritySchema
|
||||
]);
|
||||
|
||||
export const registerCaRouter = async (server: FastifyZodProvider) => {
|
||||
@@ -52,19 +54,31 @@ export const registerCaRouter = async (server: FastifyZodProvider) => {
|
||||
req.permission
|
||||
);
|
||||
|
||||
const azureAdCsCas = await server.services.certificateAuthority.listCertificateAuthoritiesByProjectId(
|
||||
{
|
||||
projectId: req.query.projectId,
|
||||
type: CaType.AZURE_AD_CS
|
||||
},
|
||||
req.permission
|
||||
);
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
projectId: req.query.projectId,
|
||||
event: {
|
||||
type: EventType.GET_CAS,
|
||||
metadata: {
|
||||
caIds: [...(internalCas ?? []).map((ca) => ca.id), ...(acmeCas ?? []).map((ca) => ca.id)]
|
||||
caIds: [
|
||||
...(internalCas ?? []).map((ca) => ca.id),
|
||||
...(acmeCas ?? []).map((ca) => ca.id),
|
||||
...(azureAdCsCas ?? []).map((ca) => ca.id)
|
||||
]
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
certificateAuthorities: [...(internalCas ?? []), ...(acmeCas ?? [])]
|
||||
certificateAuthorities: [...(internalCas ?? []), ...(acmeCas ?? []), ...(azureAdCsCas ?? [])]
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
@@ -8,6 +8,7 @@ export enum AppConnection {
|
||||
AzureAppConfiguration = "azure-app-configuration",
|
||||
AzureClientSecrets = "azure-client-secrets",
|
||||
AzureDevOps = "azure-devops",
|
||||
AzureADCS = "azure-adcs",
|
||||
Humanitec = "humanitec",
|
||||
TerraformCloud = "terraform-cloud",
|
||||
Vercel = "vercel",
|
||||
|
||||
@@ -31,6 +31,11 @@ import {
|
||||
} from "./app-connection-types";
|
||||
import { Auth0ConnectionMethod, getAuth0ConnectionListItem, validateAuth0ConnectionCredentials } from "./auth0";
|
||||
import { AwsConnectionMethod, getAwsConnectionListItem, validateAwsConnectionCredentials } from "./aws";
|
||||
import { AzureADCSConnectionMethod } from "./azure-adcs";
|
||||
import {
|
||||
getAzureADCSConnectionListItem,
|
||||
validateAzureADCSConnectionCredentials
|
||||
} from "./azure-adcs/azure-adcs-connection-fns";
|
||||
import {
|
||||
AzureAppConfigurationConnectionMethod,
|
||||
getAzureAppConfigurationConnectionListItem,
|
||||
@@ -136,6 +141,7 @@ export const listAppConnectionOptions = () => {
|
||||
getAzureKeyVaultConnectionListItem(),
|
||||
getAzureAppConfigurationConnectionListItem(),
|
||||
getAzureDevopsConnectionListItem(),
|
||||
getAzureADCSConnectionListItem(),
|
||||
getDatabricksConnectionListItem(),
|
||||
getHumanitecConnectionListItem(),
|
||||
getTerraformCloudConnectionListItem(),
|
||||
@@ -227,6 +233,7 @@ export const validateAppConnectionCredentials = async (
|
||||
[AppConnection.AzureClientSecrets]:
|
||||
validateAzureClientSecretsConnectionCredentials as TAppConnectionCredentialsValidator,
|
||||
[AppConnection.AzureDevOps]: validateAzureDevOpsConnectionCredentials as TAppConnectionCredentialsValidator,
|
||||
[AppConnection.AzureADCS]: validateAzureADCSConnectionCredentials as TAppConnectionCredentialsValidator,
|
||||
[AppConnection.Humanitec]: validateHumanitecConnectionCredentials as TAppConnectionCredentialsValidator,
|
||||
[AppConnection.Postgres]: validateSqlConnectionCredentials as TAppConnectionCredentialsValidator,
|
||||
[AppConnection.MsSql]: validateSqlConnectionCredentials as TAppConnectionCredentialsValidator,
|
||||
@@ -300,6 +307,7 @@ export const getAppConnectionMethodName = (method: TAppConnection["method"]) =>
|
||||
case MsSqlConnectionMethod.UsernameAndPassword:
|
||||
case MySqlConnectionMethod.UsernameAndPassword:
|
||||
case OracleDBConnectionMethod.UsernameAndPassword:
|
||||
case AzureADCSConnectionMethod.UsernamePassword:
|
||||
return "Username & Password";
|
||||
case WindmillConnectionMethod.AccessToken:
|
||||
case HCVaultConnectionMethod.AccessToken:
|
||||
@@ -357,6 +365,7 @@ export const TRANSITION_CONNECTION_CREDENTIALS_TO_PLATFORM: Record<
|
||||
[AppConnection.AzureKeyVault]: platformManagedCredentialsNotSupported,
|
||||
[AppConnection.AzureAppConfiguration]: platformManagedCredentialsNotSupported,
|
||||
[AppConnection.AzureDevOps]: platformManagedCredentialsNotSupported,
|
||||
[AppConnection.AzureADCS]: platformManagedCredentialsNotSupported,
|
||||
[AppConnection.Humanitec]: platformManagedCredentialsNotSupported,
|
||||
[AppConnection.Postgres]: transferSqlConnectionCredentialsToPlatform as TAppConnectionTransitionCredentialsToPlatform,
|
||||
[AppConnection.MsSql]: transferSqlConnectionCredentialsToPlatform as TAppConnectionTransitionCredentialsToPlatform,
|
||||
|
||||
@@ -9,6 +9,7 @@ export const APP_CONNECTION_NAME_MAP: Record<AppConnection, string> = {
|
||||
[AppConnection.AzureAppConfiguration]: "Azure App Configuration",
|
||||
[AppConnection.AzureClientSecrets]: "Azure Client Secrets",
|
||||
[AppConnection.AzureDevOps]: "Azure DevOps",
|
||||
[AppConnection.AzureADCS]: "Azure ADCS",
|
||||
[AppConnection.Databricks]: "Databricks",
|
||||
[AppConnection.Humanitec]: "Humanitec",
|
||||
[AppConnection.TerraformCloud]: "Terraform Cloud",
|
||||
@@ -49,6 +50,7 @@ export const APP_CONNECTION_PLAN_MAP: Record<AppConnection, AppConnectionPlanTyp
|
||||
[AppConnection.AzureAppConfiguration]: AppConnectionPlanType.Regular,
|
||||
[AppConnection.AzureClientSecrets]: AppConnectionPlanType.Regular,
|
||||
[AppConnection.AzureDevOps]: AppConnectionPlanType.Regular,
|
||||
[AppConnection.AzureADCS]: AppConnectionPlanType.Regular,
|
||||
[AppConnection.Databricks]: AppConnectionPlanType.Regular,
|
||||
[AppConnection.Humanitec]: AppConnectionPlanType.Regular,
|
||||
[AppConnection.TerraformCloud]: AppConnectionPlanType.Regular,
|
||||
|
||||
@@ -45,6 +45,7 @@ import {
|
||||
import { ValidateAuth0ConnectionCredentialsSchema } from "./auth0";
|
||||
import { ValidateAwsConnectionCredentialsSchema } from "./aws";
|
||||
import { awsConnectionService } from "./aws/aws-connection-service";
|
||||
import { ValidateAzureADCSConnectionCredentialsSchema } from "./azure-adcs/azure-adcs-connection-schemas";
|
||||
import { ValidateAzureAppConfigurationConnectionCredentialsSchema } from "./azure-app-configuration";
|
||||
import { ValidateAzureClientSecretsConnectionCredentialsSchema } from "./azure-client-secrets";
|
||||
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.AzureAppConfiguration]: ValidateAzureAppConfigurationConnectionCredentialsSchema,
|
||||
[AppConnection.AzureDevOps]: ValidateAzureDevOpsConnectionCredentialsSchema,
|
||||
[AppConnection.AzureADCS]: ValidateAzureADCSConnectionCredentialsSchema,
|
||||
[AppConnection.Databricks]: ValidateDatabricksConnectionCredentialsSchema,
|
||||
[AppConnection.Humanitec]: ValidateHumanitecConnectionCredentialsSchema,
|
||||
[AppConnection.TerraformCloud]: ValidateTerraformCloudConnectionCredentialsSchema,
|
||||
|
||||
@@ -33,6 +33,12 @@ import {
|
||||
TAwsConnectionInput,
|
||||
TValidateAwsConnectionCredentialsSchema
|
||||
} from "./aws";
|
||||
import {
|
||||
TAzureADCSConnection,
|
||||
TAzureADCSConnectionConfig,
|
||||
TAzureADCSConnectionInput,
|
||||
TValidateAzureADCSConnectionCredentialsSchema
|
||||
} from "./azure-adcs/azure-adcs-connection-types";
|
||||
import {
|
||||
TAzureAppConfigurationConnection,
|
||||
TAzureAppConfigurationConnectionConfig,
|
||||
@@ -223,6 +229,7 @@ export type TAppConnection = { id: string } & (
|
||||
| TAzureKeyVaultConnection
|
||||
| TAzureAppConfigurationConnection
|
||||
| TAzureDevOpsConnection
|
||||
| TAzureADCSConnection
|
||||
| TDatabricksConnection
|
||||
| THumanitecConnection
|
||||
| TTerraformCloudConnection
|
||||
@@ -267,6 +274,7 @@ export type TAppConnectionInput = { id: string } & (
|
||||
| TAzureKeyVaultConnectionInput
|
||||
| TAzureAppConfigurationConnectionInput
|
||||
| TAzureDevOpsConnectionInput
|
||||
| TAzureADCSConnectionInput
|
||||
| TDatabricksConnectionInput
|
||||
| THumanitecConnectionInput
|
||||
| TTerraformCloudConnectionInput
|
||||
@@ -322,6 +330,7 @@ export type TAppConnectionConfig =
|
||||
| TAzureKeyVaultConnectionConfig
|
||||
| TAzureAppConfigurationConnectionConfig
|
||||
| TAzureDevOpsConnectionConfig
|
||||
| TAzureADCSConnectionConfig
|
||||
| TAzureClientSecretsConnectionConfig
|
||||
| TDatabricksConnectionConfig
|
||||
| THumanitecConnectionConfig
|
||||
@@ -359,6 +368,7 @@ export type TValidateAppConnectionCredentialsSchema =
|
||||
| TValidateAzureAppConfigurationConnectionCredentialsSchema
|
||||
| TValidateAzureClientSecretsConnectionCredentialsSchema
|
||||
| TValidateAzureDevOpsConnectionCredentialsSchema
|
||||
| TValidateAzureADCSConnectionCredentialsSchema
|
||||
| TValidateDatabricksConnectionCredentialsSchema
|
||||
| TValidateHumanitecConnectionCredentialsSchema
|
||||
| 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: user@domain.com
|
||||
const [user, domainPart] = inputUsername.split("@");
|
||||
if (!user || !domainPart) {
|
||||
throw new BadRequestError({
|
||||
message: "Invalid UPN format. Expected format: user@domain.com"
|
||||
});
|
||||
}
|
||||
|
||||
username = user;
|
||||
// Extract NetBIOS name from FQDN
|
||||
domain = domainPart.split(".")[0].toUpperCase();
|
||||
fullUsername = `${domain}\\${username}`;
|
||||
} else {
|
||||
// Plain username - assume local account or current domain
|
||||
username = inputUsername;
|
||||
domain = "";
|
||||
fullUsername = inputUsername;
|
||||
}
|
||||
|
||||
return { domain, username, fullUsername };
|
||||
};
|
||||
|
||||
// Helper to normalize URL
|
||||
const normalizeAdcsUrl = (url: string): string => {
|
||||
let normalizedUrl = url.trim();
|
||||
|
||||
// Remove trailing slash
|
||||
normalizedUrl = normalizedUrl.replace(/\/$/, "");
|
||||
|
||||
// Ensure HTTPS protocol
|
||||
if (normalizedUrl.startsWith("http://")) {
|
||||
normalizedUrl = normalizedUrl.replace("http://", "https://");
|
||||
} else if (!normalizedUrl.startsWith("https://")) {
|
||||
normalizedUrl = `https://${normalizedUrl}`;
|
||||
}
|
||||
|
||||
return normalizedUrl;
|
||||
};
|
||||
|
||||
// NTLM request wrapper
|
||||
const 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"
|
||||
>;
|
||||
4
backend/src/services/app-connection/azure-adcs/index.ts
Normal file
@@ -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 {
|
||||
INTERNAL = "internal",
|
||||
ACME = "acme"
|
||||
ACME = "acme",
|
||||
AZURE_AD_CS = "azure-ad-cs"
|
||||
}
|
||||
|
||||
export enum InternalCaType {
|
||||
@@ -17,3 +18,9 @@ export enum CaStatus {
|
||||
export enum CaRenewalType {
|
||||
EXISTING = "existing"
|
||||
}
|
||||
|
||||
export enum CaCapability {
|
||||
ISSUE_CERTIFICATES = "issue-certificates",
|
||||
REVOKE_CERTIFICATES = "revoke-certificates",
|
||||
RENEW_CERTIFICATES = "renew-certificates"
|
||||
}
|
||||
|
||||
@@ -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> = {
|
||||
[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 { SubscriberOperationStatus } from "../pki-subscriber/pki-subscriber-types";
|
||||
import { AcmeCertificateAuthorityFns } from "./acme/acme-certificate-authority-fns";
|
||||
import { AzureAdCsCertificateAuthorityFns } from "./azure-ad-cs/azure-ad-cs-certificate-authority-fns";
|
||||
import { TCertificateAuthorityDALFactory } from "./certificate-authority-dal";
|
||||
import { CaType } from "./certificate-authority-enums";
|
||||
import { keyAlgorithmToAlgCfg } from "./certificate-authority-fns";
|
||||
@@ -33,7 +34,7 @@ import {
|
||||
|
||||
type TCertificateAuthorityQueueFactoryDep = {
|
||||
certificateAuthorityDAL: TCertificateAuthorityDALFactory;
|
||||
appConnectionDAL: Pick<TAppConnectionDALFactory, "findById" | "update">;
|
||||
appConnectionDAL: Pick<TAppConnectionDALFactory, "findById" | "update" | "updateById">;
|
||||
appConnectionService: Pick<TAppConnectionServiceFactory, "connectAppConnectionById">;
|
||||
externalCertificateAuthorityDAL: Pick<TExternalCertificateAuthorityDALFactory, "create" | "update">;
|
||||
keyStore: Pick<TKeyStoreFactory, "acquireLock" | "setItemWithExpiry" | "getItem">;
|
||||
@@ -82,6 +83,19 @@ export const certificateAuthorityQueueFactory = ({
|
||||
projectDAL
|
||||
});
|
||||
|
||||
const azureAdCsFns = AzureAdCsCertificateAuthorityFns({
|
||||
appConnectionDAL,
|
||||
appConnectionService,
|
||||
certificateAuthorityDAL,
|
||||
externalCertificateAuthorityDAL,
|
||||
certificateDAL,
|
||||
certificateBodyDAL,
|
||||
certificateSecretDAL,
|
||||
kmsService,
|
||||
pkiSubscriberDAL,
|
||||
projectDAL
|
||||
});
|
||||
|
||||
// TODO 1: auto-periodic rotation
|
||||
// TODO 2: manual rotation
|
||||
|
||||
@@ -158,6 +172,13 @@ export const certificateAuthorityQueueFactory = ({
|
||||
lastOperationMessage: "Certificate ordered successfully",
|
||||
lastOperationAt: new Date()
|
||||
});
|
||||
} else if (caType === CaType.AZURE_AD_CS) {
|
||||
await azureAdCsFns.orderSubscriberCertificate(subscriberId);
|
||||
await pkiSubscriberDAL.updateById(subscriberId, {
|
||||
lastOperationStatus: SubscriberOperationStatus.SUCCESS,
|
||||
lastOperationMessage: "Certificate ordered successfully",
|
||||
lastOperationAt: new Date()
|
||||
});
|
||||
}
|
||||
} catch (e: unknown) {
|
||||
if (e instanceof Error) {
|
||||
|
||||
@@ -2,7 +2,11 @@ import { ForbiddenError } from "@casl/ability";
|
||||
|
||||
import { ActionProjectType, TableName } from "@app/db/schemas";
|
||||
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 { OrgServiceActor } from "@app/lib/types";
|
||||
|
||||
@@ -22,6 +26,14 @@ import {
|
||||
TCreateAcmeCertificateAuthorityDTO,
|
||||
TUpdateAcmeCertificateAuthorityDTO
|
||||
} from "./acme/acme-certificate-authority-types";
|
||||
import {
|
||||
AzureAdCsCertificateAuthorityFns,
|
||||
castDbEntryToAzureAdCsCertificateAuthority
|
||||
} from "./azure-ad-cs/azure-ad-cs-certificate-authority-fns";
|
||||
import {
|
||||
TCreateAzureAdCsCertificateAuthorityDTO,
|
||||
TUpdateAzureAdCsCertificateAuthorityDTO
|
||||
} from "./azure-ad-cs/azure-ad-cs-certificate-authority-types";
|
||||
import { TCertificateAuthorityDALFactory } from "./certificate-authority-dal";
|
||||
import { CaType } from "./certificate-authority-enums";
|
||||
import {
|
||||
@@ -34,7 +46,7 @@ import { TInternalCertificateAuthorityServiceFactory } from "./internal/internal
|
||||
import { TCreateInternalCertificateAuthorityDTO } from "./internal/internal-certificate-authority-types";
|
||||
|
||||
type TCertificateAuthorityServiceFactoryDep = {
|
||||
appConnectionDAL: Pick<TAppConnectionDALFactory, "findById" | "update">;
|
||||
appConnectionDAL: Pick<TAppConnectionDALFactory, "findById" | "update" | "updateById">;
|
||||
appConnectionService: Pick<TAppConnectionServiceFactory, "connectAppConnectionById">;
|
||||
certificateAuthorityDAL: Pick<
|
||||
TCertificateAuthorityDALFactory,
|
||||
@@ -91,6 +103,19 @@ export const certificateAuthorityServiceFactory = ({
|
||||
projectDAL
|
||||
});
|
||||
|
||||
const azureAdCsFns = AzureAdCsCertificateAuthorityFns({
|
||||
appConnectionDAL,
|
||||
appConnectionService,
|
||||
certificateAuthorityDAL,
|
||||
externalCertificateAuthorityDAL,
|
||||
certificateDAL,
|
||||
certificateBodyDAL,
|
||||
certificateSecretDAL,
|
||||
kmsService,
|
||||
pkiSubscriberDAL,
|
||||
projectDAL
|
||||
});
|
||||
|
||||
const createCertificateAuthority = async (
|
||||
{ type, projectId, name, enableDirectIssuance, configuration, status }: TCreateCertificateAuthorityDTO,
|
||||
actor: OrgServiceActor
|
||||
@@ -146,6 +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" });
|
||||
};
|
||||
|
||||
@@ -205,6 +241,10 @@ export const certificateAuthorityServiceFactory = ({
|
||||
return castDbEntryToAcmeCertificateAuthority(certificateAuthority);
|
||||
}
|
||||
|
||||
if (type === CaType.AZURE_AD_CS) {
|
||||
return castDbEntryToAzureAdCsCertificateAuthority(certificateAuthority);
|
||||
}
|
||||
|
||||
throw new BadRequestError({ message: "Invalid certificate authority type" });
|
||||
};
|
||||
|
||||
@@ -249,6 +289,10 @@ export const certificateAuthorityServiceFactory = ({
|
||||
return acmeFns.listCertificateAuthorities({ projectId });
|
||||
}
|
||||
|
||||
if (type === CaType.AZURE_AD_CS) {
|
||||
return azureAdCsFns.listCertificateAuthorities({ projectId });
|
||||
}
|
||||
|
||||
throw new BadRequestError({ message: "Invalid certificate authority type" });
|
||||
};
|
||||
|
||||
@@ -323,6 +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" });
|
||||
};
|
||||
|
||||
@@ -384,14 +439,98 @@ export const certificateAuthorityServiceFactory = ({
|
||||
return castDbEntryToAcmeCertificateAuthority(certificateAuthority);
|
||||
}
|
||||
|
||||
if (type === CaType.AZURE_AD_CS) {
|
||||
return castDbEntryToAzureAdCsCertificateAuthority(certificateAuthority);
|
||||
}
|
||||
|
||||
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 {
|
||||
createCertificateAuthority,
|
||||
findCertificateAuthorityByNameAndProjectId,
|
||||
listCertificateAuthoritiesByProjectId,
|
||||
updateCertificateAuthority,
|
||||
deleteCertificateAuthority
|
||||
deleteCertificateAuthority,
|
||||
orderSubscriberCertificate,
|
||||
getAzureAdcsTemplates
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,13 +1,23 @@
|
||||
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 {
|
||||
TInternalCertificateAuthority,
|
||||
TInternalCertificateAuthorityInput
|
||||
} from "./internal/internal-certificate-authority-types";
|
||||
|
||||
export type TCertificateAuthority = TInternalCertificateAuthority | TAcmeCertificateAuthority;
|
||||
export type TCertificateAuthority =
|
||||
| TInternalCertificateAuthority
|
||||
| TAcmeCertificateAuthority
|
||||
| TAzureAdCsCertificateAuthority;
|
||||
|
||||
export type TCertificateAuthorityInput = TInternalCertificateAuthorityInput | TAcmeCertificateAuthorityInput;
|
||||
export type TCertificateAuthorityInput =
|
||||
| TInternalCertificateAuthorityInput
|
||||
| TAcmeCertificateAuthorityInput
|
||||
| TAzureAdCsCertificateAuthorityInput;
|
||||
|
||||
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 { BadRequestError } from "@app/lib/errors";
|
||||
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 { TCertificateDALFactory } from "@app/services/certificate/certificate-dal";
|
||||
import { TCertificateSecretDALFactory } from "@app/services/certificate/certificate-secret-dal";
|
||||
@@ -19,9 +21,12 @@ import {
|
||||
TAltNameMapping
|
||||
} from "@app/services/certificate/certificate-types";
|
||||
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
||||
import { TPkiSubscriberDALFactory } from "@app/services/pki-subscriber/pki-subscriber-dal";
|
||||
import { TPkiSubscriberProperties } from "@app/services/pki-subscriber/pki-subscriber-types";
|
||||
import { TProjectDALFactory } from "@app/services/project/project-dal";
|
||||
import { getProjectKmsCertificateKeyId } from "@app/services/project/project-fns";
|
||||
|
||||
import { AzureAdCsCertificateAuthorityFns } from "../azure-ad-cs/azure-ad-cs-certificate-authority-fns";
|
||||
import { TCertificateAuthorityCertDALFactory } from "../certificate-authority-cert-dal";
|
||||
import { TCertificateAuthorityDALFactory } from "../certificate-authority-dal";
|
||||
import { CaStatus } from "../certificate-authority-enums";
|
||||
@@ -33,18 +38,102 @@ import {
|
||||
} from "../certificate-authority-fns";
|
||||
import { TCertificateAuthoritySecretDALFactory } from "../certificate-authority-secret-dal";
|
||||
import { validateAndMapAltNameType } from "../certificate-authority-validators";
|
||||
import { TExternalCertificateAuthorityDALFactory } from "../external-certificate-authority-dal";
|
||||
import { TIssueCertWithTemplateDTO } from "./internal-certificate-authority-types";
|
||||
|
||||
type TInternalCertificateAuthorityFnsDeps = {
|
||||
certificateAuthorityDAL: Pick<TCertificateAuthorityDALFactory, "findByIdWithAssociatedCa" | "findById">;
|
||||
certificateAuthorityDAL: Pick<
|
||||
TCertificateAuthorityDALFactory,
|
||||
"findByIdWithAssociatedCa" | "findById" | "create" | "transaction" | "updateById" | "findWithAssociatedCa"
|
||||
>;
|
||||
certificateAuthorityCertDAL: Pick<TCertificateAuthorityCertDALFactory, "findById">;
|
||||
certificateAuthoritySecretDAL: Pick<TCertificateAuthoritySecretDALFactory, "findOne">;
|
||||
certificateAuthorityCrlDAL: Pick<TCertificateAuthorityCrlDALFactory, "findOne">;
|
||||
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">;
|
||||
certificateBodyDAL: Pick<TCertificateBodyDALFactory, "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 = ({
|
||||
@@ -56,7 +145,11 @@ export const InternalCertificateAuthorityFns = ({
|
||||
certificateAuthorityCrlDAL,
|
||||
certificateDAL,
|
||||
certificateBodyDAL,
|
||||
certificateSecretDAL
|
||||
certificateSecretDAL,
|
||||
appConnectionDAL,
|
||||
appConnectionService,
|
||||
externalCertificateAuthorityDAL,
|
||||
pkiSubscriberDAL
|
||||
}: TInternalCertificateAuthorityFnsDeps) => {
|
||||
const issueCertificate = async (
|
||||
subscriber: TPkiSubscribers,
|
||||
@@ -102,7 +195,7 @@ export const InternalCertificateAuthorityFns = ({
|
||||
const leafKeys = await crypto.nativeCrypto.subtle.generateKey(alg, true, ["sign", "verify"]);
|
||||
|
||||
const csrObj = await x509.Pkcs10CertificateRequestGenerator.create({
|
||||
name: `CN=${subscriber.commonName}`,
|
||||
name: buildSubjectDN(subscriber.commonName, subscriber.properties as TPkiSubscriberProperties | undefined),
|
||||
keys: leafKeys,
|
||||
signingAlgorithm: alg,
|
||||
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 {
|
||||
issueCertificate,
|
||||
issueCertificateWithTemplate
|
||||
issueCertificateWithTemplate,
|
||||
issueCertificateWithAzureAdCs
|
||||
};
|
||||
};
|
||||
|
||||
@@ -10,10 +10,13 @@ import {
|
||||
} from "@app/ee/services/permission/project-permission";
|
||||
import { crypto } from "@app/lib/crypto/cryptography";
|
||||
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 { TCertificateDALFactory } from "@app/services/certificate/certificate-dal";
|
||||
import { TCertificateAuthorityCertDALFactory } from "@app/services/certificate-authority/certificate-authority-cert-dal";
|
||||
import { TCertificateAuthorityDALFactory } from "@app/services/certificate-authority/certificate-authority-dal";
|
||||
import { CaCapability, CaType } from "@app/services/certificate-authority/certificate-authority-enums";
|
||||
import { caSupportsCapability } from "@app/services/certificate-authority/certificate-authority-maps";
|
||||
import { TCertificateAuthoritySecretDALFactory } from "@app/services/certificate-authority/certificate-authority-secret-dal";
|
||||
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
||||
import { TPkiCollectionDALFactory } from "@app/services/pki-collection/pki-collection-dal";
|
||||
@@ -49,6 +52,7 @@ type TCertificateServiceFactoryDep = {
|
||||
pkiCollectionDAL: Pick<TPkiCollectionDALFactory, "findById">;
|
||||
pkiCollectionItemDAL: Pick<TPkiCollectionItemDALFactory, "create">;
|
||||
projectDAL: Pick<TProjectDALFactory, "findProjectBySlug" | "findOne" | "updateById" | "findById" | "transaction">;
|
||||
appConnectionDAL: Pick<TAppConnectionDALFactory, "findById">;
|
||||
kmsService: Pick<TKmsServiceFactory, "generateKmsKey" | "encryptWithKmsKey" | "decryptWithKmsKey">;
|
||||
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
|
||||
};
|
||||
@@ -66,6 +70,7 @@ export const certificateServiceFactory = ({
|
||||
pkiCollectionDAL,
|
||||
pkiCollectionItemDAL,
|
||||
projectDAL,
|
||||
appConnectionDAL,
|
||||
kmsService,
|
||||
permissionService
|
||||
}: TCertificateServiceFactoryDep) => {
|
||||
@@ -184,9 +189,11 @@ export const certificateServiceFactory = ({
|
||||
|
||||
const ca = await certificateAuthorityDAL.findByIdWithAssociatedCa(cert.caId);
|
||||
|
||||
if (ca.externalCa?.id) {
|
||||
// Check if the CA type supports revocation
|
||||
const caType = (ca.externalCa?.type as CaType) ?? CaType.INTERNAL;
|
||||
if (!caSupportsCapability(caType, CaCapability.REVOKE_CERTIFICATES)) {
|
||||
throw new BadRequestError({
|
||||
message: "Cannot revoke external certificates"
|
||||
message: "Certificate revocation is not supported by this certificate authority type"
|
||||
});
|
||||
}
|
||||
|
||||
@@ -218,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)
|
||||
// Only rebuild CRL for internal CAs - external CAs manage their own CRLs
|
||||
if (!ca.externalCa?.id) {
|
||||
await rebuildCaCrl({
|
||||
caId: ca.id,
|
||||
certificateAuthorityDAL,
|
||||
@@ -228,8 +240,22 @@ export const certificateServiceFactory = ({
|
||||
certificateDAL,
|
||||
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,
|
||||
enableAutoRenewal: true,
|
||||
autoRenewalPeriodInDays: true,
|
||||
lastAutoRenewAt: true
|
||||
lastAutoRenewAt: true,
|
||||
properties: true
|
||||
}).extend({
|
||||
supportsImmediateCertIssuance: z.boolean().optional()
|
||||
});
|
||||
|
||||
@@ -109,6 +109,7 @@ export const pkiSubscriberServiceFactory = ({
|
||||
extendedKeyUsages,
|
||||
enableAutoRenewal,
|
||||
autoRenewalPeriodInDays,
|
||||
properties,
|
||||
projectId,
|
||||
actorId,
|
||||
actorAuthMethod,
|
||||
@@ -157,7 +158,8 @@ export const pkiSubscriberServiceFactory = ({
|
||||
keyUsages,
|
||||
extendedKeyUsages,
|
||||
enableAutoRenewal,
|
||||
autoRenewalPeriodInDays
|
||||
autoRenewalPeriodInDays,
|
||||
properties
|
||||
});
|
||||
|
||||
return newSubscriber;
|
||||
@@ -221,6 +223,7 @@ export const pkiSubscriberServiceFactory = ({
|
||||
extendedKeyUsages,
|
||||
enableAutoRenewal,
|
||||
autoRenewalPeriodInDays,
|
||||
properties,
|
||||
actorId,
|
||||
actorAuthMethod,
|
||||
actor,
|
||||
@@ -275,7 +278,8 @@ export const pkiSubscriberServiceFactory = ({
|
||||
keyUsages,
|
||||
extendedKeyUsages,
|
||||
enableAutoRenewal,
|
||||
autoRenewalPeriodInDays
|
||||
autoRenewalPeriodInDays,
|
||||
properties
|
||||
});
|
||||
|
||||
return updatedSubscriber;
|
||||
@@ -360,7 +364,7 @@ export const pkiSubscriberServiceFactory = ({
|
||||
throw new BadRequestError({ message: "CA is disabled" });
|
||||
}
|
||||
|
||||
if (ca.externalCa?.id && ca.externalCa.type === CaType.ACME) {
|
||||
if (ca.externalCa?.id && (ca.externalCa.type === CaType.ACME || ca.externalCa.type === CaType.AZURE_AD_CS)) {
|
||||
await certificateAuthorityQueue.orderCertificateForSubscriber({
|
||||
subscriberId: subscriber.id,
|
||||
caType: ca.externalCa.type
|
||||
|
||||
@@ -18,6 +18,7 @@ export type TCreatePkiSubscriberDTO = {
|
||||
extendedKeyUsages: CertExtendedKeyUsage[];
|
||||
enableAutoRenewal?: boolean;
|
||||
autoRenewalPeriodInDays?: number;
|
||||
properties?: TPkiSubscriberProperties;
|
||||
} & TProjectPermission;
|
||||
|
||||
export type TGetPkiSubscriberDTO = {
|
||||
@@ -36,6 +37,7 @@ export type TUpdatePkiSubscriberDTO = {
|
||||
extendedKeyUsages?: CertExtendedKeyUsage[];
|
||||
enableAutoRenewal?: boolean;
|
||||
autoRenewalPeriodInDays?: number;
|
||||
properties?: TPkiSubscriberProperties;
|
||||
} & TProjectPermission;
|
||||
|
||||
export type TDeletePkiSubscriberDTO = {
|
||||
@@ -69,3 +71,13 @@ export enum SubscriberOperationStatus {
|
||||
SUCCESS = "success",
|
||||
FAILED = "failed"
|
||||
}
|
||||
|
||||
export type TPkiSubscriberProperties = {
|
||||
azureTemplateType?: string;
|
||||
organization?: string;
|
||||
organizationalUnit?: string;
|
||||
country?: string;
|
||||
state?: string;
|
||||
locality?: string;
|
||||
emailAddress?: string;
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
@@ -106,6 +106,7 @@
|
||||
"integrations/app-connections/auth0",
|
||||
"integrations/app-connections/aws",
|
||||
"integrations/app-connections/azure-app-configuration",
|
||||
"integrations/app-connections/azure-adcs",
|
||||
"integrations/app-connections/azure-client-secrets",
|
||||
"integrations/app-connections/azure-devops",
|
||||
"integrations/app-connections/azure-key-vault",
|
||||
@@ -690,6 +691,7 @@
|
||||
"documentation/platform/pki/subscribers",
|
||||
"documentation/platform/pki/certificates",
|
||||
"documentation/platform/pki/acme-ca",
|
||||
"documentation/platform/pki/azure-adcs",
|
||||
"documentation/platform/pki/est",
|
||||
"documentation/platform/pki/alerting",
|
||||
{
|
||||
@@ -1396,6 +1398,18 @@
|
||||
"api-reference/endpoints/app-connections/aws/delete"
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "Azure ADCS",
|
||||
"pages": [
|
||||
"api-reference/endpoints/app-connections/azure-adcs/list",
|
||||
"api-reference/endpoints/app-connections/azure-adcs/available",
|
||||
"api-reference/endpoints/app-connections/azure-adcs/get-by-id",
|
||||
"api-reference/endpoints/app-connections/azure-adcs/get-by-name",
|
||||
"api-reference/endpoints/app-connections/azure-adcs/create",
|
||||
"api-reference/endpoints/app-connections/azure-adcs/update",
|
||||
"api-reference/endpoints/app-connections/azure-adcs/delete"
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "Azure App Configuration",
|
||||
"pages": [
|
||||
|
||||
159
docs/documentation/platform/pki/azure-adcs.mdx
Normal file
@@ -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.
|
||||
|
||||

|
||||
</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
|
||||
|
||||

|
||||
</Step>
|
||||
|
||||
<Step title="Certificate Authority Created">
|
||||
Once created, your Azure ADCS Certificate Authority will appear in the list and be ready for use.
|
||||
|
||||

|
||||
</Step>
|
||||
|
||||
<Step title="Navigate to Subscribers">
|
||||
Go to **Subscribers** to access the subscribers page.
|
||||
|
||||

|
||||
</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)
|
||||
|
||||

|
||||
</Step>
|
||||
|
||||
<Step title="Subscriber Created">
|
||||
Your subscriber is now created and ready to issue certificates.
|
||||
|
||||

|
||||
</Step>
|
||||
|
||||
<Step title="Issue New Certificate">
|
||||
Click into your subscriber and click **Order Certificate** to generate a new certificate using your ADCS template.
|
||||
|
||||

|
||||
</Step>
|
||||
|
||||
<Step title="Certificate Created">
|
||||
Your certificate has been successfully issued by the ADCS server and is ready for use.
|
||||
|
||||

|
||||
</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.
|
||||
|
||||

|
||||
</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
|
||||
|
After Width: | Height: | Size: 600 KiB |
|
After Width: | Height: | Size: 421 KiB |
|
After Width: | Height: | Size: 477 KiB |
|
After Width: | Height: | Size: 796 KiB |
|
After Width: | Height: | Size: 765 KiB |
|
After Width: | Height: | Size: 803 KiB |
|
After Width: | Height: | Size: 501 KiB |
|
After Width: | Height: | Size: 801 KiB |
|
After Width: | Height: | Size: 778 KiB |
|
After Width: | Height: | Size: 740 KiB |
|
After Width: | Height: | Size: 497 KiB |
|
After Width: | Height: | Size: 735 KiB |
42
docs/integrations/app-connections/azure-adcs.mdx
Normal file
@@ -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.
|
||||

|
||||
</Step>
|
||||
<Step title="Add Connection">
|
||||
Select the **Azure ADCS Connection** option from the connection options modal.
|
||||

|
||||
</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 `username@domain.com`)
|
||||
- **Password**: Password for the domain administrator account
|
||||
|
||||
And click **Connect to ADCS** to establish the connection.
|
||||

|
||||
</Step>
|
||||
<Step title="Connection Created">
|
||||
Your **Azure ADCS Connection** is now available for use in your Infisical
|
||||
projects. 
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
@@ -13,6 +13,7 @@ import { AppConnection } from "@app/hooks/api/appConnections/enums";
|
||||
import {
|
||||
Auth0ConnectionMethod,
|
||||
AwsConnectionMethod,
|
||||
AzureADCSConnectionMethod,
|
||||
AzureAppConfigurationConnectionMethod,
|
||||
AzureClientSecretsConnectionMethod,
|
||||
AzureDevOpsConnectionMethod,
|
||||
@@ -76,6 +77,7 @@ export const APP_CONNECTION_MAP: Record<
|
||||
image: "Microsoft Azure.png"
|
||||
},
|
||||
[AppConnection.AzureDevOps]: { name: "Azure DevOps", image: "Microsoft Azure.png" },
|
||||
[AppConnection.AzureADCS]: { name: "Azure ADCS", image: "Microsoft Azure.png" },
|
||||
[AppConnection.Databricks]: { name: "Databricks", image: "Databricks.png" },
|
||||
[AppConnection.Humanitec]: { name: "Humanitec", image: "Humanitec.png" },
|
||||
[AppConnection.TerraformCloud]: { name: "Terraform Cloud", image: "Terraform Cloud.png" },
|
||||
@@ -151,6 +153,7 @@ export const getAppConnectionMethodDetails = (method: TAppConnection["method"])
|
||||
case MsSqlConnectionMethod.UsernameAndPassword:
|
||||
case MySqlConnectionMethod.UsernameAndPassword:
|
||||
case OracleDBConnectionMethod.UsernameAndPassword:
|
||||
case AzureADCSConnectionMethod.UsernamePassword:
|
||||
return { name: "Username & Password", icon: faLock };
|
||||
case HCVaultConnectionMethod.AccessToken:
|
||||
case TeamCityConnectionMethod.AccessToken:
|
||||
|
||||
@@ -7,6 +7,7 @@ export enum AppConnection {
|
||||
AzureAppConfiguration = "azure-app-configuration",
|
||||
AzureClientSecrets = "azure-client-secrets",
|
||||
AzureDevOps = "azure-devops",
|
||||
AzureADCS = "azure-adcs",
|
||||
Databricks = "databricks",
|
||||
Humanitec = "humanitec",
|
||||
TerraformCloud = "terraform-cloud",
|
||||
|
||||
@@ -54,13 +54,13 @@ export const useAppConnectionOptions = (
|
||||
export const useGetAppConnectionOption = <T extends AppConnection>(app: T) => {
|
||||
const { data: options = [], isPending } = useAppConnectionOptions();
|
||||
|
||||
return useMemo(
|
||||
() => ({
|
||||
option: (options.find((opt) => opt.app === app) as TAppConnectionOptionMap[T]) ?? {},
|
||||
return useMemo(() => {
|
||||
const foundOption = options.find((opt) => opt.app === app);
|
||||
return {
|
||||
option: (foundOption as TAppConnectionOptionMap[T]) ?? {},
|
||||
isLoading: isPending
|
||||
}),
|
||||
[options, app, isPending]
|
||||
);
|
||||
};
|
||||
}, [options, app, isPending]);
|
||||
};
|
||||
|
||||
export const useListAppConnections = (
|
||||
|
||||
@@ -32,7 +32,7 @@ export type TAzureKeyVaultConnectionOption = TAppConnectionOptionBase & {
|
||||
};
|
||||
|
||||
export type TAzureAppConfigurationConnectionOption = TAppConnectionOptionBase & {
|
||||
app: AppConnection.AzureKeyVault;
|
||||
app: AppConnection.AzureAppConfiguration;
|
||||
oauthClientId?: string;
|
||||
};
|
||||
|
||||
@@ -164,9 +164,14 @@ export type TOktaConnectionOption = TAppConnectionOptionBase & {
|
||||
app: AppConnection.Okta;
|
||||
};
|
||||
|
||||
export type TAzureAdCsConnectionOption = TAppConnectionOptionBase & {
|
||||
app: AppConnection.AzureADCS;
|
||||
};
|
||||
|
||||
export type TAppConnectionOption =
|
||||
| TAwsConnectionOption
|
||||
| TGitHubConnectionOption
|
||||
| TGitHubRadarConnectionOption
|
||||
| TGcpConnectionOption
|
||||
| TAzureAppConfigurationConnectionOption
|
||||
| TAzureKeyVaultConnectionOption
|
||||
@@ -184,6 +189,7 @@ export type TAppConnectionOption =
|
||||
| TWindmillConnectionOption
|
||||
| TAuth0ConnectionOption
|
||||
| THCVaultConnectionOption
|
||||
| TLdapConnectionOption
|
||||
| TTeamCityConnectionOption
|
||||
| TOCIConnectionOption
|
||||
| TOnePassConnectionOption
|
||||
@@ -196,6 +202,7 @@ export type TAppConnectionOption =
|
||||
| TZabbixConnectionOption
|
||||
| TRailwayConnectionOption
|
||||
| TChecklyConnectionOption
|
||||
| TSupabaseConnectionOption
|
||||
| TDigitalOceanConnectionOption
|
||||
| TNetlifyConnectionOption
|
||||
| TOktaConnectionOption;
|
||||
@@ -238,4 +245,5 @@ export type TAppConnectionOptionMap = {
|
||||
[AppConnection.DigitalOcean]: TDigitalOceanConnectionOption;
|
||||
[AppConnection.Netlify]: TNetlifyConnectionOption;
|
||||
[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 { TAuth0Connection } from "./auth0-connection";
|
||||
import { TAwsConnection } from "./aws-connection";
|
||||
import { TAzureADCSConnection } from "./azure-adcs-connection";
|
||||
import { TAzureAppConfigurationConnection } from "./azure-app-configuration-connection";
|
||||
import { TAzureClientSecretsConnection } from "./azure-client-secrets-connection";
|
||||
import { TAzureDevOpsConnection } from "./azure-devops-connection";
|
||||
@@ -41,6 +42,7 @@ import { TZabbixConnection } from "./zabbix-connection";
|
||||
export * from "./1password-connection";
|
||||
export * from "./auth0-connection";
|
||||
export * from "./aws-connection";
|
||||
export * from "./azure-adcs-connection";
|
||||
export * from "./azure-app-configuration-connection";
|
||||
export * from "./azure-client-secrets-connection";
|
||||
export * from "./azure-devops-connection";
|
||||
@@ -83,6 +85,7 @@ export type TAppConnection =
|
||||
| TAzureAppConfigurationConnection
|
||||
| TAzureClientSecretsConnection
|
||||
| TAzureDevOpsConnection
|
||||
| TAzureADCSConnection
|
||||
| TDatabricksConnection
|
||||
| THumanitecConnection
|
||||
| TTerraformCloudConnection
|
||||
@@ -156,6 +159,7 @@ export type TAppConnectionMap = {
|
||||
[AppConnection.AzureAppConfiguration]: TAzureAppConfigurationConnection;
|
||||
[AppConnection.AzureClientSecrets]: TAzureClientSecretsConnection;
|
||||
[AppConnection.AzureDevOps]: TAzureDevOpsConnection;
|
||||
[AppConnection.AzureADCS]: TAzureADCSConnection;
|
||||
[AppConnection.Databricks]: TDatabricksConnection;
|
||||
[AppConnection.Humanitec]: THumanitecConnection;
|
||||
[AppConnection.TerraformCloud]: TTerraformCloudConnection;
|
||||
|
||||
@@ -1,7 +1,15 @@
|
||||
import { AppConnection } from "../appConnections/enums";
|
||||
import { SshCaStatus } from "../sshCa";
|
||||
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 } = {
|
||||
[InternalCaType.ROOT]: "Root",
|
||||
@@ -24,6 +32,45 @@ export const ACME_DNS_PROVIDER_APP_CONNECTION_MAP: Record<AcmeDnsProvider, AppCo
|
||||
[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) => {
|
||||
switch (status) {
|
||||
case CaStatus.ACTIVE:
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
export enum CaType {
|
||||
INTERNAL = "internal",
|
||||
ACME = "acme"
|
||||
ACME = "acme",
|
||||
AZURE_AD_CS = "azure-ad-cs"
|
||||
}
|
||||
|
||||
export enum InternalCaType {
|
||||
@@ -22,3 +23,22 @@ export enum AcmeDnsProvider {
|
||||
ROUTE53 = "route53",
|
||||
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"
|
||||
}
|
||||
|
||||
@@ -1,4 +1,12 @@
|
||||
export { AcmeDnsProvider, CaRenewalType, CaStatus, CaType, InternalCaType } from "./enums";
|
||||
export {
|
||||
AcmeDnsProvider,
|
||||
AzureAdCsAuthMethod,
|
||||
AzureAdCsTemplateType,
|
||||
CaRenewalType,
|
||||
CaStatus,
|
||||
CaType,
|
||||
InternalCaType
|
||||
} from "./enums";
|
||||
export {
|
||||
useCreateCa,
|
||||
useCreateCertificate,
|
||||
@@ -9,6 +17,7 @@ export {
|
||||
useUpdateCa
|
||||
} from "./mutations";
|
||||
export {
|
||||
useGetAzureAdcsTemplates,
|
||||
useGetCa,
|
||||
useGetCaById,
|
||||
useGetCaCert,
|
||||
@@ -17,5 +26,6 @@ export {
|
||||
useGetCaCrls,
|
||||
useGetCaCsr,
|
||||
useListCasByProjectId,
|
||||
useListCasByTypeAndProjectId
|
||||
useListCasByTypeAndProjectId,
|
||||
useListExternalCasByProjectId
|
||||
} from "./queries";
|
||||
|
||||
@@ -39,6 +39,10 @@ export const useUpdateCa = () => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: caKeys.getCaByNameAndProjectId(caName, projectId)
|
||||
});
|
||||
// Invalidate external CAs list
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: [`external-cas-${projectId}`]
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
@@ -57,6 +61,10 @@ export const useCreateCa = () => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: caKeys.listCasByTypeAndProjectId(type, projectId)
|
||||
});
|
||||
// Invalidate external CAs list
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: [`external-cas-${projectId}`]
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
@@ -79,6 +87,10 @@ export const useDeleteCa = () => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: caKeys.listCasByTypeAndProjectId(type, projectId)
|
||||
});
|
||||
// Invalidate external CAs list
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: [`external-cas-${projectId}`]
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
@@ -17,7 +17,11 @@ export const caKeys = {
|
||||
getCaCsr: (caId: string) => [{ caId }, "ca-csr"],
|
||||
getCaCrl: (caId: string) => [{ caId }, "ca-crl"],
|
||||
getCaCertTemplates: (caId: string) => [{ caId }, "ca-cert-templates"],
|
||||
getCaEstConfig: (caId: string) => [{ caId }, "ca-est-config"]
|
||||
getCaEstConfig: (caId: string) => [{ caId }, "ca-est-config"],
|
||||
getAzureAdcsTemplates: (caId: string, projectId: string) => [
|
||||
{ caId, projectId },
|
||||
"azure-adcs-templates"
|
||||
]
|
||||
};
|
||||
|
||||
export const useGetCa = ({
|
||||
@@ -67,6 +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) => {
|
||||
return useQuery({
|
||||
queryKey: caKeys.getCaById(caId),
|
||||
@@ -156,3 +188,22 @@ export const useGetCaCertTemplates = (caId: string) => {
|
||||
enabled: Boolean(caId)
|
||||
});
|
||||
};
|
||||
|
||||
export const useGetAzureAdcsTemplates = ({
|
||||
caId,
|
||||
projectId
|
||||
}: {
|
||||
caId: string;
|
||||
projectId: string;
|
||||
}) => {
|
||||
return useQuery({
|
||||
queryKey: caKeys.getAzureAdcsTemplates(caId, projectId),
|
||||
queryFn: async () => {
|
||||
const { data } = await apiRequest.get<{
|
||||
templates: { id: string; name: string; description?: string }[];
|
||||
}>(`/api/v1/pki/ca/azure-ad-cs/${caId}/templates?projectId=${projectId}`);
|
||||
return data;
|
||||
},
|
||||
enabled: Boolean(caId && projectId)
|
||||
});
|
||||
};
|
||||
|
||||
@@ -1,5 +1,13 @@
|
||||
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 = {
|
||||
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 = {
|
||||
id: string;
|
||||
projectId: string;
|
||||
@@ -48,6 +70,7 @@ export type TInternalCertificateAuthority = {
|
||||
|
||||
export type TUnifiedCertificateAuthority =
|
||||
| TAcmeCertificateAuthority
|
||||
| TAzureAdCsCertificateAuthority
|
||||
| TInternalCertificateAuthority;
|
||||
|
||||
export type TCreateCertificateAuthorityDTO = Omit<TUnifiedCertificateAuthority, "id">;
|
||||
|
||||
@@ -10,6 +10,16 @@ export enum SubscriberOperationStatus {
|
||||
FAILED = "failed"
|
||||
}
|
||||
|
||||
export type TPkiSubscriberProperties = {
|
||||
azureTemplateType?: string;
|
||||
organization?: string;
|
||||
organizationalUnit?: string;
|
||||
country?: string;
|
||||
state?: string;
|
||||
locality?: string;
|
||||
emailAddress?: string;
|
||||
};
|
||||
|
||||
export type TPkiSubscriber = {
|
||||
id: string;
|
||||
projectId: string;
|
||||
@@ -27,6 +37,7 @@ export type TPkiSubscriber = {
|
||||
lastOperationStatus?: SubscriberOperationStatus;
|
||||
lastOperationMessage?: string;
|
||||
lastOperationAt?: string;
|
||||
properties?: TPkiSubscriberProperties;
|
||||
};
|
||||
|
||||
export type TCreatePkiSubscriberDTO = {
|
||||
@@ -40,6 +51,7 @@ export type TCreatePkiSubscriberDTO = {
|
||||
extendedKeyUsages: CertExtendedKeyUsage[];
|
||||
enableAutoRenewal?: boolean;
|
||||
autoRenewalPeriodInDays?: number;
|
||||
properties?: TPkiSubscriberProperties;
|
||||
};
|
||||
|
||||
export type TUpdatePkiSubscriberDTO = {
|
||||
@@ -55,6 +67,7 @@ export type TUpdatePkiSubscriberDTO = {
|
||||
extendedKeyUsages?: CertExtendedKeyUsage[];
|
||||
enableAutoRenewal?: boolean;
|
||||
autoRenewalPeriodInDays?: number;
|
||||
properties?: TPkiSubscriberProperties;
|
||||
};
|
||||
|
||||
export type TDeletePkiSubscriberDTO = {
|
||||
|
||||
@@ -42,29 +42,45 @@ import {
|
||||
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||
import { slugSchema } from "@app/lib/schemas";
|
||||
|
||||
const schema = z
|
||||
.object({
|
||||
const baseSchema = z.object({
|
||||
type: z.nativeEnum(CaType),
|
||||
name: slugSchema({
|
||||
field: "Name"
|
||||
}),
|
||||
enableDirectIssuance: z.boolean(),
|
||||
status: z.nativeEnum(CaStatus),
|
||||
configuration: z.object({
|
||||
status: z.nativeEnum(CaStatus)
|
||||
});
|
||||
|
||||
const acmeConfigurationSchema = z.object({
|
||||
dnsAppConnection: z.object({
|
||||
id: z.string(),
|
||||
name: z.string()
|
||||
}),
|
||||
// currently specific to Route53 & Cloudflare but can be extended to others by differentiating via the provider property
|
||||
dnsProviderConfig: z.object({
|
||||
provider: z.nativeEnum(AcmeDnsProvider),
|
||||
hostedZoneId: z.string()
|
||||
}),
|
||||
directoryUrl: z.string(),
|
||||
accountEmail: z.string()
|
||||
});
|
||||
|
||||
const 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>;
|
||||
|
||||
@@ -73,12 +89,15 @@ type Props = {
|
||||
handlePopUpToggle: (popUpName: keyof UsePopUpState<["ca"]>, state?: boolean) => void;
|
||||
};
|
||||
|
||||
const caTypes = [{ label: "ACME", value: CaType.ACME }];
|
||||
const caTypes = [
|
||||
{ label: "ACME", value: CaType.ACME },
|
||||
{ label: "Azure AD Certificate Service", value: CaType.AZURE_AD_CS }
|
||||
];
|
||||
|
||||
export const ExternalCaModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
|
||||
const { data: ca } = useGetCa({
|
||||
const { data: ca, isLoading: isCaLoading } = useGetCa({
|
||||
caName: (popUp?.ca?.data as { name: string })?.name || "",
|
||||
projectId: currentWorkspace?.id || "",
|
||||
type: (popUp?.ca?.data as { type: CaType })?.type || ""
|
||||
@@ -94,8 +113,34 @@ export const ExternalCaModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
formState: { isSubmitting },
|
||||
watch
|
||||
} = useForm<FormData>({
|
||||
resolver: zodResolver(schema),
|
||||
defaultValues: {
|
||||
resolver: zodResolver(schema)
|
||||
});
|
||||
|
||||
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,
|
||||
name: "",
|
||||
status: CaStatus.ACTIVE,
|
||||
@@ -112,11 +157,10 @@ export const ExternalCaModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
directoryUrl: "",
|
||||
accountEmail: ""
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const caType = watch("type");
|
||||
const dnsProvider = watch("configuration.dnsProviderConfig.provider");
|
||||
}
|
||||
}
|
||||
}, [popUp?.ca?.isOpen, popUp?.ca?.data, reset, ca]);
|
||||
|
||||
const { data: availableRoute53Connections, isPending: isRoute53Pending } =
|
||||
useListAvailableAppConnections(AppConnection.AWS, {
|
||||
@@ -128,25 +172,44 @@ export const ExternalCaModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
enabled: caType === CaType.ACME
|
||||
});
|
||||
|
||||
const availableConnections: TAvailableAppConnection[] = useMemo(
|
||||
() => [...(availableRoute53Connections || []), ...(availableCloudflareConnections || [])],
|
||||
[availableRoute53Connections, availableCloudflareConnections]
|
||||
);
|
||||
const { data: availableAzureConnections, isPending: isAzurePending } =
|
||||
useListAvailableAppConnections(AppConnection.AzureADCS, {
|
||||
enabled: caType === CaType.AZURE_AD_CS
|
||||
});
|
||||
|
||||
const isPending = isRoute53Pending || isCloudflarePending;
|
||||
const availableConnections: TAvailableAppConnection[] = useMemo(() => {
|
||||
if (caType === CaType.ACME) {
|
||||
return [...(availableRoute53Connections || []), ...(availableCloudflareConnections || [])];
|
||||
}
|
||||
if (caType === CaType.AZURE_AD_CS) {
|
||||
return availableAzureConnections || [];
|
||||
}
|
||||
return [];
|
||||
}, [
|
||||
caType,
|
||||
availableRoute53Connections,
|
||||
availableCloudflareConnections,
|
||||
availableAzureConnections
|
||||
]);
|
||||
|
||||
const dnsAppConnection = watch("configuration.dnsAppConnection");
|
||||
const isPending = isRoute53Pending || isCloudflarePending || isAzurePending;
|
||||
|
||||
const dnsAppConnection =
|
||||
caType === CaType.ACME && configuration && "dnsAppConnection" in configuration
|
||||
? configuration.dnsAppConnection
|
||||
: { id: "", name: "" };
|
||||
|
||||
const { data: cloudflareZones = [], isPending: isZonesPending } =
|
||||
useCloudflareConnectionListZones(dnsAppConnection.id, {
|
||||
enabled: dnsProvider === AcmeDnsProvider.Cloudflare && !!dnsAppConnection.id
|
||||
});
|
||||
|
||||
// Populate form with CA data when editing
|
||||
useEffect(() => {
|
||||
if (ca) {
|
||||
if (ca.type !== CaType.INTERNAL && availableConnections?.length) {
|
||||
if (ca && !isCaLoading) {
|
||||
if (ca.type === CaType.ACME && availableConnections?.length) {
|
||||
const selectedConnection = availableConnections?.find(
|
||||
(connection) => connection.id === ca?.configuration.dnsAppConnectionId
|
||||
(connection) => connection.id === ca.configuration.dnsAppConnectionId
|
||||
);
|
||||
|
||||
reset({
|
||||
@@ -167,32 +230,61 @@ export const ExternalCaModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
accountEmail: ca.configuration.accountEmail
|
||||
}
|
||||
});
|
||||
} else if (ca.type === CaType.AZURE_AD_CS && availableConnections?.length) {
|
||||
const selectedConnection = availableConnections?.find(
|
||||
(connection) => connection.id === ca.configuration.azureAdcsConnectionId
|
||||
);
|
||||
|
||||
reset({
|
||||
type: ca.type,
|
||||
name: ca.name,
|
||||
status: ca.status,
|
||||
enableDirectIssuance: false,
|
||||
configuration: {
|
||||
azureAdcsConnection: {
|
||||
id: ca.configuration.azureAdcsConnectionId,
|
||||
name: selectedConnection?.name || ""
|
||||
}
|
||||
}
|
||||
}, [ca, availableConnections]);
|
||||
});
|
||||
}
|
||||
}
|
||||
}, [ca, availableConnections, reset, isCaLoading]);
|
||||
|
||||
const onFormSubmit = async ({
|
||||
type,
|
||||
name,
|
||||
enableDirectIssuance,
|
||||
status,
|
||||
configuration
|
||||
configuration: formConfiguration
|
||||
}: FormData) => {
|
||||
try {
|
||||
if (!currentWorkspace?.slug) return;
|
||||
|
||||
if (ca && type !== CaType.INTERNAL) {
|
||||
let configPayload: any;
|
||||
|
||||
if (type === CaType.ACME && "dnsAppConnection" in formConfiguration) {
|
||||
configPayload = {
|
||||
dnsProviderConfig: formConfiguration.dnsProviderConfig,
|
||||
directoryUrl: formConfiguration.directoryUrl,
|
||||
accountEmail: formConfiguration.accountEmail,
|
||||
dnsAppConnectionId: formConfiguration.dnsAppConnection.id
|
||||
};
|
||||
} else if (type === CaType.AZURE_AD_CS && "azureAdcsConnection" in formConfiguration) {
|
||||
configPayload = {
|
||||
azureAdcsConnectionId: formConfiguration.azureAdcsConnection.id
|
||||
};
|
||||
}
|
||||
|
||||
if (ca) {
|
||||
await updateMutateAsync({
|
||||
caName: ca.name,
|
||||
projectId: currentWorkspace.id,
|
||||
name,
|
||||
type,
|
||||
status,
|
||||
enableDirectIssuance,
|
||||
configuration: {
|
||||
...configuration,
|
||||
dnsAppConnectionId: configuration.dnsAppConnection.id
|
||||
}
|
||||
enableDirectIssuance: type === CaType.AZURE_AD_CS ? false : enableDirectIssuance,
|
||||
configuration: configPayload
|
||||
});
|
||||
} else {
|
||||
await createMutateAsync({
|
||||
@@ -200,11 +292,8 @@ export const ExternalCaModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
name,
|
||||
type,
|
||||
status,
|
||||
enableDirectIssuance,
|
||||
configuration: {
|
||||
...configuration,
|
||||
dnsAppConnectionId: configuration.dnsAppConnection.id
|
||||
}
|
||||
enableDirectIssuance: type === CaType.AZURE_AD_CS ? false : enableDirectIssuance,
|
||||
configuration: configPayload
|
||||
});
|
||||
}
|
||||
|
||||
@@ -232,7 +321,7 @@ export const ExternalCaModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
handlePopUpToggle("ca", isOpen);
|
||||
}}
|
||||
>
|
||||
<ModalContent title={`${ca ? "View" : "Create"} External CA`}>
|
||||
<ModalContent title={`${ca ? "Edit" : "Create"} External CA`}>
|
||||
<form onSubmit={handleSubmit(onFormSubmit)}>
|
||||
{ca && (
|
||||
<FormControl label="CA ID">
|
||||
@@ -307,7 +396,11 @@ export const ExternalCaModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
<Controller
|
||||
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||
<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)}
|
||||
errorText={error?.message}
|
||||
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
|
||||
control={control}
|
||||
name="enableDirectIssuance"
|
||||
@@ -423,6 +544,7 @@ export const ExternalCaModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<div className="flex items-center">
|
||||
<Button
|
||||
className="mr-4"
|
||||
|
||||
@@ -27,7 +27,7 @@ import {
|
||||
Tr
|
||||
} from "@app/components/v2";
|
||||
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 { UsePopUpState } from "@app/hooks/usePopUp";
|
||||
|
||||
@@ -45,7 +45,7 @@ type Props = {
|
||||
|
||||
export const ExternalCaTable = ({ handlePopUpOpen }: Props) => {
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
const { data, isPending } = useListCasByTypeAndProjectId(CaType.ACME, currentWorkspace.id);
|
||||
const { data, isPending } = useListExternalCasByProjectId(currentWorkspace.id);
|
||||
|
||||
return (
|
||||
<div>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useState } from "react";
|
||||
import { useMemo, useState } from "react";
|
||||
import {
|
||||
faBan,
|
||||
faCertificate,
|
||||
@@ -36,6 +36,9 @@ import {
|
||||
useWorkspace
|
||||
} from "@app/context";
|
||||
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 { UsePopUpState } from "@app/hooks/usePopUp";
|
||||
|
||||
@@ -66,6 +69,20 @@ export const CertificatesTable = ({ handlePopUpOpen }: Props) => {
|
||||
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 (
|
||||
<TableContainer>
|
||||
<Table>
|
||||
@@ -155,6 +172,18 @@ export const CertificatesTable = ({ handlePopUpOpen }: Props) => {
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
</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
|
||||
I={ProjectPermissionCertificateActions.Delete}
|
||||
a={ProjectPermissionSub.Certificates}
|
||||
@@ -162,7 +191,8 @@ export const CertificatesTable = ({ handlePopUpOpen }: Props) => {
|
||||
{(isAllowed) => (
|
||||
<DropdownMenuItem
|
||||
className={twMerge(
|
||||
!isAllowed && "pointer-events-none cursor-not-allowed opacity-50"
|
||||
!isAllowed &&
|
||||
"pointer-events-none cursor-not-allowed opacity-50"
|
||||
)}
|
||||
onClick={async () =>
|
||||
handlePopUpOpen("revokeCertificate", {
|
||||
@@ -176,6 +206,8 @@ export const CertificatesTable = ({ handlePopUpOpen }: Props) => {
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
);
|
||||
})()}
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionCertificateActions.Delete}
|
||||
a={ProjectPermissionSub.Certificates}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useState } from "react";
|
||||
import { useMemo, useState } from "react";
|
||||
import { subject } from "@casl/ability";
|
||||
import { faCertificate, faEllipsis, faTrash } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
@@ -31,6 +31,9 @@ import {
|
||||
useWorkspace
|
||||
} from "@app/context";
|
||||
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 { 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) => {
|
||||
if (status === CertStatus.REVOKED) {
|
||||
return <Badge variant="danger">Revoked</Badge>;
|
||||
@@ -121,6 +138,17 @@ export const PkiSubscriberCertificatesTable = ({ subscriberName, handlePopUpOpen
|
||||
: "-"}
|
||||
</Td>
|
||||
<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>
|
||||
<DropdownMenuTrigger asChild className="rounded-lg">
|
||||
<div className="hover:text-primary-400 data-[state=open]:text-primary-400">
|
||||
@@ -137,7 +165,8 @@ export const PkiSubscriberCertificatesTable = ({ subscriberName, handlePopUpOpen
|
||||
{(isAllowed) => (
|
||||
<DropdownMenuItem
|
||||
className={twMerge(
|
||||
!isAllowed && "pointer-events-none cursor-not-allowed opacity-50"
|
||||
!isAllowed &&
|
||||
"pointer-events-none cursor-not-allowed opacity-50"
|
||||
)}
|
||||
onClick={() =>
|
||||
handlePopUpOpen &&
|
||||
@@ -154,6 +183,8 @@ export const PkiSubscriberCertificatesTable = ({ subscriberName, handlePopUpOpen
|
||||
</ProjectPermissionCan>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
);
|
||||
})()}
|
||||
</Td>
|
||||
</Tr>
|
||||
);
|
||||
|
||||
@@ -26,6 +26,7 @@ import { useWorkspace } from "@app/context";
|
||||
import {
|
||||
CaType,
|
||||
useCreatePkiSubscriber,
|
||||
useGetAzureAdcsTemplates,
|
||||
useGetPkiSubscriber,
|
||||
useListCasByProjectId,
|
||||
useListWorkspacePkiSubscribers,
|
||||
@@ -77,7 +78,15 @@ const schema = z
|
||||
}),
|
||||
enableAutoRenewal: z.boolean().optional().default(false),
|
||||
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();
|
||||
|
||||
@@ -121,7 +130,14 @@ export const PkiSubscriberModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
extendedKeyUsages: {},
|
||||
enableAutoRenewal: false,
|
||||
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 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(() => {
|
||||
if (pkiSubscriber) {
|
||||
reset({
|
||||
name: pkiSubscriber.name,
|
||||
caId: pkiSubscriber.caId || "",
|
||||
commonName: pkiSubscriber.commonName,
|
||||
subjectAlternativeNames: pkiSubscriber.subjectAlternativeNames.join(", ") || "",
|
||||
ttl: pkiSubscriber.ttl || "",
|
||||
keyUsages: Object.fromEntries((pkiSubscriber.keyUsages || []).map((name) => [name, true])),
|
||||
extendedKeyUsages: Object.fromEntries(
|
||||
(pkiSubscriber.extendedKeyUsages || []).map((name) => [name, true])
|
||||
),
|
||||
enableAutoRenewal: pkiSubscriber.enableAutoRenewal || false,
|
||||
renewalBefore: pkiSubscriber.autoRenewalPeriodInDays || 7,
|
||||
renewalUnit: TimeUnit.DAY
|
||||
});
|
||||
} else {
|
||||
reset({
|
||||
name: "",
|
||||
caId: "",
|
||||
commonName: "",
|
||||
subjectAlternativeNames: "",
|
||||
ttl: "",
|
||||
keyUsages: {
|
||||
[CertKeyUsage.DIGITAL_SIGNATURE]: true,
|
||||
[CertKeyUsage.KEY_ENCIPHERMENT]: true
|
||||
},
|
||||
extendedKeyUsages: {},
|
||||
enableAutoRenewal: false,
|
||||
renewalBefore: 7,
|
||||
renewalUnit: TimeUnit.DAY
|
||||
});
|
||||
// Set all values directly and immediately
|
||||
setValue("name", pkiSubscriber.name);
|
||||
setValue("caId", pkiSubscriber.caId || "");
|
||||
setValue("commonName", pkiSubscriber.commonName);
|
||||
setValue("subjectAlternativeNames", pkiSubscriber.subjectAlternativeNames.join(", "));
|
||||
setValue("ttl", pkiSubscriber.ttl || "");
|
||||
setValue(
|
||||
"keyUsages",
|
||||
Object.fromEntries((pkiSubscriber.keyUsages || []).map((name) => [name, true]))
|
||||
);
|
||||
setValue(
|
||||
"extendedKeyUsages",
|
||||
Object.fromEntries((pkiSubscriber.extendedKeyUsages || []).map((name) => [name, true]))
|
||||
);
|
||||
setValue("enableAutoRenewal", pkiSubscriber.enableAutoRenewal || false);
|
||||
setValue("renewalBefore", pkiSubscriber.autoRenewalPeriodInDays || 7);
|
||||
setValue("renewalUnit", TimeUnit.DAY);
|
||||
|
||||
// Set Azure template immediately
|
||||
setValue("azureTemplateType", pkiSubscriber.properties?.azureTemplateType || "");
|
||||
|
||||
// Set all Additional Subject Fields immediately
|
||||
setValue("organization", pkiSubscriber.properties?.organization || "");
|
||||
setValue("organizationalUnit", pkiSubscriber.properties?.organizationalUnit || "");
|
||||
setValue("country", pkiSubscriber.properties?.country || "");
|
||||
setValue("state", pkiSubscriber.properties?.state || "");
|
||||
setValue("locality", pkiSubscriber.properties?.locality || "");
|
||||
setValue("emailAddress", pkiSubscriber.properties?.emailAddress || "");
|
||||
}
|
||||
}, [pkiSubscriber, reset]);
|
||||
}, [pkiSubscriber, setValue]);
|
||||
|
||||
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);
|
||||
}
|
||||
}, [cas, setValue]);
|
||||
}, [cas, setValue, pkiSubscriber]);
|
||||
|
||||
const onFormSubmit = async ({
|
||||
name,
|
||||
@@ -180,7 +204,14 @@ export const PkiSubscriberModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
extendedKeyUsages,
|
||||
enableAutoRenewal,
|
||||
renewalBefore,
|
||||
renewalUnit
|
||||
renewalUnit,
|
||||
azureTemplateType,
|
||||
organization,
|
||||
organizationalUnit,
|
||||
country,
|
||||
state,
|
||||
locality,
|
||||
emailAddress
|
||||
}: FormData) => {
|
||||
try {
|
||||
if (!projectId) return;
|
||||
@@ -205,11 +236,26 @@ export const PkiSubscriberModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
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)
|
||||
.map(([key]) => key as CertKeyUsage);
|
||||
|
||||
const extendedKeyUsagesList = Object.entries(extendedKeyUsages)
|
||||
const extendedKeyUsagesList =
|
||||
selectedCa?.type === CaType.AZURE_AD_CS
|
||||
? []
|
||||
: Object.entries(extendedKeyUsages)
|
||||
.filter(([, value]) => value)
|
||||
.map(([key]) => key as CertExtendedKeyUsage);
|
||||
|
||||
@@ -222,6 +268,17 @@ export const PkiSubscriberModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
? convertTimeUnitValueToDays(renewalUnit, renewalBefore)
|
||||
: 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) {
|
||||
await updateMutateAsync({
|
||||
subscriberName: pkiSubscriber.name,
|
||||
@@ -234,7 +291,8 @@ export const PkiSubscriberModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
keyUsages: keyUsagesList,
|
||||
extendedKeyUsages: extendedKeyUsagesList,
|
||||
enableAutoRenewal,
|
||||
autoRenewalPeriodInDays
|
||||
autoRenewalPeriodInDays,
|
||||
properties: Object.keys(properties).length > 0 ? properties : undefined
|
||||
});
|
||||
} else {
|
||||
await createMutateAsync({
|
||||
@@ -247,7 +305,8 @@ export const PkiSubscriberModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
keyUsages: keyUsagesList,
|
||||
extendedKeyUsages: extendedKeyUsagesList,
|
||||
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`}>
|
||||
<form
|
||||
onSubmit={handleSubmit(onFormSubmit, (fields) => {
|
||||
setTabValue(
|
||||
["name", "caId", "commonName", "subjectAlternativeNames", "ttl"].includes(
|
||||
Object.keys(fields)[0]
|
||||
)
|
||||
? FormTab.Configuration
|
||||
: FormTab.Advanced
|
||||
);
|
||||
})}
|
||||
>
|
||||
<form key={pkiSubscriber?.id || "new"} onSubmit={handleSubmit(onFormSubmit)}>
|
||||
<Tabs value={tabValue} onValueChange={(value) => setTabValue(value as FormTab)}>
|
||||
<TabList>
|
||||
<Tab value={FormTab.Configuration}>Configuration</Tab>
|
||||
@@ -323,8 +372,7 @@ export const PkiSubscriberModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
isRequired
|
||||
>
|
||||
<Select
|
||||
defaultValue={field.value}
|
||||
{...field}
|
||||
value={field.value}
|
||||
onValueChange={(e) => onChange(e)}
|
||||
className="w-full"
|
||||
>
|
||||
@@ -342,6 +390,34 @@ export const PkiSubscriberModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
</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
|
||||
control={control}
|
||||
name="commonName"
|
||||
@@ -369,6 +445,102 @@ export const PkiSubscriberModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
</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="admin@example.com" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
</Accordion>
|
||||
|
||||
{selectedCa?.type !== CaType.ACME && (
|
||||
<Controller
|
||||
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">
|
||||
<AccordionItem value="key-usages" className="data-[state=open]:border-none">
|
||||
<AccordionTrigger className="h-fit flex-none pl-1 text-sm">
|
||||
@@ -517,8 +689,7 @@ export const PkiSubscriberModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
isError={Boolean(error)}
|
||||
>
|
||||
<Select
|
||||
defaultValue={field.value}
|
||||
{...field}
|
||||
value={field.value}
|
||||
onValueChange={(e) => onChange(e)}
|
||||
className="w-48"
|
||||
>
|
||||
|
||||
@@ -12,6 +12,7 @@ import { AppConnectionHeader } from "../AppConnectionHeader";
|
||||
import { OnePassConnectionForm } from "./1PasswordConnectionForm";
|
||||
import { Auth0ConnectionForm } from "./Auth0ConnectionForm";
|
||||
import { AwsConnectionForm } from "./AwsConnectionForm";
|
||||
import { AzureADCSConnectionForm } from "./AzureADCSConnectionForm";
|
||||
import { AzureAppConfigurationConnectionForm } from "./AzureAppConfigurationConnectionForm";
|
||||
import { AzureClientSecretsConnectionForm } from "./AzureClientSecretsConnectionForm";
|
||||
import { AzureDevOpsConnectionForm } from "./AzureDevOpsConnectionForm";
|
||||
@@ -96,6 +97,8 @@ const CreateForm = ({ app, onComplete }: CreateFormProps) => {
|
||||
return <AzureKeyVaultConnectionForm onSubmit={onSubmit} />;
|
||||
case AppConnection.AzureAppConfiguration:
|
||||
return <AzureAppConfigurationConnectionForm onSubmit={onSubmit} />;
|
||||
case AppConnection.AzureADCS:
|
||||
return <AzureADCSConnectionForm onSubmit={onSubmit} />;
|
||||
case AppConnection.Databricks:
|
||||
return <DatabricksConnectionForm onSubmit={onSubmit} />;
|
||||
case AppConnection.Humanitec:
|
||||
@@ -208,6 +211,8 @@ const UpdateForm = ({ appConnection, onComplete }: UpdateFormProps) => {
|
||||
return (
|
||||
<AzureAppConfigurationConnectionForm appConnection={appConnection} onSubmit={onSubmit} />
|
||||
);
|
||||
case AppConnection.AzureADCS:
|
||||
return <AzureADCSConnectionForm appConnection={appConnection} onSubmit={onSubmit} />;
|
||||
case AppConnection.Databricks:
|
||||
return <DatabricksConnectionForm onSubmit={onSubmit} appConnection={appConnection} />;
|
||||
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>
|
||||
);
|
||||
};
|
||||