Compare commits

..

57 Commits

Author SHA1 Message Date
79680b6a73 Merge pull request #2340 from Infisical/misc/added-timeout-for-hijacked-est-connection
misc: added timeout for est connection
2024-08-27 23:31:07 +08:00
58838c541f misc: added timeout for est connection 2024-08-27 23:26:56 +08:00
03cc71cfed Merge pull request #2284 from Infisical/feature/est-simpleenroll
Certificate EST protocol (simpleenroll, simplereenroll, cacerts)
2024-08-27 13:42:58 +08:00
02529106c9 Merge pull request #2336 from akhilmhdh/fix/scim-error
fix: resolved scim group update failing
2024-08-26 16:34:27 -04:00
=
d939ff289d fix: resolved scim group update failing 2024-08-27 01:50:26 +05:30
d1816c3051 Merge pull request #2334 from Infisical/daniel/azure-devops-docs
Docs: Azure DevOps Integration
2024-08-26 23:49:23 +04:00
cb350788c0 Update create.tsx 2024-08-26 23:21:56 +04:00
cd58768d6f Updated images 2024-08-26 23:20:51 +04:00
dcd6f4d55d Fix: Updated Azure DevOps integration styling 2024-08-26 23:12:00 +04:00
3c828614b8 Fix: Azure DevOps Label naming typos 2024-08-26 22:44:11 +04:00
09e7988596 Docs: Azure DevOps Integration 2024-08-26 22:43:49 +04:00
f40df19334 misc: finalized est config schema 2024-08-27 02:15:01 +08:00
76c9d3488b Merge remote-tracking branch 'origin/main' into feature/est-simpleenroll 2024-08-27 02:13:59 +08:00
0809da33e0 misc: improved docs and added support for curl clients 2024-08-27 02:05:35 +08:00
b528eec4bb Merge pull request #2333 from Infisical/daniel/secret-change-emails
Feat: Email notification on secret change requests
2024-08-26 21:50:30 +04:00
5179103680 Update SecretApprovalRequest.tsx 2024-08-26 21:46:05 +04:00
25a9e5f58a Update SecretApprovalRequest.tsx 2024-08-26 21:42:47 +04:00
8ddfe7b6e9 Update secret-approval-request-fns.ts 2024-08-26 20:53:43 +04:00
c23f21d57a Update SecretApprovalRequest.tsx 2024-08-26 20:21:05 +04:00
1242a43d98 Feat: Open approval with ID in URL 2024-08-26 20:04:06 +04:00
1655ca27d1 Fix: Creation of secret approval policies 2024-08-26 20:02:58 +04:00
2bcead03b0 Feat: Send secret change request emails to approvers 2024-08-26 19:55:04 +04:00
41ab1972ce Feat: Find project and include org dal 2024-08-26 19:54:48 +04:00
b00fff6922 Update index.ts 2024-08-26 19:54:15 +04:00
97b01ca5f8 Feat: Send secret change request emails to approvers 2024-08-26 19:54:01 +04:00
c2bd6f5ef3 Feat: Send secret change request emails to approvers 2024-08-26 19:53:49 +04:00
18efc9a6de Include more user details 2024-08-26 19:53:17 +04:00
436ccb25fb Merge pull request #2331 from Infisical/daniel/presist-selfhosting-domains
Feat: Persistent self-hosting domains on `infisical login`
2024-08-26 18:04:25 +04:00
8f08a352dd Merge pull request #2310 from Infisical/daniel/azure-devops-integration
Feat: Azure DevOps Integration
2024-08-26 18:04:04 +04:00
00f86cfd00 misc: addressed review comments 2024-08-26 21:10:29 +08:00
3944aafb11 Use slices 2024-08-26 15:18:45 +04:00
1f24d02c5e Fix: Do not save duplicate domains 2024-08-26 15:08:51 +04:00
f560534493 Replace custom pkcs7 fns with module 2024-08-25 20:21:53 -07:00
7a2f0214f3 Feat: Persist self-hosting domains on infisical login 2024-08-24 13:18:03 +04:00
1317266415 Merge remote-tracking branch 'origin' into feature/est-simpleenroll 2024-08-22 14:54:34 -07:00
288d7e88ae misc: made SSL header key configurable via env 2024-08-23 01:38:12 +08:00
f88389bf9e misc: added general format 2024-08-22 21:05:34 +08:00
2e88c5e2c5 misc: improved url examples in est doc 2024-08-22 21:02:38 +08:00
73f3b8173e doc: added guide for EST usage' 2024-08-22 20:44:21 +08:00
aa5b88ff04 misc: removed enrollment options from CA page 2024-08-22 15:40:36 +08:00
b7caff88cf feat: finished up EST cacerts 2024-08-22 15:39:53 +08:00
760a1e917a feat: added simplereenroll 2024-08-20 23:56:27 +08:00
2d7ff66246 Merge branch 'feature/est-simpleenroll' of https://github.com/Infisical/infisical into feature/est-simpleenroll 2024-08-20 15:31:58 +08:00
179497e830 misc: moved est logic to service 2024-08-20 15:31:10 +08:00
4c08c80e5b Merge remote-tracking branch 'origin' into feature/est-simpleenroll 2024-08-19 14:53:04 -07:00
7d6af64904 misc: added proxy header for amazon mtls client cert 2024-08-20 01:53:47 +08:00
16519f9486 feat: added reading SANs from CSR 2024-08-20 01:39:40 +08:00
bb27d38a12 misc: ui form adjustments 2024-08-19 21:39:00 +08:00
5b26928751 misc: added audit logs 2024-08-19 20:25:07 +08:00
f425e7e48f misc: addressed alignment issue 2024-08-19 19:50:09 +08:00
4601f46afb misc: finalized variable naming 2024-08-19 19:33:46 +08:00
692bdc060c misc: updated est configuration to be binded to certificate template 2024-08-19 19:26:20 +08:00
3a4f8c2e54 Merge branch 'feature/certificate-template' into feature/est-simpleenroll 2024-08-19 17:04:22 +08:00
146c4284a2 feat: integrated to est routes 2024-08-14 20:52:21 +08:00
5ae33b9f3b misc: minor UI updates 2024-08-14 01:10:25 +08:00
1f38b92ec6 feat: finished up integration for est config management 2024-08-14 01:00:31 +08:00
f2a49a79f0 feat: initial simpleenroll setup (mvp) 2024-08-13 23:22:47 +08:00
69 changed files with 2049 additions and 171 deletions

View File

@ -70,3 +70,5 @@ NEXT_PUBLIC_CAPTCHA_SITE_KEY=
PLAIN_API_KEY=
PLAIN_WISH_LABEL_IDS=
SSL_CLIENT_CERTIFICATE_HEADER_KEY=

View File

@ -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",

View File

@ -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",

View File

@ -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;

View File

@ -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,

View File

@ -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);
}

View 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>
>;

View File

@ -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";

View File

@ -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",

View File

@ -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({

View File

@ -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;

View File

@ -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
? [

View File

@ -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
});
}
}

View File

@ -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;

View File

@ -20,7 +20,15 @@ export const secretApprovalPolicyDALFactory = (db: TDbClient) => {
`${TableName.SecretApprovalPolicy}.id`,
`${TableName.SecretApprovalPolicyApprover}.policyId`
)
.select(tx.ref("approverUserId").withSchema(TableName.SecretApprovalPolicyApprover))
.leftJoin(TableName.Users, `${TableName.SecretApprovalPolicyApprover}.approverUserId`, `${TableName.Users}.id`)
.select(
tx.ref("approverUserId").withSchema(TableName.SecretApprovalPolicyApprover),
tx.ref("email").withSchema(TableName.Users).as("approverEmail"),
tx.ref("firstName").withSchema(TableName.Users).as("approverFirstName"),
tx.ref("lastName").withSchema(TableName.Users).as("approverLastName")
)
.select(
tx.ref("name").withSchema(TableName.Environment).as("envName"),
tx.ref("slug").withSchema(TableName.Environment).as("envSlug"),
@ -47,8 +55,11 @@ export const secretApprovalPolicyDALFactory = (db: TDbClient) => {
{
key: "approverUserId",
label: "userApprovers" as const,
mapper: ({ approverUserId }) => ({
userId: approverUserId
mapper: ({ approverUserId, approverEmail, approverFirstName, approverLastName }) => ({
userId: approverUserId,
email: approverEmail,
firstName: approverFirstName,
lastName: approverLastName
})
}
]

View File

@ -0,0 +1,46 @@
import { TSecretApprovalRequests } from "@app/db/schemas";
import { getConfig } from "@app/lib/config/env";
import { TProjectDALFactory } from "@app/services/project/project-dal";
import { SmtpTemplates, TSmtpService } from "@app/services/smtp/smtp-service";
import { TSecretApprovalPolicyDALFactory } from "../secret-approval-policy/secret-approval-policy-dal";
type TSendApprovalEmails = {
secretApprovalPolicyDAL: Pick<TSecretApprovalPolicyDALFactory, "findById">;
projectDAL: Pick<TProjectDALFactory, "findProjectWithOrg">;
smtpService: Pick<TSmtpService, "sendMail">;
projectId: string;
secretApprovalRequest: TSecretApprovalRequests;
};
export const sendApprovalEmailsFn = async ({
secretApprovalPolicyDAL,
projectDAL,
smtpService,
projectId,
secretApprovalRequest
}: TSendApprovalEmails) => {
const cfg = getConfig();
const policy = await secretApprovalPolicyDAL.findById(secretApprovalRequest.policyId);
const project = await projectDAL.findProjectWithOrg(projectId);
// now we need to go through each of the reviewers and print out all the commits that they need to approve
for await (const reviewerUser of policy.userApprovers) {
await smtpService.sendMail({
recipients: [reviewerUser?.email as string],
subjectLine: "Infisical Secret Change Request",
substitutions: {
firstName: reviewerUser.firstName,
projectName: project.name,
organizationName: project.organization.name,
approvalUrl: `${cfg.isDevelopmentMode ? "https" : "http"}://${cfg.SITE_URL}/project/${
project.id
}/approval?requestId=${secretApprovalRequest.id}`
},
template: SmtpTemplates.SecretApprovalRequestNeedsReview
});
}
};

View File

@ -53,8 +53,10 @@ import { TUserDALFactory } from "@app/services/user/user-dal";
import { TLicenseServiceFactory } from "../license/license-service";
import { TPermissionServiceFactory } from "../permission/permission-service";
import { ProjectPermissionActions, ProjectPermissionSub } from "../permission/project-permission";
import { TSecretApprovalPolicyDALFactory } from "../secret-approval-policy/secret-approval-policy-dal";
import { TSecretSnapshotServiceFactory } from "../secret-snapshot/secret-snapshot-service";
import { TSecretApprovalRequestDALFactory } from "./secret-approval-request-dal";
import { sendApprovalEmailsFn } from "./secret-approval-request-fns";
import { TSecretApprovalRequestReviewerDALFactory } from "./secret-approval-request-reviewer-dal";
import { TSecretApprovalRequestSecretDALFactory } from "./secret-approval-request-secret-dal";
import {
@ -89,7 +91,10 @@ type TSecretApprovalRequestServiceFactoryDep = {
smtpService: Pick<TSmtpService, "sendMail">;
userDAL: Pick<TUserDALFactory, "find" | "findOne">;
projectEnvDAL: Pick<TProjectEnvDALFactory, "findOne">;
projectDAL: Pick<TProjectDALFactory, "checkProjectUpgradeStatus" | "findById" | "findProjectById">;
projectDAL: Pick<
TProjectDALFactory,
"checkProjectUpgradeStatus" | "findById" | "findProjectById" | "findProjectWithOrg"
>;
secretQueueService: Pick<TSecretQueueFactory, "syncSecrets" | "removeSecretReminder">;
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey" | "encryptWithInputKey" | "decryptWithInputKey">;
secretV2BridgeDAL: Pick<
@ -98,6 +103,7 @@ type TSecretApprovalRequestServiceFactoryDep = {
>;
secretVersionV2BridgeDAL: Pick<TSecretVersionV2DALFactory, "insertMany" | "findLatestVersionMany">;
secretVersionTagV2BridgeDAL: Pick<TSecretVersionV2TagDALFactory, "insertMany">;
secretApprovalPolicyDAL: Pick<TSecretApprovalPolicyDALFactory, "findById">;
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
};
@ -121,6 +127,7 @@ export const secretApprovalRequestServiceFactory = ({
smtpService,
userDAL,
projectEnvDAL,
secretApprovalPolicyDAL,
kmsService,
secretV2BridgeDAL,
secretVersionV2BridgeDAL,
@ -1061,6 +1068,15 @@ export const secretApprovalRequestServiceFactory = ({
}
return { ...doc, commits: approvalCommits };
});
await sendApprovalEmailsFn({
projectDAL,
secretApprovalPolicyDAL,
secretApprovalRequest,
smtpService,
projectId
});
return secretApprovalRequest;
};
@ -1311,8 +1327,17 @@ export const secretApprovalRequestServiceFactory = ({
tx
);
}
return { ...doc, commits: approvalCommits };
});
await sendApprovalEmailsFn({
projectDAL,
secretApprovalPolicyDAL,
secretApprovalRequest,
smtpService,
projectId
});
return secretApprovalRequest;
};

View File

@ -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,

View File

@ -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) {

View 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
});
}
});
};

View File

@ -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({
@ -845,6 +862,7 @@ export const registerRoutes = async (
secretQueueService,
kmsService,
secretV2BridgeDAL,
secretApprovalPolicyDAL,
secretVersionV2BridgeDAL,
secretVersionTagV2BridgeDAL,
smtpService,
@ -1195,6 +1213,7 @@ export const registerRoutes = async (
certificateAuthority: certificateAuthorityService,
certificateTemplate: certificateTemplateService,
certificateAuthorityCrl: certificateAuthorityCrlService,
certificateEst: certificateEstService,
pkiAlert: pkiAlertService,
pkiCollection: pkiCollectionService,
secretScanning: secretScanningService,
@ -1262,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) => {

View File

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

View File

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

View File

@ -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;
}
});
};

View File

@ -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,

View File

@ -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;

View 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;
};

View 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
};
};

View File

@ -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;
};

View File

@ -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
};
};

View File

@ -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">);

View File

@ -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;
};

View File

@ -279,6 +279,34 @@ export const projectDALFactory = (db: TDbClient) => {
}
};
const findProjectWithOrg = async (projectId: string) => {
// we just need the project, and we need to include a new .organization field that includes the org from the orgId reference
const project = await db(TableName.Project)
.where({ [`${TableName.Project}.id` as "id"]: projectId })
.join(TableName.Organization, `${TableName.Organization}.id`, `${TableName.Project}.orgId`)
.select(
db.ref("id").withSchema(TableName.Organization).as("organizationId"),
db.ref("name").withSchema(TableName.Organization).as("organizationName")
)
.select(selectAllTableCols(TableName.Project))
.first();
if (!project) {
throw new BadRequestError({ message: "Project not found" });
}
return {
...ProjectsSchema.parse(project),
organization: {
id: project.organizationId,
name: project.organizationName
}
};
};
return {
...projectOrm,
findAllProjects,
@ -288,6 +316,7 @@ export const projectDALFactory = (db: TDbClient) => {
findProjectById,
findProjectByFilter,
findProjectBySlug,
findProjectWithOrg,
checkProjectUpgradeStatus
};
};

View File

@ -25,6 +25,7 @@ export enum SmtpTemplates {
UnlockAccount = "unlockAccount.handlebars",
AccessApprovalRequest = "accessApprovalRequest.handlebars",
AccessSecretRequestBypassed = "accessSecretRequestBypassed.handlebars",
SecretApprovalRequestNeedsReview = "secretApprovalRequestNeedsReview.handlebars",
HistoricalSecretList = "historicalSecretLeakIncident.handlebars",
NewDeviceJoin = "newDevice.handlebars",
OrgInvite = "organizationInvitation.handlebars",

View File

@ -0,0 +1,22 @@
<html>
<head>
<meta charset="utf-8" />
<meta http-equiv="x-ua-compatible" content="ie=edge" />
<title>Secret Change Approval Request</title>
</head>
<body>
<h2>Hi {{firstName}},</h2>
<h2>New secret change requests are pending review.</h2>
<br />
<p>You have a secret change request pending your review in project "{{projectName}}", in the "{{organizationName}}"
organization.</p>
<p>
View the request and approve or deny it
<a href="{{approvalUrl}}">here</a>.
</p>
</body>
</html>

View File

@ -8,6 +8,7 @@ import (
"encoding/hex"
"encoding/json"
"os"
"slices"
"strings"
"time"
@ -152,6 +153,28 @@ var loginCmd = &cobra.Command{
DisableFlagsInUseLine: true,
Run: func(cmd *cobra.Command, args []string) {
clearSelfHostedDomains, err := cmd.Flags().GetBool("clear-domains")
if err != nil {
util.HandleError(err)
}
if clearSelfHostedDomains {
infisicalConfig, err := util.GetConfigFile()
if err != nil {
util.HandleError(err)
}
infisicalConfig.Domains = []string{}
err = util.WriteConfigFile(&infisicalConfig)
if err != nil {
util.HandleError(err)
}
fmt.Println("Cleared all self-hosted domains from the config file")
return
}
infisicalClient := infisicalSdk.NewInfisicalClient(infisicalSdk.Config{
SiteUrl: config.INFISICAL_URL,
UserAgent: api.USER_AGENT,
@ -464,6 +487,7 @@ func cliDefaultLogin(userCredentialsToBeStored *models.UserCredentials) {
func init() {
rootCmd.AddCommand(loginCmd)
loginCmd.Flags().Bool("clear-domains", false, "clear all self-hosting domains from the config file")
loginCmd.Flags().BoolP("interactive", "i", false, "login via the command line")
loginCmd.Flags().String("method", "user", "login method [user, universal-auth]")
loginCmd.Flags().Bool("plain", false, "only output the token without any formatting")
@ -499,10 +523,12 @@ func DomainOverridePrompt() (bool, error) {
}
func askForDomain() error {
//query user to choose between Infisical cloud or self hosting
// query user to choose between Infisical cloud or self hosting
const (
INFISICAL_CLOUD = "Infisical Cloud"
SELF_HOSTING = "Self Hosting"
ADD_NEW_DOMAIN = "Add a new domain"
)
options := []string{INFISICAL_CLOUD, SELF_HOSTING}
@ -524,6 +550,36 @@ func askForDomain() error {
return nil
}
infisicalConfig, err := util.GetConfigFile()
if err != nil {
return fmt.Errorf("askForDomain: unable to get config file because [err=%s]", err)
}
if infisicalConfig.Domains != nil && len(infisicalConfig.Domains) > 0 {
// If domains are present in the config, let the user select from the list or select to add a new domain
items := append(infisicalConfig.Domains, ADD_NEW_DOMAIN)
prompt := promptui.Select{
Label: "Which domain would you like to use?",
Items: items,
Size: 5,
}
_, selectedOption, err := prompt.Run()
if err != nil {
return err
}
if selectedOption != ADD_NEW_DOMAIN {
config.INFISICAL_URL = fmt.Sprintf("%s/api", selectedOption)
config.INFISICAL_LOGIN_URL = fmt.Sprintf("%s/login", selectedOption)
return nil
}
}
urlValidation := func(input string) error {
_, err := url.ParseRequestURI(input)
if err != nil {
@ -542,12 +598,23 @@ func askForDomain() error {
if err != nil {
return err
}
//trimmed the '/' from the end of the self hosting url
// Trimmed the '/' from the end of the self hosting url, and set the api & login url
domain = strings.TrimRight(domain, "/")
//set api and login url
config.INFISICAL_URL = fmt.Sprintf("%s/api", domain)
config.INFISICAL_LOGIN_URL = fmt.Sprintf("%s/login", domain)
//return nil
// Write the new domain to the config file, to allow the user to select it in the future if needed
// First check if infiscialConfig.Domains already includes the domain, if it does, do not add it again
if !slices.Contains(infisicalConfig.Domains, domain) {
infisicalConfig.Domains = append(infisicalConfig.Domains, domain)
err = util.WriteConfigFile(&infisicalConfig)
if err != nil {
return fmt.Errorf("askForDomain: unable to write domains to config file because [err=%s]", err)
}
}
return nil
}

View File

@ -16,6 +16,7 @@ type ConfigFile struct {
LoggedInUsers []LoggedInUser `json:"loggedInUsers,omitempty"`
VaultBackendType string `json:"vaultBackendType,omitempty"`
VaultBackendPassphrase string `json:"vaultBackendPassphrase,omitempty"`
Domains []string `json:"domains,omitempty"`
}
type LoggedInUser struct {

View File

@ -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:

View 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
![est enrollment dashboard](/images/platform/pki/est/template-enroll-hover.png)
3. Select **EST** as the client enrollment method and fill up the remaining fields.
![est enrollment modal create](/images/platform/pki/est/template-enrollment-modal.png)
- **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.
![est enrollment modal create](/images/platform/pki/est/template-enrollment-est-label.png)
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.

Binary file not shown.

After

Width:  |  Height:  |  Size: 136 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 197 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 192 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 111 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 93 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 723 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 652 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 612 KiB

View 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">
![integrations](../../images/integrations/azure-devops/overview-page.png)
</Step>
<Step title="Create a new token">
Make sure the newly created token has Read/Write access to the Release scope.
![integrations](../../images/integrations/azure-devops/create-new-token.png)
<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.
![integrations](../../images/integrations/azure-devops/new-token-created.png)
</Step>
</Steps>
#### Setup the Infisical Azure DevOps integration
Navigate to your project's integrations tab and select the 'Azure DevOps' integration.
![integrations](../../images/integrations.png)
<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.
![integrations](../../images/integrations/azure-devops/new-infiscial-integration-step-1.png)
</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.
![integrations](../../images/integrations/azure-devops/new-infiscial-integration-step-2.png)
</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.

View File

@ -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",

View File

@ -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 } = {

View File

@ -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"
}

View File

@ -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;

View File

@ -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";

View File

@ -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) => {

View File

@ -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";

View File

@ -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));
}
});
};

View File

@ -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)
});
};

View File

@ -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;
};

View File

@ -9,7 +9,15 @@ export const useCreateSecretApprovalPolicy = () => {
const queryClient = useQueryClient();
return useMutation<{}, {}, TCreateSecretPolicyDTO>({
mutationFn: async ({ environment, workspaceId, approvals, approvers, secretPath, name, enforcementLevel }) => {
mutationFn: async ({
environment,
workspaceId,
approvals,
approvers,
secretPath,
name,
enforcementLevel
}) => {
const { data } = await apiRequest.post("/api/v1/secret-approvals", {
environment,
workspaceId,

View File

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

View File

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

View File

@ -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 />;
}

View File

@ -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>
);
};

View File

@ -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 ${

View File

@ -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,

View File

@ -45,11 +45,11 @@ const formSchema = z
name: z.string().optional(),
secretPath: z.string().optional(),
approvals: z.number().min(1),
approverUserIds: z.string().array().min(1),
approvers: z.string().array().min(1),
policyType: z.nativeEnum(PolicyType),
enforcementLevel: z.nativeEnum(EnforcementLevel)
})
.refine((data) => data.approvals <= data.approverUserIds.length, {
.refine((data) => data.approvals <= data.approvers.length, {
path: ["approvals"],
message: "The number of approvals should be lower than the number of approvers."
});
@ -75,8 +75,7 @@ export const AccessPolicyForm = ({
? {
...editValues,
environment: editValues.environment.slug,
approverUserIds:
editValues?.userApprovers?.map((user) => user.userId) || editValues?.approvers
approvers: editValues?.userApprovers?.map((user) => user.userId) || editValues?.approvers
}
: undefined
});
@ -264,7 +263,7 @@ export const AccessPolicyForm = ({
/>
<Controller
control={control}
name="approverUserIds"
name="approvers"
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
label="Required Approvers"

View File

@ -1,4 +1,5 @@
import { Fragment, useState } from "react";
import { Fragment, useEffect, useState } from "react";
import { useRouter } from "next/router";
import {
faCheck,
faCheckCircle,
@ -31,7 +32,7 @@ import {
useGetSecretApprovalRequests,
useGetWorkspaceUsers
} from "@app/hooks/api";
import { ApprovalStatus, TSecretApprovalRequest } from "@app/hooks/api/types";
import { ApprovalStatus } from "@app/hooks/api/types";
import {
generateCommitText,
@ -41,12 +42,13 @@ import {
export const SecretApprovalRequest = () => {
const { currentWorkspace } = useWorkspace();
const workspaceId = currentWorkspace?.id || "";
const [selectedApproval, setSelectedApproval] = useState<TSecretApprovalRequest | null>(null);
const [selectedApprovalId, setSelectedApprovalId] = useState<string | null>(null);
// filters
const [statusFilter, setStatusFilter] = useState<"open" | "close">("open");
const [envFilter, setEnvFilter] = useState<string>();
const [committerFilter, setCommitterFilter] = useState<string>();
const [usingUrlRequestId, setUsingUrlRequestId] = useState(false);
const {
data: secretApprovalRequests,
@ -64,12 +66,21 @@ export const SecretApprovalRequest = () => {
const { data: secretApprovalRequestCount, isSuccess: isSecretApprovalReqCountSuccess } =
useGetSecretApprovalRequestCount({ workspaceId });
const { user: userSession } = useUser();
const router = useRouter();
const { permission } = useProjectPermission();
const { data: members } = useGetWorkspaceUsers(workspaceId);
const isSecretApprovalScreen = Boolean(selectedApproval);
const isSecretApprovalScreen = Boolean(selectedApprovalId);
const { requestId } = router.query;
useEffect(() => {
if (!requestId || usingUrlRequestId) return;
setSelectedApprovalId(requestId as string);
setUsingUrlRequestId(true);
}, [requestId]);
const handleGoBackSecretRequestDetail = () => {
setSelectedApproval(null);
setSelectedApprovalId(null);
refetch({ refetchPage: (_page, index) => index === 0 });
};
@ -88,7 +99,7 @@ export const SecretApprovalRequest = () => {
>
<SecretApprovalRequestChanges
workspaceId={workspaceId}
approvalRequestId={selectedApproval?.id || ""}
approvalRequestId={selectedApprovalId || ""}
onGoBack={handleGoBackSecretRequestDetail}
/>
</motion.div>
@ -219,9 +230,9 @@ export const SecretApprovalRequest = () => {
className="flex flex-col px-8 py-4 hover:bg-mineshaft-700"
role="button"
tabIndex={0}
onClick={() => setSelectedApproval(secretApproval)}
onClick={() => setSelectedApprovalId(secretApproval.id)}
onKeyDown={(evt) => {
if (evt.key === "Enter") setSelectedApproval(secretApproval);
if (evt.key === "Enter") setSelectedApprovalId(secretApproval.id);
}}
>
<div className="mb-1">

View File

@ -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;

View File

@ -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;