Merge pull request #4422 from Infisical/feat/azurePkiConnector
Added Microsoft ADCS PKI Connector
290
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",
|
||||
@@ -12956,216 +12957,6 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@swc/core": {
|
||||
"version": "1.3.107",
|
||||
"resolved": "https://registry.npmjs.org/@swc/core/-/core-1.3.107.tgz",
|
||||
"integrity": "sha512-zKhqDyFcTsyLIYK1iEmavljZnf4CCor5pF52UzLAz4B6Nu/4GLU+2LQVAf+oRHjusG39PTPjd2AlRT3f3QWfsQ==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@swc/counter": "^0.1.1",
|
||||
"@swc/types": "^0.1.5"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/swc"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@swc/core-darwin-arm64": "1.3.107",
|
||||
"@swc/core-darwin-x64": "1.3.107",
|
||||
"@swc/core-linux-arm-gnueabihf": "1.3.107",
|
||||
"@swc/core-linux-arm64-gnu": "1.3.107",
|
||||
"@swc/core-linux-arm64-musl": "1.3.107",
|
||||
"@swc/core-linux-x64-gnu": "1.3.107",
|
||||
"@swc/core-linux-x64-musl": "1.3.107",
|
||||
"@swc/core-win32-arm64-msvc": "1.3.107",
|
||||
"@swc/core-win32-ia32-msvc": "1.3.107",
|
||||
"@swc/core-win32-x64-msvc": "1.3.107"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@swc/helpers": "^0.5.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@swc/helpers": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@swc/core-darwin-arm64": {
|
||||
"version": "1.3.107",
|
||||
"resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.3.107.tgz",
|
||||
"integrity": "sha512-47tD/5vSXWxPd0j/ZllyQUg4bqalbQTsmqSw0J4dDdS82MWqCAwUErUrAZPRjBkjNQ6Kmrf5rpCWaGTtPw+ngw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/@swc/core-darwin-x64": {
|
||||
"version": "1.3.107",
|
||||
"resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.3.107.tgz",
|
||||
"integrity": "sha512-hwiLJ2ulNkBGAh1m1eTfeY1417OAYbRGcb/iGsJ+LuVLvKAhU/itzsl535CvcwAlt2LayeCFfcI8gdeOLeZa9A==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/@swc/core-linux-arm-gnueabihf": {
|
||||
"version": "1.3.107",
|
||||
"resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.3.107.tgz",
|
||||
"integrity": "sha512-I2wzcC0KXqh0OwymCmYwNRgZ9nxX7DWnOOStJXV3pS0uB83TXAkmqd7wvMBuIl9qu4Hfomi9aDM7IlEEn9tumQ==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/@swc/core-linux-arm64-gnu": {
|
||||
"version": "1.3.107",
|
||||
"resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.3.107.tgz",
|
||||
"integrity": "sha512-HWgnn7JORYlOYnGsdunpSF8A+BCZKPLzLtEUA27/M/ZuANcMZabKL9Zurt7XQXq888uJFAt98Gy+59PU90aHKg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/@swc/core-linux-arm64-musl": {
|
||||
"version": "1.3.107",
|
||||
"resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.3.107.tgz",
|
||||
"integrity": "sha512-vfPF74cWfAm8hyhS8yvYI94ucMHIo8xIYU+oFOW9uvDlGQRgnUf/6DEVbLyt/3yfX5723Ln57U8uiMALbX5Pyw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/@swc/core-linux-x64-gnu": {
|
||||
"version": "1.3.107",
|
||||
"resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.3.107.tgz",
|
||||
"integrity": "sha512-uBVNhIg0ip8rH9OnOsCARUFZ3Mq3tbPHxtmWk9uAa5u8jQwGWeBx5+nTHpDOVd3YxKb6+5xDEI/edeeLpha/9g==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/@swc/core-linux-x64-musl": {
|
||||
"version": "1.3.107",
|
||||
"resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.3.107.tgz",
|
||||
"integrity": "sha512-mvACkUvzSIB12q1H5JtabWATbk3AG+pQgXEN95AmEX2ZA5gbP9+B+mijsg7Sd/3tboHr7ZHLz/q3SHTvdFJrEw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/@swc/core-win32-arm64-msvc": {
|
||||
"version": "1.3.107",
|
||||
"resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.3.107.tgz",
|
||||
"integrity": "sha512-J3P14Ngy/1qtapzbguEH41kY109t6DFxfbK4Ntz9dOWNuVY3o9/RTB841ctnJk0ZHEG+BjfCJjsD2n8H5HcaOA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/@swc/core-win32-ia32-msvc": {
|
||||
"version": "1.3.107",
|
||||
"resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.3.107.tgz",
|
||||
"integrity": "sha512-ZBUtgyjTHlz8TPJh7kfwwwFma+ktr6OccB1oXC8fMSopD0AxVnQasgun3l3099wIsAB9eEsJDQ/3lDkOLs1gBA==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/@swc/core-win32-x64-msvc": {
|
||||
"version": "1.3.107",
|
||||
"resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.3.107.tgz",
|
||||
"integrity": "sha512-Eyzo2XRqWOxqhE1gk9h7LWmUf4Bp4Xn2Ttb0ayAXFp6YSTxQIThXcT9kipXZqcpxcmDwoq8iWbbf2P8XL743EA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/@swc/counter": {
|
||||
"version": "0.1.3",
|
||||
"resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz",
|
||||
@@ -13183,14 +12974,6 @@
|
||||
"tslib": "^2.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@swc/types": {
|
||||
"version": "0.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.5.tgz",
|
||||
"integrity": "sha512-myfUej5naTBWnqOCc/MdVOLVjXUXtIA+NpDrDBKJtLLg2shUjBu3cZmB/85RyitKc55+lUUyl7oRfLOvkr2hsw==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@techteamer/ocsp": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@techteamer/ocsp/-/ocsp-1.0.1.tgz",
|
||||
@@ -15195,6 +14978,18 @@
|
||||
"proxy-from-env": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/axios-ntlm": {
|
||||
"version": "1.4.4",
|
||||
"resolved": "https://registry.npmjs.org/axios-ntlm/-/axios-ntlm-1.4.4.tgz",
|
||||
"integrity": "sha512-kpCRdzMfL8gi0Z0o96P3QPAK4XuC8iciGgxGXe+PeQ4oyjI2LZN8WSOKbu0Y9Jo3T/A7pB81n6jYVPIpglEuRA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"axios": "^1.8.4",
|
||||
"des.js": "^1.1.0",
|
||||
"dev-null": "^0.1.1",
|
||||
"js-md4": "^0.3.2"
|
||||
}
|
||||
},
|
||||
"node_modules/axios-retry": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/axios-retry/-/axios-retry-4.0.0.tgz",
|
||||
@@ -16954,6 +16749,16 @@
|
||||
"resolved": "https://registry.npmjs.org/deprecation/-/deprecation-2.3.1.tgz",
|
||||
"integrity": "sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ=="
|
||||
},
|
||||
"node_modules/des.js": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/des.js/-/des.js-1.1.0.tgz",
|
||||
"integrity": "sha512-r17GxjhUCjSRy8aiJpr8/UadFIzMzJGexI3Nmz4ADi9LYSFx4gTBp80+NaX/YsXWWLhpZ7v/v/ubEc/bCNfKwg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"inherits": "^2.0.1",
|
||||
"minimalistic-assert": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/destroy": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz",
|
||||
@@ -16981,6 +16786,12 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/dev-null": {
|
||||
"version": "0.1.1",
|
||||
"resolved": "https://registry.npmjs.org/dev-null/-/dev-null-0.1.1.tgz",
|
||||
"integrity": "sha512-nMNZG0zfMgmdv8S5O0TM5cpwNbGKRGPCxVsr0SmA3NZZy9CYBbuNLL0PD3Acx9e5LIUgwONXtM9kM6RlawPxEQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/diff": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz",
|
||||
@@ -19029,49 +18840,6 @@
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
|
||||
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
|
||||
},
|
||||
"node_modules/gcp-metadata": {
|
||||
"version": "5.3.0",
|
||||
"resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-5.3.0.tgz",
|
||||
"integrity": "sha512-FNTkdNEnBdlqF2oatizolQqNANMrcqJt6AAYt99B3y1aLLC8Hc5IOBb+ZnnzllodEEf6xMBp6wRcBbc16fa65w==",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"gaxios": "^5.0.0",
|
||||
"json-bigint": "^1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/gcp-metadata/node_modules/gaxios": {
|
||||
"version": "5.1.3",
|
||||
"resolved": "https://registry.npmjs.org/gaxios/-/gaxios-5.1.3.tgz",
|
||||
"integrity": "sha512-95hVgBRgEIRQQQHIbnxBXeHbW4TqFk4ZDJW7wmVtvYar72FdhRIo1UGOLS2eRAKCPEdPBWu+M7+A33D9CdX9rA==",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"extend": "^3.0.2",
|
||||
"https-proxy-agent": "^5.0.0",
|
||||
"is-stream": "^2.0.0",
|
||||
"node-fetch": "^2.6.9"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/gcp-metadata/node_modules/is-stream": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz",
|
||||
"integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/generate-function": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.3.1.tgz",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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,8 @@ export const PkiSubscribersSchema = z.object({
|
||||
lastAutoRenewAt: z.date().nullable().optional(),
|
||||
lastOperationStatus: z.string().nullable().optional(),
|
||||
lastOperationMessage: z.string().nullable().optional(),
|
||||
lastOperationAt: z.date().nullable().optional()
|
||||
lastOperationAt: z.date().nullable().optional(),
|
||||
properties: z.unknown().nullable().optional()
|
||||
});
|
||||
|
||||
export type TPkiSubscribers = z.infer<typeof PkiSubscribersSchema>;
|
||||
|
||||
@@ -281,6 +281,7 @@ export enum EventType {
|
||||
UPDATE_SSH_CERTIFICATE_TEMPLATE = "update-ssh-certificate-template",
|
||||
DELETE_SSH_CERTIFICATE_TEMPLATE = "delete-ssh-certificate-template",
|
||||
GET_SSH_CERTIFICATE_TEMPLATE = "get-ssh-certificate-template",
|
||||
GET_AZURE_AD_TEMPLATES = "get-azure-ad-templates",
|
||||
GET_SSH_HOST = "get-ssh-host",
|
||||
CREATE_SSH_HOST = "create-ssh-host",
|
||||
UPDATE_SSH_HOST = "update-ssh-host",
|
||||
@@ -2497,6 +2498,14 @@ interface CreateCertificateTemplateEstConfig {
|
||||
};
|
||||
}
|
||||
|
||||
interface GetAzureAdCsTemplatesEvent {
|
||||
type: EventType.GET_AZURE_AD_TEMPLATES;
|
||||
metadata: {
|
||||
caId: string;
|
||||
amount: number;
|
||||
};
|
||||
}
|
||||
|
||||
interface UpdateCertificateTemplateEstConfig {
|
||||
type: EventType.UPDATE_CERTIFICATE_TEMPLATE_EST_CONFIG;
|
||||
metadata: {
|
||||
@@ -3636,6 +3645,7 @@ export type Event =
|
||||
| CreateCertificateTemplateEstConfig
|
||||
| UpdateCertificateTemplateEstConfig
|
||||
| GetCertificateTemplateEstConfig
|
||||
| GetAzureAdCsTemplatesEvent
|
||||
| AttemptCreateSlackIntegration
|
||||
| AttemptReinstallSlackIntegration
|
||||
| UpdateSlackIntegration
|
||||
|
||||
@@ -2312,6 +2312,15 @@ export const AppConnections = {
|
||||
OKTA: {
|
||||
instanceUrl: "The URL used to access your Okta organization.",
|
||||
apiToken: "The API token used to authenticate with Okta."
|
||||
},
|
||||
AZURE_ADCS: {
|
||||
adcsUrl:
|
||||
"The HTTPS URL of the Azure ADCS instance to connect with (e.g., 'https://adcs.yourdomain.com/certsrv').",
|
||||
username: "The username used to access Azure ADCS (format: 'DOMAIN\\username' or 'username@domain.com').",
|
||||
password: "The password used to access Azure ADCS.",
|
||||
sslRejectUnauthorized:
|
||||
"Whether or not to reject unauthorized SSL certificates (true/false). Set to false only in test environments with self-signed certificates.",
|
||||
sslCertificate: "The SSL certificate (PEM format) to use for secure connection."
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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,78 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
||||
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
|
||||
});
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
projectId: req.query.projectId,
|
||||
event: {
|
||||
type: EventType.GET_AZURE_AD_TEMPLATES,
|
||||
metadata: {
|
||||
caId: req.params.caId,
|
||||
amount: templates.length
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return { templates };
|
||||
}
|
||||
});
|
||||
};
|
||||
@@ -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
|
||||
};
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import RE2 from "re2";
|
||||
import { z } from "zod";
|
||||
|
||||
import { CertificatesSchema } from "@app/db/schemas";
|
||||
@@ -112,7 +113,88 @@ export const registerPkiSubscriberRouter = async (server: FastifyZodProvider) =>
|
||||
.transform((arr) => Array.from(new Set(arr)))
|
||||
.describe(PKI_SUBSCRIBERS.CREATE.extendedKeyUsages),
|
||||
enableAutoRenewal: z.boolean().optional().describe(PKI_SUBSCRIBERS.CREATE.enableAutoRenewal),
|
||||
autoRenewalPeriodInDays: z.number().min(1).optional().describe(PKI_SUBSCRIBERS.CREATE.autoRenewalPeriodInDays)
|
||||
autoRenewalPeriodInDays: z.number().min(1).optional().describe(PKI_SUBSCRIBERS.CREATE.autoRenewalPeriodInDays),
|
||||
properties: z
|
||||
.object({
|
||||
azureTemplateType: z.string().optional().describe("Azure ADCS Certificate Template Type"),
|
||||
organization: z
|
||||
.string()
|
||||
.trim()
|
||||
.min(1)
|
||||
.max(64, "Organization cannot exceed 64 characters")
|
||||
.regex(
|
||||
new RE2('^[^,=+<>#;\\\\"/\\r\\n\\t]*$'),
|
||||
'Organization contains invalid characters: , = + < > # ; \\ " / \\r \\n \\t'
|
||||
)
|
||||
.regex(
|
||||
new RE2("^[^\\\\s\\\\-_.]+.*[^\\\\s\\\\-_.]+$|^[^\\\\s\\\\-_.]{1}$"),
|
||||
"Organization cannot start or end with spaces, hyphens, underscores, or periods"
|
||||
)
|
||||
.optional()
|
||||
.describe("Organization (O) - Maximum 64 characters, no special DN characters"),
|
||||
organizationalUnit: z
|
||||
.string()
|
||||
.trim()
|
||||
.min(1)
|
||||
.max(64, "Organizational Unit cannot exceed 64 characters")
|
||||
.regex(
|
||||
new RE2('^[^,=+<>#;\\\\"/\\r\\n\\t]*$'),
|
||||
'Organizational Unit contains invalid characters: , = + < > # ; \\ " / \\r \\n \\t'
|
||||
)
|
||||
.regex(
|
||||
new RE2("^[^\\\\s\\\\-_.]+.*[^\\\\s\\\\-_.]+$|^[^\\\\s\\\\-_.]{1}$"),
|
||||
"Organizational Unit cannot start or end with spaces, hyphens, underscores, or periods"
|
||||
)
|
||||
.optional()
|
||||
.describe("Organizational Unit (OU) - Maximum 64 characters, no special DN characters"),
|
||||
country: z
|
||||
.string()
|
||||
.trim()
|
||||
.length(2, "Country must be exactly 2 characters")
|
||||
.regex(new RE2("^[A-Z]{2}$"), "Country must be exactly 2 uppercase letters")
|
||||
.optional()
|
||||
.describe("Country (C) - Two uppercase letter country code (e.g., US, CA, GB)"),
|
||||
state: z
|
||||
.string()
|
||||
.trim()
|
||||
.min(1)
|
||||
.max(64, "State cannot exceed 64 characters")
|
||||
.regex(
|
||||
new RE2('^[^,=+<>#;\\\\"/\\r\\n\\t]*$'),
|
||||
'State contains invalid characters: , = + < > # ; \\ " / \\r \\n \\t'
|
||||
)
|
||||
.regex(
|
||||
new RE2("^[^\\\\s\\\\-_.]+.*[^\\\\s\\\\-_.]+$|^[^\\\\s\\\\-_.]{1}$"),
|
||||
"State cannot start or end with spaces, hyphens, underscores, or periods"
|
||||
)
|
||||
.optional()
|
||||
.describe("State/Province (ST) - Maximum 64 characters, no special DN characters"),
|
||||
locality: z
|
||||
.string()
|
||||
.trim()
|
||||
.min(1)
|
||||
.max(64, "Locality cannot exceed 64 characters")
|
||||
.regex(
|
||||
new RE2('^[^,=+<>#;\\\\"/\\r\\n\\t]*$'),
|
||||
'Locality contains invalid characters: , = + < > # ; \\ " / \\r \\n \\t'
|
||||
)
|
||||
.regex(
|
||||
new RE2("^[^\\\\s\\\\-_.]+.*[^\\\\s\\\\-_.]+$|^[^\\\\s\\\\-_.]{1}$"),
|
||||
"Locality cannot start or end with spaces, hyphens, underscores, or periods"
|
||||
)
|
||||
.optional()
|
||||
.describe("Locality (L) - Maximum 64 characters, no special DN characters"),
|
||||
emailAddress: z
|
||||
.string()
|
||||
.trim()
|
||||
.email("Email Address must be a valid email format")
|
||||
.min(6, "Email Address must be at least 6 characters")
|
||||
.max(64, "Email Address cannot exceed 64 characters")
|
||||
.optional()
|
||||
.describe("Email Address - Valid email format between 6 and 64 characters")
|
||||
})
|
||||
.optional()
|
||||
.describe("Additional subscriber properties and subject fields")
|
||||
}),
|
||||
response: {
|
||||
200: sanitizedPkiSubscriber
|
||||
@@ -199,7 +281,88 @@ export const registerPkiSubscriberRouter = async (server: FastifyZodProvider) =>
|
||||
.optional()
|
||||
.describe(PKI_SUBSCRIBERS.UPDATE.extendedKeyUsages),
|
||||
enableAutoRenewal: z.boolean().optional().describe(PKI_SUBSCRIBERS.UPDATE.enableAutoRenewal),
|
||||
autoRenewalPeriodInDays: z.number().min(1).optional().describe(PKI_SUBSCRIBERS.UPDATE.autoRenewalPeriodInDays)
|
||||
autoRenewalPeriodInDays: z.number().min(1).optional().describe(PKI_SUBSCRIBERS.UPDATE.autoRenewalPeriodInDays),
|
||||
properties: z
|
||||
.object({
|
||||
azureTemplateType: z.string().optional().describe("Azure ADCS Certificate Template Type"),
|
||||
organization: z
|
||||
.string()
|
||||
.trim()
|
||||
.min(1)
|
||||
.max(64, "Organization cannot exceed 64 characters")
|
||||
.regex(
|
||||
new RE2('^[^,=+<>#;\\\\"/\\r\\n\\t]*$'),
|
||||
'Organization contains invalid characters: , = + < > # ; \\ " / \\r \\n \\t'
|
||||
)
|
||||
.regex(
|
||||
new RE2("^[^\\\\s\\\\-_.]+.*[^\\\\s\\\\-_.]+$|^[^\\\\s\\\\-_.]{1}$"),
|
||||
"Organization cannot start or end with spaces, hyphens, underscores, or periods"
|
||||
)
|
||||
.optional()
|
||||
.describe("Organization (O) - Maximum 64 characters, no special DN characters"),
|
||||
organizationalUnit: z
|
||||
.string()
|
||||
.trim()
|
||||
.min(1)
|
||||
.max(64, "Organizational Unit cannot exceed 64 characters")
|
||||
.regex(
|
||||
new RE2('^[^,=+<>#;\\\\"/\\r\\n\\t]*$'),
|
||||
'Organizational Unit contains invalid characters: , = + < > # ; \\ " / \\r \\n \\t'
|
||||
)
|
||||
.regex(
|
||||
new RE2("^[^\\\\s\\\\-_.]+.*[^\\\\s\\\\-_.]+$|^[^\\\\s\\\\-_.]{1}$"),
|
||||
"Organizational Unit cannot start or end with spaces, hyphens, underscores, or periods"
|
||||
)
|
||||
.optional()
|
||||
.describe("Organizational Unit (OU) - Maximum 64 characters, no special DN characters"),
|
||||
country: z
|
||||
.string()
|
||||
.trim()
|
||||
.length(2, "Country must be exactly 2 characters")
|
||||
.regex(new RE2("^[A-Z]{2}$"), "Country must be exactly 2 uppercase letters")
|
||||
.optional()
|
||||
.describe("Country (C) - Two uppercase letter country code (e.g., US, CA, GB)"),
|
||||
state: z
|
||||
.string()
|
||||
.trim()
|
||||
.min(1)
|
||||
.max(64, "State cannot exceed 64 characters")
|
||||
.regex(
|
||||
new RE2('^[^,=+<>#;\\\\"/\\r\\n\\t]*$'),
|
||||
'State contains invalid characters: , = + < > # ; \\ " / \\r \\n \\t'
|
||||
)
|
||||
.regex(
|
||||
new RE2("^[^\\\\s\\\\-_.]+.*[^\\\\s\\\\-_.]+$|^[^\\\\s\\\\-_.]{1}$"),
|
||||
"State cannot start or end with spaces, hyphens, underscores, or periods"
|
||||
)
|
||||
.optional()
|
||||
.describe("State/Province (ST) - Maximum 64 characters, no special DN characters"),
|
||||
locality: z
|
||||
.string()
|
||||
.trim()
|
||||
.min(1)
|
||||
.max(64, "Locality cannot exceed 64 characters")
|
||||
.regex(
|
||||
new RE2('^[^,=+<>#;\\\\"/\\r\\n\\t]*$'),
|
||||
'Locality contains invalid characters: , = + < > # ; \\ " / \\r \\n \\t'
|
||||
)
|
||||
.regex(
|
||||
new RE2("^[^\\\\s\\\\-_.]+.*[^\\\\s\\\\-_.]+$|^[^\\\\s\\\\-_.]{1}$"),
|
||||
"Locality cannot start or end with spaces, hyphens, underscores, or periods"
|
||||
)
|
||||
.optional()
|
||||
.describe("Locality (L) - Maximum 64 characters, no special DN characters"),
|
||||
emailAddress: z
|
||||
.string()
|
||||
.trim()
|
||||
.email("Email Address must be a valid email format")
|
||||
.min(6, "Email Address must be at least 6 characters")
|
||||
.max(64, "Email Address cannot exceed 64 characters")
|
||||
.optional()
|
||||
.describe("Email Address - Valid email format between 6 and 64 characters")
|
||||
})
|
||||
.optional()
|
||||
.describe("Additional subscriber properties and subject fields")
|
||||
}),
|
||||
response: {
|
||||
200: sanitizedPkiSubscriber
|
||||
|
||||
@@ -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,455 @@
|
||||
/* 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 { NtlmClient } from "axios-ntlm";
|
||||
import https from "https";
|
||||
|
||||
import { BadRequestError, NotFoundError } from "@app/lib/errors";
|
||||
import { blockLocalAndPrivateIpAddresses } from "@app/lib/validator/validate-url";
|
||||
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";
|
||||
|
||||
// Type definitions for axios-ntlm
|
||||
interface AxiosNtlmConfig {
|
||||
ntlm: {
|
||||
domain: string;
|
||||
username: string;
|
||||
password: string;
|
||||
};
|
||||
httpsAgent?: https.Agent;
|
||||
url: string;
|
||||
method?: string;
|
||||
data?: string;
|
||||
headers?: Record<string, string>;
|
||||
}
|
||||
|
||||
interface AxiosNtlmResponse {
|
||||
status: number;
|
||||
data: string;
|
||||
headers: unknown;
|
||||
}
|
||||
|
||||
// Types for credential parsing
|
||||
interface ParsedCredentials {
|
||||
domain: string;
|
||||
username: string;
|
||||
fullUsername: string; // domain\username format
|
||||
}
|
||||
|
||||
// Helper function to parse and normalize credentials for Windows authentication
|
||||
const parseCredentials = (inputUsername: string): ParsedCredentials => {
|
||||
// Ensure inputUsername is a string
|
||||
if (typeof inputUsername !== "string" || !inputUsername.trim()) {
|
||||
throw new BadRequestError({
|
||||
message: "Username must be a non-empty string"
|
||||
});
|
||||
}
|
||||
|
||||
let domain = "";
|
||||
let username = "";
|
||||
let fullUsername = "";
|
||||
|
||||
if (inputUsername.includes("\\")) {
|
||||
// Already in domain\username format
|
||||
const parts = inputUsername.split("\\");
|
||||
if (parts.length === 2) {
|
||||
[domain, username] = parts;
|
||||
fullUsername = inputUsername;
|
||||
} else {
|
||||
throw new BadRequestError({
|
||||
message: "Invalid domain\\username format. Expected format: DOMAIN\\username"
|
||||
});
|
||||
}
|
||||
} else if (inputUsername.includes("@")) {
|
||||
// UPN format: user@domain.com
|
||||
const [user, domainPart] = inputUsername.split("@");
|
||||
if (!user || !domainPart) {
|
||||
throw new BadRequestError({
|
||||
message: "Invalid UPN format. Expected format: user@domain.com"
|
||||
});
|
||||
}
|
||||
|
||||
username = user;
|
||||
// Extract NetBIOS name from FQDN
|
||||
domain = domainPart.split(".")[0].toUpperCase();
|
||||
fullUsername = `${domain}\\${username}`;
|
||||
} else {
|
||||
// Plain username - assume local account or current domain
|
||||
username = inputUsername;
|
||||
domain = "";
|
||||
fullUsername = inputUsername;
|
||||
}
|
||||
|
||||
return { domain, username, fullUsername };
|
||||
};
|
||||
|
||||
// Helper to normalize URL
|
||||
const normalizeAdcsUrl = (url: string): string => {
|
||||
let normalizedUrl = url.trim();
|
||||
|
||||
// Remove trailing slash
|
||||
normalizedUrl = normalizedUrl.replace(/\/$/, "");
|
||||
|
||||
// Ensure HTTPS protocol
|
||||
if (normalizedUrl.startsWith("http://")) {
|
||||
normalizedUrl = normalizedUrl.replace("http://", "https://");
|
||||
} else if (!normalizedUrl.startsWith("https://")) {
|
||||
normalizedUrl = `https://${normalizedUrl}`;
|
||||
}
|
||||
|
||||
return normalizedUrl;
|
||||
};
|
||||
|
||||
// NTLM request wrapper
|
||||
const createHttpsAgent = (sslRejectUnauthorized: boolean, sslCertificate?: string): https.Agent => {
|
||||
const agentOptions: https.AgentOptions = {
|
||||
rejectUnauthorized: sslRejectUnauthorized,
|
||||
keepAlive: true, // axios-ntlm needs keepAlive for NTLM handshake
|
||||
ca: sslCertificate ? [sslCertificate.trim()] : undefined,
|
||||
// Disable hostname verification as Microsoft servers by default use local IPs for certificates
|
||||
// which may not match the hostname used to connect
|
||||
checkServerIdentity: () => undefined
|
||||
};
|
||||
|
||||
return new https.Agent(agentOptions);
|
||||
};
|
||||
|
||||
const axiosNtlmRequest = async (config: AxiosNtlmConfig): Promise<AxiosNtlmResponse> => {
|
||||
const method = config.method || "GET";
|
||||
|
||||
const credentials = {
|
||||
username: config.ntlm.username,
|
||||
password: config.ntlm.password,
|
||||
domain: config.ntlm.domain || "",
|
||||
workstation: ""
|
||||
};
|
||||
|
||||
const axiosConfig = {
|
||||
httpsAgent: config.httpsAgent,
|
||||
timeout: 30000
|
||||
};
|
||||
|
||||
const client = NtlmClient(credentials, axiosConfig);
|
||||
|
||||
const requestOptions: { url: string; method: string; data?: string; headers?: Record<string, string> } = {
|
||||
url: config.url,
|
||||
method
|
||||
};
|
||||
|
||||
if (config.data) {
|
||||
requestOptions.data = config.data;
|
||||
}
|
||||
|
||||
if (config.headers) {
|
||||
requestOptions.headers = config.headers;
|
||||
}
|
||||
|
||||
const response = await client(requestOptions);
|
||||
|
||||
return {
|
||||
status: response.status,
|
||||
data: response.data,
|
||||
headers: response.headers
|
||||
};
|
||||
};
|
||||
|
||||
// Test ADCS connectivity and authentication using NTLM
|
||||
const testAdcsConnection = async (
|
||||
credentials: ParsedCredentials,
|
||||
password: string,
|
||||
baseUrl: string,
|
||||
sslRejectUnauthorized: boolean = true,
|
||||
sslCertificate?: string
|
||||
): Promise<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 shouldRejectUnauthorized = sslRejectUnauthorized;
|
||||
|
||||
const httpsAgent = createHttpsAgent(shouldRejectUnauthorized, sslCertificate);
|
||||
|
||||
const response = await axiosNtlmRequest({
|
||||
url: testUrl,
|
||||
method: "GET",
|
||||
httpsAgent,
|
||||
ntlm: {
|
||||
domain: credentials.domain,
|
||||
username: credentials.username,
|
||||
password
|
||||
}
|
||||
});
|
||||
|
||||
// Check if we got a successful response
|
||||
if (response.status === 200) {
|
||||
const responseText = response.data;
|
||||
|
||||
// Verify this is actually an ADCS server by checking content
|
||||
const adcsIndicators = [
|
||||
"Microsoft Active Directory Certificate Services",
|
||||
"Certificate Services",
|
||||
"Request a certificate",
|
||||
"certsrv",
|
||||
"Certificate Template",
|
||||
"Web Enrollment"
|
||||
];
|
||||
|
||||
const isAdcsServer = adcsIndicators.some((indicator) =>
|
||||
responseText.toLowerCase().includes(indicator.toLowerCase())
|
||||
);
|
||||
|
||||
if (isAdcsServer) {
|
||||
// Successfully authenticated and confirmed ADCS
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if (response.status === 401) {
|
||||
throw new BadRequestError({
|
||||
message: "Authentication failed. Please verify your credentials are correct."
|
||||
});
|
||||
}
|
||||
|
||||
if (response.status === 403) {
|
||||
throw new BadRequestError({
|
||||
message: "Access denied. Your account may not have permission to access ADCS web enrollment."
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof BadRequestError) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
// Handle network and connection errors
|
||||
if (error instanceof Error) {
|
||||
if (error.message.includes("ENOTFOUND")) {
|
||||
throw new BadRequestError({
|
||||
message: "Cannot resolve ADCS server hostname. Please verify the URL is correct."
|
||||
});
|
||||
}
|
||||
if (error.message.includes("ECONNREFUSED")) {
|
||||
throw new BadRequestError({
|
||||
message: "Connection refused by ADCS server. Please verify the server is running and accessible."
|
||||
});
|
||||
}
|
||||
if (error.message.includes("ETIMEDOUT") || error.message.includes("timeout")) {
|
||||
throw new BadRequestError({
|
||||
message: "Connection timeout. Please verify the server is accessible and not blocked by firewall."
|
||||
});
|
||||
}
|
||||
if (error.message.includes("certificate") || error.message.includes("SSL") || error.message.includes("TLS")) {
|
||||
throw new BadRequestError({
|
||||
message: `SSL/TLS certificate error: ${error.message}. This may indicate a certificate verification failure.`
|
||||
});
|
||||
}
|
||||
if (error.message.includes("DEPTH_ZERO_SELF_SIGNED_CERT")) {
|
||||
throw new BadRequestError({
|
||||
message:
|
||||
"Self-signed certificate detected. Either provide the server's certificate or set 'sslRejectUnauthorized' to false."
|
||||
});
|
||||
}
|
||||
if (error.message.includes("UNABLE_TO_VERIFY_LEAF_SIGNATURE")) {
|
||||
throw new BadRequestError({
|
||||
message: "Unable to verify certificate signature. Please provide the correct CA certificate."
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Continue to next endpoint for other errors
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// If we get here, no endpoint worked
|
||||
throw new BadRequestError({
|
||||
message: "Could not connect to ADCS server. Please verify the server URL and that Web Enrollment is enabled."
|
||||
});
|
||||
};
|
||||
|
||||
// Create authenticated NTLM client for ADCS operations
|
||||
const createNtlmClient = (
|
||||
username: string,
|
||||
password: string,
|
||||
baseUrl: string,
|
||||
sslRejectUnauthorized: boolean = true,
|
||||
sslCertificate?: string
|
||||
) => {
|
||||
const parsedCredentials = parseCredentials(username);
|
||||
const normalizedUrl = normalizeAdcsUrl(baseUrl);
|
||||
|
||||
return {
|
||||
get: async (endpoint: string, additionalHeaders: Record<string, string> = {}) => {
|
||||
const shouldRejectUnauthorized = sslRejectUnauthorized;
|
||||
|
||||
const httpsAgent = createHttpsAgent(shouldRejectUnauthorized, sslCertificate);
|
||||
|
||||
return axiosNtlmRequest({
|
||||
url: `${normalizedUrl}${endpoint}`,
|
||||
method: "GET",
|
||||
httpsAgent,
|
||||
headers: additionalHeaders,
|
||||
ntlm: {
|
||||
domain: parsedCredentials.domain,
|
||||
username: parsedCredentials.username,
|
||||
password
|
||||
}
|
||||
});
|
||||
},
|
||||
post: async (endpoint: string, body: string, additionalHeaders: Record<string, string> = {}) => {
|
||||
const shouldRejectUnauthorized = sslRejectUnauthorized;
|
||||
|
||||
const httpsAgent = createHttpsAgent(shouldRejectUnauthorized, sslCertificate);
|
||||
|
||||
return axiosNtlmRequest({
|
||||
url: `${normalizedUrl}${endpoint}`,
|
||||
method: "POST",
|
||||
httpsAgent,
|
||||
data: body,
|
||||
headers: {
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
...additionalHeaders
|
||||
},
|
||||
ntlm: {
|
||||
domain: parsedCredentials.domain,
|
||||
username: parsedCredentials.username,
|
||||
password
|
||||
}
|
||||
});
|
||||
},
|
||||
baseUrl: normalizedUrl,
|
||||
credentials: parsedCredentials
|
||||
};
|
||||
};
|
||||
|
||||
export const getAzureADCSConnectionCredentials = async (
|
||||
connectionId: string,
|
||||
appConnectionDAL: Pick<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;
|
||||
sslRejectUnauthorized?: boolean;
|
||||
sslCertificate?: string;
|
||||
};
|
||||
|
||||
return {
|
||||
username: credentials.username,
|
||||
password: credentials.password,
|
||||
adcsUrl: credentials.adcsUrl,
|
||||
sslRejectUnauthorized: credentials.sslRejectUnauthorized ?? true,
|
||||
sslCertificate: credentials.sslCertificate
|
||||
};
|
||||
|
||||
default:
|
||||
throw new BadRequestError({
|
||||
message: `Unsupported Azure ADCS connection method: ${appConnection.method}`
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export const validateAzureADCSConnectionCredentials = async (appConnection: TAzureADCSConnectionConfig) => {
|
||||
const { credentials } = appConnection;
|
||||
|
||||
try {
|
||||
// Parse and validate credentials
|
||||
const parsedCredentials = parseCredentials(credentials.username);
|
||||
const normalizedUrl = normalizeAdcsUrl(credentials.adcsUrl);
|
||||
|
||||
// Validate URL to prevent DNS manipulation attacks and SSRF
|
||||
await blockLocalAndPrivateIpAddresses(normalizedUrl);
|
||||
|
||||
// Test the connection using NTLM
|
||||
await testAdcsConnection(
|
||||
parsedCredentials,
|
||||
credentials.password,
|
||||
normalizedUrl,
|
||||
credentials.sslRejectUnauthorized ?? true,
|
||||
credentials.sslCertificate
|
||||
);
|
||||
|
||||
// If we get here, authentication was successful
|
||||
return {
|
||||
username: credentials.username,
|
||||
password: credentials.password,
|
||||
adcsUrl: credentials.adcsUrl,
|
||||
sslRejectUnauthorized: credentials.sslRejectUnauthorized ?? true,
|
||||
sslCertificate: credentials.sslCertificate
|
||||
};
|
||||
} catch (error) {
|
||||
if (error instanceof BadRequestError) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
// Handle unexpected errors
|
||||
let errorMessage = "Unable to validate ADCS connection.";
|
||||
if (error instanceof Error) {
|
||||
if (error.message.includes("401") || error.message.includes("Unauthorized")) {
|
||||
errorMessage = "NTLM authentication failed. Please verify your username, password, and domain are correct.";
|
||||
} else if (error.message.includes("ENOTFOUND") || error.message.includes("ECONNREFUSED")) {
|
||||
errorMessage = "Cannot connect to the ADCS server. Please verify the server URL is correct and accessible.";
|
||||
} else if (error.message.includes("timeout")) {
|
||||
errorMessage = "Connection to ADCS server timed out. Please verify the server is accessible.";
|
||||
} else if (
|
||||
error.message.includes("certificate") ||
|
||||
error.message.includes("SSL") ||
|
||||
error.message.includes("TLS") ||
|
||||
error.message.includes("DEPTH_ZERO_SELF_SIGNED_CERT") ||
|
||||
error.message.includes("UNABLE_TO_VERIFY_LEAF_SIGNATURE")
|
||||
) {
|
||||
errorMessage = `SSL/TLS certificate error: ${error.message}. The server certificate may be self-signed or the CA certificate may be incorrect.`;
|
||||
}
|
||||
}
|
||||
|
||||
throw new BadRequestError({
|
||||
message: `Failed to validate Azure ADCS connection: ${errorMessage} Details: ${
|
||||
error instanceof Error ? error.message : "Unknown error"
|
||||
}`
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export const getAzureADCSConnectionListItem = () => ({
|
||||
name: "Azure ADCS" as const,
|
||||
app: AppConnection.AzureADCS as const,
|
||||
methods: [AzureADCSConnectionMethod.UsernamePassword] as [AzureADCSConnectionMethod.UsernamePassword]
|
||||
});
|
||||
|
||||
// Export helper functions for use in certificate ordering
|
||||
export const createAdcsHttpClient = (
|
||||
username: string,
|
||||
password: string,
|
||||
baseUrl: string,
|
||||
sslRejectUnauthorized: boolean = true,
|
||||
sslCertificate?: string
|
||||
) => {
|
||||
return createNtlmClient(username, password, baseUrl, sslRejectUnauthorized, sslCertificate);
|
||||
};
|
||||
@@ -0,0 +1,88 @@
|
||||
import z from "zod";
|
||||
|
||||
import { AppConnections } from "@app/lib/api-docs";
|
||||
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
||||
import {
|
||||
BaseAppConnectionSchema,
|
||||
GenericCreateAppConnectionFieldsSchema,
|
||||
GenericUpdateAppConnectionFieldsSchema
|
||||
} from "@app/services/app-connection/app-connection-schemas";
|
||||
|
||||
import { AzureADCSConnectionMethod } from "./azure-adcs-connection-enums";
|
||||
|
||||
export const AzureADCSUsernamePasswordCredentialsSchema = z.object({
|
||||
adcsUrl: z
|
||||
.string()
|
||||
.trim()
|
||||
.min(1, "ADCS URL required")
|
||||
.max(255)
|
||||
.refine((value) => value.startsWith("https://"), "ADCS URL must use HTTPS")
|
||||
.describe(AppConnections.CREDENTIALS.AZURE_ADCS.adcsUrl),
|
||||
username: z
|
||||
.string()
|
||||
.trim()
|
||||
.min(1, "Username required")
|
||||
.max(255)
|
||||
.describe(AppConnections.CREDENTIALS.AZURE_ADCS.username),
|
||||
password: z
|
||||
.string()
|
||||
.trim()
|
||||
.min(1, "Password required")
|
||||
.max(255)
|
||||
.describe(AppConnections.CREDENTIALS.AZURE_ADCS.password),
|
||||
sslRejectUnauthorized: z.boolean().optional().describe(AppConnections.CREDENTIALS.AZURE_ADCS.sslRejectUnauthorized),
|
||||
sslCertificate: z
|
||||
.string()
|
||||
.trim()
|
||||
.transform((value) => value || undefined)
|
||||
.optional()
|
||||
.describe(AppConnections.CREDENTIALS.AZURE_ADCS.sslCertificate)
|
||||
});
|
||||
|
||||
const BaseAzureADCSConnectionSchema = BaseAppConnectionSchema.extend({ app: z.literal(AppConnection.AzureADCS) });
|
||||
|
||||
export const AzureADCSConnectionSchema = BaseAzureADCSConnectionSchema.extend({
|
||||
method: z.literal(AzureADCSConnectionMethod.UsernamePassword),
|
||||
credentials: AzureADCSUsernamePasswordCredentialsSchema
|
||||
});
|
||||
|
||||
export const SanitizedAzureADCSConnectionSchema = z.discriminatedUnion("method", [
|
||||
BaseAzureADCSConnectionSchema.extend({
|
||||
method: z.literal(AzureADCSConnectionMethod.UsernamePassword),
|
||||
credentials: AzureADCSUsernamePasswordCredentialsSchema.pick({
|
||||
username: true,
|
||||
adcsUrl: true,
|
||||
sslRejectUnauthorized: true,
|
||||
sslCertificate: true
|
||||
})
|
||||
})
|
||||
]);
|
||||
|
||||
export const ValidateAzureADCSConnectionCredentialsSchema = z.discriminatedUnion("method", [
|
||||
z.object({
|
||||
method: z
|
||||
.literal(AzureADCSConnectionMethod.UsernamePassword)
|
||||
.describe(AppConnections.CREATE(AppConnection.AzureADCS).method),
|
||||
credentials: AzureADCSUsernamePasswordCredentialsSchema.describe(
|
||||
AppConnections.CREATE(AppConnection.AzureADCS).credentials
|
||||
)
|
||||
})
|
||||
]);
|
||||
|
||||
export const CreateAzureADCSConnectionSchema = ValidateAzureADCSConnectionCredentialsSchema.and(
|
||||
GenericCreateAppConnectionFieldsSchema(AppConnection.AzureADCS)
|
||||
);
|
||||
|
||||
export const UpdateAzureADCSConnectionSchema = z
|
||||
.object({
|
||||
credentials: AzureADCSUsernamePasswordCredentialsSchema.optional().describe(
|
||||
AppConnections.UPDATE(AppConnection.AzureADCS).credentials
|
||||
)
|
||||
})
|
||||
.and(GenericUpdateAppConnectionFieldsSchema(AppConnection.AzureADCS));
|
||||
|
||||
export const AzureADCSConnectionListItemSchema = z.object({
|
||||
name: z.literal("Azure ADCS"),
|
||||
app: z.literal(AppConnection.AzureADCS),
|
||||
methods: z.nativeEnum(AzureADCSConnectionMethod).array()
|
||||
});
|
||||
@@ -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,29 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { CaType } from "../certificate-authority-enums";
|
||||
import {
|
||||
BaseCertificateAuthoritySchema,
|
||||
GenericCreateCertificateAuthorityFieldsSchema,
|
||||
GenericUpdateCertificateAuthorityFieldsSchema
|
||||
} from "../certificate-authority-schemas";
|
||||
|
||||
export const AzureAdCsCertificateAuthorityConfigurationSchema = z.object({
|
||||
azureAdcsConnectionId: z.string().uuid().trim().describe("Azure ADCS Connection ID")
|
||||
});
|
||||
|
||||
export const AzureAdCsCertificateAuthoritySchema = BaseCertificateAuthoritySchema.extend({
|
||||
type: z.literal(CaType.AZURE_AD_CS),
|
||||
configuration: AzureAdCsCertificateAuthorityConfigurationSchema
|
||||
});
|
||||
|
||||
export const CreateAzureAdCsCertificateAuthoritySchema = GenericCreateCertificateAuthorityFieldsSchema(
|
||||
CaType.AZURE_AD_CS
|
||||
).extend({
|
||||
configuration: AzureAdCsCertificateAuthorityConfigurationSchema
|
||||
});
|
||||
|
||||
export const UpdateAzureAdCsCertificateAuthoritySchema = GenericUpdateCertificateAuthorityFieldsSchema(
|
||||
CaType.AZURE_AD_CS
|
||||
).extend({
|
||||
configuration: AzureAdCsCertificateAuthorityConfigurationSchema.optional()
|
||||
});
|
||||
@@ -0,0 +1,13 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import {
|
||||
AzureAdCsCertificateAuthoritySchema,
|
||||
CreateAzureAdCsCertificateAuthoritySchema,
|
||||
UpdateAzureAdCsCertificateAuthoritySchema
|
||||
} from "./azure-ad-cs-certificate-authority-schemas";
|
||||
|
||||
export type TAzureAdCsCertificateAuthority = z.infer<typeof AzureAdCsCertificateAuthoritySchema>;
|
||||
|
||||
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,29 @@
|
||||
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) {
|
||||
|
||||
@@ -22,6 +22,14 @@ import {
|
||||
TCreateAcmeCertificateAuthorityDTO,
|
||||
TUpdateAcmeCertificateAuthorityDTO
|
||||
} from "./acme/acme-certificate-authority-types";
|
||||
import {
|
||||
AzureAdCsCertificateAuthorityFns,
|
||||
castDbEntryToAzureAdCsCertificateAuthority
|
||||
} from "./azure-ad-cs/azure-ad-cs-certificate-authority-fns";
|
||||
import {
|
||||
TCreateAzureAdCsCertificateAuthorityDTO,
|
||||
TUpdateAzureAdCsCertificateAuthorityDTO
|
||||
} from "./azure-ad-cs/azure-ad-cs-certificate-authority-types";
|
||||
import { TCertificateAuthorityDALFactory } from "./certificate-authority-dal";
|
||||
import { CaType } from "./certificate-authority-enums";
|
||||
import {
|
||||
@@ -34,7 +42,7 @@ import { TInternalCertificateAuthorityServiceFactory } from "./internal/internal
|
||||
import { TCreateInternalCertificateAuthorityDTO } from "./internal/internal-certificate-authority-types";
|
||||
|
||||
type TCertificateAuthorityServiceFactoryDep = {
|
||||
appConnectionDAL: Pick<TAppConnectionDALFactory, "findById" | "update">;
|
||||
appConnectionDAL: Pick<TAppConnectionDALFactory, "findById" | "update" | "updateById">;
|
||||
appConnectionService: Pick<TAppConnectionServiceFactory, "connectAppConnectionById">;
|
||||
certificateAuthorityDAL: Pick<
|
||||
TCertificateAuthorityDALFactory,
|
||||
@@ -91,6 +99,19 @@ export const certificateAuthorityServiceFactory = ({
|
||||
projectDAL
|
||||
});
|
||||
|
||||
const azureAdCsFns = AzureAdCsCertificateAuthorityFns({
|
||||
appConnectionDAL,
|
||||
appConnectionService,
|
||||
certificateAuthorityDAL,
|
||||
externalCertificateAuthorityDAL,
|
||||
certificateDAL,
|
||||
certificateBodyDAL,
|
||||
certificateSecretDAL,
|
||||
kmsService,
|
||||
pkiSubscriberDAL,
|
||||
projectDAL
|
||||
});
|
||||
|
||||
const createCertificateAuthority = async (
|
||||
{ type, projectId, name, enableDirectIssuance, configuration, status }: TCreateCertificateAuthorityDTO,
|
||||
actor: OrgServiceActor
|
||||
@@ -146,6 +167,17 @@ export const certificateAuthorityServiceFactory = ({
|
||||
});
|
||||
}
|
||||
|
||||
if (type === CaType.AZURE_AD_CS) {
|
||||
return azureAdCsFns.createCertificateAuthority({
|
||||
name,
|
||||
projectId,
|
||||
configuration: configuration as TCreateAzureAdCsCertificateAuthorityDTO["configuration"],
|
||||
enableDirectIssuance,
|
||||
status,
|
||||
actor
|
||||
});
|
||||
}
|
||||
|
||||
throw new BadRequestError({ message: "Invalid certificate authority type" });
|
||||
};
|
||||
|
||||
@@ -205,6 +237,10 @@ export const certificateAuthorityServiceFactory = ({
|
||||
return castDbEntryToAcmeCertificateAuthority(certificateAuthority);
|
||||
}
|
||||
|
||||
if (type === CaType.AZURE_AD_CS) {
|
||||
return castDbEntryToAzureAdCsCertificateAuthority(certificateAuthority);
|
||||
}
|
||||
|
||||
throw new BadRequestError({ message: "Invalid certificate authority type" });
|
||||
};
|
||||
|
||||
@@ -249,6 +285,10 @@ export const certificateAuthorityServiceFactory = ({
|
||||
return acmeFns.listCertificateAuthorities({ projectId });
|
||||
}
|
||||
|
||||
if (type === CaType.AZURE_AD_CS) {
|
||||
return azureAdCsFns.listCertificateAuthorities({ projectId });
|
||||
}
|
||||
|
||||
throw new BadRequestError({ message: "Invalid certificate authority type" });
|
||||
};
|
||||
|
||||
@@ -323,6 +363,17 @@ export const certificateAuthorityServiceFactory = ({
|
||||
});
|
||||
}
|
||||
|
||||
if (type === CaType.AZURE_AD_CS) {
|
||||
return azureAdCsFns.updateCertificateAuthority({
|
||||
id: certificateAuthority.id,
|
||||
configuration: configuration as TUpdateAzureAdCsCertificateAuthorityDTO["configuration"],
|
||||
enableDirectIssuance,
|
||||
actor,
|
||||
status,
|
||||
name
|
||||
});
|
||||
}
|
||||
|
||||
throw new BadRequestError({ message: "Invalid certificate authority type" });
|
||||
};
|
||||
|
||||
@@ -384,14 +435,54 @@ export const certificateAuthorityServiceFactory = ({
|
||||
return castDbEntryToAcmeCertificateAuthority(certificateAuthority);
|
||||
}
|
||||
|
||||
if (type === CaType.AZURE_AD_CS) {
|
||||
return castDbEntryToAzureAdCsCertificateAuthority(certificateAuthority);
|
||||
}
|
||||
|
||||
throw new BadRequestError({ message: "Invalid certificate authority type" });
|
||||
};
|
||||
|
||||
const getAzureAdcsTemplates = async ({
|
||||
caId,
|
||||
projectId,
|
||||
actor,
|
||||
actorId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
}: {
|
||||
caId: string;
|
||||
projectId: string;
|
||||
actor: OrgServiceActor["type"];
|
||||
actorId: string;
|
||||
actorAuthMethod: OrgServiceActor["authMethod"];
|
||||
actorOrgId?: string;
|
||||
}) => {
|
||||
const { permission } = await permissionService.getProjectPermission({
|
||||
actor,
|
||||
actorId,
|
||||
projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId,
|
||||
actionProjectType: ActionProjectType.CertificateManager
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Read,
|
||||
ProjectPermissionSub.CertificateAuthorities
|
||||
);
|
||||
|
||||
return azureAdCsFns.getTemplates({
|
||||
caId,
|
||||
projectId
|
||||
});
|
||||
};
|
||||
|
||||
return {
|
||||
createCertificateAuthority,
|
||||
findCertificateAuthorityByNameAndProjectId,
|
||||
listCertificateAuthoritiesByProjectId,
|
||||
updateCertificateAuthority,
|
||||
deleteCertificateAuthority
|
||||
deleteCertificateAuthority,
|
||||
getAzureAdcsTemplates
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,13 +1,23 @@
|
||||
import { TAcmeCertificateAuthority, TAcmeCertificateAuthorityInput } from "./acme/acme-certificate-authority-types";
|
||||
import {
|
||||
TAzureAdCsCertificateAuthority,
|
||||
TCreateAzureAdCsCertificateAuthorityDTO
|
||||
} from "./azure-ad-cs/azure-ad-cs-certificate-authority-types";
|
||||
import { CaType } from "./certificate-authority-enums";
|
||||
import {
|
||||
TInternalCertificateAuthority,
|
||||
TInternalCertificateAuthorityInput
|
||||
} from "./internal/internal-certificate-authority-types";
|
||||
|
||||
export type TCertificateAuthority = TInternalCertificateAuthority | TAcmeCertificateAuthority;
|
||||
export type TCertificateAuthority =
|
||||
| TInternalCertificateAuthority
|
||||
| TAcmeCertificateAuthority
|
||||
| TAzureAdCsCertificateAuthority;
|
||||
|
||||
export type TCertificateAuthorityInput = TInternalCertificateAuthorityInput | TAcmeCertificateAuthorityInput;
|
||||
export type TCertificateAuthorityInput =
|
||||
| TInternalCertificateAuthorityInput
|
||||
| TAcmeCertificateAuthorityInput
|
||||
| TCreateAzureAdCsCertificateAuthorityDTO;
|
||||
|
||||
export type TCreateCertificateAuthorityDTO = Omit<TCertificateAuthority, "id">;
|
||||
|
||||
|
||||
@@ -36,12 +36,18 @@ import { validateAndMapAltNameType } from "../certificate-authority-validators";
|
||||
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">;
|
||||
|
||||
@@ -14,6 +14,8 @@ import { TCertificateBodyDALFactory } from "@app/services/certificate/certificat
|
||||
import { TCertificateDALFactory } from "@app/services/certificate/certificate-dal";
|
||||
import { TCertificateAuthorityCertDALFactory } from "@app/services/certificate-authority/certificate-authority-cert-dal";
|
||||
import { TCertificateAuthorityDALFactory } from "@app/services/certificate-authority/certificate-authority-dal";
|
||||
import { CaCapability, CaType } from "@app/services/certificate-authority/certificate-authority-enums";
|
||||
import { caSupportsCapability } from "@app/services/certificate-authority/certificate-authority-maps";
|
||||
import { TCertificateAuthoritySecretDALFactory } from "@app/services/certificate-authority/certificate-authority-secret-dal";
|
||||
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
||||
import { TPkiCollectionDALFactory } from "@app/services/pki-collection/pki-collection-dal";
|
||||
@@ -184,9 +186,11 @@ export const certificateServiceFactory = ({
|
||||
|
||||
const ca = await certificateAuthorityDAL.findByIdWithAssociatedCa(cert.caId);
|
||||
|
||||
if (ca.externalCa?.id) {
|
||||
// Check if the CA type supports revocation
|
||||
const caType = (ca.externalCa?.type as CaType) ?? CaType.INTERNAL;
|
||||
if (!caSupportsCapability(caType, CaCapability.REVOKE_CERTIFICATES)) {
|
||||
throw new BadRequestError({
|
||||
message: "Cannot revoke external certificates"
|
||||
message: "Certificate revocation is not supported by this certificate authority type"
|
||||
});
|
||||
}
|
||||
|
||||
@@ -218,18 +222,37 @@ export const certificateServiceFactory = ({
|
||||
}
|
||||
);
|
||||
|
||||
// rebuild CRL (TODO: move to interval-based cron job)
|
||||
await rebuildCaCrl({
|
||||
caId: ca.id,
|
||||
certificateAuthorityDAL,
|
||||
certificateAuthorityCrlDAL,
|
||||
certificateAuthoritySecretDAL,
|
||||
projectDAL,
|
||||
certificateDAL,
|
||||
kmsService
|
||||
});
|
||||
// Note: External CA revocation handling would go here for supported CA types
|
||||
// Currently, only internal CAs and ACME CAs support revocation
|
||||
|
||||
return { revokedAt, cert, ca: expandInternalCa(ca) };
|
||||
// rebuild CRL (TODO: move to interval-based cron job)
|
||||
// Only rebuild CRL for internal CAs - external CAs manage their own CRLs
|
||||
if (!ca.externalCa?.id) {
|
||||
await rebuildCaCrl({
|
||||
caId: ca.id,
|
||||
certificateAuthorityDAL,
|
||||
certificateAuthorityCrlDAL,
|
||||
certificateAuthoritySecretDAL,
|
||||
projectDAL,
|
||||
certificateDAL,
|
||||
kmsService
|
||||
});
|
||||
}
|
||||
|
||||
// Return appropriate CA format based on CA type
|
||||
const caResult = ca.externalCa?.id
|
||||
? {
|
||||
id: ca.id,
|
||||
name: ca.name,
|
||||
projectId: ca.projectId,
|
||||
status: ca.status,
|
||||
enableDirectIssuance: ca.enableDirectIssuance,
|
||||
type: ca.externalCa.type,
|
||||
externalCa: ca.externalCa
|
||||
}
|
||||
: expandInternalCa(ca);
|
||||
|
||||
return { revokedAt, cert, ca: caResult };
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -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": [
|
||||
|
||||
206
docs/documentation/platform/pki/azure-adcs.mdx
Normal file
@@ -0,0 +1,206 @@
|
||||
---
|
||||
title: "Certificates with Azure ADCS"
|
||||
description: "Learn how to issue and manage certificates using Microsoft Active Directory Certificate Services (ADCS) with Infisical."
|
||||
---
|
||||
|
||||
Issue and manage certificates using Microsoft Active Directory Certificate Services (ADCS) for enterprise-grade certificate management integrated with your existing Windows infrastructure.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
Before setting up ADCS integration, ensure you have:
|
||||
|
||||
- Microsoft Active Directory Certificate Services (ADCS) server running and accessible
|
||||
- Domain administrator account with certificate management permissions
|
||||
- ADCS web enrollment enabled on your server
|
||||
- Network connectivity from Infisical to the ADCS server
|
||||
- **IP whitelisting**: Your ADCS server must allow connections from Infisical's IP addresses
|
||||
- For Infisical Cloud instances, see [Networking Configuration](/documentation/setup/networking) for the list of IPs to whitelist
|
||||
- For self-hosted instances, whitelist your Infisical server's IP address
|
||||
- Azure ADCS app connection configured (see [Azure ADCS Connection](/integrations/app-connections/azure-adcs))
|
||||
|
||||
## Complete Workflow: From Setup to Certificate Issuance
|
||||
|
||||
This section walks you through the complete end-to-end process of setting up Azure ADCS integration and issuing your first certificate.
|
||||
|
||||
<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 Issuance Limitations
|
||||
|
||||
### Immediate Issuance Only
|
||||
|
||||
<Warning>
|
||||
**Manual Approval Not Supported**: Infisical currently supports only **immediate certificate issuance**. Certificates that require manual approval or are held by ADCS policies cannot be issued through Infisical yet.
|
||||
</Warning>
|
||||
|
||||
For successful certificate issuance, ensure your ADCS templates and policies are configured to:
|
||||
- **Auto-approve** certificate requests without manual intervention
|
||||
- **Not require** administrator approval for the templates you plan to use
|
||||
- **Allow** the connection account to request and receive certificates immediately
|
||||
|
||||
### What Happens with Manual Approval
|
||||
|
||||
If a certificate request requires manual approval:
|
||||
1. The request will be submitted to ADCS successfully
|
||||
2. Infisical will attempt to retrieve the certificate with exponential backoff (up to 5 retries over ~1 minute)
|
||||
3. If the certificate is not approved within this timeframe, the request will **fail**
|
||||
4. **No background polling**: Currently, Infisical does not check for certificates that might be approved hours or days later
|
||||
|
||||
<Info>
|
||||
**Future Enhancement**: Background polling for delayed certificate approvals is planned for future releases.
|
||||
</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
|
||||
|
||||
**Certificate Request Pending/Timeout**
|
||||
- Check if your ADCS template requires manual approval - Infisical only supports immediate issuance
|
||||
- Verify the certificate template is configured for auto-approval
|
||||
- Ensure your connection account has sufficient permissions to request certificates without approval
|
||||
- Review ADCS server policies that might be holding the certificate request
|
||||
|
||||
**Network Connectivity Issues**
|
||||
- Verify your ADCS server's firewall allows connections from Infisical
|
||||
- For Infisical Cloud: Ensure Infisical's IP addresses are whitelisted (see [Networking Configuration](/documentation/setup/networking))
|
||||
- For self-hosted: Whitelist your Infisical server's IP address on the ADCS server
|
||||
- Test HTTPS connectivity to the ADCS web enrollment endpoint
|
||||
- Check for any network security appliances blocking the connection
|
||||
|
||||
**Authentication Failures**
|
||||
- Verify ADCS connection credentials
|
||||
- Check domain account permissions
|
||||
- Ensure network connectivity to ADCS server
|
||||
|
||||
**SSL/TLS Certificate Errors**
|
||||
- For ADCS servers with self-signed or private certificates: disable "Reject Unauthorized" in the SSL tab of your Azure ADCS app connection, or provide the certificate in PEM format
|
||||
- Common SSL errors: `UNABLE_TO_VERIFY_LEAF_SIGNATURE`, `SELF_SIGNED_CERT_IN_CHAIN`, `CERT_HAS_EXPIRED`
|
||||
- The SSL configuration applies to all HTTPS communications between Infisical and your ADCS server
|
||||
- Only HTTPS URLs are supported - HTTP connections are not allowed for security reasons
|
||||
|
After Width: | Height: | Size: 600 KiB |
|
After Width: | Height: | Size: 448 KiB |
|
After Width: | Height: | Size: 449 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 |
70
docs/integrations/app-connections/azure-adcs.mdx
Normal file
@@ -0,0 +1,70 @@
|
||||
---
|
||||
title: "Azure ADCS Connection"
|
||||
description: "Learn how to configure an Azure ADCS Connection for Infisical certificate management."
|
||||
---
|
||||
|
||||
Connect Infisical to Microsoft Active Directory Certificate Services (ADCS) for automated certificate issuance and management.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Microsoft Active Directory Certificate Services (ADCS) server running and accessible
|
||||
- Domain administrator account with certificate management permissions
|
||||
- Network connectivity from Infisical to the ADCS server
|
||||
- ADCS web enrollment enabled on your server
|
||||
|
||||
## Connection Setup
|
||||
|
||||
<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">
|
||||
**Configuration Tab:**
|
||||
- **Name**: Friendly name for this ADCS connection (e.g., "Production ADCS")
|
||||
- **ADCS URL**: Your ADCS HTTPS URL (e.g., `https://adcs.yourdomain.com/certsrv`) - only HTTPS is supported
|
||||
- **Username**: Domain administrator username (format: `DOMAIN\username` or `username@domain.com`)
|
||||
- **Password**: Password for the domain administrator account
|
||||
|
||||
**SSL Tab:**
|
||||
- **SSL Certificate**: Optional PEM certificate for custom CA certificates or self-signed certificates
|
||||
- **Reject Unauthorized**: Whether to reject connections with invalid SSL certificates (it's recommended to keep it enabled)
|
||||
|
||||
<Note>
|
||||
**Getting the SSL Certificate**: If your ADCS server uses a self-signed certificate or a certificate from a custom CA, you can obtain the certificate using OpenSSL or PowerShell:
|
||||
|
||||
**Using OpenSSL:**
|
||||
```bash
|
||||
openssl s_client -connect your-adcs-server:443 -showcerts
|
||||
```
|
||||
|
||||
**Using PowerShell (from inside your ADCS Server):**
|
||||
```powershell
|
||||
Import-Module WebAdministration -ErrorAction SilentlyContinue
|
||||
$binding = Get-WebBinding -Protocol https -Port 443
|
||||
if ($binding -and $binding.certificateHash) {
|
||||
$cert = Get-ChildItem -Path "Cert:\LocalMachine\My\$($binding.certificateHash)"
|
||||
if ($cert) {
|
||||
$certBase64 = [System.Convert]::ToBase64String($cert.RawData)
|
||||
$certPem = "-----BEGIN CERTIFICATE-----`n" + ($certBase64 -replace '(.{64})', '$1`n') + "`n-----END CERTIFICATE-----"
|
||||
Write-Output $certPem
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Copy the certificate content (including `-----BEGIN CERTIFICATE-----` and `-----END CERTIFICATE-----` lines) and paste it into the SSL Certificate field.
|
||||
</Note>
|
||||
|
||||
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,9 +202,11 @@ export type TAppConnectionOption =
|
||||
| TZabbixConnectionOption
|
||||
| TRailwayConnectionOption
|
||||
| TChecklyConnectionOption
|
||||
| TSupabaseConnectionOption
|
||||
| TDigitalOceanConnectionOption
|
||||
| TNetlifyConnectionOption
|
||||
| TOktaConnectionOption;
|
||||
| TOktaConnectionOption
|
||||
| TAzureAdCsConnectionOption;
|
||||
|
||||
export type TAppConnectionOptionMap = {
|
||||
[AppConnection.AWS]: TAwsConnectionOption;
|
||||
@@ -238,4 +246,5 @@ export type TAppConnectionOptionMap = {
|
||||
[AppConnection.DigitalOcean]: TDigitalOceanConnectionOption;
|
||||
[AppConnection.Netlify]: TNetlifyConnectionOption;
|
||||
[AppConnection.Okta]: TOktaConnectionOption;
|
||||
[AppConnection.AzureADCS]: TAzureAdCsConnectionOption;
|
||||
};
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { AppConnection } from "../enums";
|
||||
import { TRootAppConnection } from "./root-connection";
|
||||
|
||||
export enum AzureADCSConnectionMethod {
|
||||
UsernamePassword = "username-password"
|
||||
}
|
||||
|
||||
export const CreateAzureADCSConnectionSchema = z.object({
|
||||
adcsUrl: z.string().url().min(1, "ADCS URL is required"),
|
||||
username: z.string().min(1, "Username is required"),
|
||||
password: z.string().min(1, "Password is required")
|
||||
});
|
||||
|
||||
export type TCreateAzureADCSConnection = z.infer<typeof CreateAzureADCSConnectionSchema>;
|
||||
|
||||
export type TAzureADCSConnection = TRootAppConnection & { app: AppConnection.AzureADCS } & {
|
||||
method: AzureADCSConnectionMethod.UsernamePassword;
|
||||
credentials: TCreateAzureADCSConnection;
|
||||
};
|
||||
@@ -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,7 @@
|
||||
import { AppConnection } from "../appConnections/enums";
|
||||
import { SshCaStatus } from "../sshCa";
|
||||
import { SshCertTemplateStatus } from "../sshCertificateTemplates";
|
||||
import { AcmeDnsProvider, CaStatus, InternalCaType } from "./enums";
|
||||
import { AcmeDnsProvider, CaCapability, CaStatus, CaType, InternalCaType } from "./enums";
|
||||
|
||||
export const caTypeToNameMap: { [K in InternalCaType]: string } = {
|
||||
[InternalCaType.ROOT]: "Root",
|
||||
@@ -24,6 +24,28 @@ export const ACME_DNS_PROVIDER_APP_CONNECTION_MAP: Record<AcmeDnsProvider, AppCo
|
||||
[AcmeDnsProvider.Cloudflare]: AppConnection.Cloudflare
|
||||
};
|
||||
|
||||
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]
|
||||
};
|
||||
|
||||
/**
|
||||
* 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,9 @@ export enum AcmeDnsProvider {
|
||||
ROUTE53 = "route53",
|
||||
Cloudflare = "cloudflare"
|
||||
}
|
||||
|
||||
export enum CaCapability {
|
||||
ISSUE_CERTIFICATES = "issue-certificates",
|
||||
REVOKE_CERTIFICATES = "revoke-certificates",
|
||||
RENEW_CERTIFICATES = "renew-certificates"
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ export {
|
||||
useUpdateCa
|
||||
} from "./mutations";
|
||||
export {
|
||||
useGetAzureAdcsTemplates,
|
||||
useGetCa,
|
||||
useGetCaById,
|
||||
useGetCaCert,
|
||||
@@ -17,5 +18,6 @@ export {
|
||||
useGetCaCrls,
|
||||
useGetCaCsr,
|
||||
useListCasByProjectId,
|
||||
useListCasByTypeAndProjectId
|
||||
useListCasByTypeAndProjectId,
|
||||
useListExternalCasByProjectId
|
||||
} from "./queries";
|
||||
|
||||
@@ -39,6 +39,10 @@ export const useUpdateCa = () => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: caKeys.getCaByNameAndProjectId(caName, projectId)
|
||||
});
|
||||
// Invalidate external CAs list
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: caKeys.listExternalCasByProjectId(projectId)
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
@@ -57,6 +61,10 @@ export const useCreateCa = () => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: caKeys.listCasByTypeAndProjectId(type, projectId)
|
||||
});
|
||||
// Invalidate external CAs list
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: caKeys.listExternalCasByProjectId(projectId)
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
@@ -79,6 +87,10 @@ export const useDeleteCa = () => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: caKeys.listCasByTypeAndProjectId(type, projectId)
|
||||
});
|
||||
// Invalidate external CAs list
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: caKeys.listExternalCasByProjectId(projectId)
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
@@ -4,20 +4,25 @@ import { apiRequest } from "@app/config/request";
|
||||
|
||||
import { TCertificateTemplate } from "../certificateTemplates/types";
|
||||
import { CaType } from "./enums";
|
||||
import { TCertificateAuthority, TUnifiedCertificateAuthority } from "./types";
|
||||
import { TAzureAdCsTemplate, TCertificateAuthority, TUnifiedCertificateAuthority } from "./types";
|
||||
|
||||
export const caKeys = {
|
||||
getCaById: (caId: string) => [{ caId }, "ca"],
|
||||
getCaByNameAndProjectId: (caName: string, projectId: string) => [{ caName, projectId }, "ca"],
|
||||
listCasByTypeAndProjectId: (type: CaType, projectId: string) => [{ type, projectId }, "cas"],
|
||||
listCasByProjectId: (projectId: string) => [{ projectId }, "cas"],
|
||||
listExternalCasByProjectId: (projectId: string) => [{ projectId }, "external-cas"],
|
||||
getCaCerts: (caId: string) => [{ caId }, "ca-cert"],
|
||||
getCaCrls: (caId: string) => [{ caId }, "ca-crls"],
|
||||
getCaCert: (caId: string) => [{ caId }, "ca-cert"],
|
||||
getCaCsr: (caId: string) => [{ caId }, "ca-csr"],
|
||||
getCaCrl: (caId: string) => [{ caId }, "ca-crl"],
|
||||
getCaCertTemplates: (caId: string) => [{ caId }, "ca-cert-templates"],
|
||||
getCaEstConfig: (caId: string) => [{ caId }, "ca-est-config"]
|
||||
getCaEstConfig: (caId: string) => [{ caId }, "ca-est-config"],
|
||||
getAzureAdcsTemplates: (caId: string, projectId: string) => [
|
||||
{ caId, projectId },
|
||||
"azure-adcs-templates"
|
||||
]
|
||||
};
|
||||
|
||||
export const useGetCa = ({
|
||||
@@ -67,6 +72,34 @@ export const useListCasByProjectId = (projectId: string) => {
|
||||
});
|
||||
};
|
||||
|
||||
export const useListExternalCasByProjectId = (projectId: string) => {
|
||||
return useQuery({
|
||||
queryKey: caKeys.listExternalCasByProjectId(projectId),
|
||||
queryFn: async () => {
|
||||
const [acmeResponse, azureAdCsResponse] = await Promise.allSettled([
|
||||
apiRequest.get<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 +189,22 @@ export const useGetCaCertTemplates = (caId: string) => {
|
||||
enabled: Boolean(caId)
|
||||
});
|
||||
};
|
||||
|
||||
export const useGetAzureAdcsTemplates = ({
|
||||
caId,
|
||||
projectId
|
||||
}: {
|
||||
caId: string;
|
||||
projectId: string;
|
||||
}) => {
|
||||
return useQuery({
|
||||
queryKey: caKeys.getAzureAdcsTemplates(caId, projectId),
|
||||
queryFn: async () => {
|
||||
const { data } = await apiRequest.get<{
|
||||
templates: TAzureAdCsTemplate[];
|
||||
}>(`/api/v1/pki/ca/azure-ad-cs/${caId}/templates?projectId=${projectId}`);
|
||||
return data;
|
||||
},
|
||||
enabled: Boolean(caId && projectId)
|
||||
});
|
||||
};
|
||||
|
||||
@@ -19,6 +19,19 @@ export type TAcmeCertificateAuthority = {
|
||||
};
|
||||
};
|
||||
|
||||
export type TAzureAdCsCertificateAuthority = {
|
||||
id: string;
|
||||
projectId: string;
|
||||
type: CaType.AZURE_AD_CS;
|
||||
status: CaStatus;
|
||||
name: string;
|
||||
enableDirectIssuance: boolean;
|
||||
configuration: {
|
||||
azureAdcsConnectionId: string;
|
||||
templateName: string;
|
||||
};
|
||||
};
|
||||
|
||||
export type TInternalCertificateAuthority = {
|
||||
id: string;
|
||||
projectId: string;
|
||||
@@ -48,6 +61,7 @@ export type TInternalCertificateAuthority = {
|
||||
|
||||
export type TUnifiedCertificateAuthority =
|
||||
| TAcmeCertificateAuthority
|
||||
| TAzureAdCsCertificateAuthority
|
||||
| TInternalCertificateAuthority;
|
||||
|
||||
export type TCreateCertificateAuthorityDTO = Omit<TUnifiedCertificateAuthority, "id">;
|
||||
@@ -114,6 +128,12 @@ export type TSignIntermediateResponse = {
|
||||
serialNumber: string;
|
||||
};
|
||||
|
||||
export type TAzureAdCsTemplate = {
|
||||
id: string;
|
||||
name: string;
|
||||
description?: string;
|
||||
};
|
||||
|
||||
export type TImportCaCertificateDTO = {
|
||||
caId: string;
|
||||
projectSlug: string;
|
||||
|
||||
@@ -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({
|
||||
type: z.nativeEnum(CaType),
|
||||
name: slugSchema({
|
||||
field: "Name"
|
||||
}),
|
||||
enableDirectIssuance: z.boolean(),
|
||||
status: z.nativeEnum(CaStatus),
|
||||
configuration: z.object({
|
||||
dnsAppConnection: z.object({
|
||||
id: z.string(),
|
||||
name: z.string()
|
||||
}),
|
||||
// currently specific to Route53 & Cloudflare but can be extended to others by differentiating via the provider property
|
||||
dnsProviderConfig: z.object({
|
||||
provider: z.nativeEnum(AcmeDnsProvider),
|
||||
hostedZoneId: z.string()
|
||||
}),
|
||||
directoryUrl: z.string(),
|
||||
accountEmail: z.string()
|
||||
})
|
||||
const baseSchema = z.object({
|
||||
type: z.nativeEnum(CaType),
|
||||
name: slugSchema({
|
||||
field: "Name"
|
||||
}),
|
||||
enableDirectIssuance: z.boolean(),
|
||||
status: z.nativeEnum(CaStatus)
|
||||
});
|
||||
|
||||
const acmeConfigurationSchema = z.object({
|
||||
dnsAppConnection: z.object({
|
||||
id: z.string(),
|
||||
name: z.string()
|
||||
}),
|
||||
dnsProviderConfig: z.object({
|
||||
provider: z.nativeEnum(AcmeDnsProvider),
|
||||
hostedZoneId: z.string()
|
||||
}),
|
||||
directoryUrl: z.string(),
|
||||
accountEmail: z.string()
|
||||
});
|
||||
|
||||
const azureAdCsConfigurationSchema = z.object({
|
||||
azureAdcsConnection: z.object({
|
||||
id: z.string(),
|
||||
name: z.string()
|
||||
})
|
||||
.required();
|
||||
});
|
||||
|
||||
const schema = z.discriminatedUnion("type", [
|
||||
baseSchema.extend({
|
||||
type: z.literal(CaType.ACME),
|
||||
configuration: acmeConfigurationSchema
|
||||
}),
|
||||
baseSchema.extend({
|
||||
type: z.literal(CaType.AZURE_AD_CS),
|
||||
configuration: azureAdCsConfigurationSchema
|
||||
})
|
||||
]);
|
||||
|
||||
export type FormData = z.infer<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,29 +113,54 @@ export const ExternalCaModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
formState: { isSubmitting },
|
||||
watch
|
||||
} = useForm<FormData>({
|
||||
resolver: zodResolver(schema),
|
||||
defaultValues: {
|
||||
type: CaType.ACME,
|
||||
name: "",
|
||||
status: CaStatus.ACTIVE,
|
||||
enableDirectIssuance: true,
|
||||
configuration: {
|
||||
dnsAppConnection: {
|
||||
id: "",
|
||||
name: ""
|
||||
},
|
||||
dnsProviderConfig: {
|
||||
provider: AcmeDnsProvider.ROUTE53,
|
||||
hostedZoneId: ""
|
||||
},
|
||||
directoryUrl: "",
|
||||
accountEmail: ""
|
||||
}
|
||||
}
|
||||
resolver: zodResolver(schema)
|
||||
});
|
||||
|
||||
const caType = watch("type");
|
||||
const dnsProvider = watch("configuration.dnsProviderConfig.provider");
|
||||
const configuration = watch("configuration");
|
||||
const dnsProvider =
|
||||
caType === CaType.ACME && configuration && "dnsProviderConfig" in configuration
|
||||
? configuration.dnsProviderConfig.provider
|
||||
: undefined;
|
||||
|
||||
useEffect(() => {
|
||||
const initialType = (popUp?.ca?.data as { type: CaType })?.type;
|
||||
if (!ca && popUp?.ca?.isOpen) {
|
||||
if (initialType === CaType.AZURE_AD_CS) {
|
||||
reset({
|
||||
type: CaType.AZURE_AD_CS,
|
||||
name: "",
|
||||
status: CaStatus.ACTIVE,
|
||||
enableDirectIssuance: false,
|
||||
configuration: {
|
||||
azureAdcsConnection: {
|
||||
id: "",
|
||||
name: ""
|
||||
}
|
||||
}
|
||||
});
|
||||
} else {
|
||||
reset({
|
||||
type: CaType.ACME,
|
||||
name: "",
|
||||
status: CaStatus.ACTIVE,
|
||||
enableDirectIssuance: true,
|
||||
configuration: {
|
||||
dnsAppConnection: {
|
||||
id: "",
|
||||
name: ""
|
||||
},
|
||||
dnsProviderConfig: {
|
||||
provider: AcmeDnsProvider.ROUTE53,
|
||||
hostedZoneId: ""
|
||||
},
|
||||
directoryUrl: "",
|
||||
accountEmail: ""
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}, [popUp?.ca?.isOpen, popUp?.ca?.data, reset, ca]);
|
||||
|
||||
const { data: availableRoute53Connections, isPending: isRoute53Pending } =
|
||||
useListAvailableAppConnections(AppConnection.AWS, {
|
||||
@@ -128,25 +172,44 @@ export const ExternalCaModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
enabled: caType === CaType.ACME
|
||||
});
|
||||
|
||||
const availableConnections: TAvailableAppConnection[] = useMemo(
|
||||
() => [...(availableRoute53Connections || []), ...(availableCloudflareConnections || [])],
|
||||
[availableRoute53Connections, availableCloudflareConnections]
|
||||
);
|
||||
const { data: availableAzureConnections, isPending: isAzurePending } =
|
||||
useListAvailableAppConnections(AppConnection.AzureADCS, {
|
||||
enabled: caType === CaType.AZURE_AD_CS
|
||||
});
|
||||
|
||||
const isPending = isRoute53Pending || isCloudflarePending;
|
||||
const availableConnections: TAvailableAppConnection[] = useMemo(() => {
|
||||
if (caType === CaType.ACME) {
|
||||
return [...(availableRoute53Connections || []), ...(availableCloudflareConnections || [])];
|
||||
}
|
||||
if (caType === CaType.AZURE_AD_CS) {
|
||||
return availableAzureConnections || [];
|
||||
}
|
||||
return [];
|
||||
}, [
|
||||
caType,
|
||||
availableRoute53Connections,
|
||||
availableCloudflareConnections,
|
||||
availableAzureConnections
|
||||
]);
|
||||
|
||||
const dnsAppConnection = watch("configuration.dnsAppConnection");
|
||||
const isPending = isRoute53Pending || isCloudflarePending || isAzurePending;
|
||||
|
||||
const dnsAppConnection =
|
||||
caType === CaType.ACME && configuration && "dnsAppConnection" in configuration
|
||||
? configuration.dnsAppConnection
|
||||
: { id: "", name: "" };
|
||||
|
||||
const { data: cloudflareZones = [], isPending: isZonesPending } =
|
||||
useCloudflareConnectionListZones(dnsAppConnection.id, {
|
||||
enabled: dnsProvider === AcmeDnsProvider.Cloudflare && !!dnsAppConnection.id
|
||||
});
|
||||
|
||||
// Populate form with CA data when editing
|
||||
useEffect(() => {
|
||||
if (ca) {
|
||||
if (ca.type !== CaType.INTERNAL && availableConnections?.length) {
|
||||
if (ca && !isCaLoading) {
|
||||
if (ca.type === CaType.ACME && availableConnections?.length) {
|
||||
const selectedConnection = availableConnections?.find(
|
||||
(connection) => connection.id === ca?.configuration.dnsAppConnectionId
|
||||
(connection) => connection.id === ca.configuration.dnsAppConnectionId
|
||||
);
|
||||
|
||||
reset({
|
||||
@@ -167,32 +230,63 @@ export const ExternalCaModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
accountEmail: ca.configuration.accountEmail
|
||||
}
|
||||
});
|
||||
} else if (ca.type === CaType.AZURE_AD_CS && availableConnections?.length) {
|
||||
const selectedConnection = availableConnections?.find(
|
||||
(connection) => connection.id === ca.configuration.azureAdcsConnectionId
|
||||
);
|
||||
|
||||
reset({
|
||||
type: ca.type,
|
||||
name: ca.name,
|
||||
status: ca.status,
|
||||
enableDirectIssuance: false,
|
||||
configuration: {
|
||||
azureAdcsConnection: {
|
||||
id: ca.configuration.azureAdcsConnectionId,
|
||||
name: selectedConnection?.name || ""
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}, [ca, availableConnections]);
|
||||
}, [ca, availableConnections, reset, isCaLoading]);
|
||||
|
||||
const onFormSubmit = async ({
|
||||
type,
|
||||
name,
|
||||
enableDirectIssuance,
|
||||
status,
|
||||
configuration
|
||||
configuration: formConfiguration
|
||||
}: FormData) => {
|
||||
try {
|
||||
if (!currentWorkspace?.slug) return;
|
||||
|
||||
if (ca && type !== CaType.INTERNAL) {
|
||||
let configPayload: any;
|
||||
|
||||
if (type === CaType.ACME && "dnsAppConnection" in formConfiguration) {
|
||||
configPayload = {
|
||||
dnsProviderConfig: formConfiguration.dnsProviderConfig,
|
||||
directoryUrl: formConfiguration.directoryUrl,
|
||||
accountEmail: formConfiguration.accountEmail,
|
||||
dnsAppConnectionId: formConfiguration.dnsAppConnection.id
|
||||
};
|
||||
} else if (type === CaType.AZURE_AD_CS && "azureAdcsConnection" in formConfiguration) {
|
||||
configPayload = {
|
||||
azureAdcsConnectionId: formConfiguration.azureAdcsConnection.id
|
||||
};
|
||||
} else {
|
||||
throw new Error("Invalid certificate authority configuration");
|
||||
}
|
||||
|
||||
if (ca) {
|
||||
await updateMutateAsync({
|
||||
caName: ca.name,
|
||||
projectId: currentWorkspace.id,
|
||||
name,
|
||||
type,
|
||||
status,
|
||||
enableDirectIssuance,
|
||||
configuration: {
|
||||
...configuration,
|
||||
dnsAppConnectionId: configuration.dnsAppConnection.id
|
||||
}
|
||||
enableDirectIssuance: type === CaType.AZURE_AD_CS ? false : enableDirectIssuance,
|
||||
configuration: configPayload
|
||||
});
|
||||
} else {
|
||||
await createMutateAsync({
|
||||
@@ -200,11 +294,8 @@ export const ExternalCaModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
name,
|
||||
type,
|
||||
status,
|
||||
enableDirectIssuance,
|
||||
configuration: {
|
||||
...configuration,
|
||||
dnsAppConnectionId: configuration.dnsAppConnection.id
|
||||
}
|
||||
enableDirectIssuance: type === CaType.AZURE_AD_CS ? false : enableDirectIssuance,
|
||||
configuration: configPayload
|
||||
});
|
||||
}
|
||||
|
||||
@@ -232,7 +323,7 @@ export const ExternalCaModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
handlePopUpToggle("ca", isOpen);
|
||||
}}
|
||||
>
|
||||
<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 +398,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,23 +501,52 @@ export const ExternalCaModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
<Controller
|
||||
control={control}
|
||||
name="enableDirectIssuance"
|
||||
render={({ field, fieldState: { error } }) => {
|
||||
return (
|
||||
<FormControl isError={Boolean(error)} errorText={error?.message} className="my-8">
|
||||
<Switch
|
||||
id="is-active"
|
||||
onCheckedChange={(value) => field.onChange(value)}
|
||||
isChecked={field.value}
|
||||
>
|
||||
<p className="w-full">Enable Direct Issuance</p>
|
||||
</Switch>
|
||||
{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"
|
||||
render={({ field, fieldState: { error } }) => {
|
||||
return (
|
||||
<FormControl isError={Boolean(error)} errorText={error?.message} className="my-8">
|
||||
<Switch
|
||||
id="is-active"
|
||||
onCheckedChange={(value) => field.onChange(value)}
|
||||
isChecked={field.value}
|
||||
>
|
||||
<p className="w-full">Enable Direct Issuance</p>
|
||||
</Switch>
|
||||
</FormControl>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<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,27 +172,43 @@ export const CertificatesTable = ({ handlePopUpOpen }: Props) => {
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionCertificateActions.Delete}
|
||||
a={ProjectPermissionSub.Certificates}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<DropdownMenuItem
|
||||
className={twMerge(
|
||||
!isAllowed && "pointer-events-none cursor-not-allowed opacity-50"
|
||||
)}
|
||||
onClick={async () =>
|
||||
handlePopUpOpen("revokeCertificate", {
|
||||
serialNumber: certificate.serialNumber
|
||||
})
|
||||
}
|
||||
disabled={!isAllowed}
|
||||
icon={<FontAwesomeIcon icon={faBan} />}
|
||||
{/* Only show revoke button if CA supports revocation */}
|
||||
{(() => {
|
||||
const caType = caCapabilityMap[certificate.caId];
|
||||
// If caId not found in map, assume CA supports revocation to avoid hiding revoke option
|
||||
const supportsRevocation =
|
||||
!caType ||
|
||||
caSupportsCapability(caType, CaCapability.REVOKE_CERTIFICATES);
|
||||
|
||||
if (!supportsRevocation) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionCertificateActions.Delete}
|
||||
a={ProjectPermissionSub.Certificates}
|
||||
>
|
||||
Revoke Certificate
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
{(isAllowed) => (
|
||||
<DropdownMenuItem
|
||||
className={twMerge(
|
||||
!isAllowed &&
|
||||
"pointer-events-none cursor-not-allowed opacity-50"
|
||||
)}
|
||||
onClick={async () =>
|
||||
handlePopUpOpen("revokeCertificate", {
|
||||
serialNumber: certificate.serialNumber
|
||||
})
|
||||
}
|
||||
disabled={!isAllowed}
|
||||
icon={<FontAwesomeIcon icon={faBan} />}
|
||||
>
|
||||
Revoke Certificate
|
||||
</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,39 +138,53 @@ export const PkiSubscriberCertificatesTable = ({ subscriberName, handlePopUpOpen
|
||||
: "-"}
|
||||
</Td>
|
||||
<Td className="flex justify-end">
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild className="rounded-lg">
|
||||
<div className="hover:text-primary-400 data-[state=open]:text-primary-400">
|
||||
<Tooltip content="More options">
|
||||
<FontAwesomeIcon size="lg" icon={faEllipsis} />
|
||||
</Tooltip>
|
||||
</div>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="start" className="p-1">
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionPkiSubscriberActions.Delete}
|
||||
a={ProjectPermissionSub.PkiSubscribers}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<DropdownMenuItem
|
||||
className={twMerge(
|
||||
!isAllowed && "pointer-events-none cursor-not-allowed opacity-50"
|
||||
)}
|
||||
onClick={() =>
|
||||
handlePopUpOpen &&
|
||||
handlePopUpOpen("revokeCertificate", {
|
||||
serialNumber: certificate.serialNumber
|
||||
})
|
||||
}
|
||||
disabled={!isAllowed}
|
||||
icon={<FontAwesomeIcon icon={faTrash} />}
|
||||
{(() => {
|
||||
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">
|
||||
<Tooltip content="More options">
|
||||
<FontAwesomeIcon size="lg" icon={faEllipsis} />
|
||||
</Tooltip>
|
||||
</div>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="start" className="p-1">
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionPkiSubscriberActions.Delete}
|
||||
a={ProjectPermissionSub.PkiSubscribers}
|
||||
>
|
||||
Revoke Certificate
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
{(isAllowed) => (
|
||||
<DropdownMenuItem
|
||||
className={twMerge(
|
||||
!isAllowed &&
|
||||
"pointer-events-none cursor-not-allowed opacity-50"
|
||||
)}
|
||||
onClick={() =>
|
||||
handlePopUpOpen &&
|
||||
handlePopUpOpen("revokeCertificate", {
|
||||
serialNumber: certificate.serialNumber
|
||||
})
|
||||
}
|
||||
disabled={!isAllowed}
|
||||
icon={<FontAwesomeIcon icon={faTrash} />}
|
||||
>
|
||||
Revoke Certificate
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
);
|
||||
})()}
|
||||
</Td>
|
||||
</Tr>
|
||||
);
|
||||
|
||||
@@ -26,6 +26,7 @@ import { useWorkspace } from "@app/context";
|
||||
import {
|
||||
CaType,
|
||||
useCreatePkiSubscriber,
|
||||
useGetAzureAdcsTemplates,
|
||||
useGetPkiSubscriber,
|
||||
useListCasByProjectId,
|
||||
useListWorkspacePkiSubscribers,
|
||||
@@ -77,7 +78,80 @@ 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 only
|
||||
azureTemplateType: z.string().optional(),
|
||||
organization: z
|
||||
.string()
|
||||
.trim()
|
||||
.max(64, "Organization cannot exceed 64 characters")
|
||||
.regex(
|
||||
/^[^,=+<>#;\\"/\r\n\t]*$/,
|
||||
'Organization contains invalid characters: , = + < > # ; \\ " / \\r \\n \\t'
|
||||
)
|
||||
.regex(
|
||||
/^[^\s\-_.]+.*[^\s\-_.]+$|^[^\s\-_.]{1}$/,
|
||||
"Organization cannot start or end with spaces, hyphens, underscores, or periods"
|
||||
)
|
||||
.optional()
|
||||
.or(z.literal("")),
|
||||
organizationalUnit: z
|
||||
.string()
|
||||
.trim()
|
||||
.max(64, "Organizational Unit cannot exceed 64 characters")
|
||||
.regex(
|
||||
/^[^,=+<>#;\\"/\r\n\t]*$/,
|
||||
'Organizational Unit contains invalid characters: , = + < > # ; \\ " / \\r \\n \\t'
|
||||
)
|
||||
.regex(
|
||||
/^[^\s\-_.]+.*[^\s\-_.]+$|^[^\s\-_.]{1}$/,
|
||||
"Organizational Unit cannot start or end with spaces, hyphens, underscores, or periods"
|
||||
)
|
||||
.optional()
|
||||
.or(z.literal("")),
|
||||
country: z
|
||||
.string()
|
||||
.trim()
|
||||
.length(2, "Country must be exactly 2 characters")
|
||||
.regex(/^[A-Z]{2}$/, "Country must be exactly 2 uppercase letters")
|
||||
.optional()
|
||||
.or(z.literal("")),
|
||||
state: z
|
||||
.string()
|
||||
.trim()
|
||||
.max(64, "State cannot exceed 64 characters")
|
||||
.regex(
|
||||
/^[^,=+<>#;\\"/\r\n\t]*$/,
|
||||
'State contains invalid characters: , = + < > # ; \\ " / \\r \\n \\t'
|
||||
)
|
||||
.regex(
|
||||
/^[^\s\-_.]+.*[^\s\-_.]+$|^[^\s\-_.]{1}$/,
|
||||
"State cannot start or end with spaces, hyphens, underscores, or periods"
|
||||
)
|
||||
.optional()
|
||||
.or(z.literal("")),
|
||||
locality: z
|
||||
.string()
|
||||
.trim()
|
||||
.max(64, "Locality cannot exceed 64 characters")
|
||||
.regex(
|
||||
/^[^,=+<>#;\\"/\r\n\t]*$/,
|
||||
'Locality contains invalid characters: , = + < > # ; \\ " / \\r \\n \\t'
|
||||
)
|
||||
.regex(
|
||||
/^[^\s\-_.]+.*[^\s\-_.]+$|^[^\s\-_.]{1}$/,
|
||||
"Locality cannot start or end with spaces, hyphens, underscores, or periods"
|
||||
)
|
||||
.optional()
|
||||
.or(z.literal("")),
|
||||
emailAddress: z
|
||||
.string()
|
||||
.trim()
|
||||
.email("Email Address must be a valid email format")
|
||||
.min(6, "Email Address must be at least 6 characters")
|
||||
.max(64, "Email Address cannot exceed 64 characters")
|
||||
.optional()
|
||||
.or(z.literal(""))
|
||||
})
|
||||
.required();
|
||||
|
||||
@@ -121,7 +195,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 +210,52 @@ 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
|
||||
});
|
||||
|
||||
// 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 +267,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,13 +299,28 @@ export const PkiSubscriberModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
return;
|
||||
}
|
||||
|
||||
const keyUsagesList = Object.entries(keyUsages)
|
||||
.filter(([, value]) => value)
|
||||
.map(([key]) => key as CertKeyUsage);
|
||||
// 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 extendedKeyUsagesList = Object.entries(extendedKeyUsages)
|
||||
.filter(([, value]) => value)
|
||||
.map(([key]) => key as CertExtendedKeyUsage);
|
||||
const keyUsagesList =
|
||||
selectedCa?.type === CaType.AZURE_AD_CS
|
||||
? []
|
||||
: Object.entries(keyUsages)
|
||||
.filter(([, value]) => value)
|
||||
.map(([key]) => key as CertKeyUsage);
|
||||
|
||||
const extendedKeyUsagesList =
|
||||
selectedCa?.type === CaType.AZURE_AD_CS
|
||||
? []
|
||||
: Object.entries(extendedKeyUsages)
|
||||
.filter(([, value]) => value)
|
||||
.map(([key]) => key as CertExtendedKeyUsage);
|
||||
|
||||
const subjectAlternativeNamesList = subjectAlternativeNames
|
||||
.split(",")
|
||||
@@ -222,6 +331,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 +354,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 +368,8 @@ export const PkiSubscriberModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
keyUsages: keyUsagesList,
|
||||
extendedKeyUsages: extendedKeyUsagesList,
|
||||
enableAutoRenewal,
|
||||
autoRenewalPeriodInDays
|
||||
autoRenewalPeriodInDays,
|
||||
properties: Object.keys(properties).length > 0 ? properties : undefined
|
||||
});
|
||||
}
|
||||
|
||||
@@ -276,17 +398,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 +435,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 +453,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 +508,120 @@ export const PkiSubscriberModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
|
||||
{/* Additional Subject Fields - Only for Azure ADCS CAs */}
|
||||
{selectedCa?.type === CaType.AZURE_AD_CS && (
|
||||
<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}
|
||||
tooltipText="Maximum 64 characters. No special DN characters allowed. Cannot start/end with spaces, hyphens, underscores, or periods."
|
||||
>
|
||||
<Input {...field} placeholder="Example Corp" maxLength={64} />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="organizationalUnit"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Organizational Unit (OU)"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
tooltipText="Maximum 64 characters. No special DN characters allowed. Cannot start/end with spaces, hyphens, underscores, or periods."
|
||||
>
|
||||
<Input {...field} placeholder="IT Department" maxLength={64} />
|
||||
</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}
|
||||
tooltipText="Exactly 2 uppercase letters (ISO 3166-1 alpha-2 code). Examples: US, CA, GB, DE"
|
||||
>
|
||||
<Input
|
||||
{...field}
|
||||
placeholder="US"
|
||||
maxLength={2}
|
||||
style={{ textTransform: "uppercase" }}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="state"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="State/Province (ST)"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
tooltipText="Maximum 64 characters. No special DN characters allowed. Cannot start/end with spaces, hyphens, underscores, or periods."
|
||||
>
|
||||
<Input {...field} placeholder="California" maxLength={64} />
|
||||
</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}
|
||||
tooltipText="Maximum 64 characters. No special DN characters allowed. Cannot start/end with spaces, hyphens, underscores, or periods."
|
||||
>
|
||||
<Input {...field} placeholder="San Francisco" maxLength={64} />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="emailAddress"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Email Address"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
tooltipText="Valid email format, 6-64 characters. Example: admin@example.com"
|
||||
>
|
||||
<Input
|
||||
{...field}
|
||||
type="email"
|
||||
placeholder="admin@example.com"
|
||||
maxLength={64}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
</Accordion>
|
||||
)}
|
||||
|
||||
{selectedCa?.type !== CaType.ACME && (
|
||||
<Controller
|
||||
control={control}
|
||||
@@ -385,7 +638,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 +770,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,294 @@
|
||||
import { useState } from "react";
|
||||
import { Controller, FormProvider, useForm } from "react-hook-form";
|
||||
import { faQuestionCircle } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { Tab } from "@headlessui/react";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { z } from "zod";
|
||||
|
||||
import {
|
||||
Button,
|
||||
FormControl,
|
||||
Input,
|
||||
ModalClose,
|
||||
SecretInput,
|
||||
Select,
|
||||
SelectItem,
|
||||
Switch,
|
||||
TextArea,
|
||||
Tooltip
|
||||
} 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()
|
||||
.trim()
|
||||
.min(1, "ADCS URL required")
|
||||
.refine((value) => value.startsWith("https://"), "ADCS URL must use HTTPS"),
|
||||
username: z.string().trim().min(1, "Username required"),
|
||||
password: z.string().trim().min(1, "Password required"),
|
||||
sslRejectUnauthorized: z.boolean().optional(),
|
||||
sslCertificate: z
|
||||
.string()
|
||||
.trim()
|
||||
.transform((value) => value || undefined)
|
||||
.optional()
|
||||
})
|
||||
})
|
||||
]);
|
||||
|
||||
type FormData = z.infer<typeof formSchema>;
|
||||
|
||||
export const AzureADCSConnectionForm = ({ appConnection, onSubmit }: Props) => {
|
||||
const isUpdate = Boolean(appConnection);
|
||||
const [selectedTabIndex, setSelectedTabIndex] = useState(0);
|
||||
|
||||
const form = useForm<FormData>({
|
||||
resolver: zodResolver(formSchema),
|
||||
defaultValues: appConnection ?? {
|
||||
app: AppConnection.AzureADCS,
|
||||
method: AzureADCSConnectionMethod.UsernamePassword,
|
||||
name: "",
|
||||
description: "",
|
||||
credentials: {
|
||||
adcsUrl: "",
|
||||
username: "",
|
||||
password: "",
|
||||
sslRejectUnauthorized: true,
|
||||
sslCertificate: undefined
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const {
|
||||
handleSubmit,
|
||||
control,
|
||||
formState: { isSubmitting, isDirty },
|
||||
watch
|
||||
} = form;
|
||||
|
||||
const sslEnabled = watch("credentials.adcsUrl")?.startsWith("https://") ?? false;
|
||||
|
||||
return (
|
||||
<FormProvider {...form}>
|
||||
<form
|
||||
onSubmit={(e) => {
|
||||
setSelectedTabIndex(0);
|
||||
handleSubmit(onSubmit)(e);
|
||||
}}
|
||||
>
|
||||
{!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>
|
||||
)}
|
||||
/>
|
||||
<Tab.Group selectedIndex={selectedTabIndex} onChange={setSelectedTabIndex}>
|
||||
<Tab.List
|
||||
className={`-pb-1 ${selectedTabIndex === 1 ? "mb-3" : "mb-6"} w-full border-b-2 border-mineshaft-600`}
|
||||
>
|
||||
<Tab
|
||||
className={({ selected }) =>
|
||||
`w-30 -mb-[0.14rem] px-4 py-2 text-sm font-medium outline-none disabled:opacity-60 ${
|
||||
selected
|
||||
? "border-b-2 border-mineshaft-300 text-mineshaft-200"
|
||||
: "text-bunker-300"
|
||||
}`
|
||||
}
|
||||
>
|
||||
Configuration
|
||||
</Tab>
|
||||
<Tab
|
||||
className={({ selected }) =>
|
||||
`w-30 -mb-[0.14rem] px-4 py-2 text-sm font-medium outline-none disabled:opacity-60 ${
|
||||
selected
|
||||
? "border-b-2 border-mineshaft-300 text-mineshaft-200"
|
||||
: "text-bunker-300"
|
||||
}`
|
||||
}
|
||||
>
|
||||
SSL ({sslEnabled ? "Enabled" : "Disabled"})
|
||||
</Tab>
|
||||
</Tab.List>
|
||||
{selectedTabIndex === 1 && (
|
||||
<div className="mb-2 text-xs text-mineshaft-300">
|
||||
SSL configuration for HTTPS connections
|
||||
</div>
|
||||
)}
|
||||
<Tab.Panels className="mb-4 rounded border border-mineshaft-600 bg-mineshaft-700/70 p-3 pb-0">
|
||||
<Tab.Panel>
|
||||
<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/certsrv" />
|
||||
</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 or user@domain.com" />
|
||||
</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>
|
||||
</Tab.Panel>
|
||||
<Tab.Panel>
|
||||
<Controller
|
||||
name="credentials.sslCertificate"
|
||||
control={control}
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
errorText={error?.message}
|
||||
isError={Boolean(error?.message)}
|
||||
className={sslEnabled ? "" : "opacity-50"}
|
||||
label="SSL Certificate"
|
||||
isOptional
|
||||
>
|
||||
<TextArea
|
||||
className="h-[3.6rem] !resize-none"
|
||||
{...field}
|
||||
isDisabled={!sslEnabled}
|
||||
placeholder="-----BEGIN CERTIFICATE-----
|
||||
...
|
||||
-----END CERTIFICATE-----"
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
name="credentials.sslRejectUnauthorized"
|
||||
control={control}
|
||||
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
className={sslEnabled ? "" : "opacity-50"}
|
||||
isError={Boolean(error?.message)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Switch
|
||||
className="bg-mineshaft-400/50 shadow-inner data-[state=checked]:bg-green/80"
|
||||
id="ssl-reject-unauthorized"
|
||||
thumbClassName="bg-mineshaft-800"
|
||||
isChecked={sslEnabled ? value : false}
|
||||
onCheckedChange={onChange}
|
||||
isDisabled={!sslEnabled}
|
||||
>
|
||||
<p className="w-[9.5rem]">
|
||||
Reject Unauthorized
|
||||
<Tooltip
|
||||
className="max-w-md"
|
||||
content={
|
||||
<p>
|
||||
If enabled, Infisical will only connect to the ADCS server if it has a
|
||||
valid, trusted SSL certificate. Disable only in test environments with
|
||||
self-signed certificates.
|
||||
</p>
|
||||
}
|
||||
>
|
||||
<FontAwesomeIcon icon={faQuestionCircle} size="sm" className="ml-1" />
|
||||
</Tooltip>
|
||||
</p>
|
||||
</Switch>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</Tab.Panel>
|
||||
</Tab.Panels>
|
||||
</Tab.Group>
|
||||
<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>
|
||||
);
|
||||
};
|
||||