Compare commits
39 Commits
infisical/
...
infisical/
Author | SHA1 | Date | |
---|---|---|---|
79680b6a73 | |||
58838c541f | |||
03cc71cfed | |||
02529106c9 | |||
d939ff289d | |||
d1816c3051 | |||
cb350788c0 | |||
cd58768d6f | |||
dcd6f4d55d | |||
3c828614b8 | |||
09e7988596 | |||
f40df19334 | |||
76c9d3488b | |||
0809da33e0 | |||
00f86cfd00 | |||
f560534493 | |||
1317266415 | |||
288d7e88ae | |||
f88389bf9e | |||
2e88c5e2c5 | |||
73f3b8173e | |||
aa5b88ff04 | |||
b7caff88cf | |||
760a1e917a | |||
2d7ff66246 | |||
179497e830 | |||
4c08c80e5b | |||
7d6af64904 | |||
16519f9486 | |||
bb27d38a12 | |||
5b26928751 | |||
f425e7e48f | |||
4601f46afb | |||
692bdc060c | |||
3a4f8c2e54 | |||
146c4284a2 | |||
5ae33b9f3b | |||
1f38b92ec6 | |||
f2a49a79f0 |
@ -70,3 +70,5 @@ NEXT_PUBLIC_CAPTCHA_SITE_KEY=
|
||||
|
||||
PLAIN_API_KEY=
|
||||
PLAIN_WISH_LABEL_IDS=
|
||||
|
||||
SSL_CLIENT_CERTIFICATE_HEADER_KEY=
|
||||
|
42
backend/package-lock.json
generated
@ -74,6 +74,7 @@
|
||||
"pg-query-stream": "^4.5.3",
|
||||
"picomatch": "^3.0.1",
|
||||
"pino": "^8.16.2",
|
||||
"pkijs": "^3.2.4",
|
||||
"posthog-node": "^3.6.2",
|
||||
"probot": "^13.0.0",
|
||||
"safe-regex": "^2.1.1",
|
||||
@ -4457,6 +4458,17 @@
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/@noble/hashes": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.4.0.tgz",
|
||||
"integrity": "sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg==",
|
||||
"engines": {
|
||||
"node": ">= 16"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://paulmillr.com/funding/"
|
||||
}
|
||||
},
|
||||
"node_modules/@node-saml/node-saml": {
|
||||
"version": "4.0.5",
|
||||
"resolved": "https://registry.npmjs.org/@node-saml/node-saml/-/node-saml-4.0.5.tgz",
|
||||
@ -8481,6 +8493,14 @@
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/bytestreamjs": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/bytestreamjs/-/bytestreamjs-2.0.1.tgz",
|
||||
"integrity": "sha512-U1Z/ob71V/bXfVABvNr/Kumf5VyeQRBEm6Txb0PQ6S7V5GpBM3w4Cbqz/xPDicR5tN0uvDifng8C+5qECeGwyQ==",
|
||||
"engines": {
|
||||
"node": ">=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/cac": {
|
||||
"version": "6.7.14",
|
||||
"resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz",
|
||||
@ -14120,6 +14140,22 @@
|
||||
"pathe": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/pkijs": {
|
||||
"version": "3.2.4",
|
||||
"resolved": "https://registry.npmjs.org/pkijs/-/pkijs-3.2.4.tgz",
|
||||
"integrity": "sha512-Et9V5QpvBilPFgagJcaKBqXjKrrgF5JL2mSDELk1vvbOTt4fuBhSSsGn9Tcz0TQTfS5GCpXQ31Whrpqeqp0VRg==",
|
||||
"dependencies": {
|
||||
"@noble/hashes": "^1.4.0",
|
||||
"asn1js": "^3.0.5",
|
||||
"bytestreamjs": "^2.0.0",
|
||||
"pvtsutils": "^1.3.2",
|
||||
"pvutils": "^1.1.3",
|
||||
"tslib": "^2.6.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/plimit-lit": {
|
||||
"version": "1.6.1",
|
||||
"resolved": "https://registry.npmjs.org/plimit-lit/-/plimit-lit-1.6.1.tgz",
|
||||
@ -16268,9 +16304,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/tslib": {
|
||||
"version": "2.6.2",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz",
|
||||
"integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q=="
|
||||
"version": "2.6.3",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.3.tgz",
|
||||
"integrity": "sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ=="
|
||||
},
|
||||
"node_modules/tsup": {
|
||||
"version": "8.0.1",
|
||||
|
@ -171,6 +171,7 @@
|
||||
"pg-query-stream": "^4.5.3",
|
||||
"picomatch": "^3.0.1",
|
||||
"pino": "^8.16.2",
|
||||
"pkijs": "^3.2.4",
|
||||
"posthog-node": "^3.6.2",
|
||||
"probot": "^13.0.0",
|
||||
"safe-regex": "^2.1.1",
|
||||
|
2
backend/src/@types/fastify.d.ts
vendored
@ -36,6 +36,7 @@ import { ActorAuthMethod, ActorType } from "@app/services/auth/auth-type";
|
||||
import { TAuthTokenServiceFactory } from "@app/services/auth-token/auth-token-service";
|
||||
import { TCertificateServiceFactory } from "@app/services/certificate/certificate-service";
|
||||
import { TCertificateAuthorityServiceFactory } from "@app/services/certificate-authority/certificate-authority-service";
|
||||
import { TCertificateEstServiceFactory } from "@app/services/certificate-est/certificate-est-service";
|
||||
import { TCertificateTemplateServiceFactory } from "@app/services/certificate-template/certificate-template-service";
|
||||
import { TGroupProjectServiceFactory } from "@app/services/group-project/group-project-service";
|
||||
import { TIdentityServiceFactory } from "@app/services/identity/identity-service";
|
||||
@ -160,6 +161,7 @@ declare module "fastify" {
|
||||
certificateTemplate: TCertificateTemplateServiceFactory;
|
||||
certificateAuthority: TCertificateAuthorityServiceFactory;
|
||||
certificateAuthorityCrl: TCertificateAuthorityCrlServiceFactory;
|
||||
certificateEst: TCertificateEstServiceFactory;
|
||||
pkiCollection: TPkiCollectionServiceFactory;
|
||||
secretScanning: TSecretScanningServiceFactory;
|
||||
license: TLicenseServiceFactory;
|
||||
|
8
backend/src/@types/knex.d.ts
vendored
@ -53,6 +53,9 @@ import {
|
||||
TCertificateSecretsUpdate,
|
||||
TCertificatesInsert,
|
||||
TCertificatesUpdate,
|
||||
TCertificateTemplateEstConfigs,
|
||||
TCertificateTemplateEstConfigsInsert,
|
||||
TCertificateTemplateEstConfigsUpdate,
|
||||
TCertificateTemplates,
|
||||
TCertificateTemplatesInsert,
|
||||
TCertificateTemplatesUpdate,
|
||||
@ -372,6 +375,11 @@ declare module "knex/types/tables" {
|
||||
TCertificateTemplatesInsert,
|
||||
TCertificateTemplatesUpdate
|
||||
>;
|
||||
[TableName.CertificateTemplateEstConfig]: KnexOriginal.CompositeTableType<
|
||||
TCertificateTemplateEstConfigs,
|
||||
TCertificateTemplateEstConfigsInsert,
|
||||
TCertificateTemplateEstConfigsUpdate
|
||||
>;
|
||||
[TableName.CertificateBody]: KnexOriginal.CompositeTableType<
|
||||
TCertificateBodies,
|
||||
TCertificateBodiesInsert,
|
||||
|
@ -0,0 +1,26 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TableName } from "../schemas";
|
||||
import { createOnUpdateTrigger, dropOnUpdateTrigger } from "../utils";
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
const hasEstConfigTable = await knex.schema.hasTable(TableName.CertificateTemplateEstConfig);
|
||||
if (!hasEstConfigTable) {
|
||||
await knex.schema.createTable(TableName.CertificateTemplateEstConfig, (tb) => {
|
||||
tb.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
|
||||
tb.uuid("certificateTemplateId").notNullable().unique();
|
||||
tb.foreign("certificateTemplateId").references("id").inTable(TableName.CertificateTemplate).onDelete("CASCADE");
|
||||
tb.binary("encryptedCaChain").notNullable();
|
||||
tb.string("hashedPassphrase").notNullable();
|
||||
tb.boolean("isEnabled").notNullable();
|
||||
tb.timestamps(true, true, true);
|
||||
});
|
||||
|
||||
await createOnUpdateTrigger(knex, TableName.CertificateTemplateEstConfig);
|
||||
}
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
await knex.schema.dropTableIfExists(TableName.CertificateTemplateEstConfig);
|
||||
await dropOnUpdateTrigger(knex, TableName.CertificateTemplateEstConfig);
|
||||
}
|
29
backend/src/db/schemas/certificate-template-est-configs.ts
Normal file
@ -0,0 +1,29 @@
|
||||
// Code generated by automation script, DO NOT EDIT.
|
||||
// Automated by pulling database and generating zod schema
|
||||
// To update. Just run npm run generate:schema
|
||||
// Written by akhilmhdh.
|
||||
|
||||
import { z } from "zod";
|
||||
|
||||
import { zodBuffer } from "@app/lib/zod";
|
||||
|
||||
import { TImmutableDBKeys } from "./models";
|
||||
|
||||
export const CertificateTemplateEstConfigsSchema = z.object({
|
||||
id: z.string().uuid(),
|
||||
certificateTemplateId: z.string().uuid(),
|
||||
encryptedCaChain: zodBuffer,
|
||||
hashedPassphrase: z.string(),
|
||||
isEnabled: z.boolean(),
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date()
|
||||
});
|
||||
|
||||
export type TCertificateTemplateEstConfigs = z.infer<typeof CertificateTemplateEstConfigsSchema>;
|
||||
export type TCertificateTemplateEstConfigsInsert = Omit<
|
||||
z.input<typeof CertificateTemplateEstConfigsSchema>,
|
||||
TImmutableDBKeys
|
||||
>;
|
||||
export type TCertificateTemplateEstConfigsUpdate = Partial<
|
||||
Omit<z.input<typeof CertificateTemplateEstConfigsSchema>, TImmutableDBKeys>
|
||||
>;
|
@ -14,6 +14,7 @@ export * from "./certificate-authority-crl";
|
||||
export * from "./certificate-authority-secret";
|
||||
export * from "./certificate-bodies";
|
||||
export * from "./certificate-secrets";
|
||||
export * from "./certificate-template-est-configs";
|
||||
export * from "./certificate-templates";
|
||||
export * from "./certificates";
|
||||
export * from "./dynamic-secret-leases";
|
||||
|
@ -3,6 +3,7 @@ import { z } from "zod";
|
||||
export enum TableName {
|
||||
Users = "users",
|
||||
CertificateAuthority = "certificate_authorities",
|
||||
CertificateTemplateEstConfig = "certificate_template_est_configs",
|
||||
CertificateAuthorityCert = "certificate_authority_certs",
|
||||
CertificateAuthoritySecret = "certificate_authority_secret",
|
||||
CertificateAuthorityCrl = "certificate_authority_crl",
|
||||
|
@ -9,7 +9,10 @@ export const registerScimRouter = async (server: FastifyZodProvider) => {
|
||||
server.addContentTypeParser("application/scim+json", { parseAs: "string" }, (_, body, done) => {
|
||||
try {
|
||||
const strBody = body instanceof Buffer ? body.toString() : body;
|
||||
|
||||
if (!strBody) {
|
||||
done(null, undefined);
|
||||
return;
|
||||
}
|
||||
const json: unknown = JSON.parse(strBody);
|
||||
done(null, json);
|
||||
} catch (err) {
|
||||
@ -474,18 +477,18 @@ export const registerScimRouter = async (server: FastifyZodProvider) => {
|
||||
Operations: z.array(
|
||||
z.union([
|
||||
z.object({
|
||||
op: z.literal("replace"),
|
||||
op: z.union([z.literal("replace"), z.literal("Replace")]),
|
||||
value: z.object({
|
||||
id: z.string().trim(),
|
||||
displayName: z.string().trim()
|
||||
})
|
||||
}),
|
||||
z.object({
|
||||
op: z.literal("remove"),
|
||||
op: z.union([z.literal("remove"), z.literal("Remove")]),
|
||||
path: z.string().trim()
|
||||
}),
|
||||
z.object({
|
||||
op: z.literal("add"),
|
||||
op: z.union([z.literal("add"), z.literal("Add")]),
|
||||
path: z.string().trim(),
|
||||
value: z.array(
|
||||
z.object({
|
||||
|
@ -166,7 +166,10 @@ export enum EventType {
|
||||
CREATE_CERTIFICATE_TEMPLATE = "create-certificate-template",
|
||||
UPDATE_CERTIFICATE_TEMPLATE = "update-certificate-template",
|
||||
DELETE_CERTIFICATE_TEMPLATE = "delete-certificate-template",
|
||||
GET_CERTIFICATE_TEMPLATE = "get-certificate-template"
|
||||
GET_CERTIFICATE_TEMPLATE = "get-certificate-template",
|
||||
CREATE_CERTIFICATE_TEMPLATE_EST_CONFIG = "create-certificate-template-est-config",
|
||||
UPDATE_CERTIFICATE_TEMPLATE_EST_CONFIG = "update-certificate-template-est-config",
|
||||
GET_CERTIFICATE_TEMPLATE_EST_CONFIG = "get-certificate-template-est-config"
|
||||
}
|
||||
|
||||
interface UserActorMetadata {
|
||||
@ -1420,6 +1423,29 @@ interface OrgAdminAccessProjectEvent {
|
||||
}; // no metadata yet
|
||||
}
|
||||
|
||||
interface CreateCertificateTemplateEstConfig {
|
||||
type: EventType.CREATE_CERTIFICATE_TEMPLATE_EST_CONFIG;
|
||||
metadata: {
|
||||
certificateTemplateId: string;
|
||||
isEnabled: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
interface UpdateCertificateTemplateEstConfig {
|
||||
type: EventType.UPDATE_CERTIFICATE_TEMPLATE_EST_CONFIG;
|
||||
metadata: {
|
||||
certificateTemplateId: string;
|
||||
isEnabled: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
interface GetCertificateTemplateEstConfig {
|
||||
type: EventType.GET_CERTIFICATE_TEMPLATE_EST_CONFIG;
|
||||
metadata: {
|
||||
certificateTemplateId: string;
|
||||
};
|
||||
}
|
||||
|
||||
export type Event =
|
||||
| GetSecretsEvent
|
||||
| GetSecretEvent
|
||||
@ -1547,4 +1573,7 @@ export type Event =
|
||||
| CreateCertificateTemplate
|
||||
| UpdateCertificateTemplate
|
||||
| GetCertificateTemplate
|
||||
| DeleteCertificateTemplate;
|
||||
| DeleteCertificateTemplate
|
||||
| CreateCertificateTemplateEstConfig
|
||||
| UpdateCertificateTemplateEstConfig
|
||||
| GetCertificateTemplateEstConfig;
|
||||
|
@ -50,8 +50,8 @@ export const buildScimUser = ({
|
||||
orgMembershipId: string;
|
||||
username: string;
|
||||
email?: string | null;
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
firstName: string | null | undefined;
|
||||
lastName: string | null | undefined;
|
||||
groups?: {
|
||||
value: string;
|
||||
display: string;
|
||||
@ -64,9 +64,9 @@ export const buildScimUser = ({
|
||||
userName: username,
|
||||
displayName: `${firstName} ${lastName}`,
|
||||
name: {
|
||||
givenName: firstName,
|
||||
givenName: firstName || "",
|
||||
middleName: null,
|
||||
familyName: lastName
|
||||
familyName: lastName || ""
|
||||
},
|
||||
emails: email
|
||||
? [
|
||||
|
@ -267,8 +267,8 @@ export const scimServiceFactory = ({
|
||||
orgMembershipId: membership.id,
|
||||
username: membership.externalId ?? membership.username,
|
||||
email: membership.email ?? "",
|
||||
firstName: membership.firstName as string,
|
||||
lastName: membership.lastName as string,
|
||||
firstName: membership.firstName,
|
||||
lastName: membership.lastName,
|
||||
active: membership.isActive,
|
||||
groups: groupMembershipsInOrg.map((group) => ({
|
||||
value: group.groupId,
|
||||
@ -427,8 +427,8 @@ export const scimServiceFactory = ({
|
||||
return buildScimUser({
|
||||
orgMembershipId: createdOrgMembership.id,
|
||||
username: externalId,
|
||||
firstName: createdUser.firstName as string,
|
||||
lastName: createdUser.lastName as string,
|
||||
firstName: createdUser.firstName,
|
||||
lastName: createdUser.lastName,
|
||||
email: createdUser.email ?? "",
|
||||
active: createdOrgMembership.isActive
|
||||
});
|
||||
@ -483,8 +483,8 @@ export const scimServiceFactory = ({
|
||||
orgMembershipId: membership.id,
|
||||
username: membership.externalId ?? membership.username,
|
||||
email: membership.email,
|
||||
firstName: membership.firstName as string,
|
||||
lastName: membership.lastName as string,
|
||||
firstName: membership.firstName,
|
||||
lastName: membership.lastName,
|
||||
active
|
||||
});
|
||||
};
|
||||
@ -527,8 +527,8 @@ export const scimServiceFactory = ({
|
||||
orgMembershipId: membership.id,
|
||||
username: membership.externalId ?? membership.username,
|
||||
email: membership.email,
|
||||
firstName: membership.firstName as string,
|
||||
lastName: membership.lastName as string,
|
||||
firstName: membership.firstName,
|
||||
lastName: membership.lastName,
|
||||
active,
|
||||
groups: groupMembershipsInOrg.map((group) => ({
|
||||
value: group.groupId,
|
||||
@ -884,59 +884,50 @@ export const scimServiceFactory = ({
|
||||
}
|
||||
|
||||
for await (const operation of operations) {
|
||||
switch (operation.op) {
|
||||
case "replace": {
|
||||
group = await groupDAL.updateById(group.id, {
|
||||
name: operation.value.displayName
|
||||
if (operation.op === "replace" || operation.op === "Replace") {
|
||||
group = await groupDAL.updateById(group.id, {
|
||||
name: operation.value.displayName
|
||||
});
|
||||
} else if (operation.op === "add" || operation.op === "Add") {
|
||||
try {
|
||||
const orgMemberships = await orgMembershipDAL.find({
|
||||
$in: {
|
||||
id: operation.value.map((member) => member.value)
|
||||
}
|
||||
});
|
||||
break;
|
||||
}
|
||||
case "add": {
|
||||
try {
|
||||
const orgMemberships = await orgMembershipDAL.find({
|
||||
$in: {
|
||||
id: operation.value.map((member) => member.value)
|
||||
}
|
||||
});
|
||||
|
||||
await addUsersToGroupByUserIds({
|
||||
group,
|
||||
userIds: orgMemberships.map((membership) => membership.userId as string),
|
||||
userDAL,
|
||||
userGroupMembershipDAL,
|
||||
orgDAL,
|
||||
groupProjectDAL,
|
||||
projectKeyDAL,
|
||||
projectDAL,
|
||||
projectBotDAL
|
||||
});
|
||||
} catch {
|
||||
logger.info("Repeat SCIM user-group add operation");
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
case "remove": {
|
||||
const orgMembershipId = extractScimValueFromPath(operation.path);
|
||||
if (!orgMembershipId) throw new ScimRequestError({ detail: "Invalid path value", status: 400 });
|
||||
const orgMembership = await orgMembershipDAL.findById(orgMembershipId);
|
||||
if (!orgMembership) throw new ScimRequestError({ detail: "Org Membership Not Found", status: 400 });
|
||||
await removeUsersFromGroupByUserIds({
|
||||
await addUsersToGroupByUserIds({
|
||||
group,
|
||||
userIds: [orgMembership.userId as string],
|
||||
userIds: orgMemberships.map((membership) => membership.userId as string),
|
||||
userDAL,
|
||||
userGroupMembershipDAL,
|
||||
orgDAL,
|
||||
groupProjectDAL,
|
||||
projectKeyDAL
|
||||
});
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
throw new ScimRequestError({
|
||||
detail: "Invalid Operation",
|
||||
status: 400
|
||||
projectKeyDAL,
|
||||
projectDAL,
|
||||
projectBotDAL
|
||||
});
|
||||
} catch {
|
||||
logger.info("Repeat SCIM user-group add operation");
|
||||
}
|
||||
} else if (operation.op === "remove" || operation.op === "Remove") {
|
||||
const orgMembershipId = extractScimValueFromPath(operation.path);
|
||||
if (!orgMembershipId) throw new ScimRequestError({ detail: "Invalid path value", status: 400 });
|
||||
const orgMembership = await orgMembershipDAL.findById(orgMembershipId);
|
||||
if (!orgMembership) throw new ScimRequestError({ detail: "Org Membership Not Found", status: 400 });
|
||||
await removeUsersFromGroupByUserIds({
|
||||
group,
|
||||
userIds: [orgMembership.userId as string],
|
||||
userDAL,
|
||||
userGroupMembershipDAL,
|
||||
groupProjectDAL,
|
||||
projectKeyDAL
|
||||
});
|
||||
} else {
|
||||
throw new ScimRequestError({
|
||||
detail: "Invalid Operation",
|
||||
status: 400
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -110,8 +110,10 @@ export type TUpdateScimGroupNamePatchDTO = {
|
||||
operations: (TRemoveOp | TReplaceOp | TAddOp)[];
|
||||
};
|
||||
|
||||
// akhilmhdh: I know, this is done due to lack of time. Need to change later to support as normalized rather than like this
|
||||
// Forgive akhil blame tony
|
||||
type TReplaceOp = {
|
||||
op: "replace";
|
||||
op: "replace" | "Replace";
|
||||
value: {
|
||||
id: string;
|
||||
displayName: string;
|
||||
@ -119,12 +121,12 @@ type TReplaceOp = {
|
||||
};
|
||||
|
||||
type TRemoveOp = {
|
||||
op: "remove";
|
||||
op: "remove" | "Remove";
|
||||
path: string;
|
||||
};
|
||||
|
||||
type TAddOp = {
|
||||
op: "add";
|
||||
op: "add" | "Add";
|
||||
path: string;
|
||||
value: {
|
||||
value: string;
|
||||
|
@ -142,7 +142,8 @@ const envSchema = z
|
||||
CAPTCHA_SECRET: zpStr(z.string().optional()),
|
||||
PLAIN_API_KEY: zpStr(z.string().optional()),
|
||||
PLAIN_WISH_LABEL_IDS: zpStr(z.string().optional()),
|
||||
DISABLE_AUDIT_LOG_GENERATION: zodStrBool.default("false")
|
||||
DISABLE_AUDIT_LOG_GENERATION: zodStrBool.default("false"),
|
||||
SSL_CLIENT_CERTIFICATE_HEADER_KEY: zpStr(z.string().optional()).default("x-ssl-client-cert")
|
||||
})
|
||||
.transform((data) => ({
|
||||
...data,
|
||||
|
@ -57,7 +57,6 @@ const extractAuth = async (req: FastifyRequest, jwtSecret: string) => {
|
||||
return { authMode: AuthMode.API_KEY, token: apiKey, actor: ActorType.USER } as const;
|
||||
}
|
||||
const authHeader = req.headers?.authorization;
|
||||
|
||||
if (!authHeader) return { authMode: null, token: null };
|
||||
|
||||
const authTokenValue = authHeader.slice(7); // slice of after Bearer
|
||||
@ -103,12 +102,13 @@ export const injectIdentity = fp(async (server: FastifyZodProvider) => {
|
||||
server.decorateRequest("auth", null);
|
||||
server.addHook("onRequest", async (req) => {
|
||||
const appCfg = getConfig();
|
||||
const { authMode, token, actor } = await extractAuth(req, appCfg.AUTH_SECRET);
|
||||
|
||||
if (req.url.includes("/api/v3/auth/")) {
|
||||
if (req.url.includes(".well-known/est") || req.url.includes("/api/v3/auth/")) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { authMode, token, actor } = await extractAuth(req, appCfg.AUTH_SECRET);
|
||||
|
||||
if (!authMode) return;
|
||||
|
||||
switch (authMode) {
|
||||
|
173
backend/src/server/routes/est/certificate-est-router.ts
Normal file
@ -0,0 +1,173 @@
|
||||
import bcrypt from "bcrypt";
|
||||
import { z } from "zod";
|
||||
|
||||
import { getConfig } from "@app/lib/config/env";
|
||||
import { BadRequestError, UnauthorizedError } from "@app/lib/errors";
|
||||
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||
|
||||
export const registerCertificateEstRouter = async (server: FastifyZodProvider) => {
|
||||
const appCfg = getConfig();
|
||||
|
||||
// add support for CSR bodies
|
||||
server.addContentTypeParser("application/pkcs10", { parseAs: "string" }, (_, body, done) => {
|
||||
try {
|
||||
let csrBody = body as string;
|
||||
// some EST clients send CSRs in PEM format and some in base64 format
|
||||
// for CSRs sent in PEM, we leave them as is
|
||||
// for CSRs sent in base64, we preprocess them to remove new lines and spaces
|
||||
if (!csrBody.includes("BEGIN CERTIFICATE REQUEST")) {
|
||||
csrBody = csrBody.replace(/\n/g, "").replace(/ /g, "");
|
||||
}
|
||||
|
||||
done(null, csrBody);
|
||||
} catch (err) {
|
||||
const error = err as Error;
|
||||
done(error, undefined);
|
||||
}
|
||||
});
|
||||
|
||||
// Authenticate EST client using Passphrase
|
||||
server.addHook("onRequest", async (req, res) => {
|
||||
const { authorization } = req.headers;
|
||||
const urlFragments = req.url.split("/");
|
||||
|
||||
// cacerts endpoint should not have any authentication
|
||||
if (urlFragments[urlFragments.length - 1] === "cacerts") {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!authorization) {
|
||||
const wwwAuthenticateHeader = "WWW-Authenticate";
|
||||
const errAuthRequired = "Authentication required";
|
||||
|
||||
await res.hijack();
|
||||
|
||||
// definitive connection timeout to clean-up open connections and prevent memory leak
|
||||
res.raw.setTimeout(10 * 1000, () => {
|
||||
res.raw.end();
|
||||
});
|
||||
|
||||
res.raw.setHeader(wwwAuthenticateHeader, `Basic realm="infisical"`);
|
||||
res.raw.setHeader("Content-Length", 0);
|
||||
res.raw.statusCode = 401;
|
||||
|
||||
// Write the error message to the response without ending the connection
|
||||
res.raw.write(errAuthRequired);
|
||||
|
||||
// flush headers
|
||||
res.raw.flushHeaders();
|
||||
return;
|
||||
}
|
||||
|
||||
const certificateTemplateId = urlFragments.slice(-2)[0];
|
||||
const estConfig = await server.services.certificateTemplate.getEstConfiguration({
|
||||
isInternal: true,
|
||||
certificateTemplateId
|
||||
});
|
||||
|
||||
if (!estConfig.isEnabled) {
|
||||
throw new BadRequestError({
|
||||
message: "EST is disabled"
|
||||
});
|
||||
}
|
||||
|
||||
const rawCredential = authorization?.split(" ").pop();
|
||||
if (!rawCredential) {
|
||||
throw new UnauthorizedError({ message: "Missing HTTP credentials" });
|
||||
}
|
||||
|
||||
// expected format is user:password
|
||||
const basicCredential = atob(rawCredential);
|
||||
const password = basicCredential.split(":").pop();
|
||||
if (!password) {
|
||||
throw new BadRequestError({
|
||||
message: "No password provided"
|
||||
});
|
||||
}
|
||||
|
||||
const isPasswordValid = await bcrypt.compare(password, estConfig.hashedPassphrase);
|
||||
if (!isPasswordValid) {
|
||||
throw new UnauthorizedError({
|
||||
message: "Invalid credentials"
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "POST",
|
||||
url: "/:certificateTemplateId/simpleenroll",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
schema: {
|
||||
body: z.string().min(1),
|
||||
params: z.object({
|
||||
certificateTemplateId: z.string().min(1)
|
||||
}),
|
||||
response: {
|
||||
200: z.string()
|
||||
}
|
||||
},
|
||||
handler: async (req, res) => {
|
||||
void res.header("Content-Type", "application/pkcs7-mime; smime-type=certs-only");
|
||||
void res.header("Content-Transfer-Encoding", "base64");
|
||||
|
||||
return server.services.certificateEst.simpleEnroll({
|
||||
csr: req.body,
|
||||
certificateTemplateId: req.params.certificateTemplateId,
|
||||
sslClientCert: req.headers[appCfg.SSL_CLIENT_CERTIFICATE_HEADER_KEY] as string
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "POST",
|
||||
url: "/:certificateTemplateId/simplereenroll",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
schema: {
|
||||
body: z.string().min(1),
|
||||
params: z.object({
|
||||
certificateTemplateId: z.string().min(1)
|
||||
}),
|
||||
response: {
|
||||
200: z.string()
|
||||
}
|
||||
},
|
||||
handler: async (req, res) => {
|
||||
void res.header("Content-Type", "application/pkcs7-mime; smime-type=certs-only");
|
||||
void res.header("Content-Transfer-Encoding", "base64");
|
||||
|
||||
return server.services.certificateEst.simpleReenroll({
|
||||
csr: req.body,
|
||||
certificateTemplateId: req.params.certificateTemplateId,
|
||||
sslClientCert: req.headers[appCfg.SSL_CLIENT_CERTIFICATE_HEADER_KEY] as string
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: "/:certificateTemplateId/cacerts",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
params: z.object({
|
||||
certificateTemplateId: z.string().min(1)
|
||||
}),
|
||||
response: {
|
||||
200: z.string()
|
||||
}
|
||||
},
|
||||
handler: async (req, res) => {
|
||||
void res.header("Content-Type", "application/pkcs7-mime; smime-type=certs-only");
|
||||
void res.header("Content-Transfer-Encoding", "base64");
|
||||
|
||||
return server.services.certificateEst.getCaCerts({
|
||||
certificateTemplateId: req.params.certificateTemplateId
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
@ -90,7 +90,9 @@ import { certificateAuthorityDALFactory } from "@app/services/certificate-author
|
||||
import { certificateAuthorityQueueFactory } from "@app/services/certificate-authority/certificate-authority-queue";
|
||||
import { certificateAuthoritySecretDALFactory } from "@app/services/certificate-authority/certificate-authority-secret-dal";
|
||||
import { certificateAuthorityServiceFactory } from "@app/services/certificate-authority/certificate-authority-service";
|
||||
import { certificateEstServiceFactory } from "@app/services/certificate-est/certificate-est-service";
|
||||
import { certificateTemplateDALFactory } from "@app/services/certificate-template/certificate-template-dal";
|
||||
import { certificateTemplateEstConfigDALFactory } from "@app/services/certificate-template/certificate-template-est-config-dal";
|
||||
import { certificateTemplateServiceFactory } from "@app/services/certificate-template/certificate-template-service";
|
||||
import { groupProjectDALFactory } from "@app/services/group-project/group-project-dal";
|
||||
import { groupProjectMembershipRoleDALFactory } from "@app/services/group-project/group-project-membership-role-dal";
|
||||
@ -195,6 +197,7 @@ import { injectIdentity } from "../plugins/auth/inject-identity";
|
||||
import { injectPermission } from "../plugins/auth/inject-permission";
|
||||
import { injectRateLimits } from "../plugins/inject-rate-limits";
|
||||
import { registerSecretScannerGhApp } from "../plugins/secret-scanner";
|
||||
import { registerCertificateEstRouter } from "./est/certificate-est-router";
|
||||
import { registerV1Routes } from "./v1";
|
||||
import { registerV2Routes } from "./v2";
|
||||
import { registerV3Routes } from "./v3";
|
||||
@ -600,6 +603,7 @@ export const registerRoutes = async (
|
||||
const certificateAuthoritySecretDAL = certificateAuthoritySecretDALFactory(db);
|
||||
const certificateAuthorityCrlDAL = certificateAuthorityCrlDALFactory(db);
|
||||
const certificateTemplateDAL = certificateTemplateDALFactory(db);
|
||||
const certificateTemplateEstConfigDAL = certificateTemplateEstConfigDALFactory(db);
|
||||
|
||||
const certificateDAL = certificateDALFactory(db);
|
||||
const certificateBodyDAL = certificateBodyDALFactory(db);
|
||||
@ -657,8 +661,21 @@ export const registerRoutes = async (
|
||||
|
||||
const certificateTemplateService = certificateTemplateServiceFactory({
|
||||
certificateTemplateDAL,
|
||||
certificateTemplateEstConfigDAL,
|
||||
certificateAuthorityDAL,
|
||||
permissionService
|
||||
permissionService,
|
||||
kmsService,
|
||||
projectDAL
|
||||
});
|
||||
|
||||
const certificateEstService = certificateEstServiceFactory({
|
||||
certificateAuthorityService,
|
||||
certificateTemplateService,
|
||||
certificateTemplateDAL,
|
||||
certificateAuthorityCertDAL,
|
||||
certificateAuthorityDAL,
|
||||
projectDAL,
|
||||
kmsService
|
||||
});
|
||||
|
||||
const pkiAlertService = pkiAlertServiceFactory({
|
||||
@ -1196,6 +1213,7 @@ export const registerRoutes = async (
|
||||
certificateAuthority: certificateAuthorityService,
|
||||
certificateTemplate: certificateTemplateService,
|
||||
certificateAuthorityCrl: certificateAuthorityCrlService,
|
||||
certificateEst: certificateEstService,
|
||||
pkiAlert: pkiAlertService,
|
||||
pkiCollection: pkiCollectionService,
|
||||
secretScanning: secretScanningService,
|
||||
@ -1263,6 +1281,9 @@ export const registerRoutes = async (
|
||||
}
|
||||
});
|
||||
|
||||
// register special routes
|
||||
await server.register(registerCertificateEstRouter, { prefix: "/.well-known/est" });
|
||||
|
||||
// register routes for v1
|
||||
await server.register(
|
||||
async (v1Server) => {
|
||||
|
@ -669,6 +669,7 @@ export const registerCaRouter = async (server: FastifyZodProvider) => {
|
||||
handler: async (req) => {
|
||||
const { certificate, certificateChain, issuingCaCertificate, serialNumber, ca } =
|
||||
await server.services.certificateAuthority.signCertFromCa({
|
||||
isInternal: false,
|
||||
caId: req.params.caId,
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
@ -691,7 +692,7 @@ export const registerCaRouter = async (server: FastifyZodProvider) => {
|
||||
});
|
||||
|
||||
return {
|
||||
certificate,
|
||||
certificate: certificate.toString("pem"),
|
||||
certificateChain,
|
||||
issuingCaCertificate,
|
||||
serialNumber
|
||||
|
@ -210,6 +210,7 @@ export const registerCertRouter = async (server: FastifyZodProvider) => {
|
||||
handler: async (req) => {
|
||||
const { certificate, certificateChain, issuingCaCertificate, serialNumber, ca } =
|
||||
await server.services.certificateAuthority.signCertFromCa({
|
||||
isInternal: false,
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
@ -231,7 +232,7 @@ export const registerCertRouter = async (server: FastifyZodProvider) => {
|
||||
});
|
||||
|
||||
return {
|
||||
certificate,
|
||||
certificate: certificate.toString("pem"),
|
||||
certificateChain,
|
||||
issuingCaCertificate,
|
||||
serialNumber
|
||||
|
@ -1,6 +1,7 @@
|
||||
import ms from "ms";
|
||||
import { z } from "zod";
|
||||
|
||||
import { CertificateTemplateEstConfigsSchema } from "@app/db/schemas";
|
||||
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
||||
import { CERTIFICATE_TEMPLATES } from "@app/lib/api-docs";
|
||||
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||
@ -9,6 +10,12 @@ import { AuthMode } from "@app/services/auth/auth-type";
|
||||
import { sanitizedCertificateTemplate } from "@app/services/certificate-template/certificate-template-schema";
|
||||
import { validateTemplateRegexField } from "@app/services/certificate-template/certificate-template-validators";
|
||||
|
||||
const sanitizedEstConfig = CertificateTemplateEstConfigsSchema.pick({
|
||||
id: true,
|
||||
certificateTemplateId: true,
|
||||
isEnabled: true
|
||||
});
|
||||
|
||||
export const registerCertificateTemplateRouter = async (server: FastifyZodProvider) => {
|
||||
server.route({
|
||||
method: "GET",
|
||||
@ -202,4 +209,141 @@ export const registerCertificateTemplateRouter = async (server: FastifyZodProvid
|
||||
return certificateTemplate;
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "POST",
|
||||
url: "/:certificateTemplateId/est-config",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
schema: {
|
||||
description: "Create Certificate Template EST configuration",
|
||||
params: z.object({
|
||||
certificateTemplateId: z.string().trim()
|
||||
}),
|
||||
body: z.object({
|
||||
caChain: z.string().trim().min(1),
|
||||
passphrase: z.string().min(1),
|
||||
isEnabled: z.boolean().default(true)
|
||||
}),
|
||||
response: {
|
||||
200: sanitizedEstConfig
|
||||
}
|
||||
},
|
||||
handler: async (req) => {
|
||||
const estConfig = await server.services.certificateTemplate.createEstConfiguration({
|
||||
certificateTemplateId: req.params.certificateTemplateId,
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId,
|
||||
...req.body
|
||||
});
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
projectId: estConfig.projectId,
|
||||
event: {
|
||||
type: EventType.CREATE_CERTIFICATE_TEMPLATE_EST_CONFIG,
|
||||
metadata: {
|
||||
certificateTemplateId: estConfig.certificateTemplateId,
|
||||
isEnabled: estConfig.isEnabled as boolean
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return estConfig;
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "PATCH",
|
||||
url: "/:certificateTemplateId/est-config",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
schema: {
|
||||
description: "Update Certificate Template EST configuration",
|
||||
params: z.object({
|
||||
certificateTemplateId: z.string().trim()
|
||||
}),
|
||||
body: z.object({
|
||||
caChain: z.string().trim().min(1).optional(),
|
||||
passphrase: z.string().min(1).optional(),
|
||||
isEnabled: z.boolean().optional()
|
||||
}),
|
||||
response: {
|
||||
200: sanitizedEstConfig
|
||||
}
|
||||
},
|
||||
handler: async (req) => {
|
||||
const estConfig = await server.services.certificateTemplate.updateEstConfiguration({
|
||||
certificateTemplateId: req.params.certificateTemplateId,
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId,
|
||||
...req.body
|
||||
});
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
projectId: estConfig.projectId,
|
||||
event: {
|
||||
type: EventType.UPDATE_CERTIFICATE_TEMPLATE_EST_CONFIG,
|
||||
metadata: {
|
||||
certificateTemplateId: estConfig.certificateTemplateId,
|
||||
isEnabled: estConfig.isEnabled as boolean
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return estConfig;
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: "/:certificateTemplateId/est-config",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
schema: {
|
||||
description: "Get Certificate Template EST configuration",
|
||||
params: z.object({
|
||||
certificateTemplateId: z.string().trim()
|
||||
}),
|
||||
response: {
|
||||
200: sanitizedEstConfig.extend({
|
||||
caChain: z.string()
|
||||
})
|
||||
}
|
||||
},
|
||||
handler: async (req) => {
|
||||
const estConfig = await server.services.certificateTemplate.getEstConfiguration({
|
||||
isInternal: false,
|
||||
certificateTemplateId: req.params.certificateTemplateId,
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId
|
||||
});
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
projectId: estConfig.projectId,
|
||||
event: {
|
||||
type: EventType.GET_CERTIFICATE_TEMPLATE_EST_CONFIG,
|
||||
metadata: {
|
||||
certificateTemplateId: estConfig.certificateTemplateId
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return estConfig;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
@ -1295,24 +1295,23 @@ export const certificateAuthorityServiceFactory = ({
|
||||
* Return new leaf certificate issued by CA with id [caId].
|
||||
* Note: CSR is generated externally and submitted to Infisical.
|
||||
*/
|
||||
const signCertFromCa = async ({
|
||||
caId,
|
||||
certificateTemplateId,
|
||||
csr,
|
||||
pkiCollectionId,
|
||||
friendlyName,
|
||||
commonName,
|
||||
altNames,
|
||||
ttl,
|
||||
notBefore,
|
||||
notAfter,
|
||||
actorId,
|
||||
actorAuthMethod,
|
||||
actor,
|
||||
actorOrgId
|
||||
}: TSignCertFromCaDTO) => {
|
||||
const signCertFromCa = async (dto: TSignCertFromCaDTO) => {
|
||||
let ca: TCertificateAuthorities | undefined;
|
||||
let certificateTemplate: TCertificateTemplates | undefined;
|
||||
|
||||
const {
|
||||
caId,
|
||||
certificateTemplateId,
|
||||
csr,
|
||||
pkiCollectionId,
|
||||
friendlyName,
|
||||
commonName,
|
||||
altNames,
|
||||
ttl,
|
||||
notBefore,
|
||||
notAfter
|
||||
} = dto;
|
||||
|
||||
let collectionId = pkiCollectionId;
|
||||
|
||||
if (caId) {
|
||||
@ -1333,15 +1332,20 @@ export const certificateAuthorityServiceFactory = ({
|
||||
throw new BadRequestError({ message: "CA not found" });
|
||||
}
|
||||
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
actorId,
|
||||
ca.projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
if (!dto.isInternal) {
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
dto.actor,
|
||||
dto.actorId,
|
||||
ca.projectId,
|
||||
dto.actorAuthMethod,
|
||||
dto.actorOrgId
|
||||
);
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Create, ProjectPermissionSub.Certificates);
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Create,
|
||||
ProjectPermissionSub.Certificates
|
||||
);
|
||||
}
|
||||
|
||||
if (ca.status === CaStatus.DISABLED) throw new BadRequestError({ message: "CA is disabled" });
|
||||
if (!ca.activeCaCertId) throw new BadRequestError({ message: "CA does not have a certificate installed" });
|
||||
@ -1382,6 +1386,8 @@ export const certificateAuthorityServiceFactory = ({
|
||||
notAfterDate = new Date(notAfter);
|
||||
} else if (ttl) {
|
||||
notAfterDate = new Date(new Date().getTime() + ms(ttl));
|
||||
} else if (certificateTemplate?.ttl) {
|
||||
notAfterDate = new Date(new Date().getTime() + ms(certificateTemplate.ttl));
|
||||
}
|
||||
|
||||
const caCertNotBeforeDate = new Date(caCertObj.notBefore);
|
||||
@ -1426,6 +1432,7 @@ export const certificateAuthorityServiceFactory = ({
|
||||
await x509.SubjectKeyIdentifierExtension.create(csrObj.publicKey)
|
||||
];
|
||||
|
||||
let altNamesFromCsr: string = "";
|
||||
let altNamesArray: {
|
||||
type: "email" | "dns";
|
||||
value: string;
|
||||
@ -1454,7 +1461,24 @@ export const certificateAuthorityServiceFactory = ({
|
||||
// If altName is neither a valid email nor a valid hostname, throw an error or handle it accordingly
|
||||
throw new Error(`Invalid altName: ${altName}`);
|
||||
});
|
||||
} else {
|
||||
// attempt to read from CSR if altNames is not explicitly provided
|
||||
const sanExtension = csrObj.extensions.find((ext) => ext.type === "2.5.29.17");
|
||||
if (sanExtension) {
|
||||
const sanNames = new x509.GeneralNames(sanExtension.value);
|
||||
|
||||
altNamesArray = sanNames.items
|
||||
.filter((value) => value.type === "email" || value.type === "dns")
|
||||
.map((name) => ({
|
||||
type: name.type as "email" | "dns",
|
||||
value: name.value
|
||||
}));
|
||||
|
||||
altNamesFromCsr = sanNames.items.map((item) => item.value).join(",");
|
||||
}
|
||||
}
|
||||
|
||||
if (altNamesArray.length) {
|
||||
const altNamesExtension = new x509.SubjectAlternativeNameExtension(altNamesArray, false);
|
||||
extensions.push(altNamesExtension);
|
||||
}
|
||||
@ -1500,7 +1524,7 @@ export const certificateAuthorityServiceFactory = ({
|
||||
status: CertStatus.ACTIVE,
|
||||
friendlyName: friendlyName || csrObj.subject,
|
||||
commonName: cn,
|
||||
altNames,
|
||||
altNames: altNamesFromCsr || altNames,
|
||||
serialNumber,
|
||||
notBefore: notBeforeDate,
|
||||
notAfter: notAfterDate
|
||||
@ -1538,7 +1562,7 @@ export const certificateAuthorityServiceFactory = ({
|
||||
});
|
||||
|
||||
return {
|
||||
certificate: leafCert.toString("pem"),
|
||||
certificate: leafCert,
|
||||
certificateChain: `${issuingCaCertificate}\n${caCertChain}`.trim(),
|
||||
issuingCaCertificate,
|
||||
serialNumber,
|
||||
|
@ -97,18 +97,33 @@ export type TIssueCertFromCaDTO = {
|
||||
notAfter?: string;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
export type TSignCertFromCaDTO = {
|
||||
caId?: string;
|
||||
csr: string;
|
||||
certificateTemplateId?: string;
|
||||
pkiCollectionId?: string;
|
||||
friendlyName?: string;
|
||||
commonName?: string;
|
||||
altNames: string;
|
||||
ttl: string;
|
||||
notBefore?: string;
|
||||
notAfter?: string;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
export type TSignCertFromCaDTO =
|
||||
| {
|
||||
isInternal: true;
|
||||
caId?: string;
|
||||
csr: string;
|
||||
certificateTemplateId?: string;
|
||||
pkiCollectionId?: string;
|
||||
friendlyName?: string;
|
||||
commonName?: string;
|
||||
altNames?: string;
|
||||
ttl?: string;
|
||||
notBefore?: string;
|
||||
notAfter?: string;
|
||||
}
|
||||
| ({
|
||||
isInternal: false;
|
||||
caId?: string;
|
||||
csr: string;
|
||||
certificateTemplateId?: string;
|
||||
pkiCollectionId?: string;
|
||||
friendlyName?: string;
|
||||
commonName?: string;
|
||||
altNames: string;
|
||||
ttl: string;
|
||||
notBefore?: string;
|
||||
notAfter?: string;
|
||||
} & Omit<TProjectPermission, "projectId">);
|
||||
|
||||
export type TDNParts = {
|
||||
commonName?: string;
|
||||
|
24
backend/src/services/certificate-est/certificate-est-fns.ts
Normal file
@ -0,0 +1,24 @@
|
||||
import { Certificate, ContentInfo, EncapsulatedContentInfo, SignedData } from "pkijs";
|
||||
|
||||
export const convertRawCertsToPkcs7 = (rawCertificate: ArrayBuffer[]) => {
|
||||
const certs = rawCertificate.map((rawCert) => Certificate.fromBER(rawCert));
|
||||
const cmsSigned = new SignedData({
|
||||
encapContentInfo: new EncapsulatedContentInfo({
|
||||
eContentType: "1.2.840.113549.1.7.1" // not encrypted and not compressed data
|
||||
}),
|
||||
certificates: certs
|
||||
});
|
||||
|
||||
const cmsContent = new ContentInfo({
|
||||
contentType: "1.2.840.113549.1.7.2", // SignedData
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||
content: cmsSigned.toSchema()
|
||||
});
|
||||
|
||||
const derBuffer = cmsContent.toSchema().toBER(false);
|
||||
const base64Pkcs7 = Buffer.from(derBuffer)
|
||||
.toString("base64")
|
||||
.replace(/(.{64})/g, "$1\n"); // we add a linebreak for CURL clients
|
||||
|
||||
return base64Pkcs7;
|
||||
};
|
231
backend/src/services/certificate-est/certificate-est-service.ts
Normal file
@ -0,0 +1,231 @@
|
||||
import * as x509 from "@peculiar/x509";
|
||||
|
||||
import { BadRequestError, NotFoundError, UnauthorizedError } from "@app/lib/errors";
|
||||
|
||||
import { isCertChainValid } from "../certificate/certificate-fns";
|
||||
import { TCertificateAuthorityCertDALFactory } from "../certificate-authority/certificate-authority-cert-dal";
|
||||
import { TCertificateAuthorityDALFactory } from "../certificate-authority/certificate-authority-dal";
|
||||
import { getCaCertChain, getCaCertChains } from "../certificate-authority/certificate-authority-fns";
|
||||
import { TCertificateAuthorityServiceFactory } from "../certificate-authority/certificate-authority-service";
|
||||
import { TCertificateTemplateDALFactory } from "../certificate-template/certificate-template-dal";
|
||||
import { TCertificateTemplateServiceFactory } from "../certificate-template/certificate-template-service";
|
||||
import { TKmsServiceFactory } from "../kms/kms-service";
|
||||
import { TProjectDALFactory } from "../project/project-dal";
|
||||
import { convertRawCertsToPkcs7 } from "./certificate-est-fns";
|
||||
|
||||
type TCertificateEstServiceFactoryDep = {
|
||||
certificateAuthorityService: Pick<TCertificateAuthorityServiceFactory, "signCertFromCa">;
|
||||
certificateTemplateService: Pick<TCertificateTemplateServiceFactory, "getEstConfiguration">;
|
||||
certificateTemplateDAL: Pick<TCertificateTemplateDALFactory, "findById">;
|
||||
certificateAuthorityDAL: Pick<TCertificateAuthorityDALFactory, "findById">;
|
||||
certificateAuthorityCertDAL: Pick<TCertificateAuthorityCertDALFactory, "find" | "findById">;
|
||||
projectDAL: Pick<TProjectDALFactory, "findOne" | "updateById" | "transaction">;
|
||||
kmsService: Pick<TKmsServiceFactory, "decryptWithKmsKey" | "generateKmsKey">;
|
||||
};
|
||||
|
||||
export type TCertificateEstServiceFactory = ReturnType<typeof certificateEstServiceFactory>;
|
||||
|
||||
export const certificateEstServiceFactory = ({
|
||||
certificateAuthorityService,
|
||||
certificateTemplateService,
|
||||
certificateTemplateDAL,
|
||||
certificateAuthorityCertDAL,
|
||||
certificateAuthorityDAL,
|
||||
projectDAL,
|
||||
kmsService
|
||||
}: TCertificateEstServiceFactoryDep) => {
|
||||
const simpleReenroll = async ({
|
||||
csr,
|
||||
certificateTemplateId,
|
||||
sslClientCert
|
||||
}: {
|
||||
csr: string;
|
||||
certificateTemplateId: string;
|
||||
sslClientCert: string;
|
||||
}) => {
|
||||
const estConfig = await certificateTemplateService.getEstConfiguration({
|
||||
isInternal: true,
|
||||
certificateTemplateId
|
||||
});
|
||||
|
||||
if (!estConfig.isEnabled) {
|
||||
throw new BadRequestError({
|
||||
message: "EST is disabled"
|
||||
});
|
||||
}
|
||||
|
||||
const certTemplate = await certificateTemplateDAL.findById(certificateTemplateId);
|
||||
|
||||
const leafCertificate = decodeURIComponent(sslClientCert).match(
|
||||
/-----BEGIN CERTIFICATE-----[\s\S]+?-----END CERTIFICATE-----/g
|
||||
)?.[0];
|
||||
|
||||
if (!leafCertificate) {
|
||||
throw new UnauthorizedError({ message: "Missing client certificate" });
|
||||
}
|
||||
|
||||
const cert = new x509.X509Certificate(leafCertificate);
|
||||
// We have to assert that the client certificate provided can be traced back to the Root CA
|
||||
const caCertChains = await getCaCertChains({
|
||||
caId: certTemplate.caId,
|
||||
certificateAuthorityCertDAL,
|
||||
certificateAuthorityDAL,
|
||||
projectDAL,
|
||||
kmsService
|
||||
});
|
||||
|
||||
const verifiedChains = await Promise.all(
|
||||
caCertChains.map((chain) => {
|
||||
const caCert = new x509.X509Certificate(chain.certificate);
|
||||
const caChain =
|
||||
chain.certificateChain
|
||||
.match(/-----BEGIN CERTIFICATE-----[\s\S]+?-----END CERTIFICATE-----/g)
|
||||
?.map((c) => new x509.X509Certificate(c)) || [];
|
||||
|
||||
return isCertChainValid([cert, caCert, ...caChain]);
|
||||
})
|
||||
);
|
||||
|
||||
if (!verifiedChains.some(Boolean)) {
|
||||
throw new BadRequestError({
|
||||
message: "Invalid client certificate: unable to build a valid certificate chain"
|
||||
});
|
||||
}
|
||||
|
||||
// We ensure that the Subject and SubjectAltNames of the CSR and the existing certificate are exactly the same
|
||||
const csrObj = new x509.Pkcs10CertificateRequest(csr);
|
||||
if (csrObj.subject !== cert.subject) {
|
||||
throw new BadRequestError({
|
||||
message: "Subject mismatch"
|
||||
});
|
||||
}
|
||||
|
||||
let csrSanSet: Set<string> = new Set();
|
||||
const csrSanExtension = csrObj.extensions.find((ext) => ext.type === "2.5.29.17");
|
||||
if (csrSanExtension) {
|
||||
const sanNames = new x509.GeneralNames(csrSanExtension.value);
|
||||
csrSanSet = new Set([...sanNames.items.map((name) => `${name.type}-${name.value}`)]);
|
||||
}
|
||||
|
||||
let certSanSet: Set<string> = new Set();
|
||||
const certSanExtension = cert.extensions.find((ext) => ext.type === "2.5.29.17");
|
||||
if (certSanExtension) {
|
||||
const sanNames = new x509.GeneralNames(certSanExtension.value);
|
||||
certSanSet = new Set([...sanNames.items.map((name) => `${name.type}-${name.value}`)]);
|
||||
}
|
||||
|
||||
if (csrSanSet.size !== certSanSet.size || ![...csrSanSet].every((element) => certSanSet.has(element))) {
|
||||
throw new BadRequestError({
|
||||
message: "Subject alternative names mismatch"
|
||||
});
|
||||
}
|
||||
|
||||
const { certificate } = await certificateAuthorityService.signCertFromCa({
|
||||
isInternal: true,
|
||||
certificateTemplateId,
|
||||
csr
|
||||
});
|
||||
|
||||
return convertRawCertsToPkcs7([certificate.rawData]);
|
||||
};
|
||||
|
||||
const simpleEnroll = async ({
|
||||
csr,
|
||||
certificateTemplateId,
|
||||
sslClientCert
|
||||
}: {
|
||||
csr: string;
|
||||
certificateTemplateId: string;
|
||||
sslClientCert: string;
|
||||
}) => {
|
||||
/* We first have to assert that the client certificate provided can be traced back to the attached
|
||||
CA chain in the EST configuration
|
||||
*/
|
||||
const estConfig = await certificateTemplateService.getEstConfiguration({
|
||||
isInternal: true,
|
||||
certificateTemplateId
|
||||
});
|
||||
|
||||
if (!estConfig.isEnabled) {
|
||||
throw new BadRequestError({
|
||||
message: "EST is disabled"
|
||||
});
|
||||
}
|
||||
|
||||
const caCerts = estConfig.caChain
|
||||
.match(/-----BEGIN CERTIFICATE-----[\s\S]+?-----END CERTIFICATE-----/g)
|
||||
?.map((cert) => {
|
||||
return new x509.X509Certificate(cert);
|
||||
});
|
||||
|
||||
if (!caCerts) {
|
||||
throw new BadRequestError({ message: "Failed to parse certificate chain" });
|
||||
}
|
||||
|
||||
const leafCertificate = decodeURIComponent(sslClientCert).match(
|
||||
/-----BEGIN CERTIFICATE-----[\s\S]+?-----END CERTIFICATE-----/g
|
||||
)?.[0];
|
||||
|
||||
if (!leafCertificate) {
|
||||
throw new BadRequestError({ message: "Missing client certificate" });
|
||||
}
|
||||
|
||||
const certObj = new x509.X509Certificate(leafCertificate);
|
||||
if (!(await isCertChainValid([certObj, ...caCerts]))) {
|
||||
throw new BadRequestError({ message: "Invalid certificate chain" });
|
||||
}
|
||||
|
||||
const { certificate } = await certificateAuthorityService.signCertFromCa({
|
||||
isInternal: true,
|
||||
certificateTemplateId,
|
||||
csr
|
||||
});
|
||||
|
||||
return convertRawCertsToPkcs7([certificate.rawData]);
|
||||
};
|
||||
|
||||
/**
|
||||
* Return the CA certificate and CA certificate chain for the CA bound to
|
||||
* the certificate template with id [certificateTemplateId] as part of EST protocol
|
||||
*/
|
||||
const getCaCerts = async ({ certificateTemplateId }: { certificateTemplateId: string }) => {
|
||||
const certTemplate = await certificateTemplateDAL.findById(certificateTemplateId);
|
||||
if (!certTemplate) {
|
||||
throw new NotFoundError({
|
||||
message: "Certificate template not found"
|
||||
});
|
||||
}
|
||||
|
||||
const ca = await certificateAuthorityDAL.findById(certTemplate.caId);
|
||||
if (!ca) {
|
||||
throw new NotFoundError({
|
||||
message: "Certificate Authority not found"
|
||||
});
|
||||
}
|
||||
|
||||
const { caCert, caCertChain } = await getCaCertChain({
|
||||
caCertId: ca.activeCaCertId as string,
|
||||
certificateAuthorityDAL,
|
||||
certificateAuthorityCertDAL,
|
||||
projectDAL,
|
||||
kmsService
|
||||
});
|
||||
|
||||
const certificates = caCertChain
|
||||
.match(/-----BEGIN CERTIFICATE-----[\s\S]+?-----END CERTIFICATE-----/g)
|
||||
?.map((cert) => new x509.X509Certificate(cert));
|
||||
|
||||
if (!certificates) {
|
||||
throw new BadRequestError({ message: "Failed to parse certificate chain" });
|
||||
}
|
||||
|
||||
const caCertificate = new x509.X509Certificate(caCert);
|
||||
return convertRawCertsToPkcs7([caCertificate.rawData, ...certificates.map((cert) => cert.rawData)]);
|
||||
};
|
||||
|
||||
return {
|
||||
simpleEnroll,
|
||||
simpleReenroll,
|
||||
getCaCerts
|
||||
};
|
||||
};
|
@ -0,0 +1,11 @@
|
||||
import { TDbClient } from "@app/db";
|
||||
import { TableName } from "@app/db/schemas";
|
||||
import { ormify } from "@app/lib/knex";
|
||||
|
||||
export type TCertificateTemplateEstConfigDALFactory = ReturnType<typeof certificateTemplateEstConfigDALFactory>;
|
||||
|
||||
export const certificateTemplateEstConfigDALFactory = (db: TDbClient) => {
|
||||
const certificateTemplateEstConfigOrm = ormify(db, TableName.CertificateTemplateEstConfig);
|
||||
|
||||
return certificateTemplateEstConfigOrm;
|
||||
};
|
@ -1,20 +1,35 @@
|
||||
import { ForbiddenError } from "@casl/ability";
|
||||
import * as x509 from "@peculiar/x509";
|
||||
import bcrypt from "bcrypt";
|
||||
|
||||
import { TCertificateTemplateEstConfigsUpdate } from "@app/db/schemas";
|
||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
|
||||
import { getConfig } from "@app/lib/config/env";
|
||||
import { BadRequestError, NotFoundError } from "@app/lib/errors";
|
||||
|
||||
import { isCertChainValid } from "../certificate/certificate-fns";
|
||||
import { TCertificateAuthorityDALFactory } from "../certificate-authority/certificate-authority-dal";
|
||||
import { TKmsServiceFactory } from "../kms/kms-service";
|
||||
import { TProjectDALFactory } from "../project/project-dal";
|
||||
import { getProjectKmsCertificateKeyId } from "../project/project-fns";
|
||||
import { TCertificateTemplateDALFactory } from "./certificate-template-dal";
|
||||
import { TCertificateTemplateEstConfigDALFactory } from "./certificate-template-est-config-dal";
|
||||
import {
|
||||
TCreateCertTemplateDTO,
|
||||
TCreateEstConfigurationDTO,
|
||||
TDeleteCertTemplateDTO,
|
||||
TGetCertTemplateDTO,
|
||||
TUpdateCertTemplateDTO
|
||||
TGetEstConfigurationDTO,
|
||||
TUpdateCertTemplateDTO,
|
||||
TUpdateEstConfigurationDTO
|
||||
} from "./certificate-template-types";
|
||||
|
||||
type TCertificateTemplateServiceFactoryDep = {
|
||||
certificateTemplateDAL: TCertificateTemplateDALFactory;
|
||||
certificateTemplateEstConfigDAL: TCertificateTemplateEstConfigDALFactory;
|
||||
projectDAL: Pick<TProjectDALFactory, "findProjectBySlug" | "findOne" | "updateById" | "findById" | "transaction">;
|
||||
kmsService: Pick<TKmsServiceFactory, "generateKmsKey" | "encryptWithKmsKey" | "decryptWithKmsKey">;
|
||||
certificateAuthorityDAL: Pick<TCertificateAuthorityDALFactory, "findById">;
|
||||
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
|
||||
};
|
||||
@ -23,8 +38,11 @@ export type TCertificateTemplateServiceFactory = ReturnType<typeof certificateTe
|
||||
|
||||
export const certificateTemplateServiceFactory = ({
|
||||
certificateTemplateDAL,
|
||||
certificateTemplateEstConfigDAL,
|
||||
certificateAuthorityDAL,
|
||||
permissionService
|
||||
permissionService,
|
||||
kmsService,
|
||||
projectDAL
|
||||
}: TCertificateTemplateServiceFactoryDep) => {
|
||||
const createCertTemplate = async ({
|
||||
caId,
|
||||
@ -187,10 +205,228 @@ export const certificateTemplateServiceFactory = ({
|
||||
return certTemplate;
|
||||
};
|
||||
|
||||
const createEstConfiguration = async ({
|
||||
certificateTemplateId,
|
||||
caChain,
|
||||
passphrase,
|
||||
isEnabled,
|
||||
actorId,
|
||||
actorAuthMethod,
|
||||
actor,
|
||||
actorOrgId
|
||||
}: TCreateEstConfigurationDTO) => {
|
||||
const certTemplate = await certificateTemplateDAL.getById(certificateTemplateId);
|
||||
if (!certTemplate) {
|
||||
throw new NotFoundError({
|
||||
message: "Certificate template not found."
|
||||
});
|
||||
}
|
||||
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
actorId,
|
||||
certTemplate.projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Edit,
|
||||
ProjectPermissionSub.CertificateTemplates
|
||||
);
|
||||
|
||||
const appCfg = getConfig();
|
||||
|
||||
const certificateManagerKmsId = await getProjectKmsCertificateKeyId({
|
||||
projectId: certTemplate.projectId,
|
||||
projectDAL,
|
||||
kmsService
|
||||
});
|
||||
|
||||
// validate CA chain
|
||||
const certificates = caChain
|
||||
.match(/-----BEGIN CERTIFICATE-----[\s\S]+?-----END CERTIFICATE-----/g)
|
||||
?.map((cert) => new x509.X509Certificate(cert));
|
||||
|
||||
if (!certificates) {
|
||||
throw new BadRequestError({ message: "Failed to parse certificate chain" });
|
||||
}
|
||||
|
||||
if (!(await isCertChainValid(certificates))) {
|
||||
throw new BadRequestError({ message: "Invalid certificate chain" });
|
||||
}
|
||||
|
||||
const kmsEncryptor = await kmsService.encryptWithKmsKey({
|
||||
kmsId: certificateManagerKmsId
|
||||
});
|
||||
|
||||
const { cipherTextBlob: encryptedCaChain } = await kmsEncryptor({
|
||||
plainText: Buffer.from(caChain)
|
||||
});
|
||||
|
||||
const hashedPassphrase = await bcrypt.hash(passphrase, appCfg.SALT_ROUNDS);
|
||||
const estConfig = await certificateTemplateEstConfigDAL.create({
|
||||
certificateTemplateId,
|
||||
hashedPassphrase,
|
||||
encryptedCaChain,
|
||||
isEnabled
|
||||
});
|
||||
|
||||
return { ...estConfig, projectId: certTemplate.projectId };
|
||||
};
|
||||
|
||||
const updateEstConfiguration = async ({
|
||||
certificateTemplateId,
|
||||
caChain,
|
||||
passphrase,
|
||||
isEnabled,
|
||||
actorId,
|
||||
actorAuthMethod,
|
||||
actor,
|
||||
actorOrgId
|
||||
}: TUpdateEstConfigurationDTO) => {
|
||||
const certTemplate = await certificateTemplateDAL.getById(certificateTemplateId);
|
||||
if (!certTemplate) {
|
||||
throw new NotFoundError({
|
||||
message: "Certificate template not found."
|
||||
});
|
||||
}
|
||||
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
actorId,
|
||||
certTemplate.projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Edit,
|
||||
ProjectPermissionSub.CertificateTemplates
|
||||
);
|
||||
|
||||
const originalCaEstConfig = await certificateTemplateEstConfigDAL.findOne({
|
||||
certificateTemplateId
|
||||
});
|
||||
|
||||
if (!originalCaEstConfig) {
|
||||
throw new NotFoundError({
|
||||
message: "EST configuration not found"
|
||||
});
|
||||
}
|
||||
|
||||
const appCfg = getConfig();
|
||||
|
||||
const certificateManagerKmsId = await getProjectKmsCertificateKeyId({
|
||||
projectId: certTemplate.projectId,
|
||||
projectDAL,
|
||||
kmsService
|
||||
});
|
||||
|
||||
const updatedData: TCertificateTemplateEstConfigsUpdate = {
|
||||
isEnabled
|
||||
};
|
||||
|
||||
if (caChain) {
|
||||
const certificates = caChain
|
||||
.match(/-----BEGIN CERTIFICATE-----[\s\S]+?-----END CERTIFICATE-----/g)
|
||||
?.map((cert) => new x509.X509Certificate(cert));
|
||||
|
||||
if (!certificates) {
|
||||
throw new BadRequestError({ message: "Failed to parse certificate chain" });
|
||||
}
|
||||
|
||||
if (!(await isCertChainValid(certificates))) {
|
||||
throw new BadRequestError({ message: "Invalid certificate chain" });
|
||||
}
|
||||
|
||||
const kmsEncryptor = await kmsService.encryptWithKmsKey({
|
||||
kmsId: certificateManagerKmsId
|
||||
});
|
||||
|
||||
const { cipherTextBlob: encryptedCaChain } = await kmsEncryptor({
|
||||
plainText: Buffer.from(caChain)
|
||||
});
|
||||
|
||||
updatedData.encryptedCaChain = encryptedCaChain;
|
||||
}
|
||||
|
||||
if (passphrase) {
|
||||
const hashedPassphrase = await bcrypt.hash(passphrase, appCfg.SALT_ROUNDS);
|
||||
updatedData.hashedPassphrase = hashedPassphrase;
|
||||
}
|
||||
|
||||
const estConfig = await certificateTemplateEstConfigDAL.updateById(originalCaEstConfig.id, updatedData);
|
||||
|
||||
return { ...estConfig, projectId: certTemplate.projectId };
|
||||
};
|
||||
|
||||
const getEstConfiguration = async (dto: TGetEstConfigurationDTO) => {
|
||||
const { certificateTemplateId } = dto;
|
||||
|
||||
const certTemplate = await certificateTemplateDAL.getById(certificateTemplateId);
|
||||
if (!certTemplate) {
|
||||
throw new NotFoundError({
|
||||
message: "Certificate template not found."
|
||||
});
|
||||
}
|
||||
|
||||
if (!dto.isInternal) {
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
dto.actor,
|
||||
dto.actorId,
|
||||
certTemplate.projectId,
|
||||
dto.actorAuthMethod,
|
||||
dto.actorOrgId
|
||||
);
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Edit,
|
||||
ProjectPermissionSub.CertificateTemplates
|
||||
);
|
||||
}
|
||||
|
||||
const estConfig = await certificateTemplateEstConfigDAL.findOne({
|
||||
certificateTemplateId
|
||||
});
|
||||
|
||||
if (!estConfig) {
|
||||
throw new NotFoundError({
|
||||
message: "EST configuration not found"
|
||||
});
|
||||
}
|
||||
|
||||
const certificateManagerKmsId = await getProjectKmsCertificateKeyId({
|
||||
projectId: certTemplate.projectId,
|
||||
projectDAL,
|
||||
kmsService
|
||||
});
|
||||
|
||||
const kmsDecryptor = await kmsService.decryptWithKmsKey({
|
||||
kmsId: certificateManagerKmsId
|
||||
});
|
||||
|
||||
const decryptedCaChain = await kmsDecryptor({
|
||||
cipherTextBlob: estConfig.encryptedCaChain
|
||||
});
|
||||
|
||||
return {
|
||||
certificateTemplateId,
|
||||
id: estConfig.id,
|
||||
isEnabled: estConfig.isEnabled,
|
||||
caChain: decryptedCaChain.toString(),
|
||||
hashedPassphrase: estConfig.hashedPassphrase,
|
||||
projectId: certTemplate.projectId
|
||||
};
|
||||
};
|
||||
|
||||
return {
|
||||
createCertTemplate,
|
||||
getCertTemplate,
|
||||
deleteCertTemplate,
|
||||
updateCertTemplate
|
||||
updateCertTemplate,
|
||||
createEstConfiguration,
|
||||
updateEstConfiguration,
|
||||
getEstConfiguration
|
||||
};
|
||||
};
|
||||
|
@ -26,3 +26,27 @@ export type TGetCertTemplateDTO = {
|
||||
export type TDeleteCertTemplateDTO = {
|
||||
id: string;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
export type TCreateEstConfigurationDTO = {
|
||||
certificateTemplateId: string;
|
||||
caChain: string;
|
||||
passphrase: string;
|
||||
isEnabled: boolean;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
export type TUpdateEstConfigurationDTO = {
|
||||
certificateTemplateId: string;
|
||||
caChain?: string;
|
||||
passphrase?: string;
|
||||
isEnabled?: boolean;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
export type TGetEstConfigurationDTO =
|
||||
| {
|
||||
isInternal: true;
|
||||
certificateTemplateId: string;
|
||||
}
|
||||
| ({
|
||||
isInternal: false;
|
||||
certificateTemplateId: string;
|
||||
} & Omit<TProjectPermission, "projectId">);
|
||||
|
@ -24,3 +24,19 @@ export const revocationReasonToCrlCode = (crlReason: CrlReason) => {
|
||||
return x509.X509CrlReason.unspecified;
|
||||
}
|
||||
};
|
||||
|
||||
export const isCertChainValid = async (certificates: x509.X509Certificate[]) => {
|
||||
if (certificates.length === 1) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const leafCert = certificates[0];
|
||||
const chain = new x509.X509ChainBuilder({
|
||||
certificates: certificates.slice(1)
|
||||
});
|
||||
|
||||
const chainItems = await chain.build(leafCert);
|
||||
|
||||
// chain.build() implicitly verifies the chain
|
||||
return chainItems.length === certificates.length;
|
||||
};
|
||||
|
@ -7,6 +7,7 @@ services:
|
||||
restart: always
|
||||
ports:
|
||||
- 8080:80
|
||||
- 8443:443
|
||||
volumes:
|
||||
- ./nginx/default.dev.conf:/etc/nginx/conf.d/default.conf:ro
|
||||
depends_on:
|
||||
|
57
docs/documentation/platform/pki/est.mdx
Normal file
@ -0,0 +1,57 @@
|
||||
---
|
||||
title: "Enrollment over Secure Transport (EST)"
|
||||
sidebarTitle: "Enrollment over Secure Transport (EST)"
|
||||
description: "Learn how to manage certificate enrollment of clients using EST"
|
||||
---
|
||||
|
||||
## Concept
|
||||
|
||||
Enrollment over Secure Transport (EST) is a protocol used to automate the secure provisioning of digital certificates for devices and applications over a secure HTTPS connection. It is primarily used when a client device needs to obtain or renew a certificate from a Certificate Authority (CA) on Infisical in a secure and standardized manner. EST is commonly employed in environments requiring strong authentication and encrypted communication, such as in IoT, enterprise networks, and secure web services.
|
||||
|
||||
Infisical's EST service is based on [RFC 7030](https://datatracker.ietf.org/doc/html/rfc7030) and implements the following endpoints:
|
||||
|
||||
- **cacerts** - provides the necessary CA chain for the client to validate certificates issued by the CA.
|
||||
- **simpleenroll** - allows an EST client to request a new certificate from Infisical's EST server
|
||||
- **simplereenroll** - similar to the /simpleenroll endpoint but is used for renewing an existing certificate.
|
||||
|
||||
These endpoints are exposed on port 8443 under the .well-known/est path e.g.
|
||||
`https://app.infisical.com:8443/.well-known/est/estLabel/cacerts`
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- You need to have an existing [CA hierarchy](/documentation/platform/pki/private-ca).
|
||||
- The client devices need to have a bootstrap/pre-installed certificate.
|
||||
- The client devices must trust the server certificates used by Infisical's EST server. If the devices are new or lack existing trust configurations, you need to manually establish trust for the appropriate certificates. When using Infisical Cloud, this means establishing trust for certificates issued by AWS.
|
||||
|
||||
## Guide to configuring EST
|
||||
|
||||
1. Set up a certificate template with your selected issuing CA. This template will define the policies and parameters for certificates issued through EST. For detailed instructions on configuring a certificate template, refer to the certificate templates [documentation](/documentation/platform/pki/certificate-templates).
|
||||
|
||||
2. Proceed to the certificate template's enrollment settings
|
||||

|
||||
|
||||
3. Select **EST** as the client enrollment method and fill up the remaining fields.
|
||||
|
||||

|
||||
|
||||
- **Certificate Authority Chain** - This is the certificate chain used to validate your devices' manufacturing/pre-installed certificates. This will be used to authenticate your devices with Infisical's EST server.
|
||||
- **Passphrase** - This is also used to authenticate your devices with Infisical's EST server. When configuring the clients, use the value defined here as the EST password.
|
||||
|
||||
For security reasons, Infisical authenticates EST clients using both client certificate and passphrase.
|
||||
|
||||
4. Once the configuration of enrollment options is completed, a new **EST Label** field appears in the enrollment settings. This is the value to use as label in the URL when configuring the connection of EST clients to Infisical.
|
||||

|
||||
|
||||
For demonstration, the complete URL of the supported EST endpoints will look like the following:
|
||||
|
||||
- https://app.infisical.com:8443/.well-known/est/f110f308-9888-40ab-b228-237b12de8b96/cacerts
|
||||
- https://app.infisical.com:8443/.well-known/est/f110f308-9888-40ab-b228-237b12de8b96/simpleenroll
|
||||
- https://app.infisical.com:8443/.well-known/est/f110f308-9888-40ab-b228-237b12de8b96/simplereenroll
|
||||
|
||||
## Setting up EST clients
|
||||
|
||||
- To use the EST passphrase in your clients, configure it as the EST password. The EST username can be set to any arbitrary value.
|
||||
- Use the appropriate client certificates for invoking the EST endpoints.
|
||||
- For `simpleenroll`, use the bootstrapped/manufacturer client certificate.
|
||||
- For `simplereenroll`, use a valid EST-issued client certificate.
|
||||
- When configuring the PKCS#12 objects for the client certificates, only include the leaf certificate and the private key.
|
BIN
docs/images/integrations/azure-devops/create-new-token.png
Normal file
After Width: | Height: | Size: 136 KiB |
After Width: | Height: | Size: 197 KiB |
After Width: | Height: | Size: 192 KiB |
BIN
docs/images/integrations/azure-devops/new-token-created.png
Normal file
After Width: | Height: | Size: 111 KiB |
BIN
docs/images/integrations/azure-devops/overview-page.png
Normal file
After Width: | Height: | Size: 93 KiB |
BIN
docs/images/platform/pki/est/template-enroll-hover.png
Normal file
After Width: | Height: | Size: 723 KiB |
BIN
docs/images/platform/pki/est/template-enrollment-est-label.png
Normal file
After Width: | Height: | Size: 652 KiB |
BIN
docs/images/platform/pki/est/template-enrollment-modal.png
Normal file
After Width: | Height: | Size: 612 KiB |
55
docs/integrations/cloud/azure-devops.mdx
Normal file
@ -0,0 +1,55 @@
|
||||
---
|
||||
title: "Azure DevOps"
|
||||
description: "How to sync secrets from Infisical to Azure DevOps"
|
||||
---
|
||||
|
||||
### Usage
|
||||
Prerequisites:
|
||||
|
||||
- Set up and add envars to [Infisical Cloud](https://app.infisical.com).
|
||||
- Create a new [Azure DevOps](https://dev.azure.com) project if you don't have one already.
|
||||
|
||||
|
||||
#### Create a new Azure DevOps personal access token (PAT)
|
||||
You'll need to create a new personal access token (PAT) in order to authenticate Infisical with Azure DevOps.
|
||||
<Steps>
|
||||
<Step title="Navigate to Azure DevOps">
|
||||

|
||||
</Step>
|
||||
<Step title="Create a new token">
|
||||
Make sure the newly created token has Read/Write access to the Release scope.
|
||||

|
||||
|
||||
<Note>
|
||||
Please make sure that the token has access to the following scopes: Variable Groups _(read/write)_, Release _(read/write)_, Project and Team _(read)_, Service Connections _(read & query)_
|
||||
</Note>
|
||||
</Step>
|
||||
<Step title="Copy the new access token">
|
||||
Copy the newly created token as this will be used to authenticate Infisical with Azure DevOps.
|
||||

|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
#### Setup the Infisical Azure DevOps integration
|
||||
Navigate to your project's integrations tab and select the 'Azure DevOps' integration.
|
||||

|
||||
|
||||
<Steps>
|
||||
<Step title="Authorize Infisical for Azure DevOps">
|
||||
Enter your credentials that you obtained from the previous step.
|
||||
|
||||
1. Azure DevOps API token is the personal access token (PAT) you created in the previous step.
|
||||
2. Azure DevOps organization name is the name of your Azure DevOps organization.
|
||||
|
||||

|
||||
</Step>
|
||||
<Step title="Configure the integration">
|
||||
Select Infisical project and secret path you want to sync into Azure DevOps.
|
||||
Finally, press create integration to start syncing secrets to Azure DevOps.
|
||||
|
||||

|
||||
</Step>
|
||||
|
||||
</Steps>
|
||||
Now you have successfully integrated Infisical with Azure DevOps. Your existing and future secret changes will automatically sync to Azure DevOps.
|
||||
You can view your secrets by navigating to your Azure DevOps project and selecting the 'Library' tab under 'Pipelines' in the 'Library' section.
|
@ -109,6 +109,7 @@
|
||||
"documentation/platform/pki/private-ca",
|
||||
"documentation/platform/pki/certificates",
|
||||
"documentation/platform/pki/certificate-templates",
|
||||
"documentation/platform/pki/est",
|
||||
"documentation/platform/pki/alerting"
|
||||
]
|
||||
},
|
||||
@ -324,6 +325,7 @@
|
||||
},
|
||||
"integrations/cloud/vercel",
|
||||
"integrations/cloud/azure-key-vault",
|
||||
"integrations/cloud/azure-devops",
|
||||
"integrations/cloud/gcp-secret-manager",
|
||||
{
|
||||
"group": "Cloudflare",
|
||||
|
@ -72,7 +72,12 @@ export const eventToNameMap: { [K in EventType]: string } = {
|
||||
[EventType.CREATE_CERTIFICATE_TEMPLATE]: "Create certificate template",
|
||||
[EventType.UPDATE_CERTIFICATE_TEMPLATE]: "Update certificate template",
|
||||
[EventType.DELETE_CERTIFICATE_TEMPLATE]: "Delete certificate template",
|
||||
[EventType.GET_CERTIFICATE_TEMPLATE]: "Get certificate template"
|
||||
[EventType.GET_CERTIFICATE_TEMPLATE]: "Get certificate template",
|
||||
[EventType.GET_CERTIFICATE_TEMPLATE_EST_CONFIG]: "Get certificate template EST configuration",
|
||||
[EventType.CREATE_CERTIFICATE_TEMPLATE_EST_CONFIG]:
|
||||
"Create certificate template EST configuration",
|
||||
[EventType.UPDATE_CERTIFICATE_TEMPLATE_EST_CONFIG]:
|
||||
"Update certificate template EST configuration"
|
||||
};
|
||||
|
||||
export const userAgentTTypeoNameMap: { [K in UserAgentType]: string } = {
|
||||
|
@ -86,5 +86,8 @@ export enum EventType {
|
||||
CREATE_CERTIFICATE_TEMPLATE = "create-certificate-template",
|
||||
UPDATE_CERTIFICATE_TEMPLATE = "update-certificate-template",
|
||||
DELETE_CERTIFICATE_TEMPLATE = "delete-certificate-template",
|
||||
GET_CERTIFICATE_TEMPLATE = "get-certificate-template"
|
||||
GET_CERTIFICATE_TEMPLATE = "get-certificate-template",
|
||||
CREATE_CERTIFICATE_TEMPLATE_EST_CONFIG = "create-certificate-template-est-config",
|
||||
UPDATE_CERTIFICATE_TEMPLATE_EST_CONFIG = "update-certificate-template-est-config",
|
||||
GET_CERTIFICATE_TEMPLATE_EST_CONFIG = "get-certificate-template-est-config"
|
||||
}
|
||||
|
@ -719,6 +719,29 @@ interface DeleteCertificateTemplate {
|
||||
};
|
||||
}
|
||||
|
||||
interface CreateCertificateTemplateEstConfig {
|
||||
type: EventType.CREATE_CERTIFICATE_TEMPLATE_EST_CONFIG;
|
||||
metadata: {
|
||||
certificateTemplateId: string;
|
||||
isEnabled: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
interface UpdateCertificateTemplateEstConfig {
|
||||
type: EventType.UPDATE_CERTIFICATE_TEMPLATE_EST_CONFIG;
|
||||
metadata: {
|
||||
certificateTemplateId: string;
|
||||
isEnabled: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
interface GetCertificateTemplateEstConfig {
|
||||
type: EventType.GET_CERTIFICATE_TEMPLATE_EST_CONFIG;
|
||||
metadata: {
|
||||
certificateTemplateId: string;
|
||||
};
|
||||
}
|
||||
|
||||
export type Event =
|
||||
| GetSecretsEvent
|
||||
| GetSecretEvent
|
||||
@ -791,7 +814,10 @@ export type Event =
|
||||
| CreateCertificateTemplate
|
||||
| UpdateCertificateTemplate
|
||||
| GetCertificateTemplate
|
||||
| DeleteCertificateTemplate;
|
||||
| DeleteCertificateTemplate
|
||||
| UpdateCertificateTemplateEstConfig
|
||||
| CreateCertificateTemplateEstConfig
|
||||
| GetCertificateTemplateEstConfig;
|
||||
|
||||
export type AuditLog = {
|
||||
id: string;
|
||||
|
@ -8,4 +8,4 @@ export {
|
||||
useSignIntermediate,
|
||||
useUpdateCa
|
||||
} from "./mutations";
|
||||
export { useGetCaById, useGetCaCert, useGetCaCerts, useGetCaCrls,useGetCaCsr } from "./queries";
|
||||
export { useGetCaById, useGetCaCert, useGetCaCerts, useGetCaCrls, useGetCaCsr } from "./queries";
|
||||
|
@ -10,7 +10,8 @@ export const caKeys = {
|
||||
getCaCrls: (caId: string) => [{ caId }, "ca-crls"],
|
||||
getCaCert: (caId: string) => [{ caId }, "ca-cert"],
|
||||
getCaCsr: (caId: string) => [{ caId }, "ca-csr"],
|
||||
getCaCrl: (caId: string) => [{ caId }, "ca-crl"]
|
||||
getCaCrl: (caId: string) => [{ caId }, "ca-crl"],
|
||||
getCaEstConfig: (caId: string) => [{ caId }, "ca-est-config"]
|
||||
};
|
||||
|
||||
export const useGetCaById = (caId: string) => {
|
||||
|
@ -1,2 +1,8 @@
|
||||
export { useCreateCertTemplate, useDeleteCertTemplate, useUpdateCertTemplate } from "./mutations";
|
||||
export { useGetCertTemplate } from "./queries";
|
||||
export {
|
||||
useCreateCertTemplate,
|
||||
useCreateEstConfig,
|
||||
useDeleteCertTemplate,
|
||||
useUpdateCertTemplate,
|
||||
useUpdateEstConfig
|
||||
} from "./mutations";
|
||||
export { useGetCertTemplate, useGetEstConfig } from "./queries";
|
||||
|
@ -7,8 +7,10 @@ import { certTemplateKeys } from "./queries";
|
||||
import {
|
||||
TCertificateTemplate,
|
||||
TCreateCertificateTemplateDTO,
|
||||
TCreateEstConfigDTO,
|
||||
TDeleteCertificateTemplateDTO,
|
||||
TUpdateCertificateTemplateDTO
|
||||
TUpdateCertificateTemplateDTO,
|
||||
TUpdateEstConfigDTO
|
||||
} from "./types";
|
||||
|
||||
export const useCreateCertTemplate = () => {
|
||||
@ -57,3 +59,35 @@ export const useDeleteCertTemplate = () => {
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const useCreateEstConfig = () => {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation<{}, {}, TCreateEstConfigDTO>({
|
||||
mutationFn: async (body) => {
|
||||
const { data } = await apiRequest.post(
|
||||
`/api/v1/pki/certificate-templates/${body.certificateTemplateId}/est-config`,
|
||||
body
|
||||
);
|
||||
return data;
|
||||
},
|
||||
onSuccess: (_, { certificateTemplateId }) => {
|
||||
queryClient.invalidateQueries(certTemplateKeys.getEstConfig(certificateTemplateId));
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const useUpdateEstConfig = () => {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation<{}, {}, TUpdateEstConfigDTO>({
|
||||
mutationFn: async (body) => {
|
||||
const { data } = await apiRequest.patch(
|
||||
`/api/v1/pki/certificate-templates/${body.certificateTemplateId}/est-config`,
|
||||
body
|
||||
);
|
||||
return data;
|
||||
},
|
||||
onSuccess: (_, { certificateTemplateId }) => {
|
||||
queryClient.invalidateQueries(certTemplateKeys.getEstConfig(certificateTemplateId));
|
||||
}
|
||||
});
|
||||
};
|
||||
|
@ -2,10 +2,11 @@ import { useQuery } from "@tanstack/react-query";
|
||||
|
||||
import { apiRequest } from "@app/config/request";
|
||||
|
||||
import { TCertificateTemplate } from "./types";
|
||||
import { TCertificateTemplate, TEstConfig } from "./types";
|
||||
|
||||
export const certTemplateKeys = {
|
||||
getCertTemplateById: (id: string) => [{ id }, "cert-template"]
|
||||
getCertTemplateById: (id: string) => [{ id }, "cert-template"],
|
||||
getEstConfig: (id: string) => [{ id }, "cert-template-est-config"]
|
||||
};
|
||||
|
||||
export const useGetCertTemplate = (id: string) => {
|
||||
@ -20,3 +21,17 @@ export const useGetCertTemplate = (id: string) => {
|
||||
enabled: Boolean(id)
|
||||
});
|
||||
};
|
||||
|
||||
export const useGetEstConfig = (certificateTemplateId: string) => {
|
||||
return useQuery({
|
||||
queryKey: certTemplateKeys.getEstConfig(certificateTemplateId),
|
||||
queryFn: async () => {
|
||||
const { data: estConfig } = await apiRequest.get<TEstConfig>(
|
||||
`/api/v1/pki/certificate-templates/${certificateTemplateId}/est-config`
|
||||
);
|
||||
|
||||
return estConfig;
|
||||
},
|
||||
enabled: Boolean(certificateTemplateId)
|
||||
});
|
||||
};
|
||||
|
@ -35,3 +35,24 @@ export type TDeleteCertificateTemplateDTO = {
|
||||
id: string;
|
||||
projectId: string;
|
||||
};
|
||||
|
||||
export type TCreateEstConfigDTO = {
|
||||
certificateTemplateId: string;
|
||||
caChain: string;
|
||||
passphrase: string;
|
||||
isEnabled: boolean;
|
||||
};
|
||||
|
||||
export type TUpdateEstConfigDTO = {
|
||||
certificateTemplateId: string;
|
||||
caChain?: string;
|
||||
passphrase?: string;
|
||||
isEnabled?: boolean;
|
||||
};
|
||||
|
||||
export type TEstConfig = {
|
||||
id: string;
|
||||
certificateTemplateId: string;
|
||||
caChain: string;
|
||||
isEnabled: false;
|
||||
};
|
||||
|
@ -1,5 +1,10 @@
|
||||
import { useState } from "react";
|
||||
import Head from "next/head";
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
import { faArrowUpRightFromSquare, faBookOpen } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import { useSaveIntegrationAccessToken } from "@app/hooks/api";
|
||||
|
||||
@ -42,16 +47,49 @@ export default function AzureDevopsCreateIntegrationPage() {
|
||||
|
||||
return (
|
||||
<div className="flex h-full w-full items-center justify-center">
|
||||
<Card className="max-w-md rounded-md p-8">
|
||||
<CardTitle className="text-center">AzureDevops Integration</CardTitle>
|
||||
<Head>
|
||||
<title>Authorize Azure DevOps Integration</title>
|
||||
<link rel="icon" href="/infisical.ico" />
|
||||
</Head>
|
||||
<Card className="max-w-lg rounded-md border border-mineshaft-600">
|
||||
<CardTitle
|
||||
className="px-6 text-left text-xl"
|
||||
subTitle="After adding the details below, you will be prompted to set up an integration for a particular Infisical project and environment."
|
||||
>
|
||||
<div className="flex flex-row items-center">
|
||||
<div className="inline flex items-center">
|
||||
<Image
|
||||
src="/images/integrations/Amazon Web Services.png"
|
||||
height={35}
|
||||
width={35}
|
||||
alt="Azure DevOps logo"
|
||||
/>
|
||||
</div>
|
||||
<span className="ml-1.5">Azure DevOps Integration </span>
|
||||
<Link href="https://infisical.com/docs/integrations/cloud/azure-devops" passHref>
|
||||
<a target="_blank" rel="noopener noreferrer">
|
||||
<div className="ml-2 mb-1 inline-block cursor-default rounded-md bg-yellow/20 px-1.5 pb-[0.03rem] pt-[0.04rem] text-sm text-yellow opacity-80 hover:opacity-100">
|
||||
<FontAwesomeIcon icon={faBookOpen} className="mr-1.5" />
|
||||
Docs
|
||||
<FontAwesomeIcon
|
||||
icon={faArrowUpRightFromSquare}
|
||||
className="ml-1.5 mb-[0.07rem] text-xxs"
|
||||
/>
|
||||
</div>
|
||||
</a>
|
||||
</Link>
|
||||
</div>
|
||||
</CardTitle>{" "}
|
||||
<FormControl
|
||||
label="AzureDevops API Token"
|
||||
className="px-6"
|
||||
label="Azure DevOps API Token"
|
||||
errorText={apiKeyErrorText}
|
||||
isError={apiKeyErrorText !== "" ?? false}
|
||||
>
|
||||
<Input placeholder="" value={apiKey} onChange={(e) => setApiKey(e.target.value)} />
|
||||
</FormControl>
|
||||
<FormControl
|
||||
className="px-6"
|
||||
label="Azure DevOps Organization Name"
|
||||
tooltipText="This is the slug of the organization. An example would be 'my-acme-org'"
|
||||
errorText={apiKeyErrorText}
|
||||
@ -63,14 +101,14 @@ export default function AzureDevopsCreateIntegrationPage() {
|
||||
onChange={(e) => setDevopsOrgName(e.target.value)}
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
<Button
|
||||
onClick={handleButtonClick}
|
||||
color="mineshaft"
|
||||
className="mt-4"
|
||||
colorSchema="primary"
|
||||
variant="outline_bg"
|
||||
className="mb-6 mt-2 ml-auto mr-6 w-min"
|
||||
isLoading={isLoading}
|
||||
>
|
||||
Connect to AzureDevOps
|
||||
Connect to Azure DevOps
|
||||
</Button>
|
||||
</Card>
|
||||
</div>
|
||||
|
@ -1,5 +1,10 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import Head from "next/head";
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
import { faArrowUpRightFromSquare, faBookOpen } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import queryString from "query-string";
|
||||
|
||||
import { useCreateIntegration } from "@app/hooks/api";
|
||||
@ -83,9 +88,40 @@ export default function AzureDevopsCreateIntegrationPage() {
|
||||
integrationAuthApps &&
|
||||
targetApp ? (
|
||||
<div className="flex h-full w-full items-center justify-center">
|
||||
<Card className="max-w-md rounded-md p-8">
|
||||
<CardTitle className="text-center">AzureDevops Integration</CardTitle>
|
||||
<FormControl label="Project Environment" className="mt-4">
|
||||
<Head>
|
||||
<title>Set Up Azure DevOps Integration</title>
|
||||
<link rel="icon" href="/infisical.ico" />
|
||||
</Head>
|
||||
<Card className="max-w-lg rounded-md border border-mineshaft-600">
|
||||
<CardTitle
|
||||
className="px-6 text-left text-xl"
|
||||
subTitle="Choose which environment in Infisical you want to sync to secrets in Azure DevOps."
|
||||
>
|
||||
<div className="flex flex-row items-center">
|
||||
<div className="inline flex items-center">
|
||||
<Image
|
||||
src="/images/integrations/Amazon Web Services.png"
|
||||
height={35}
|
||||
width={35}
|
||||
alt="Azure DevOps logo"
|
||||
/>
|
||||
</div>
|
||||
<span className="ml-1.5">Azure DevOps Integration </span>
|
||||
<Link href="https://infisical.com/docs/integrations/cloud/azure-devops" passHref>
|
||||
<a target="_blank" rel="noopener noreferrer">
|
||||
<div className="ml-2 mb-1 inline-block cursor-default rounded-md bg-yellow/20 px-1.5 pb-[0.03rem] pt-[0.04rem] text-sm text-yellow opacity-80 hover:opacity-100">
|
||||
<FontAwesomeIcon icon={faBookOpen} className="mr-1.5" />
|
||||
Docs
|
||||
<FontAwesomeIcon
|
||||
icon={faArrowUpRightFromSquare}
|
||||
className="ml-1.5 mb-[0.07rem] text-xxs"
|
||||
/>
|
||||
</div>
|
||||
</a>
|
||||
</Link>
|
||||
</div>
|
||||
</CardTitle>
|
||||
<FormControl label="Project Environment" className="mt-4 px-6">
|
||||
<Select
|
||||
value={selectedSourceEnvironment}
|
||||
onValueChange={(val) => setSelectedSourceEnvironment(val)}
|
||||
@ -101,14 +137,14 @@ export default function AzureDevopsCreateIntegrationPage() {
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
<FormControl label="Secrets Path">
|
||||
<FormControl className="px-6" label="Secrets Path">
|
||||
<Input
|
||||
value={secretPath}
|
||||
onChange={(evt) => setSecretPath(evt.target.value)}
|
||||
placeholder="Provide a path, default is /"
|
||||
/>
|
||||
</FormControl>
|
||||
<FormControl label="AzureDevops Project" className="mt-4">
|
||||
<FormControl label="Azure DevOps Project" className="px-6">
|
||||
<Select
|
||||
value={targetApp}
|
||||
onValueChange={(val) => setTargetApp(val)}
|
||||
@ -133,10 +169,10 @@ export default function AzureDevopsCreateIntegrationPage() {
|
||||
</FormControl>
|
||||
<Button
|
||||
onClick={handleButtonClick}
|
||||
color="mineshaft"
|
||||
className="mt-4"
|
||||
colorSchema="primary"
|
||||
variant="outline_bg"
|
||||
className="mb-6 mt-2 ml-auto mr-6 w-min"
|
||||
isLoading={isLoading}
|
||||
isDisabled={integrationAuthApps.length === 0}
|
||||
>
|
||||
Create Integration
|
||||
</Button>
|
||||
|
@ -428,6 +428,20 @@ export const LogsTableRow = ({ auditLog }: Props) => {
|
||||
<p>{`Certificate Template ID: ${event.metadata.certificateTemplateId}`}</p>
|
||||
</Td>
|
||||
);
|
||||
case EventType.CREATE_CERTIFICATE_TEMPLATE_EST_CONFIG:
|
||||
case EventType.UPDATE_CERTIFICATE_TEMPLATE_EST_CONFIG:
|
||||
return (
|
||||
<Td>
|
||||
<p>{`Certificate Template ID: ${event.metadata.certificateTemplateId}`}</p>
|
||||
<p>{`Enabled: ${event.metadata.isEnabled}`}</p>
|
||||
</Td>
|
||||
);
|
||||
case EventType.GET_CERTIFICATE_TEMPLATE_EST_CONFIG:
|
||||
return (
|
||||
<Td>
|
||||
<p>{`Certificate Template ID: ${event.metadata.certificateTemplateId}`}</p>
|
||||
</Td>
|
||||
);
|
||||
default:
|
||||
return <Td />;
|
||||
}
|
||||
|
@ -0,0 +1,225 @@
|
||||
import { useEffect } from "react";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import z from "zod";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import {
|
||||
Button,
|
||||
FormControl,
|
||||
Input,
|
||||
Modal,
|
||||
ModalContent,
|
||||
Select,
|
||||
SelectItem,
|
||||
Switch,
|
||||
TextArea
|
||||
} from "@app/components/v2";
|
||||
import { useToggle } from "@app/hooks";
|
||||
import { useCreateEstConfig, useGetEstConfig, useUpdateEstConfig } from "@app/hooks/api";
|
||||
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||
|
||||
enum EnrollmentMethod {
|
||||
EST = "est"
|
||||
}
|
||||
|
||||
type Props = {
|
||||
popUp: UsePopUpState<["enrollmentOptions"]>;
|
||||
handlePopUpToggle: (
|
||||
popUpName: keyof UsePopUpState<["enrollmentOptions"]>,
|
||||
state?: boolean
|
||||
) => void;
|
||||
};
|
||||
|
||||
const schema = z.object({
|
||||
method: z.nativeEnum(EnrollmentMethod),
|
||||
caChain: z.string(),
|
||||
passphrase: z.string().optional(),
|
||||
isEnabled: z.boolean()
|
||||
});
|
||||
|
||||
export type FormData = z.infer<typeof schema>;
|
||||
|
||||
export const CertificateTemplateEnrollmentModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
const popUpData = popUp?.enrollmentOptions?.data as {
|
||||
id: string;
|
||||
};
|
||||
const certificateTemplateId = popUpData?.id;
|
||||
|
||||
const { data } = useGetEstConfig(certificateTemplateId);
|
||||
|
||||
const {
|
||||
control,
|
||||
handleSubmit,
|
||||
reset,
|
||||
setError,
|
||||
formState: { isSubmitting }
|
||||
} = useForm<FormData>({
|
||||
resolver: zodResolver(schema)
|
||||
});
|
||||
|
||||
const { mutateAsync: createEstConfig } = useCreateEstConfig();
|
||||
const { mutateAsync: updateEstConfig } = useUpdateEstConfig();
|
||||
const [isPassphraseFocused, setIsPassphraseFocused] = useToggle(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (data) {
|
||||
reset({
|
||||
caChain: data.caChain,
|
||||
isEnabled: data.isEnabled
|
||||
});
|
||||
} else {
|
||||
reset({
|
||||
caChain: "",
|
||||
isEnabled: false
|
||||
});
|
||||
}
|
||||
}, [data]);
|
||||
|
||||
const onFormSubmit = async ({ caChain, passphrase, isEnabled }: FormData) => {
|
||||
try {
|
||||
if (data) {
|
||||
await updateEstConfig({
|
||||
certificateTemplateId,
|
||||
caChain,
|
||||
passphrase,
|
||||
isEnabled
|
||||
});
|
||||
} else {
|
||||
if (!passphrase) {
|
||||
setError("passphrase", { message: "Passphrase is required to setup EST" });
|
||||
return;
|
||||
}
|
||||
|
||||
await createEstConfig({
|
||||
certificateTemplateId,
|
||||
caChain,
|
||||
passphrase,
|
||||
isEnabled
|
||||
});
|
||||
}
|
||||
|
||||
handlePopUpToggle("enrollmentOptions", false);
|
||||
|
||||
createNotification({
|
||||
text: "Successfully saved changes",
|
||||
type: "success"
|
||||
});
|
||||
|
||||
reset();
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isOpen={popUp?.enrollmentOptions?.isOpen}
|
||||
onOpenChange={(isOpen) => {
|
||||
handlePopUpToggle("enrollmentOptions", isOpen);
|
||||
reset();
|
||||
}}
|
||||
>
|
||||
<ModalContent title="Manage Enrollment Options">
|
||||
<form onSubmit={handleSubmit(onFormSubmit)}>
|
||||
<Controller
|
||||
control={control}
|
||||
name="method"
|
||||
defaultValue={EnrollmentMethod.EST}
|
||||
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Client Enrollment Method"
|
||||
errorText={error?.message}
|
||||
isError={Boolean(error)}
|
||||
>
|
||||
<Select
|
||||
defaultValue={field.value}
|
||||
{...field}
|
||||
onValueChange={(e) => onChange(e)}
|
||||
className="w-full"
|
||||
>
|
||||
<SelectItem value={EnrollmentMethod.EST} key={EnrollmentMethod.EST}>
|
||||
EST
|
||||
</SelectItem>
|
||||
</Select>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
{data && (
|
||||
<FormControl label="EST Label">
|
||||
<Input value={data.certificateTemplateId} isDisabled className="bg-white/[0.07]" />
|
||||
</FormControl>
|
||||
)}
|
||||
<Controller
|
||||
control={control}
|
||||
name="caChain"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Certificate Authority Chain"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
isRequired
|
||||
>
|
||||
<TextArea
|
||||
{...field}
|
||||
className="min-h-[15rem] border-none bg-mineshaft-900 text-gray-400"
|
||||
reSize="none"
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="passphrase"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl label="Passphrase" isError={Boolean(error)} errorText={error?.message}>
|
||||
<Input
|
||||
{...field}
|
||||
type={isPassphraseFocused ? "text" : "password"}
|
||||
onFocus={() => setIsPassphraseFocused.on()}
|
||||
onBlur={() => setIsPassphraseFocused.off()}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="isEnabled"
|
||||
render={({ field, fieldState: { error } }) => {
|
||||
return (
|
||||
<FormControl isError={Boolean(error)} errorText={error?.message}>
|
||||
<Switch
|
||||
id="is-active"
|
||||
onCheckedChange={(value) => field.onChange(value)}
|
||||
isChecked={field.value}
|
||||
>
|
||||
<p className="ml-1 w-full">EST Enabled</p>
|
||||
</Switch>
|
||||
</FormControl>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
|
||||
<div className="mt-8 flex items-center">
|
||||
<Button
|
||||
className="mr-4"
|
||||
size="sm"
|
||||
type="submit"
|
||||
isLoading={isSubmitting}
|
||||
isDisabled={isSubmitting}
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
<Button
|
||||
colorSchema="secondary"
|
||||
variant="plain"
|
||||
onClick={() => handlePopUpToggle("enrollmentOptions", false)}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
);
|
||||
};
|
@ -8,13 +8,15 @@ import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@a
|
||||
import { usePopUp } from "@app/hooks";
|
||||
import { useDeleteCertTemplate } from "@app/hooks/api";
|
||||
|
||||
import { CertificateTemplateEnrollmentModal } from "./CertificateTemplateEnrollmentModal";
|
||||
import { CertificateTemplateModal } from "./CertificateTemplateModal";
|
||||
import { CertificateTemplatesTable } from "./CertificateTemplatesTable";
|
||||
|
||||
export const CertificateTemplatesSection = () => {
|
||||
const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([
|
||||
"certificateTemplate",
|
||||
"deleteCertificateTemplate"
|
||||
"deleteCertificateTemplate",
|
||||
"enrollmentOptions"
|
||||
] as const);
|
||||
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
@ -52,7 +54,7 @@ export const CertificateTemplatesSection = () => {
|
||||
<p className="text-xl font-semibold text-mineshaft-100">Certificate Templates</p>
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Create}
|
||||
a={ProjectPermissionSub.Certificates}
|
||||
a={ProjectPermissionSub.CertificateTemplates}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<Button
|
||||
@ -69,6 +71,7 @@ export const CertificateTemplatesSection = () => {
|
||||
</div>
|
||||
<CertificateTemplatesTable handlePopUpOpen={handlePopUpOpen} />
|
||||
<CertificateTemplateModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
|
||||
<CertificateTemplateEnrollmentModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
|
||||
<DeleteActionModal
|
||||
isOpen={popUp.deleteCertificateTemplate.isOpen}
|
||||
title={`Are you sure want to delete the certificate template ${
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { faEllipsis, faFileAlt, faTrash } from "@fortawesome/free-solid-svg-icons";
|
||||
import { faEllipsis, faFileAlt, faTrash, faUserPlus } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
@ -25,7 +25,9 @@ import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||
|
||||
type Props = {
|
||||
handlePopUpOpen: (
|
||||
popUpName: keyof UsePopUpState<["certificateTemplate", "deleteCertificateTemplate"]>,
|
||||
popUpName: keyof UsePopUpState<
|
||||
["certificateTemplate", "deleteCertificateTemplate", "enrollmentOptions"]
|
||||
>,
|
||||
data?: {
|
||||
id?: string;
|
||||
name?: string;
|
||||
@ -74,10 +76,31 @@ export const CertificateTemplatesTable = ({ handlePopUpOpen }: Props) => {
|
||||
id: certificateTemplate.id
|
||||
})
|
||||
}
|
||||
icon={<FontAwesomeIcon icon={faFileAlt} />}
|
||||
icon={<FontAwesomeIcon icon={faFileAlt} size="sm" className="mr-1" />}
|
||||
>
|
||||
Manage Policies
|
||||
</DropdownMenuItem>
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Edit}
|
||||
a={ProjectPermissionSub.CertificateTemplates}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<DropdownMenuItem
|
||||
onClick={() =>
|
||||
handlePopUpOpen("enrollmentOptions", {
|
||||
id: certificateTemplate.id
|
||||
})
|
||||
}
|
||||
className={twMerge(
|
||||
!isAllowed && "pointer-events-none cursor-not-allowed opacity-50"
|
||||
)}
|
||||
disabled={!isAllowed}
|
||||
icon={<FontAwesomeIcon icon={faUserPlus} size="sm" />}
|
||||
>
|
||||
Manage Enrollment
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Delete}
|
||||
a={ProjectPermissionSub.CertificateTemplates}
|
||||
@ -88,7 +111,7 @@ export const CertificateTemplatesTable = ({ handlePopUpOpen }: Props) => {
|
||||
!isAllowed && "pointer-events-none cursor-not-allowed opacity-50"
|
||||
)}
|
||||
disabled={!isAllowed}
|
||||
icon={<FontAwesomeIcon icon={faTrash} />}
|
||||
icon={<FontAwesomeIcon icon={faTrash} size="sm" className="mr-1" />}
|
||||
onClick={() =>
|
||||
handlePopUpOpen("deleteCertificateTemplate", {
|
||||
id: certificateTemplate.id,
|
||||
|
@ -15,6 +15,23 @@ server {
|
||||
proxy_cookie_path / "/; HttpOnly; SameSite=strict";
|
||||
}
|
||||
|
||||
location /.well-known/est {
|
||||
proxy_set_header X-Real-RIP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
|
||||
proxy_set_header Host $http_host;
|
||||
proxy_set_header X-NginX-Proxy true;
|
||||
|
||||
# specific for infisical cloud setup, needed for server-side mTLS
|
||||
proxy_set_header X-SSL-Client-Cert $http_x_amzn_mtls_clientcert
|
||||
|
||||
proxy_pass http://backend:4000;
|
||||
proxy_redirect off;
|
||||
|
||||
# proxy_cookie_path / "/; secure; HttpOnly; SameSite=strict";
|
||||
proxy_cookie_path / "/; HttpOnly; SameSite=strict";
|
||||
}
|
||||
|
||||
# location /git-app-api {
|
||||
# proxy_set_header X-Real-RIP $remote_addr;
|
||||
# proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
|
@ -1,5 +1,8 @@
|
||||
server {
|
||||
listen 80;
|
||||
|
||||
large_client_header_buffers 8 128k;
|
||||
client_header_buffer_size 128k;
|
||||
|
||||
location /api {
|
||||
proxy_set_header X-Real-RIP $remote_addr;
|
||||
@ -13,7 +16,26 @@ server {
|
||||
|
||||
proxy_cookie_path / "/; HttpOnly; SameSite=strict";
|
||||
}
|
||||
|
||||
|
||||
location /.well-known/est {
|
||||
|
||||
proxy_set_header X-Real-RIP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
|
||||
proxy_set_header Host $http_host;
|
||||
proxy_set_header X-NginX-Proxy true;
|
||||
|
||||
proxy_set_header X-SSL-Client-Cert $ssl_client_escaped_cert;
|
||||
# proxy_set_header X-SSL-Client-Cert $http_x_ssl_client_cert;
|
||||
# proxy_pass_request_headers on;
|
||||
|
||||
proxy_pass http://backend:4000;
|
||||
proxy_redirect off;
|
||||
|
||||
# proxy_cookie_path / "/; secure; HttpOnly; SameSite=strict";
|
||||
proxy_cookie_path / "/; HttpOnly; SameSite=strict";
|
||||
}
|
||||
|
||||
location / {
|
||||
include /etc/nginx/mime.types;
|
||||
|
||||
|