Compare commits
104 Commits
crl-update
...
mzidul-wjd
Author | SHA1 | Date | |
---|---|---|---|
2d26febe58 | |||
c23ad8ebf2 | |||
bad068ef19 | |||
79680b6a73 | |||
58838c541f | |||
03cc71cfed | |||
02529106c9 | |||
d939ff289d | |||
d1816c3051 | |||
cb350788c0 | |||
cd58768d6f | |||
dcd6f4d55d | |||
3c828614b8 | |||
09e7988596 | |||
f40df19334 | |||
76c9d3488b | |||
0809da33e0 | |||
b528eec4bb | |||
5179103680 | |||
25a9e5f58a | |||
8ddfe7b6e9 | |||
c23f21d57a | |||
1242a43d98 | |||
1655ca27d1 | |||
2bcead03b0 | |||
41ab1972ce | |||
b00fff6922 | |||
97b01ca5f8 | |||
c2bd6f5ef3 | |||
18efc9a6de | |||
436ccb25fb | |||
8f08a352dd | |||
00f86cfd00 | |||
3944aafb11 | |||
a6b852fab9 | |||
2a043afe11 | |||
df8f2cf9ab | |||
a18015b1e5 | |||
8b80622d2f | |||
c0fd0a56f3 | |||
326764dd41 | |||
1f24d02c5e | |||
c130fbddd9 | |||
f560534493 | |||
10a97f4522 | |||
7a2f0214f3 | |||
a2b994ab23 | |||
c4715124dc | |||
67c1cb9bf1 | |||
68b1984a76 | |||
28ecc37163 | |||
a6a2e2bae0 | |||
d8bbfacae0 | |||
58549c398f | |||
842ed62bec | |||
06d8800ee0 | |||
2ecfd1bb7e | |||
783d4c7bd6 | |||
fbf3f26abd | |||
1d09693041 | |||
626e37e3d0 | |||
07fd67b328 | |||
3f1f018adc | |||
fe04e6d20c | |||
d7171a1617 | |||
384a0daa31 | |||
c5c949e034 | |||
c2c9edf156 | |||
c8248ef4e9 | |||
9f6a6a7b7c | |||
121b642d50 | |||
59b16f647e | |||
2ab5932693 | |||
8dfcef3900 | |||
8ca70eec44 | |||
60df59c7f0 | |||
e231c531a6 | |||
d48bb910fa | |||
1317266415 | |||
f0938330a7 | |||
e1bb0ac3ad | |||
f54d930de2 | |||
288d7e88ae | |||
f88389bf9e | |||
2e88c5e2c5 | |||
73f3b8173e | |||
aa5b88ff04 | |||
b7caff88cf | |||
760a1e917a | |||
2d7ff66246 | |||
179497e830 | |||
4c08c80e5b | |||
7d6af64904 | |||
16519f9486 | |||
bb27d38a12 | |||
5b26928751 | |||
f425e7e48f | |||
4601f46afb | |||
692bdc060c | |||
3a4f8c2e54 | |||
146c4284a2 | |||
5ae33b9f3b | |||
1f38b92ec6 | |||
f2a49a79f0 |
@ -70,3 +70,5 @@ NEXT_PUBLIC_CAPTCHA_SITE_KEY=
|
||||
|
||||
PLAIN_API_KEY=
|
||||
PLAIN_WISH_LABEL_IDS=
|
||||
|
||||
SSL_CLIENT_CERTIFICATE_HEADER_KEY=
|
||||
|
42
backend/package-lock.json
generated
@ -74,6 +74,7 @@
|
||||
"pg-query-stream": "^4.5.3",
|
||||
"picomatch": "^3.0.1",
|
||||
"pino": "^8.16.2",
|
||||
"pkijs": "^3.2.4",
|
||||
"posthog-node": "^3.6.2",
|
||||
"probot": "^13.0.0",
|
||||
"safe-regex": "^2.1.1",
|
||||
@ -4457,6 +4458,17 @@
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/@noble/hashes": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.4.0.tgz",
|
||||
"integrity": "sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg==",
|
||||
"engines": {
|
||||
"node": ">= 16"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://paulmillr.com/funding/"
|
||||
}
|
||||
},
|
||||
"node_modules/@node-saml/node-saml": {
|
||||
"version": "4.0.5",
|
||||
"resolved": "https://registry.npmjs.org/@node-saml/node-saml/-/node-saml-4.0.5.tgz",
|
||||
@ -8481,6 +8493,14 @@
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/bytestreamjs": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/bytestreamjs/-/bytestreamjs-2.0.1.tgz",
|
||||
"integrity": "sha512-U1Z/ob71V/bXfVABvNr/Kumf5VyeQRBEm6Txb0PQ6S7V5GpBM3w4Cbqz/xPDicR5tN0uvDifng8C+5qECeGwyQ==",
|
||||
"engines": {
|
||||
"node": ">=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/cac": {
|
||||
"version": "6.7.14",
|
||||
"resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz",
|
||||
@ -14120,6 +14140,22 @@
|
||||
"pathe": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/pkijs": {
|
||||
"version": "3.2.4",
|
||||
"resolved": "https://registry.npmjs.org/pkijs/-/pkijs-3.2.4.tgz",
|
||||
"integrity": "sha512-Et9V5QpvBilPFgagJcaKBqXjKrrgF5JL2mSDELk1vvbOTt4fuBhSSsGn9Tcz0TQTfS5GCpXQ31Whrpqeqp0VRg==",
|
||||
"dependencies": {
|
||||
"@noble/hashes": "^1.4.0",
|
||||
"asn1js": "^3.0.5",
|
||||
"bytestreamjs": "^2.0.0",
|
||||
"pvtsutils": "^1.3.2",
|
||||
"pvutils": "^1.1.3",
|
||||
"tslib": "^2.6.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/plimit-lit": {
|
||||
"version": "1.6.1",
|
||||
"resolved": "https://registry.npmjs.org/plimit-lit/-/plimit-lit-1.6.1.tgz",
|
||||
@ -16268,9 +16304,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/tslib": {
|
||||
"version": "2.6.2",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz",
|
||||
"integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q=="
|
||||
"version": "2.6.3",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.3.tgz",
|
||||
"integrity": "sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ=="
|
||||
},
|
||||
"node_modules/tsup": {
|
||||
"version": "8.0.1",
|
||||
|
@ -171,6 +171,7 @@
|
||||
"pg-query-stream": "^4.5.3",
|
||||
"picomatch": "^3.0.1",
|
||||
"pino": "^8.16.2",
|
||||
"pkijs": "^3.2.4",
|
||||
"posthog-node": "^3.6.2",
|
||||
"probot": "^13.0.0",
|
||||
"safe-regex": "^2.1.1",
|
||||
|
2
backend/src/@types/fastify.d.ts
vendored
@ -36,6 +36,7 @@ import { ActorAuthMethod, ActorType } from "@app/services/auth/auth-type";
|
||||
import { TAuthTokenServiceFactory } from "@app/services/auth-token/auth-token-service";
|
||||
import { TCertificateServiceFactory } from "@app/services/certificate/certificate-service";
|
||||
import { TCertificateAuthorityServiceFactory } from "@app/services/certificate-authority/certificate-authority-service";
|
||||
import { TCertificateEstServiceFactory } from "@app/services/certificate-est/certificate-est-service";
|
||||
import { TCertificateTemplateServiceFactory } from "@app/services/certificate-template/certificate-template-service";
|
||||
import { TGroupProjectServiceFactory } from "@app/services/group-project/group-project-service";
|
||||
import { TIdentityServiceFactory } from "@app/services/identity/identity-service";
|
||||
@ -160,6 +161,7 @@ declare module "fastify" {
|
||||
certificateTemplate: TCertificateTemplateServiceFactory;
|
||||
certificateAuthority: TCertificateAuthorityServiceFactory;
|
||||
certificateAuthorityCrl: TCertificateAuthorityCrlServiceFactory;
|
||||
certificateEst: TCertificateEstServiceFactory;
|
||||
pkiCollection: TPkiCollectionServiceFactory;
|
||||
secretScanning: TSecretScanningServiceFactory;
|
||||
license: TLicenseServiceFactory;
|
||||
|
8
backend/src/@types/knex.d.ts
vendored
@ -53,6 +53,9 @@ import {
|
||||
TCertificateSecretsUpdate,
|
||||
TCertificatesInsert,
|
||||
TCertificatesUpdate,
|
||||
TCertificateTemplateEstConfigs,
|
||||
TCertificateTemplateEstConfigsInsert,
|
||||
TCertificateTemplateEstConfigsUpdate,
|
||||
TCertificateTemplates,
|
||||
TCertificateTemplatesInsert,
|
||||
TCertificateTemplatesUpdate,
|
||||
@ -372,6 +375,11 @@ declare module "knex/types/tables" {
|
||||
TCertificateTemplatesInsert,
|
||||
TCertificateTemplatesUpdate
|
||||
>;
|
||||
[TableName.CertificateTemplateEstConfig]: KnexOriginal.CompositeTableType<
|
||||
TCertificateTemplateEstConfigs,
|
||||
TCertificateTemplateEstConfigsInsert,
|
||||
TCertificateTemplateEstConfigsUpdate
|
||||
>;
|
||||
[TableName.CertificateBody]: KnexOriginal.CompositeTableType<
|
||||
TCertificateBodies,
|
||||
TCertificateBodiesInsert,
|
||||
|
@ -0,0 +1,26 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TableName } from "../schemas";
|
||||
import { createOnUpdateTrigger, dropOnUpdateTrigger } from "../utils";
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
const hasEstConfigTable = await knex.schema.hasTable(TableName.CertificateTemplateEstConfig);
|
||||
if (!hasEstConfigTable) {
|
||||
await knex.schema.createTable(TableName.CertificateTemplateEstConfig, (tb) => {
|
||||
tb.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
|
||||
tb.uuid("certificateTemplateId").notNullable().unique();
|
||||
tb.foreign("certificateTemplateId").references("id").inTable(TableName.CertificateTemplate).onDelete("CASCADE");
|
||||
tb.binary("encryptedCaChain").notNullable();
|
||||
tb.string("hashedPassphrase").notNullable();
|
||||
tb.boolean("isEnabled").notNullable();
|
||||
tb.timestamps(true, true, true);
|
||||
});
|
||||
|
||||
await createOnUpdateTrigger(knex, TableName.CertificateTemplateEstConfig);
|
||||
}
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
await knex.schema.dropTableIfExists(TableName.CertificateTemplateEstConfig);
|
||||
await dropOnUpdateTrigger(knex, TableName.CertificateTemplateEstConfig);
|
||||
}
|
29
backend/src/db/schemas/certificate-template-est-configs.ts
Normal file
@ -0,0 +1,29 @@
|
||||
// Code generated by automation script, DO NOT EDIT.
|
||||
// Automated by pulling database and generating zod schema
|
||||
// To update. Just run npm run generate:schema
|
||||
// Written by akhilmhdh.
|
||||
|
||||
import { z } from "zod";
|
||||
|
||||
import { zodBuffer } from "@app/lib/zod";
|
||||
|
||||
import { TImmutableDBKeys } from "./models";
|
||||
|
||||
export const CertificateTemplateEstConfigsSchema = z.object({
|
||||
id: z.string().uuid(),
|
||||
certificateTemplateId: z.string().uuid(),
|
||||
encryptedCaChain: zodBuffer,
|
||||
hashedPassphrase: z.string(),
|
||||
isEnabled: z.boolean(),
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date()
|
||||
});
|
||||
|
||||
export type TCertificateTemplateEstConfigs = z.infer<typeof CertificateTemplateEstConfigsSchema>;
|
||||
export type TCertificateTemplateEstConfigsInsert = Omit<
|
||||
z.input<typeof CertificateTemplateEstConfigsSchema>,
|
||||
TImmutableDBKeys
|
||||
>;
|
||||
export type TCertificateTemplateEstConfigsUpdate = Partial<
|
||||
Omit<z.input<typeof CertificateTemplateEstConfigsSchema>, TImmutableDBKeys>
|
||||
>;
|
@ -14,6 +14,7 @@ export * from "./certificate-authority-crl";
|
||||
export * from "./certificate-authority-secret";
|
||||
export * from "./certificate-bodies";
|
||||
export * from "./certificate-secrets";
|
||||
export * from "./certificate-template-est-configs";
|
||||
export * from "./certificate-templates";
|
||||
export * from "./certificates";
|
||||
export * from "./dynamic-secret-leases";
|
||||
|
@ -3,6 +3,7 @@ import { z } from "zod";
|
||||
export enum TableName {
|
||||
Users = "users",
|
||||
CertificateAuthority = "certificate_authorities",
|
||||
CertificateTemplateEstConfig = "certificate_template_est_configs",
|
||||
CertificateAuthorityCert = "certificate_authority_certs",
|
||||
CertificateAuthoritySecret = "certificate_authority_secret",
|
||||
CertificateAuthorityCrl = "certificate_authority_crl",
|
||||
|
@ -9,7 +9,10 @@ export const registerScimRouter = async (server: FastifyZodProvider) => {
|
||||
server.addContentTypeParser("application/scim+json", { parseAs: "string" }, (_, body, done) => {
|
||||
try {
|
||||
const strBody = body instanceof Buffer ? body.toString() : body;
|
||||
|
||||
if (!strBody) {
|
||||
done(null, undefined);
|
||||
return;
|
||||
}
|
||||
const json: unknown = JSON.parse(strBody);
|
||||
done(null, json);
|
||||
} catch (err) {
|
||||
@ -474,18 +477,18 @@ export const registerScimRouter = async (server: FastifyZodProvider) => {
|
||||
Operations: z.array(
|
||||
z.union([
|
||||
z.object({
|
||||
op: z.literal("replace"),
|
||||
op: z.union([z.literal("replace"), z.literal("Replace")]),
|
||||
value: z.object({
|
||||
id: z.string().trim(),
|
||||
displayName: z.string().trim()
|
||||
})
|
||||
}),
|
||||
z.object({
|
||||
op: z.literal("remove"),
|
||||
op: z.union([z.literal("remove"), z.literal("Remove")]),
|
||||
path: z.string().trim()
|
||||
}),
|
||||
z.object({
|
||||
op: z.literal("add"),
|
||||
op: z.union([z.literal("add"), z.literal("Add")]),
|
||||
path: z.string().trim(),
|
||||
value: z.array(
|
||||
z.object({
|
||||
|
@ -166,7 +166,10 @@ export enum EventType {
|
||||
CREATE_CERTIFICATE_TEMPLATE = "create-certificate-template",
|
||||
UPDATE_CERTIFICATE_TEMPLATE = "update-certificate-template",
|
||||
DELETE_CERTIFICATE_TEMPLATE = "delete-certificate-template",
|
||||
GET_CERTIFICATE_TEMPLATE = "get-certificate-template"
|
||||
GET_CERTIFICATE_TEMPLATE = "get-certificate-template",
|
||||
CREATE_CERTIFICATE_TEMPLATE_EST_CONFIG = "create-certificate-template-est-config",
|
||||
UPDATE_CERTIFICATE_TEMPLATE_EST_CONFIG = "update-certificate-template-est-config",
|
||||
GET_CERTIFICATE_TEMPLATE_EST_CONFIG = "get-certificate-template-est-config"
|
||||
}
|
||||
|
||||
interface UserActorMetadata {
|
||||
@ -1420,6 +1423,29 @@ interface OrgAdminAccessProjectEvent {
|
||||
}; // no metadata yet
|
||||
}
|
||||
|
||||
interface CreateCertificateTemplateEstConfig {
|
||||
type: EventType.CREATE_CERTIFICATE_TEMPLATE_EST_CONFIG;
|
||||
metadata: {
|
||||
certificateTemplateId: string;
|
||||
isEnabled: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
interface UpdateCertificateTemplateEstConfig {
|
||||
type: EventType.UPDATE_CERTIFICATE_TEMPLATE_EST_CONFIG;
|
||||
metadata: {
|
||||
certificateTemplateId: string;
|
||||
isEnabled: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
interface GetCertificateTemplateEstConfig {
|
||||
type: EventType.GET_CERTIFICATE_TEMPLATE_EST_CONFIG;
|
||||
metadata: {
|
||||
certificateTemplateId: string;
|
||||
};
|
||||
}
|
||||
|
||||
export type Event =
|
||||
| GetSecretsEvent
|
||||
| GetSecretEvent
|
||||
@ -1547,4 +1573,7 @@ export type Event =
|
||||
| CreateCertificateTemplate
|
||||
| UpdateCertificateTemplate
|
||||
| GetCertificateTemplate
|
||||
| DeleteCertificateTemplate;
|
||||
| DeleteCertificateTemplate
|
||||
| CreateCertificateTemplateEstConfig
|
||||
| UpdateCertificateTemplateEstConfig
|
||||
| GetCertificateTemplateEstConfig;
|
||||
|
@ -98,6 +98,7 @@ export const dynamicSecretServiceFactory = ({
|
||||
if (!isConnected) throw new BadRequestError({ message: "Provider connection failed" });
|
||||
|
||||
const encryptedInput = infisicalSymmetricEncypt(JSON.stringify(inputs));
|
||||
|
||||
const dynamicSecretCfg = await dynamicSecretDAL.create({
|
||||
type: provider.type,
|
||||
version: 1,
|
||||
|
@ -126,7 +126,6 @@ const buildMemberPermission = () => {
|
||||
can(OrgPermissionActions.Read, OrgPermissionSubjects.Workspace);
|
||||
can(OrgPermissionActions.Create, OrgPermissionSubjects.Workspace);
|
||||
can(OrgPermissionActions.Read, OrgPermissionSubjects.Member);
|
||||
can(OrgPermissionActions.Create, OrgPermissionSubjects.Member);
|
||||
can(OrgPermissionActions.Read, OrgPermissionSubjects.Groups);
|
||||
can(OrgPermissionActions.Read, OrgPermissionSubjects.Role);
|
||||
can(OrgPermissionActions.Read, OrgPermissionSubjects.Settings);
|
||||
|
@ -50,8 +50,8 @@ export const buildScimUser = ({
|
||||
orgMembershipId: string;
|
||||
username: string;
|
||||
email?: string | null;
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
firstName: string | null | undefined;
|
||||
lastName: string | null | undefined;
|
||||
groups?: {
|
||||
value: string;
|
||||
display: string;
|
||||
@ -64,9 +64,9 @@ export const buildScimUser = ({
|
||||
userName: username,
|
||||
displayName: `${firstName} ${lastName}`,
|
||||
name: {
|
||||
givenName: firstName,
|
||||
givenName: firstName || "",
|
||||
middleName: null,
|
||||
familyName: lastName
|
||||
familyName: lastName || ""
|
||||
},
|
||||
emails: email
|
||||
? [
|
||||
|
@ -267,8 +267,8 @@ export const scimServiceFactory = ({
|
||||
orgMembershipId: membership.id,
|
||||
username: membership.externalId ?? membership.username,
|
||||
email: membership.email ?? "",
|
||||
firstName: membership.firstName as string,
|
||||
lastName: membership.lastName as string,
|
||||
firstName: membership.firstName,
|
||||
lastName: membership.lastName,
|
||||
active: membership.isActive,
|
||||
groups: groupMembershipsInOrg.map((group) => ({
|
||||
value: group.groupId,
|
||||
@ -427,8 +427,8 @@ export const scimServiceFactory = ({
|
||||
return buildScimUser({
|
||||
orgMembershipId: createdOrgMembership.id,
|
||||
username: externalId,
|
||||
firstName: createdUser.firstName as string,
|
||||
lastName: createdUser.lastName as string,
|
||||
firstName: createdUser.firstName,
|
||||
lastName: createdUser.lastName,
|
||||
email: createdUser.email ?? "",
|
||||
active: createdOrgMembership.isActive
|
||||
});
|
||||
@ -483,8 +483,8 @@ export const scimServiceFactory = ({
|
||||
orgMembershipId: membership.id,
|
||||
username: membership.externalId ?? membership.username,
|
||||
email: membership.email,
|
||||
firstName: membership.firstName as string,
|
||||
lastName: membership.lastName as string,
|
||||
firstName: membership.firstName,
|
||||
lastName: membership.lastName,
|
||||
active
|
||||
});
|
||||
};
|
||||
@ -527,8 +527,8 @@ export const scimServiceFactory = ({
|
||||
orgMembershipId: membership.id,
|
||||
username: membership.externalId ?? membership.username,
|
||||
email: membership.email,
|
||||
firstName: membership.firstName as string,
|
||||
lastName: membership.lastName as string,
|
||||
firstName: membership.firstName,
|
||||
lastName: membership.lastName,
|
||||
active,
|
||||
groups: groupMembershipsInOrg.map((group) => ({
|
||||
value: group.groupId,
|
||||
@ -884,59 +884,50 @@ export const scimServiceFactory = ({
|
||||
}
|
||||
|
||||
for await (const operation of operations) {
|
||||
switch (operation.op) {
|
||||
case "replace": {
|
||||
group = await groupDAL.updateById(group.id, {
|
||||
name: operation.value.displayName
|
||||
if (operation.op === "replace" || operation.op === "Replace") {
|
||||
group = await groupDAL.updateById(group.id, {
|
||||
name: operation.value.displayName
|
||||
});
|
||||
} else if (operation.op === "add" || operation.op === "Add") {
|
||||
try {
|
||||
const orgMemberships = await orgMembershipDAL.find({
|
||||
$in: {
|
||||
id: operation.value.map((member) => member.value)
|
||||
}
|
||||
});
|
||||
break;
|
||||
}
|
||||
case "add": {
|
||||
try {
|
||||
const orgMemberships = await orgMembershipDAL.find({
|
||||
$in: {
|
||||
id: operation.value.map((member) => member.value)
|
||||
}
|
||||
});
|
||||
|
||||
await addUsersToGroupByUserIds({
|
||||
group,
|
||||
userIds: orgMemberships.map((membership) => membership.userId as string),
|
||||
userDAL,
|
||||
userGroupMembershipDAL,
|
||||
orgDAL,
|
||||
groupProjectDAL,
|
||||
projectKeyDAL,
|
||||
projectDAL,
|
||||
projectBotDAL
|
||||
});
|
||||
} catch {
|
||||
logger.info("Repeat SCIM user-group add operation");
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
case "remove": {
|
||||
const orgMembershipId = extractScimValueFromPath(operation.path);
|
||||
if (!orgMembershipId) throw new ScimRequestError({ detail: "Invalid path value", status: 400 });
|
||||
const orgMembership = await orgMembershipDAL.findById(orgMembershipId);
|
||||
if (!orgMembership) throw new ScimRequestError({ detail: "Org Membership Not Found", status: 400 });
|
||||
await removeUsersFromGroupByUserIds({
|
||||
await addUsersToGroupByUserIds({
|
||||
group,
|
||||
userIds: [orgMembership.userId as string],
|
||||
userIds: orgMemberships.map((membership) => membership.userId as string),
|
||||
userDAL,
|
||||
userGroupMembershipDAL,
|
||||
orgDAL,
|
||||
groupProjectDAL,
|
||||
projectKeyDAL
|
||||
});
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
throw new ScimRequestError({
|
||||
detail: "Invalid Operation",
|
||||
status: 400
|
||||
projectKeyDAL,
|
||||
projectDAL,
|
||||
projectBotDAL
|
||||
});
|
||||
} catch {
|
||||
logger.info("Repeat SCIM user-group add operation");
|
||||
}
|
||||
} else if (operation.op === "remove" || operation.op === "Remove") {
|
||||
const orgMembershipId = extractScimValueFromPath(operation.path);
|
||||
if (!orgMembershipId) throw new ScimRequestError({ detail: "Invalid path value", status: 400 });
|
||||
const orgMembership = await orgMembershipDAL.findById(orgMembershipId);
|
||||
if (!orgMembership) throw new ScimRequestError({ detail: "Org Membership Not Found", status: 400 });
|
||||
await removeUsersFromGroupByUserIds({
|
||||
group,
|
||||
userIds: [orgMembership.userId as string],
|
||||
userDAL,
|
||||
userGroupMembershipDAL,
|
||||
groupProjectDAL,
|
||||
projectKeyDAL
|
||||
});
|
||||
} else {
|
||||
throw new ScimRequestError({
|
||||
detail: "Invalid Operation",
|
||||
status: 400
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -110,8 +110,10 @@ export type TUpdateScimGroupNamePatchDTO = {
|
||||
operations: (TRemoveOp | TReplaceOp | TAddOp)[];
|
||||
};
|
||||
|
||||
// akhilmhdh: I know, this is done due to lack of time. Need to change later to support as normalized rather than like this
|
||||
// Forgive akhil blame tony
|
||||
type TReplaceOp = {
|
||||
op: "replace";
|
||||
op: "replace" | "Replace";
|
||||
value: {
|
||||
id: string;
|
||||
displayName: string;
|
||||
@ -119,12 +121,12 @@ type TReplaceOp = {
|
||||
};
|
||||
|
||||
type TRemoveOp = {
|
||||
op: "remove";
|
||||
op: "remove" | "Remove";
|
||||
path: string;
|
||||
};
|
||||
|
||||
type TAddOp = {
|
||||
op: "add";
|
||||
op: "add" | "Add";
|
||||
path: string;
|
||||
value: {
|
||||
value: string;
|
||||
|
@ -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
|
||||
})
|
||||
}
|
||||
]
|
||||
|
@ -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
|
||||
});
|
||||
}
|
||||
};
|
@ -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;
|
||||
};
|
||||
|
||||
|
@ -74,6 +74,7 @@ const envSchema = z
|
||||
JWT_AUTH_LIFETIME: zpStr(z.string().default("10d")),
|
||||
JWT_SIGNUP_LIFETIME: zpStr(z.string().default("15m")),
|
||||
JWT_REFRESH_LIFETIME: zpStr(z.string().default("90d")),
|
||||
JWT_INVITE_LIFETIME: zpStr(z.string().default("1d")),
|
||||
JWT_MFA_LIFETIME: zpStr(z.string().default("5m")),
|
||||
JWT_PROVIDER_AUTH_LIFETIME: zpStr(z.string().default("15m")),
|
||||
// Oauth
|
||||
@ -141,7 +142,8 @@ const envSchema = z
|
||||
CAPTCHA_SECRET: zpStr(z.string().optional()),
|
||||
PLAIN_API_KEY: zpStr(z.string().optional()),
|
||||
PLAIN_WISH_LABEL_IDS: zpStr(z.string().optional()),
|
||||
DISABLE_AUDIT_LOG_GENERATION: zodStrBool.default("false")
|
||||
DISABLE_AUDIT_LOG_GENERATION: zodStrBool.default("false"),
|
||||
SSL_CLIENT_CERTIFICATE_HEADER_KEY: zpStr(z.string().optional()).default("x-ssl-client-cert")
|
||||
})
|
||||
.transform((data) => ({
|
||||
...data,
|
||||
|
@ -57,7 +57,6 @@ const extractAuth = async (req: FastifyRequest, jwtSecret: string) => {
|
||||
return { authMode: AuthMode.API_KEY, token: apiKey, actor: ActorType.USER } as const;
|
||||
}
|
||||
const authHeader = req.headers?.authorization;
|
||||
|
||||
if (!authHeader) return { authMode: null, token: null };
|
||||
|
||||
const authTokenValue = authHeader.slice(7); // slice of after Bearer
|
||||
@ -103,12 +102,13 @@ export const injectIdentity = fp(async (server: FastifyZodProvider) => {
|
||||
server.decorateRequest("auth", null);
|
||||
server.addHook("onRequest", async (req) => {
|
||||
const appCfg = getConfig();
|
||||
const { authMode, token, actor } = await extractAuth(req, appCfg.AUTH_SECRET);
|
||||
|
||||
if (req.url.includes("/api/v3/auth/")) {
|
||||
if (req.url.includes(".well-known/est") || req.url.includes("/api/v3/auth/")) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { authMode, token, actor } = await extractAuth(req, appCfg.AUTH_SECRET);
|
||||
|
||||
if (!authMode) return;
|
||||
|
||||
switch (authMode) {
|
||||
|
173
backend/src/server/routes/est/certificate-est-router.ts
Normal file
@ -0,0 +1,173 @@
|
||||
import bcrypt from "bcrypt";
|
||||
import { z } from "zod";
|
||||
|
||||
import { getConfig } from "@app/lib/config/env";
|
||||
import { BadRequestError, UnauthorizedError } from "@app/lib/errors";
|
||||
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||
|
||||
export const registerCertificateEstRouter = async (server: FastifyZodProvider) => {
|
||||
const appCfg = getConfig();
|
||||
|
||||
// add support for CSR bodies
|
||||
server.addContentTypeParser("application/pkcs10", { parseAs: "string" }, (_, body, done) => {
|
||||
try {
|
||||
let csrBody = body as string;
|
||||
// some EST clients send CSRs in PEM format and some in base64 format
|
||||
// for CSRs sent in PEM, we leave them as is
|
||||
// for CSRs sent in base64, we preprocess them to remove new lines and spaces
|
||||
if (!csrBody.includes("BEGIN CERTIFICATE REQUEST")) {
|
||||
csrBody = csrBody.replace(/\n/g, "").replace(/ /g, "");
|
||||
}
|
||||
|
||||
done(null, csrBody);
|
||||
} catch (err) {
|
||||
const error = err as Error;
|
||||
done(error, undefined);
|
||||
}
|
||||
});
|
||||
|
||||
// Authenticate EST client using Passphrase
|
||||
server.addHook("onRequest", async (req, res) => {
|
||||
const { authorization } = req.headers;
|
||||
const urlFragments = req.url.split("/");
|
||||
|
||||
// cacerts endpoint should not have any authentication
|
||||
if (urlFragments[urlFragments.length - 1] === "cacerts") {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!authorization) {
|
||||
const wwwAuthenticateHeader = "WWW-Authenticate";
|
||||
const errAuthRequired = "Authentication required";
|
||||
|
||||
await res.hijack();
|
||||
|
||||
// definitive connection timeout to clean-up open connections and prevent memory leak
|
||||
res.raw.setTimeout(10 * 1000, () => {
|
||||
res.raw.end();
|
||||
});
|
||||
|
||||
res.raw.setHeader(wwwAuthenticateHeader, `Basic realm="infisical"`);
|
||||
res.raw.setHeader("Content-Length", 0);
|
||||
res.raw.statusCode = 401;
|
||||
|
||||
// Write the error message to the response without ending the connection
|
||||
res.raw.write(errAuthRequired);
|
||||
|
||||
// flush headers
|
||||
res.raw.flushHeaders();
|
||||
return;
|
||||
}
|
||||
|
||||
const certificateTemplateId = urlFragments.slice(-2)[0];
|
||||
const estConfig = await server.services.certificateTemplate.getEstConfiguration({
|
||||
isInternal: true,
|
||||
certificateTemplateId
|
||||
});
|
||||
|
||||
if (!estConfig.isEnabled) {
|
||||
throw new BadRequestError({
|
||||
message: "EST is disabled"
|
||||
});
|
||||
}
|
||||
|
||||
const rawCredential = authorization?.split(" ").pop();
|
||||
if (!rawCredential) {
|
||||
throw new UnauthorizedError({ message: "Missing HTTP credentials" });
|
||||
}
|
||||
|
||||
// expected format is user:password
|
||||
const basicCredential = atob(rawCredential);
|
||||
const password = basicCredential.split(":").pop();
|
||||
if (!password) {
|
||||
throw new BadRequestError({
|
||||
message: "No password provided"
|
||||
});
|
||||
}
|
||||
|
||||
const isPasswordValid = await bcrypt.compare(password, estConfig.hashedPassphrase);
|
||||
if (!isPasswordValid) {
|
||||
throw new UnauthorizedError({
|
||||
message: "Invalid credentials"
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "POST",
|
||||
url: "/:certificateTemplateId/simpleenroll",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
schema: {
|
||||
body: z.string().min(1),
|
||||
params: z.object({
|
||||
certificateTemplateId: z.string().min(1)
|
||||
}),
|
||||
response: {
|
||||
200: z.string()
|
||||
}
|
||||
},
|
||||
handler: async (req, res) => {
|
||||
void res.header("Content-Type", "application/pkcs7-mime; smime-type=certs-only");
|
||||
void res.header("Content-Transfer-Encoding", "base64");
|
||||
|
||||
return server.services.certificateEst.simpleEnroll({
|
||||
csr: req.body,
|
||||
certificateTemplateId: req.params.certificateTemplateId,
|
||||
sslClientCert: req.headers[appCfg.SSL_CLIENT_CERTIFICATE_HEADER_KEY] as string
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "POST",
|
||||
url: "/:certificateTemplateId/simplereenroll",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
schema: {
|
||||
body: z.string().min(1),
|
||||
params: z.object({
|
||||
certificateTemplateId: z.string().min(1)
|
||||
}),
|
||||
response: {
|
||||
200: z.string()
|
||||
}
|
||||
},
|
||||
handler: async (req, res) => {
|
||||
void res.header("Content-Type", "application/pkcs7-mime; smime-type=certs-only");
|
||||
void res.header("Content-Transfer-Encoding", "base64");
|
||||
|
||||
return server.services.certificateEst.simpleReenroll({
|
||||
csr: req.body,
|
||||
certificateTemplateId: req.params.certificateTemplateId,
|
||||
sslClientCert: req.headers[appCfg.SSL_CLIENT_CERTIFICATE_HEADER_KEY] as string
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: "/:certificateTemplateId/cacerts",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
params: z.object({
|
||||
certificateTemplateId: z.string().min(1)
|
||||
}),
|
||||
response: {
|
||||
200: z.string()
|
||||
}
|
||||
},
|
||||
handler: async (req, res) => {
|
||||
void res.header("Content-Type", "application/pkcs7-mime; smime-type=certs-only");
|
||||
void res.header("Content-Transfer-Encoding", "base64");
|
||||
|
||||
return server.services.certificateEst.getCaCerts({
|
||||
certificateTemplateId: req.params.certificateTemplateId
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
@ -90,7 +90,9 @@ import { certificateAuthorityDALFactory } from "@app/services/certificate-author
|
||||
import { certificateAuthorityQueueFactory } from "@app/services/certificate-authority/certificate-authority-queue";
|
||||
import { certificateAuthoritySecretDALFactory } from "@app/services/certificate-authority/certificate-authority-secret-dal";
|
||||
import { certificateAuthorityServiceFactory } from "@app/services/certificate-authority/certificate-authority-service";
|
||||
import { certificateEstServiceFactory } from "@app/services/certificate-est/certificate-est-service";
|
||||
import { certificateTemplateDALFactory } from "@app/services/certificate-template/certificate-template-dal";
|
||||
import { certificateTemplateEstConfigDALFactory } from "@app/services/certificate-template/certificate-template-est-config-dal";
|
||||
import { certificateTemplateServiceFactory } from "@app/services/certificate-template/certificate-template-service";
|
||||
import { groupProjectDALFactory } from "@app/services/group-project/group-project-dal";
|
||||
import { groupProjectMembershipRoleDALFactory } from "@app/services/group-project/group-project-membership-role-dal";
|
||||
@ -195,6 +197,7 @@ import { injectIdentity } from "../plugins/auth/inject-identity";
|
||||
import { injectPermission } from "../plugins/auth/inject-permission";
|
||||
import { injectRateLimits } from "../plugins/inject-rate-limits";
|
||||
import { registerSecretScannerGhApp } from "../plugins/secret-scanner";
|
||||
import { registerCertificateEstRouter } from "./est/certificate-est-router";
|
||||
import { registerV1Routes } from "./v1";
|
||||
import { registerV2Routes } from "./v2";
|
||||
import { registerV3Routes } from "./v3";
|
||||
@ -477,9 +480,12 @@ export const registerRoutes = async (
|
||||
orgRoleDAL,
|
||||
permissionService,
|
||||
orgDAL,
|
||||
userGroupMembershipDAL,
|
||||
projectBotDAL,
|
||||
incidentContactDAL,
|
||||
tokenService,
|
||||
projectUserAdditionalPrivilegeDAL,
|
||||
projectUserMembershipRoleDAL,
|
||||
projectDAL,
|
||||
projectMembershipDAL,
|
||||
orgMembershipDAL,
|
||||
@ -499,6 +505,8 @@ export const registerRoutes = async (
|
||||
projectDAL,
|
||||
projectBotDAL,
|
||||
groupProjectDAL,
|
||||
projectMembershipDAL,
|
||||
projectUserMembershipRoleDAL,
|
||||
orgDAL,
|
||||
orgService,
|
||||
licenseService
|
||||
@ -595,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);
|
||||
@ -652,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({
|
||||
@ -683,6 +705,7 @@ export const registerRoutes = async (
|
||||
orgDAL,
|
||||
orgService,
|
||||
projectMembershipDAL,
|
||||
projectRoleDAL,
|
||||
folderDAL,
|
||||
licenseService,
|
||||
certificateAuthorityDAL,
|
||||
@ -839,6 +862,7 @@ export const registerRoutes = async (
|
||||
secretQueueService,
|
||||
kmsService,
|
||||
secretV2BridgeDAL,
|
||||
secretApprovalPolicyDAL,
|
||||
secretVersionV2BridgeDAL,
|
||||
secretVersionTagV2BridgeDAL,
|
||||
smtpService,
|
||||
@ -1189,6 +1213,7 @@ export const registerRoutes = async (
|
||||
certificateAuthority: certificateAuthorityService,
|
||||
certificateTemplate: certificateTemplateService,
|
||||
certificateAuthorityCrl: certificateAuthorityCrlService,
|
||||
certificateEst: certificateEstService,
|
||||
pkiAlert: pkiAlertService,
|
||||
pkiCollection: pkiCollectionService,
|
||||
secretScanning: secretScanningService,
|
||||
@ -1256,6 +1281,9 @@ export const registerRoutes = async (
|
||||
}
|
||||
});
|
||||
|
||||
// register special routes
|
||||
await server.register(registerCertificateEstRouter, { prefix: "/.well-known/est" });
|
||||
|
||||
// register routes for v1
|
||||
await server.register(
|
||||
async (v1Server) => {
|
||||
|
@ -669,6 +669,7 @@ export const registerCaRouter = async (server: FastifyZodProvider) => {
|
||||
handler: async (req) => {
|
||||
const { certificate, certificateChain, issuingCaCertificate, serialNumber, ca } =
|
||||
await server.services.certificateAuthority.signCertFromCa({
|
||||
isInternal: false,
|
||||
caId: req.params.caId,
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
@ -691,7 +692,7 @@ export const registerCaRouter = async (server: FastifyZodProvider) => {
|
||||
});
|
||||
|
||||
return {
|
||||
certificate,
|
||||
certificate: certificate.toString("pem"),
|
||||
certificateChain,
|
||||
issuingCaCertificate,
|
||||
serialNumber
|
||||
|
@ -210,6 +210,7 @@ export const registerCertRouter = async (server: FastifyZodProvider) => {
|
||||
handler: async (req) => {
|
||||
const { certificate, certificateChain, issuingCaCertificate, serialNumber, ca } =
|
||||
await server.services.certificateAuthority.signCertFromCa({
|
||||
isInternal: false,
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
@ -231,7 +232,7 @@ export const registerCertRouter = async (server: FastifyZodProvider) => {
|
||||
});
|
||||
|
||||
return {
|
||||
certificate,
|
||||
certificate: certificate.toString("pem"),
|
||||
certificateChain,
|
||||
issuingCaCertificate,
|
||||
serialNumber
|
||||
|
@ -1,6 +1,7 @@
|
||||
import ms from "ms";
|
||||
import { z } from "zod";
|
||||
|
||||
import { CertificateTemplateEstConfigsSchema } from "@app/db/schemas";
|
||||
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
||||
import { CERTIFICATE_TEMPLATES } from "@app/lib/api-docs";
|
||||
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||
@ -9,6 +10,12 @@ import { AuthMode } from "@app/services/auth/auth-type";
|
||||
import { sanitizedCertificateTemplate } from "@app/services/certificate-template/certificate-template-schema";
|
||||
import { validateTemplateRegexField } from "@app/services/certificate-template/certificate-template-validators";
|
||||
|
||||
const sanitizedEstConfig = CertificateTemplateEstConfigsSchema.pick({
|
||||
id: true,
|
||||
certificateTemplateId: true,
|
||||
isEnabled: true
|
||||
});
|
||||
|
||||
export const registerCertificateTemplateRouter = async (server: FastifyZodProvider) => {
|
||||
server.route({
|
||||
method: "GET",
|
||||
@ -202,4 +209,141 @@ export const registerCertificateTemplateRouter = async (server: FastifyZodProvid
|
||||
return certificateTemplate;
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "POST",
|
||||
url: "/:certificateTemplateId/est-config",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
schema: {
|
||||
description: "Create Certificate Template EST configuration",
|
||||
params: z.object({
|
||||
certificateTemplateId: z.string().trim()
|
||||
}),
|
||||
body: z.object({
|
||||
caChain: z.string().trim().min(1),
|
||||
passphrase: z.string().min(1),
|
||||
isEnabled: z.boolean().default(true)
|
||||
}),
|
||||
response: {
|
||||
200: sanitizedEstConfig
|
||||
}
|
||||
},
|
||||
handler: async (req) => {
|
||||
const estConfig = await server.services.certificateTemplate.createEstConfiguration({
|
||||
certificateTemplateId: req.params.certificateTemplateId,
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId,
|
||||
...req.body
|
||||
});
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
projectId: estConfig.projectId,
|
||||
event: {
|
||||
type: EventType.CREATE_CERTIFICATE_TEMPLATE_EST_CONFIG,
|
||||
metadata: {
|
||||
certificateTemplateId: estConfig.certificateTemplateId,
|
||||
isEnabled: estConfig.isEnabled as boolean
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return estConfig;
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "PATCH",
|
||||
url: "/:certificateTemplateId/est-config",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
schema: {
|
||||
description: "Update Certificate Template EST configuration",
|
||||
params: z.object({
|
||||
certificateTemplateId: z.string().trim()
|
||||
}),
|
||||
body: z.object({
|
||||
caChain: z.string().trim().min(1).optional(),
|
||||
passphrase: z.string().min(1).optional(),
|
||||
isEnabled: z.boolean().optional()
|
||||
}),
|
||||
response: {
|
||||
200: sanitizedEstConfig
|
||||
}
|
||||
},
|
||||
handler: async (req) => {
|
||||
const estConfig = await server.services.certificateTemplate.updateEstConfiguration({
|
||||
certificateTemplateId: req.params.certificateTemplateId,
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId,
|
||||
...req.body
|
||||
});
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
projectId: estConfig.projectId,
|
||||
event: {
|
||||
type: EventType.UPDATE_CERTIFICATE_TEMPLATE_EST_CONFIG,
|
||||
metadata: {
|
||||
certificateTemplateId: estConfig.certificateTemplateId,
|
||||
isEnabled: estConfig.isEnabled as boolean
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return estConfig;
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: "/:certificateTemplateId/est-config",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
schema: {
|
||||
description: "Get Certificate Template EST configuration",
|
||||
params: z.object({
|
||||
certificateTemplateId: z.string().trim()
|
||||
}),
|
||||
response: {
|
||||
200: sanitizedEstConfig.extend({
|
||||
caChain: z.string()
|
||||
})
|
||||
}
|
||||
},
|
||||
handler: async (req) => {
|
||||
const estConfig = await server.services.certificateTemplate.getEstConfiguration({
|
||||
isInternal: false,
|
||||
certificateTemplateId: req.params.certificateTemplateId,
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId
|
||||
});
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
projectId: estConfig.projectId,
|
||||
event: {
|
||||
type: EventType.GET_CERTIFICATE_TEMPLATE_EST_CONFIG,
|
||||
metadata: {
|
||||
certificateTemplateId: estConfig.certificateTemplateId
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return estConfig;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
@ -293,6 +293,7 @@ export const registerIntegrationAuthRouter = async (server: FastifyZodProvider)
|
||||
}),
|
||||
querystring: z.object({
|
||||
teamId: z.string().trim().optional(),
|
||||
azureDevOpsOrgName: z.string().trim().optional(),
|
||||
workspaceSlug: z.string().trim().optional()
|
||||
}),
|
||||
response: {
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { UsersSchema } from "@app/db/schemas";
|
||||
import { OrgMembershipRole, ProjectMembershipRole, UsersSchema } from "@app/db/schemas";
|
||||
import { inviteUserRateLimit } from "@app/server/config/rateLimiter";
|
||||
import { getTelemetryDistinctId } from "@app/server/lib/telemetry";
|
||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||
@ -16,23 +16,37 @@ export const registerInviteOrgRouter = async (server: FastifyZodProvider) => {
|
||||
method: "POST",
|
||||
schema: {
|
||||
body: z.object({
|
||||
inviteeEmail: z.string().trim().email(),
|
||||
organizationId: z.string().trim()
|
||||
inviteeEmails: z.array(z.string().trim().email()),
|
||||
organizationId: z.string().trim(),
|
||||
projectIds: z.array(z.string().trim()).optional(),
|
||||
projectRoleSlug: z.nativeEnum(ProjectMembershipRole).optional(),
|
||||
organizationRoleSlug: z.nativeEnum(OrgMembershipRole)
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
message: z.string(),
|
||||
completeInviteLink: z.string().optional()
|
||||
completeInviteLinks: z
|
||||
.array(
|
||||
z.object({
|
||||
email: z.string(),
|
||||
link: z.string()
|
||||
})
|
||||
)
|
||||
.optional()
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
handler: async (req) => {
|
||||
if (req.auth.actor !== ActorType.USER) return;
|
||||
const completeInviteLink = await server.services.org.inviteUserToOrganization({
|
||||
|
||||
const completeInviteLinks = await server.services.org.inviteUserToOrganization({
|
||||
orgId: req.body.organizationId,
|
||||
userId: req.permission.id,
|
||||
inviteeEmail: req.body.inviteeEmail,
|
||||
inviteeEmails: req.body.inviteeEmails,
|
||||
projectIds: req.body.projectIds,
|
||||
projectRoleSlug: req.body.projectRoleSlug,
|
||||
organizationRoleSlug: req.body.organizationRoleSlug,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId
|
||||
});
|
||||
@ -41,14 +55,15 @@ export const registerInviteOrgRouter = async (server: FastifyZodProvider) => {
|
||||
event: PostHogEventTypes.UserOrgInvitation,
|
||||
distinctId: getTelemetryDistinctId(req),
|
||||
properties: {
|
||||
inviteeEmail: req.body.inviteeEmail,
|
||||
inviteeEmails: req.body.inviteeEmails,
|
||||
organizationRoleSlug: req.body.organizationRoleSlug,
|
||||
...req.auditLogInfo
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
completeInviteLink,
|
||||
message: `Send an invite link to ${req.body.inviteeEmail}`
|
||||
completeInviteLinks,
|
||||
message: `Send an invite link to ${req.body.inviteeEmails.join(", ")}`
|
||||
};
|
||||
}
|
||||
});
|
||||
|
@ -1,6 +1,12 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { IntegrationsSchema, ProjectMembershipsSchema, UserEncryptionKeysSchema, UsersSchema } from "@app/db/schemas";
|
||||
import {
|
||||
IntegrationsSchema,
|
||||
ProjectMembershipsSchema,
|
||||
ProjectRolesSchema,
|
||||
UserEncryptionKeysSchema,
|
||||
UsersSchema
|
||||
} from "@app/db/schemas";
|
||||
import { PROJECTS } from "@app/lib/api-docs";
|
||||
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||
@ -122,15 +128,31 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
querystring: z.object({
|
||||
includeRoles: z
|
||||
.enum(["true", "false"])
|
||||
.default("false")
|
||||
.transform((value) => value === "true")
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
workspaces: projectWithEnv.array()
|
||||
workspaces: projectWithEnv
|
||||
.extend({
|
||||
roles: ProjectRolesSchema.array().optional()
|
||||
})
|
||||
.array()
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY]),
|
||||
handler: async (req) => {
|
||||
const workspaces = await server.services.project.getProjects(req.permission.id);
|
||||
const workspaces = await server.services.project.getProjects({
|
||||
includeRoles: req.query.includeRoles,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actor: req.permission.type,
|
||||
actorOrgId: req.permission.orgId
|
||||
});
|
||||
return { workspaces };
|
||||
}
|
||||
});
|
||||
|
@ -179,7 +179,8 @@ export const registerSignupRouter = async (server: FastifyZodProvider) => {
|
||||
encryptedPrivateKeyIV: z.string().trim(),
|
||||
encryptedPrivateKeyTag: z.string().trim(),
|
||||
salt: z.string().trim(),
|
||||
verifier: z.string().trim()
|
||||
verifier: z.string().trim(),
|
||||
tokenMetadata: z.string().optional()
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
|
@ -1,3 +1,5 @@
|
||||
import { ProjectMembershipRole } from "@app/db/schemas";
|
||||
|
||||
export enum TokenType {
|
||||
TOKEN_EMAIL_CONFIRMATION = "emailConfirmation",
|
||||
TOKEN_EMAIL_VERIFICATION = "emailVerification", // unverified -> verified
|
||||
@ -49,3 +51,19 @@ export type TIssueAuthTokenDTO = {
|
||||
ip: string;
|
||||
userAgent: string;
|
||||
};
|
||||
|
||||
export enum TokenMetadataType {
|
||||
InviteToProjects = "projects-invite"
|
||||
}
|
||||
|
||||
export type TTokenInviteToProjectsMetadataPayload = {
|
||||
projectIds: string[];
|
||||
projectRoleSlug: ProjectMembershipRole;
|
||||
userId: string;
|
||||
orgId: string;
|
||||
};
|
||||
|
||||
export type TTokenMetadata = {
|
||||
type: TokenMetadataType.InviteToProjects;
|
||||
payload: TTokenInviteToProjectsMetadataPayload;
|
||||
};
|
||||
|
@ -583,7 +583,13 @@ export const authLoginServiceFactory = ({
|
||||
} else {
|
||||
const isLinkingRequired = !user?.authMethods?.includes(authMethod);
|
||||
if (isLinkingRequired) {
|
||||
user = await userDAL.updateById(user.id, { authMethods: [...(user.authMethods || []), authMethod] });
|
||||
// we update the names here because upon org invitation, the names are set to be NULL
|
||||
// if user is signing up with SSO after invitation, their names should be set based on their SSO profile
|
||||
user = await userDAL.updateById(user.id, {
|
||||
authMethods: [...(user.authMethods || []), authMethod],
|
||||
firstName: !user.isAccepted ? firstName : undefined,
|
||||
lastName: !user.isAccepted ? lastName : undefined
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -9,7 +9,7 @@ import { isAuthMethodSaml } from "@app/ee/services/permission/permission-fns";
|
||||
import { getConfig } from "@app/lib/config/env";
|
||||
import { infisicalSymmetricEncypt } from "@app/lib/crypto/encryption";
|
||||
import { getUserPrivateKey } from "@app/lib/crypto/srp";
|
||||
import { BadRequestError } from "@app/lib/errors";
|
||||
import { BadRequestError, UnauthorizedError } from "@app/lib/errors";
|
||||
import { isDisposableEmail } from "@app/lib/validator";
|
||||
import { TGroupProjectDALFactory } from "@app/services/group-project/group-project-dal";
|
||||
import { TProjectDALFactory } from "@app/services/project/project-dal";
|
||||
@ -17,9 +17,12 @@ import { TProjectBotDALFactory } from "@app/services/project-bot/project-bot-dal
|
||||
import { TProjectKeyDALFactory } from "@app/services/project-key/project-key-dal";
|
||||
|
||||
import { TAuthTokenServiceFactory } from "../auth-token/auth-token-service";
|
||||
import { TokenType } from "../auth-token/auth-token-types";
|
||||
import { TokenMetadataType, TokenType, TTokenMetadata } from "../auth-token/auth-token-types";
|
||||
import { TOrgDALFactory } from "../org/org-dal";
|
||||
import { TOrgServiceFactory } from "../org/org-service";
|
||||
import { TProjectMembershipDALFactory } from "../project-membership/project-membership-dal";
|
||||
import { addMembersToProject } from "../project-membership/project-membership-fns";
|
||||
import { TProjectUserMembershipRoleDALFactory } from "../project-membership/project-user-membership-role-dal";
|
||||
import { SmtpTemplates, TSmtpService } from "../smtp/smtp-service";
|
||||
import { TUserDALFactory } from "../user/user-dal";
|
||||
import { TAuthDALFactory } from "./auth-dal";
|
||||
@ -32,10 +35,14 @@ type TAuthSignupDep = {
|
||||
userDAL: TUserDALFactory;
|
||||
userGroupMembershipDAL: Pick<
|
||||
TUserGroupMembershipDALFactory,
|
||||
"find" | "transaction" | "insertMany" | "deletePendingUserGroupMembershipsByUserIds"
|
||||
| "find"
|
||||
| "transaction"
|
||||
| "insertMany"
|
||||
| "deletePendingUserGroupMembershipsByUserIds"
|
||||
| "findUserGroupMembershipsInProject"
|
||||
>;
|
||||
projectKeyDAL: Pick<TProjectKeyDALFactory, "find" | "findLatestProjectKey" | "insertMany">;
|
||||
projectDAL: Pick<TProjectDALFactory, "findProjectGhostUser">;
|
||||
projectDAL: Pick<TProjectDALFactory, "findProjectGhostUser" | "findProjectById">;
|
||||
projectBotDAL: Pick<TProjectBotDALFactory, "findOne">;
|
||||
groupProjectDAL: Pick<TGroupProjectDALFactory, "find">;
|
||||
orgService: Pick<TOrgServiceFactory, "createOrganization">;
|
||||
@ -43,6 +50,8 @@ type TAuthSignupDep = {
|
||||
tokenService: TAuthTokenServiceFactory;
|
||||
smtpService: TSmtpService;
|
||||
licenseService: Pick<TLicenseServiceFactory, "updateSubscriptionOrgMemberCount">;
|
||||
projectMembershipDAL: Pick<TProjectMembershipDALFactory, "find" | "transaction" | "insertMany">;
|
||||
projectUserMembershipRoleDAL: Pick<TProjectUserMembershipRoleDALFactory, "insertMany">;
|
||||
};
|
||||
|
||||
export type TAuthSignupFactory = ReturnType<typeof authSignupServiceFactory>;
|
||||
@ -58,6 +67,8 @@ export const authSignupServiceFactory = ({
|
||||
smtpService,
|
||||
orgService,
|
||||
orgDAL,
|
||||
projectMembershipDAL,
|
||||
projectUserMembershipRoleDAL,
|
||||
licenseService
|
||||
}: TAuthSignupDep) => {
|
||||
// first step of signup. create user and send email
|
||||
@ -301,7 +312,8 @@ export const authSignupServiceFactory = ({
|
||||
encryptedPrivateKey,
|
||||
encryptedPrivateKeyIV,
|
||||
encryptedPrivateKeyTag,
|
||||
authorization
|
||||
authorization,
|
||||
tokenMetadata
|
||||
}: TCompleteAccountInviteDTO) => {
|
||||
const user = await userDAL.findUserByUsername(email);
|
||||
if (!user || (user && user.isAccepted)) {
|
||||
@ -358,6 +370,45 @@ export const authSignupServiceFactory = ({
|
||||
tx
|
||||
);
|
||||
|
||||
if (tokenMetadata) {
|
||||
const metadataObj = jwt.verify(tokenMetadata, appCfg.AUTH_SECRET) as TTokenMetadata;
|
||||
|
||||
if (
|
||||
metadataObj?.payload?.userId !== user.id ||
|
||||
metadataObj?.payload?.orgId !== orgMembership.orgId ||
|
||||
metadataObj?.type !== TokenMetadataType.InviteToProjects
|
||||
) {
|
||||
throw new UnauthorizedError({
|
||||
message: "Malformed or invalid metadata token"
|
||||
});
|
||||
}
|
||||
|
||||
for await (const projectId of metadataObj.payload.projectIds) {
|
||||
await addMembersToProject({
|
||||
orgDAL,
|
||||
projectDAL,
|
||||
projectMembershipDAL,
|
||||
projectKeyDAL,
|
||||
userGroupMembershipDAL,
|
||||
projectBotDAL,
|
||||
projectUserMembershipRoleDAL,
|
||||
smtpService
|
||||
}).addMembersToNonE2EEProject(
|
||||
{
|
||||
emails: [user.email!],
|
||||
usernames: [],
|
||||
projectId,
|
||||
projectMembershipRole: metadataObj.payload.projectRoleSlug,
|
||||
sendEmails: false
|
||||
},
|
||||
{
|
||||
tx,
|
||||
throwOnProjectNotFound: false
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const updatedMembersips = await orgDAL.updateMembership(
|
||||
{ inviteEmail: email, status: OrgMembershipStatus.Invited },
|
||||
{ userId: us.id, status: OrgMembershipStatus.Accepted },
|
||||
|
@ -37,4 +37,5 @@ export type TCompleteAccountInviteDTO = {
|
||||
ip: string;
|
||||
userAgent: string;
|
||||
authorization: string;
|
||||
tokenMetadata?: string;
|
||||
};
|
||||
|
@ -1295,24 +1295,23 @@ export const certificateAuthorityServiceFactory = ({
|
||||
* Return new leaf certificate issued by CA with id [caId].
|
||||
* Note: CSR is generated externally and submitted to Infisical.
|
||||
*/
|
||||
const signCertFromCa = async ({
|
||||
caId,
|
||||
certificateTemplateId,
|
||||
csr,
|
||||
pkiCollectionId,
|
||||
friendlyName,
|
||||
commonName,
|
||||
altNames,
|
||||
ttl,
|
||||
notBefore,
|
||||
notAfter,
|
||||
actorId,
|
||||
actorAuthMethod,
|
||||
actor,
|
||||
actorOrgId
|
||||
}: TSignCertFromCaDTO) => {
|
||||
const signCertFromCa = async (dto: TSignCertFromCaDTO) => {
|
||||
let ca: TCertificateAuthorities | undefined;
|
||||
let certificateTemplate: TCertificateTemplates | undefined;
|
||||
|
||||
const {
|
||||
caId,
|
||||
certificateTemplateId,
|
||||
csr,
|
||||
pkiCollectionId,
|
||||
friendlyName,
|
||||
commonName,
|
||||
altNames,
|
||||
ttl,
|
||||
notBefore,
|
||||
notAfter
|
||||
} = dto;
|
||||
|
||||
let collectionId = pkiCollectionId;
|
||||
|
||||
if (caId) {
|
||||
@ -1333,15 +1332,20 @@ export const certificateAuthorityServiceFactory = ({
|
||||
throw new BadRequestError({ message: "CA not found" });
|
||||
}
|
||||
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
actorId,
|
||||
ca.projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
if (!dto.isInternal) {
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
dto.actor,
|
||||
dto.actorId,
|
||||
ca.projectId,
|
||||
dto.actorAuthMethod,
|
||||
dto.actorOrgId
|
||||
);
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Create, ProjectPermissionSub.Certificates);
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Create,
|
||||
ProjectPermissionSub.Certificates
|
||||
);
|
||||
}
|
||||
|
||||
if (ca.status === CaStatus.DISABLED) throw new BadRequestError({ message: "CA is disabled" });
|
||||
if (!ca.activeCaCertId) throw new BadRequestError({ message: "CA does not have a certificate installed" });
|
||||
@ -1382,6 +1386,8 @@ export const certificateAuthorityServiceFactory = ({
|
||||
notAfterDate = new Date(notAfter);
|
||||
} else if (ttl) {
|
||||
notAfterDate = new Date(new Date().getTime() + ms(ttl));
|
||||
} else if (certificateTemplate?.ttl) {
|
||||
notAfterDate = new Date(new Date().getTime() + ms(certificateTemplate.ttl));
|
||||
}
|
||||
|
||||
const caCertNotBeforeDate = new Date(caCertObj.notBefore);
|
||||
@ -1426,6 +1432,7 @@ export const certificateAuthorityServiceFactory = ({
|
||||
await x509.SubjectKeyIdentifierExtension.create(csrObj.publicKey)
|
||||
];
|
||||
|
||||
let altNamesFromCsr: string = "";
|
||||
let altNamesArray: {
|
||||
type: "email" | "dns";
|
||||
value: string;
|
||||
@ -1454,7 +1461,24 @@ export const certificateAuthorityServiceFactory = ({
|
||||
// If altName is neither a valid email nor a valid hostname, throw an error or handle it accordingly
|
||||
throw new Error(`Invalid altName: ${altName}`);
|
||||
});
|
||||
} else {
|
||||
// attempt to read from CSR if altNames is not explicitly provided
|
||||
const sanExtension = csrObj.extensions.find((ext) => ext.type === "2.5.29.17");
|
||||
if (sanExtension) {
|
||||
const sanNames = new x509.GeneralNames(sanExtension.value);
|
||||
|
||||
altNamesArray = sanNames.items
|
||||
.filter((value) => value.type === "email" || value.type === "dns")
|
||||
.map((name) => ({
|
||||
type: name.type as "email" | "dns",
|
||||
value: name.value
|
||||
}));
|
||||
|
||||
altNamesFromCsr = sanNames.items.map((item) => item.value).join(",");
|
||||
}
|
||||
}
|
||||
|
||||
if (altNamesArray.length) {
|
||||
const altNamesExtension = new x509.SubjectAlternativeNameExtension(altNamesArray, false);
|
||||
extensions.push(altNamesExtension);
|
||||
}
|
||||
@ -1500,7 +1524,7 @@ export const certificateAuthorityServiceFactory = ({
|
||||
status: CertStatus.ACTIVE,
|
||||
friendlyName: friendlyName || csrObj.subject,
|
||||
commonName: cn,
|
||||
altNames,
|
||||
altNames: altNamesFromCsr || altNames,
|
||||
serialNumber,
|
||||
notBefore: notBeforeDate,
|
||||
notAfter: notAfterDate
|
||||
@ -1538,7 +1562,7 @@ export const certificateAuthorityServiceFactory = ({
|
||||
});
|
||||
|
||||
return {
|
||||
certificate: leafCert.toString("pem"),
|
||||
certificate: leafCert,
|
||||
certificateChain: `${issuingCaCertificate}\n${caCertChain}`.trim(),
|
||||
issuingCaCertificate,
|
||||
serialNumber,
|
||||
|
@ -97,18 +97,33 @@ export type TIssueCertFromCaDTO = {
|
||||
notAfter?: string;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
export type TSignCertFromCaDTO = {
|
||||
caId?: string;
|
||||
csr: string;
|
||||
certificateTemplateId?: string;
|
||||
pkiCollectionId?: string;
|
||||
friendlyName?: string;
|
||||
commonName?: string;
|
||||
altNames: string;
|
||||
ttl: string;
|
||||
notBefore?: string;
|
||||
notAfter?: string;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
export type TSignCertFromCaDTO =
|
||||
| {
|
||||
isInternal: true;
|
||||
caId?: string;
|
||||
csr: string;
|
||||
certificateTemplateId?: string;
|
||||
pkiCollectionId?: string;
|
||||
friendlyName?: string;
|
||||
commonName?: string;
|
||||
altNames?: string;
|
||||
ttl?: string;
|
||||
notBefore?: string;
|
||||
notAfter?: string;
|
||||
}
|
||||
| ({
|
||||
isInternal: false;
|
||||
caId?: string;
|
||||
csr: string;
|
||||
certificateTemplateId?: string;
|
||||
pkiCollectionId?: string;
|
||||
friendlyName?: string;
|
||||
commonName?: string;
|
||||
altNames: string;
|
||||
ttl: string;
|
||||
notBefore?: string;
|
||||
notAfter?: string;
|
||||
} & Omit<TProjectPermission, "projectId">);
|
||||
|
||||
export type TDNParts = {
|
||||
commonName?: string;
|
||||
|
24
backend/src/services/certificate-est/certificate-est-fns.ts
Normal file
@ -0,0 +1,24 @@
|
||||
import { Certificate, ContentInfo, EncapsulatedContentInfo, SignedData } from "pkijs";
|
||||
|
||||
export const convertRawCertsToPkcs7 = (rawCertificate: ArrayBuffer[]) => {
|
||||
const certs = rawCertificate.map((rawCert) => Certificate.fromBER(rawCert));
|
||||
const cmsSigned = new SignedData({
|
||||
encapContentInfo: new EncapsulatedContentInfo({
|
||||
eContentType: "1.2.840.113549.1.7.1" // not encrypted and not compressed data
|
||||
}),
|
||||
certificates: certs
|
||||
});
|
||||
|
||||
const cmsContent = new ContentInfo({
|
||||
contentType: "1.2.840.113549.1.7.2", // SignedData
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||
content: cmsSigned.toSchema()
|
||||
});
|
||||
|
||||
const derBuffer = cmsContent.toSchema().toBER(false);
|
||||
const base64Pkcs7 = Buffer.from(derBuffer)
|
||||
.toString("base64")
|
||||
.replace(/(.{64})/g, "$1\n"); // we add a linebreak for CURL clients
|
||||
|
||||
return base64Pkcs7;
|
||||
};
|
231
backend/src/services/certificate-est/certificate-est-service.ts
Normal file
@ -0,0 +1,231 @@
|
||||
import * as x509 from "@peculiar/x509";
|
||||
|
||||
import { BadRequestError, NotFoundError, UnauthorizedError } from "@app/lib/errors";
|
||||
|
||||
import { isCertChainValid } from "../certificate/certificate-fns";
|
||||
import { TCertificateAuthorityCertDALFactory } from "../certificate-authority/certificate-authority-cert-dal";
|
||||
import { TCertificateAuthorityDALFactory } from "../certificate-authority/certificate-authority-dal";
|
||||
import { getCaCertChain, getCaCertChains } from "../certificate-authority/certificate-authority-fns";
|
||||
import { TCertificateAuthorityServiceFactory } from "../certificate-authority/certificate-authority-service";
|
||||
import { TCertificateTemplateDALFactory } from "../certificate-template/certificate-template-dal";
|
||||
import { TCertificateTemplateServiceFactory } from "../certificate-template/certificate-template-service";
|
||||
import { TKmsServiceFactory } from "../kms/kms-service";
|
||||
import { TProjectDALFactory } from "../project/project-dal";
|
||||
import { convertRawCertsToPkcs7 } from "./certificate-est-fns";
|
||||
|
||||
type TCertificateEstServiceFactoryDep = {
|
||||
certificateAuthorityService: Pick<TCertificateAuthorityServiceFactory, "signCertFromCa">;
|
||||
certificateTemplateService: Pick<TCertificateTemplateServiceFactory, "getEstConfiguration">;
|
||||
certificateTemplateDAL: Pick<TCertificateTemplateDALFactory, "findById">;
|
||||
certificateAuthorityDAL: Pick<TCertificateAuthorityDALFactory, "findById">;
|
||||
certificateAuthorityCertDAL: Pick<TCertificateAuthorityCertDALFactory, "find" | "findById">;
|
||||
projectDAL: Pick<TProjectDALFactory, "findOne" | "updateById" | "transaction">;
|
||||
kmsService: Pick<TKmsServiceFactory, "decryptWithKmsKey" | "generateKmsKey">;
|
||||
};
|
||||
|
||||
export type TCertificateEstServiceFactory = ReturnType<typeof certificateEstServiceFactory>;
|
||||
|
||||
export const certificateEstServiceFactory = ({
|
||||
certificateAuthorityService,
|
||||
certificateTemplateService,
|
||||
certificateTemplateDAL,
|
||||
certificateAuthorityCertDAL,
|
||||
certificateAuthorityDAL,
|
||||
projectDAL,
|
||||
kmsService
|
||||
}: TCertificateEstServiceFactoryDep) => {
|
||||
const simpleReenroll = async ({
|
||||
csr,
|
||||
certificateTemplateId,
|
||||
sslClientCert
|
||||
}: {
|
||||
csr: string;
|
||||
certificateTemplateId: string;
|
||||
sslClientCert: string;
|
||||
}) => {
|
||||
const estConfig = await certificateTemplateService.getEstConfiguration({
|
||||
isInternal: true,
|
||||
certificateTemplateId
|
||||
});
|
||||
|
||||
if (!estConfig.isEnabled) {
|
||||
throw new BadRequestError({
|
||||
message: "EST is disabled"
|
||||
});
|
||||
}
|
||||
|
||||
const certTemplate = await certificateTemplateDAL.findById(certificateTemplateId);
|
||||
|
||||
const leafCertificate = decodeURIComponent(sslClientCert).match(
|
||||
/-----BEGIN CERTIFICATE-----[\s\S]+?-----END CERTIFICATE-----/g
|
||||
)?.[0];
|
||||
|
||||
if (!leafCertificate) {
|
||||
throw new UnauthorizedError({ message: "Missing client certificate" });
|
||||
}
|
||||
|
||||
const cert = new x509.X509Certificate(leafCertificate);
|
||||
// We have to assert that the client certificate provided can be traced back to the Root CA
|
||||
const caCertChains = await getCaCertChains({
|
||||
caId: certTemplate.caId,
|
||||
certificateAuthorityCertDAL,
|
||||
certificateAuthorityDAL,
|
||||
projectDAL,
|
||||
kmsService
|
||||
});
|
||||
|
||||
const verifiedChains = await Promise.all(
|
||||
caCertChains.map((chain) => {
|
||||
const caCert = new x509.X509Certificate(chain.certificate);
|
||||
const caChain =
|
||||
chain.certificateChain
|
||||
.match(/-----BEGIN CERTIFICATE-----[\s\S]+?-----END CERTIFICATE-----/g)
|
||||
?.map((c) => new x509.X509Certificate(c)) || [];
|
||||
|
||||
return isCertChainValid([cert, caCert, ...caChain]);
|
||||
})
|
||||
);
|
||||
|
||||
if (!verifiedChains.some(Boolean)) {
|
||||
throw new BadRequestError({
|
||||
message: "Invalid client certificate: unable to build a valid certificate chain"
|
||||
});
|
||||
}
|
||||
|
||||
// We ensure that the Subject and SubjectAltNames of the CSR and the existing certificate are exactly the same
|
||||
const csrObj = new x509.Pkcs10CertificateRequest(csr);
|
||||
if (csrObj.subject !== cert.subject) {
|
||||
throw new BadRequestError({
|
||||
message: "Subject mismatch"
|
||||
});
|
||||
}
|
||||
|
||||
let csrSanSet: Set<string> = new Set();
|
||||
const csrSanExtension = csrObj.extensions.find((ext) => ext.type === "2.5.29.17");
|
||||
if (csrSanExtension) {
|
||||
const sanNames = new x509.GeneralNames(csrSanExtension.value);
|
||||
csrSanSet = new Set([...sanNames.items.map((name) => `${name.type}-${name.value}`)]);
|
||||
}
|
||||
|
||||
let certSanSet: Set<string> = new Set();
|
||||
const certSanExtension = cert.extensions.find((ext) => ext.type === "2.5.29.17");
|
||||
if (certSanExtension) {
|
||||
const sanNames = new x509.GeneralNames(certSanExtension.value);
|
||||
certSanSet = new Set([...sanNames.items.map((name) => `${name.type}-${name.value}`)]);
|
||||
}
|
||||
|
||||
if (csrSanSet.size !== certSanSet.size || ![...csrSanSet].every((element) => certSanSet.has(element))) {
|
||||
throw new BadRequestError({
|
||||
message: "Subject alternative names mismatch"
|
||||
});
|
||||
}
|
||||
|
||||
const { certificate } = await certificateAuthorityService.signCertFromCa({
|
||||
isInternal: true,
|
||||
certificateTemplateId,
|
||||
csr
|
||||
});
|
||||
|
||||
return convertRawCertsToPkcs7([certificate.rawData]);
|
||||
};
|
||||
|
||||
const simpleEnroll = async ({
|
||||
csr,
|
||||
certificateTemplateId,
|
||||
sslClientCert
|
||||
}: {
|
||||
csr: string;
|
||||
certificateTemplateId: string;
|
||||
sslClientCert: string;
|
||||
}) => {
|
||||
/* We first have to assert that the client certificate provided can be traced back to the attached
|
||||
CA chain in the EST configuration
|
||||
*/
|
||||
const estConfig = await certificateTemplateService.getEstConfiguration({
|
||||
isInternal: true,
|
||||
certificateTemplateId
|
||||
});
|
||||
|
||||
if (!estConfig.isEnabled) {
|
||||
throw new BadRequestError({
|
||||
message: "EST is disabled"
|
||||
});
|
||||
}
|
||||
|
||||
const caCerts = estConfig.caChain
|
||||
.match(/-----BEGIN CERTIFICATE-----[\s\S]+?-----END CERTIFICATE-----/g)
|
||||
?.map((cert) => {
|
||||
return new x509.X509Certificate(cert);
|
||||
});
|
||||
|
||||
if (!caCerts) {
|
||||
throw new BadRequestError({ message: "Failed to parse certificate chain" });
|
||||
}
|
||||
|
||||
const leafCertificate = decodeURIComponent(sslClientCert).match(
|
||||
/-----BEGIN CERTIFICATE-----[\s\S]+?-----END CERTIFICATE-----/g
|
||||
)?.[0];
|
||||
|
||||
if (!leafCertificate) {
|
||||
throw new BadRequestError({ message: "Missing client certificate" });
|
||||
}
|
||||
|
||||
const certObj = new x509.X509Certificate(leafCertificate);
|
||||
if (!(await isCertChainValid([certObj, ...caCerts]))) {
|
||||
throw new BadRequestError({ message: "Invalid certificate chain" });
|
||||
}
|
||||
|
||||
const { certificate } = await certificateAuthorityService.signCertFromCa({
|
||||
isInternal: true,
|
||||
certificateTemplateId,
|
||||
csr
|
||||
});
|
||||
|
||||
return convertRawCertsToPkcs7([certificate.rawData]);
|
||||
};
|
||||
|
||||
/**
|
||||
* Return the CA certificate and CA certificate chain for the CA bound to
|
||||
* the certificate template with id [certificateTemplateId] as part of EST protocol
|
||||
*/
|
||||
const getCaCerts = async ({ certificateTemplateId }: { certificateTemplateId: string }) => {
|
||||
const certTemplate = await certificateTemplateDAL.findById(certificateTemplateId);
|
||||
if (!certTemplate) {
|
||||
throw new NotFoundError({
|
||||
message: "Certificate template not found"
|
||||
});
|
||||
}
|
||||
|
||||
const ca = await certificateAuthorityDAL.findById(certTemplate.caId);
|
||||
if (!ca) {
|
||||
throw new NotFoundError({
|
||||
message: "Certificate Authority not found"
|
||||
});
|
||||
}
|
||||
|
||||
const { caCert, caCertChain } = await getCaCertChain({
|
||||
caCertId: ca.activeCaCertId as string,
|
||||
certificateAuthorityDAL,
|
||||
certificateAuthorityCertDAL,
|
||||
projectDAL,
|
||||
kmsService
|
||||
});
|
||||
|
||||
const certificates = caCertChain
|
||||
.match(/-----BEGIN CERTIFICATE-----[\s\S]+?-----END CERTIFICATE-----/g)
|
||||
?.map((cert) => new x509.X509Certificate(cert));
|
||||
|
||||
if (!certificates) {
|
||||
throw new BadRequestError({ message: "Failed to parse certificate chain" });
|
||||
}
|
||||
|
||||
const caCertificate = new x509.X509Certificate(caCert);
|
||||
return convertRawCertsToPkcs7([caCertificate.rawData, ...certificates.map((cert) => cert.rawData)]);
|
||||
};
|
||||
|
||||
return {
|
||||
simpleEnroll,
|
||||
simpleReenroll,
|
||||
getCaCerts
|
||||
};
|
||||
};
|
@ -0,0 +1,11 @@
|
||||
import { TDbClient } from "@app/db";
|
||||
import { TableName } from "@app/db/schemas";
|
||||
import { ormify } from "@app/lib/knex";
|
||||
|
||||
export type TCertificateTemplateEstConfigDALFactory = ReturnType<typeof certificateTemplateEstConfigDALFactory>;
|
||||
|
||||
export const certificateTemplateEstConfigDALFactory = (db: TDbClient) => {
|
||||
const certificateTemplateEstConfigOrm = ormify(db, TableName.CertificateTemplateEstConfig);
|
||||
|
||||
return certificateTemplateEstConfigOrm;
|
||||
};
|
@ -1,20 +1,35 @@
|
||||
import { ForbiddenError } from "@casl/ability";
|
||||
import * as x509 from "@peculiar/x509";
|
||||
import bcrypt from "bcrypt";
|
||||
|
||||
import { TCertificateTemplateEstConfigsUpdate } from "@app/db/schemas";
|
||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
|
||||
import { getConfig } from "@app/lib/config/env";
|
||||
import { BadRequestError, NotFoundError } from "@app/lib/errors";
|
||||
|
||||
import { isCertChainValid } from "../certificate/certificate-fns";
|
||||
import { TCertificateAuthorityDALFactory } from "../certificate-authority/certificate-authority-dal";
|
||||
import { TKmsServiceFactory } from "../kms/kms-service";
|
||||
import { TProjectDALFactory } from "../project/project-dal";
|
||||
import { getProjectKmsCertificateKeyId } from "../project/project-fns";
|
||||
import { TCertificateTemplateDALFactory } from "./certificate-template-dal";
|
||||
import { TCertificateTemplateEstConfigDALFactory } from "./certificate-template-est-config-dal";
|
||||
import {
|
||||
TCreateCertTemplateDTO,
|
||||
TCreateEstConfigurationDTO,
|
||||
TDeleteCertTemplateDTO,
|
||||
TGetCertTemplateDTO,
|
||||
TUpdateCertTemplateDTO
|
||||
TGetEstConfigurationDTO,
|
||||
TUpdateCertTemplateDTO,
|
||||
TUpdateEstConfigurationDTO
|
||||
} from "./certificate-template-types";
|
||||
|
||||
type TCertificateTemplateServiceFactoryDep = {
|
||||
certificateTemplateDAL: TCertificateTemplateDALFactory;
|
||||
certificateTemplateEstConfigDAL: TCertificateTemplateEstConfigDALFactory;
|
||||
projectDAL: Pick<TProjectDALFactory, "findProjectBySlug" | "findOne" | "updateById" | "findById" | "transaction">;
|
||||
kmsService: Pick<TKmsServiceFactory, "generateKmsKey" | "encryptWithKmsKey" | "decryptWithKmsKey">;
|
||||
certificateAuthorityDAL: Pick<TCertificateAuthorityDALFactory, "findById">;
|
||||
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
|
||||
};
|
||||
@ -23,8 +38,11 @@ export type TCertificateTemplateServiceFactory = ReturnType<typeof certificateTe
|
||||
|
||||
export const certificateTemplateServiceFactory = ({
|
||||
certificateTemplateDAL,
|
||||
certificateTemplateEstConfigDAL,
|
||||
certificateAuthorityDAL,
|
||||
permissionService
|
||||
permissionService,
|
||||
kmsService,
|
||||
projectDAL
|
||||
}: TCertificateTemplateServiceFactoryDep) => {
|
||||
const createCertTemplate = async ({
|
||||
caId,
|
||||
@ -187,10 +205,228 @@ export const certificateTemplateServiceFactory = ({
|
||||
return certTemplate;
|
||||
};
|
||||
|
||||
const createEstConfiguration = async ({
|
||||
certificateTemplateId,
|
||||
caChain,
|
||||
passphrase,
|
||||
isEnabled,
|
||||
actorId,
|
||||
actorAuthMethod,
|
||||
actor,
|
||||
actorOrgId
|
||||
}: TCreateEstConfigurationDTO) => {
|
||||
const certTemplate = await certificateTemplateDAL.getById(certificateTemplateId);
|
||||
if (!certTemplate) {
|
||||
throw new NotFoundError({
|
||||
message: "Certificate template not found."
|
||||
});
|
||||
}
|
||||
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
actorId,
|
||||
certTemplate.projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Edit,
|
||||
ProjectPermissionSub.CertificateTemplates
|
||||
);
|
||||
|
||||
const appCfg = getConfig();
|
||||
|
||||
const certificateManagerKmsId = await getProjectKmsCertificateKeyId({
|
||||
projectId: certTemplate.projectId,
|
||||
projectDAL,
|
||||
kmsService
|
||||
});
|
||||
|
||||
// validate CA chain
|
||||
const certificates = caChain
|
||||
.match(/-----BEGIN CERTIFICATE-----[\s\S]+?-----END CERTIFICATE-----/g)
|
||||
?.map((cert) => new x509.X509Certificate(cert));
|
||||
|
||||
if (!certificates) {
|
||||
throw new BadRequestError({ message: "Failed to parse certificate chain" });
|
||||
}
|
||||
|
||||
if (!(await isCertChainValid(certificates))) {
|
||||
throw new BadRequestError({ message: "Invalid certificate chain" });
|
||||
}
|
||||
|
||||
const kmsEncryptor = await kmsService.encryptWithKmsKey({
|
||||
kmsId: certificateManagerKmsId
|
||||
});
|
||||
|
||||
const { cipherTextBlob: encryptedCaChain } = await kmsEncryptor({
|
||||
plainText: Buffer.from(caChain)
|
||||
});
|
||||
|
||||
const hashedPassphrase = await bcrypt.hash(passphrase, appCfg.SALT_ROUNDS);
|
||||
const estConfig = await certificateTemplateEstConfigDAL.create({
|
||||
certificateTemplateId,
|
||||
hashedPassphrase,
|
||||
encryptedCaChain,
|
||||
isEnabled
|
||||
});
|
||||
|
||||
return { ...estConfig, projectId: certTemplate.projectId };
|
||||
};
|
||||
|
||||
const updateEstConfiguration = async ({
|
||||
certificateTemplateId,
|
||||
caChain,
|
||||
passphrase,
|
||||
isEnabled,
|
||||
actorId,
|
||||
actorAuthMethod,
|
||||
actor,
|
||||
actorOrgId
|
||||
}: TUpdateEstConfigurationDTO) => {
|
||||
const certTemplate = await certificateTemplateDAL.getById(certificateTemplateId);
|
||||
if (!certTemplate) {
|
||||
throw new NotFoundError({
|
||||
message: "Certificate template not found."
|
||||
});
|
||||
}
|
||||
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
actorId,
|
||||
certTemplate.projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Edit,
|
||||
ProjectPermissionSub.CertificateTemplates
|
||||
);
|
||||
|
||||
const originalCaEstConfig = await certificateTemplateEstConfigDAL.findOne({
|
||||
certificateTemplateId
|
||||
});
|
||||
|
||||
if (!originalCaEstConfig) {
|
||||
throw new NotFoundError({
|
||||
message: "EST configuration not found"
|
||||
});
|
||||
}
|
||||
|
||||
const appCfg = getConfig();
|
||||
|
||||
const certificateManagerKmsId = await getProjectKmsCertificateKeyId({
|
||||
projectId: certTemplate.projectId,
|
||||
projectDAL,
|
||||
kmsService
|
||||
});
|
||||
|
||||
const updatedData: TCertificateTemplateEstConfigsUpdate = {
|
||||
isEnabled
|
||||
};
|
||||
|
||||
if (caChain) {
|
||||
const certificates = caChain
|
||||
.match(/-----BEGIN CERTIFICATE-----[\s\S]+?-----END CERTIFICATE-----/g)
|
||||
?.map((cert) => new x509.X509Certificate(cert));
|
||||
|
||||
if (!certificates) {
|
||||
throw new BadRequestError({ message: "Failed to parse certificate chain" });
|
||||
}
|
||||
|
||||
if (!(await isCertChainValid(certificates))) {
|
||||
throw new BadRequestError({ message: "Invalid certificate chain" });
|
||||
}
|
||||
|
||||
const kmsEncryptor = await kmsService.encryptWithKmsKey({
|
||||
kmsId: certificateManagerKmsId
|
||||
});
|
||||
|
||||
const { cipherTextBlob: encryptedCaChain } = await kmsEncryptor({
|
||||
plainText: Buffer.from(caChain)
|
||||
});
|
||||
|
||||
updatedData.encryptedCaChain = encryptedCaChain;
|
||||
}
|
||||
|
||||
if (passphrase) {
|
||||
const hashedPassphrase = await bcrypt.hash(passphrase, appCfg.SALT_ROUNDS);
|
||||
updatedData.hashedPassphrase = hashedPassphrase;
|
||||
}
|
||||
|
||||
const estConfig = await certificateTemplateEstConfigDAL.updateById(originalCaEstConfig.id, updatedData);
|
||||
|
||||
return { ...estConfig, projectId: certTemplate.projectId };
|
||||
};
|
||||
|
||||
const getEstConfiguration = async (dto: TGetEstConfigurationDTO) => {
|
||||
const { certificateTemplateId } = dto;
|
||||
|
||||
const certTemplate = await certificateTemplateDAL.getById(certificateTemplateId);
|
||||
if (!certTemplate) {
|
||||
throw new NotFoundError({
|
||||
message: "Certificate template not found."
|
||||
});
|
||||
}
|
||||
|
||||
if (!dto.isInternal) {
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
dto.actor,
|
||||
dto.actorId,
|
||||
certTemplate.projectId,
|
||||
dto.actorAuthMethod,
|
||||
dto.actorOrgId
|
||||
);
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Edit,
|
||||
ProjectPermissionSub.CertificateTemplates
|
||||
);
|
||||
}
|
||||
|
||||
const estConfig = await certificateTemplateEstConfigDAL.findOne({
|
||||
certificateTemplateId
|
||||
});
|
||||
|
||||
if (!estConfig) {
|
||||
throw new NotFoundError({
|
||||
message: "EST configuration not found"
|
||||
});
|
||||
}
|
||||
|
||||
const certificateManagerKmsId = await getProjectKmsCertificateKeyId({
|
||||
projectId: certTemplate.projectId,
|
||||
projectDAL,
|
||||
kmsService
|
||||
});
|
||||
|
||||
const kmsDecryptor = await kmsService.decryptWithKmsKey({
|
||||
kmsId: certificateManagerKmsId
|
||||
});
|
||||
|
||||
const decryptedCaChain = await kmsDecryptor({
|
||||
cipherTextBlob: estConfig.encryptedCaChain
|
||||
});
|
||||
|
||||
return {
|
||||
certificateTemplateId,
|
||||
id: estConfig.id,
|
||||
isEnabled: estConfig.isEnabled,
|
||||
caChain: decryptedCaChain.toString(),
|
||||
hashedPassphrase: estConfig.hashedPassphrase,
|
||||
projectId: certTemplate.projectId
|
||||
};
|
||||
};
|
||||
|
||||
return {
|
||||
createCertTemplate,
|
||||
getCertTemplate,
|
||||
deleteCertTemplate,
|
||||
updateCertTemplate
|
||||
updateCertTemplate,
|
||||
createEstConfiguration,
|
||||
updateEstConfiguration,
|
||||
getEstConfiguration
|
||||
};
|
||||
};
|
||||
|
@ -26,3 +26,27 @@ export type TGetCertTemplateDTO = {
|
||||
export type TDeleteCertTemplateDTO = {
|
||||
id: string;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
export type TCreateEstConfigurationDTO = {
|
||||
certificateTemplateId: string;
|
||||
caChain: string;
|
||||
passphrase: string;
|
||||
isEnabled: boolean;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
export type TUpdateEstConfigurationDTO = {
|
||||
certificateTemplateId: string;
|
||||
caChain?: string;
|
||||
passphrase?: string;
|
||||
isEnabled?: boolean;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
export type TGetEstConfigurationDTO =
|
||||
| {
|
||||
isInternal: true;
|
||||
certificateTemplateId: string;
|
||||
}
|
||||
| ({
|
||||
isInternal: false;
|
||||
certificateTemplateId: string;
|
||||
} & Omit<TProjectPermission, "projectId">);
|
||||
|
@ -24,3 +24,19 @@ export const revocationReasonToCrlCode = (crlReason: CrlReason) => {
|
||||
return x509.X509CrlReason.unspecified;
|
||||
}
|
||||
};
|
||||
|
||||
export const isCertChainValid = async (certificates: x509.X509Certificate[]) => {
|
||||
if (certificates.length === 1) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const leafCert = certificates[0];
|
||||
const chain = new x509.X509ChainBuilder({
|
||||
certificates: certificates.slice(1)
|
||||
});
|
||||
|
||||
const chainItems = await chain.build(leafCert);
|
||||
|
||||
// chain.build() implicitly verifies the chain
|
||||
return chainItems.length === certificates.length;
|
||||
};
|
||||
|
@ -1030,11 +1030,31 @@ const getAppsCloud66 = async ({ accessToken }: { accessToken: string }) => {
|
||||
return apps;
|
||||
};
|
||||
|
||||
const getAppsAzureDevOps = async ({ accessToken, orgName }: { accessToken: string; orgName: string }) => {
|
||||
const res = (
|
||||
await request.get<{ count: number; value: Record<string, string>[] }>(
|
||||
`${IntegrationUrls.AZURE_DEVOPS_API_URL}/${orgName}/_apis/projects?api-version=7.2-preview.2`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Basic ${accessToken}`
|
||||
}
|
||||
}
|
||||
)
|
||||
).data;
|
||||
const apps = res.value.map((a) => ({
|
||||
name: a.name,
|
||||
appId: a.id
|
||||
}));
|
||||
|
||||
return apps;
|
||||
};
|
||||
|
||||
export const getApps = async ({
|
||||
integration,
|
||||
accessToken,
|
||||
accessId,
|
||||
teamId,
|
||||
azureDevOpsOrgName,
|
||||
workspaceSlug,
|
||||
url
|
||||
}: {
|
||||
@ -1042,6 +1062,7 @@ export const getApps = async ({
|
||||
accessToken: string;
|
||||
accessId?: string;
|
||||
teamId?: string | null;
|
||||
azureDevOpsOrgName?: string | null;
|
||||
workspaceSlug?: string;
|
||||
url?: string | null;
|
||||
}): Promise<App[]> => {
|
||||
@ -1184,6 +1205,12 @@ export const getApps = async ({
|
||||
accessToken
|
||||
});
|
||||
|
||||
case Integrations.AZURE_DEVOPS:
|
||||
return getAppsAzureDevOps({
|
||||
accessToken,
|
||||
orgName: azureDevOpsOrgName as string
|
||||
});
|
||||
|
||||
default:
|
||||
throw new BadRequestError({ message: "integration not found" });
|
||||
}
|
||||
|
@ -440,6 +440,7 @@ export const integrationAuthServiceFactory = ({
|
||||
actorOrgId,
|
||||
actorAuthMethod,
|
||||
teamId,
|
||||
azureDevOpsOrgName,
|
||||
id,
|
||||
workspaceSlug
|
||||
}: TIntegrationAuthAppsDTO) => {
|
||||
@ -462,6 +463,7 @@ export const integrationAuthServiceFactory = ({
|
||||
accessToken,
|
||||
accessId,
|
||||
teamId,
|
||||
azureDevOpsOrgName,
|
||||
workspaceSlug,
|
||||
url: integrationAuth.url
|
||||
});
|
||||
|
@ -1,3 +1,4 @@
|
||||
import { TIntegrations } from "@app/db/schemas";
|
||||
import { TProjectPermission } from "@app/lib/types";
|
||||
|
||||
export type TGetIntegrationAuthDTO = {
|
||||
@ -28,6 +29,7 @@ export type TDeleteIntegrationAuthsDTO = TProjectPermission & {
|
||||
export type TIntegrationAuthAppsDTO = {
|
||||
id: string;
|
||||
teamId?: string;
|
||||
azureDevOpsOrgName?: string;
|
||||
workspaceSlug?: string;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
@ -163,3 +165,13 @@ export type TTeamCityBuildConfig = {
|
||||
href: string;
|
||||
webUrl: string;
|
||||
};
|
||||
|
||||
export type TIntegrationsWithEnvironment = TIntegrations & {
|
||||
environment?:
|
||||
| {
|
||||
id?: string | null | undefined;
|
||||
name?: string | null | undefined;
|
||||
}
|
||||
| null
|
||||
| undefined;
|
||||
};
|
||||
|
@ -31,7 +31,8 @@ export enum Integrations {
|
||||
CLOUD_66 = "cloud-66",
|
||||
NORTHFLANK = "northflank",
|
||||
HASURA_CLOUD = "hasura-cloud",
|
||||
RUNDECK = "rundeck"
|
||||
RUNDECK = "rundeck",
|
||||
AZURE_DEVOPS = "azure-devops"
|
||||
}
|
||||
|
||||
export enum IntegrationType {
|
||||
@ -88,6 +89,7 @@ export enum IntegrationUrls {
|
||||
CLOUD_66_API_URL = "https://app.cloud66.com/api",
|
||||
NORTHFLANK_API_URL = "https://api.northflank.com",
|
||||
HASURA_CLOUD_API_URL = "https://data.pro.hasura.io/v1/graphql",
|
||||
AZURE_DEVOPS_API_URL = "https://dev.azure.com",
|
||||
|
||||
GCP_SECRET_MANAGER_SERVICE_NAME = "secretmanager.googleapis.com",
|
||||
GCP_SECRET_MANAGER_URL = `https://${GCP_SECRET_MANAGER_SERVICE_NAME}`,
|
||||
@ -378,6 +380,15 @@ export const getIntegrationOptions = async () => {
|
||||
type: "pat",
|
||||
clientId: "",
|
||||
docsLink: ""
|
||||
},
|
||||
{
|
||||
name: "Azure DevOps",
|
||||
slug: "azure-devops",
|
||||
image: "Microsoft Azure.png",
|
||||
isAvailable: true,
|
||||
type: "pat",
|
||||
clientId: "",
|
||||
docsLink: ""
|
||||
}
|
||||
];
|
||||
|
||||
|
@ -35,6 +35,7 @@ import { TCreateManySecretsRawFn, TUpdateManySecretsRawFn } from "@app/services/
|
||||
|
||||
import { TIntegrationDALFactory } from "../integration/integration-dal";
|
||||
import { IntegrationMetadataSchema } from "../integration/integration-schema";
|
||||
import { TIntegrationsWithEnvironment } from "./integration-auth-types";
|
||||
import {
|
||||
IntegrationInitialSyncBehavior,
|
||||
IntegrationMappingBehavior,
|
||||
@ -2075,6 +2076,116 @@ const syncSecretsTravisCI = async ({
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Sync/push [secrets] to GitLab repo with name [integration.app]
|
||||
*/
|
||||
const syncSecretsAzureDevops = async ({
|
||||
integrationAuth,
|
||||
integration,
|
||||
secrets,
|
||||
accessToken
|
||||
}: {
|
||||
integrationAuth: TIntegrationAuths;
|
||||
integration: TIntegrationsWithEnvironment;
|
||||
secrets: Record<string, { value: string; comment?: string }>;
|
||||
accessToken: string;
|
||||
}) => {
|
||||
if (!integration.appId || !integration.app) {
|
||||
throw new Error("Azure DevOps: orgId and projectId are required");
|
||||
}
|
||||
if (!integration.environment || !integration.environment.name) {
|
||||
throw new Error("Azure DevOps: environment is required");
|
||||
}
|
||||
const headers = {
|
||||
Authorization: `Basic ${accessToken}`
|
||||
};
|
||||
const azureDevopsApiUrl = integrationAuth.url ? `${integrationAuth.url}` : IntegrationUrls.AZURE_DEVOPS_API_URL;
|
||||
|
||||
const getEnvGroupId = async (orgId: string, project: string, env: string) => {
|
||||
let groupId;
|
||||
const url: string | null =
|
||||
`${azureDevopsApiUrl}/${orgId}/${project}/_apis/distributedtask/variablegroups?api-version=7.2-preview.2`;
|
||||
|
||||
const response = await request.get(url, { headers });
|
||||
for (const group of response.data.value) {
|
||||
const groupName = group.name;
|
||||
if (groupName === env) {
|
||||
groupId = group.id;
|
||||
return { groupId, groupName };
|
||||
}
|
||||
}
|
||||
return { groupId: "", groupName: "" };
|
||||
};
|
||||
|
||||
const { groupId, groupName } = await getEnvGroupId(integration.app, integration.appId, integration.environment.name);
|
||||
|
||||
const variables: Record<string, { value: string }> = {};
|
||||
for (const key of Object.keys(secrets)) {
|
||||
variables[key] = { value: secrets[key].value };
|
||||
}
|
||||
|
||||
if (!groupId) {
|
||||
// create new variable group if not present
|
||||
const url = `${azureDevopsApiUrl}/${integration.app}/_apis/distributedtask/variablegroups?api-version=7.2-preview.2`;
|
||||
const config = {
|
||||
method: "POST",
|
||||
url,
|
||||
data: {
|
||||
name: integration.environment.name,
|
||||
description: integration.environment.name,
|
||||
type: "Vsts",
|
||||
owner: "Library",
|
||||
variables,
|
||||
variableGroupProjectReferences: [
|
||||
{
|
||||
name: integration.environment.name,
|
||||
projectReference: {
|
||||
name: integration.appId
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
headers: {
|
||||
headers
|
||||
}
|
||||
};
|
||||
|
||||
const res = await request.post(url, config.data, config.headers);
|
||||
if (res.status !== 200) {
|
||||
throw new Error(`Azure DevOps: Failed to create variable group: ${res.statusText}`);
|
||||
}
|
||||
} else {
|
||||
// sync variables for pre-existing variable group
|
||||
const url = `${azureDevopsApiUrl}/${integration.app}/_apis/distributedtask/variablegroups/${groupId}?api-version=7.2-preview.2`;
|
||||
const config = {
|
||||
method: "PUT",
|
||||
url,
|
||||
data: {
|
||||
name: groupName,
|
||||
description: groupName,
|
||||
type: "Vsts",
|
||||
owner: "Library",
|
||||
variables,
|
||||
variableGroupProjectReferences: [
|
||||
{
|
||||
name: groupName,
|
||||
projectReference: {
|
||||
name: integration.appId
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
headers: {
|
||||
headers
|
||||
}
|
||||
};
|
||||
const res = await request.put(url, config.data, config.headers);
|
||||
if (res.status !== 200) {
|
||||
throw new Error(`Azure DevOps: Failed to update variable group: ${res.statusText}`);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Sync/push [secrets] to GitLab repo with name [integration.app]
|
||||
*/
|
||||
@ -3714,6 +3825,15 @@ export const syncIntegrationSecrets = async ({
|
||||
updateManySecretsRawFn
|
||||
});
|
||||
break;
|
||||
|
||||
case Integrations.AZURE_DEVOPS:
|
||||
await syncSecretsAzureDevops({
|
||||
integrationAuth,
|
||||
integration,
|
||||
secrets,
|
||||
accessToken
|
||||
});
|
||||
break;
|
||||
case Integrations.AWS_PARAMETER_STORE:
|
||||
response = await syncSecretsAWSParameterStore({
|
||||
integration,
|
||||
|
@ -114,10 +114,11 @@ export const orgDALFactory = (db: TDbClient) => {
|
||||
}
|
||||
};
|
||||
|
||||
const findOrgMembersByUsername = async (orgId: string, usernames: string[]) => {
|
||||
const findOrgMembersByUsername = async (orgId: string, usernames: string[], tx?: Knex) => {
|
||||
try {
|
||||
const members = await db
|
||||
.replicaNode()(TableName.OrgMembership)
|
||||
const conn = tx || db;
|
||||
const members = await conn(TableName.OrgMembership)
|
||||
// .replicaNode()(TableName.OrgMembership)
|
||||
.where(`${TableName.OrgMembership}.orgId`, orgId)
|
||||
.join(TableName.Users, `${TableName.OrgMembership}.userId`, `${TableName.Users}.id`)
|
||||
.leftJoin<TUserEncryptionKeys>(
|
||||
@ -126,18 +127,18 @@ export const orgDALFactory = (db: TDbClient) => {
|
||||
`${TableName.Users}.id`
|
||||
)
|
||||
.select(
|
||||
db.ref("id").withSchema(TableName.OrgMembership),
|
||||
db.ref("inviteEmail").withSchema(TableName.OrgMembership),
|
||||
db.ref("orgId").withSchema(TableName.OrgMembership),
|
||||
db.ref("role").withSchema(TableName.OrgMembership),
|
||||
db.ref("roleId").withSchema(TableName.OrgMembership),
|
||||
db.ref("status").withSchema(TableName.OrgMembership),
|
||||
db.ref("username").withSchema(TableName.Users),
|
||||
db.ref("email").withSchema(TableName.Users),
|
||||
db.ref("firstName").withSchema(TableName.Users),
|
||||
db.ref("lastName").withSchema(TableName.Users),
|
||||
db.ref("id").withSchema(TableName.Users).as("userId"),
|
||||
db.ref("publicKey").withSchema(TableName.UserEncryptionKey)
|
||||
conn.ref("id").withSchema(TableName.OrgMembership),
|
||||
conn.ref("inviteEmail").withSchema(TableName.OrgMembership),
|
||||
conn.ref("orgId").withSchema(TableName.OrgMembership),
|
||||
conn.ref("role").withSchema(TableName.OrgMembership),
|
||||
conn.ref("roleId").withSchema(TableName.OrgMembership),
|
||||
conn.ref("status").withSchema(TableName.OrgMembership),
|
||||
conn.ref("username").withSchema(TableName.Users),
|
||||
conn.ref("email").withSchema(TableName.Users),
|
||||
conn.ref("firstName").withSchema(TableName.Users),
|
||||
conn.ref("lastName").withSchema(TableName.Users),
|
||||
conn.ref("id").withSchema(TableName.Users).as("userId"),
|
||||
conn.ref("publicKey").withSchema(TableName.UserEncryptionKey)
|
||||
)
|
||||
.where({ isGhost: false })
|
||||
.whereIn("username", usernames);
|
||||
|
@ -4,9 +4,17 @@ import crypto from "crypto";
|
||||
import jwt from "jsonwebtoken";
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { OrgMembershipRole, OrgMembershipStatus, TableName } from "@app/db/schemas";
|
||||
import {
|
||||
OrgMembershipRole,
|
||||
OrgMembershipStatus,
|
||||
ProjectMembershipRole,
|
||||
ProjectVersion,
|
||||
TableName,
|
||||
TUsers
|
||||
} from "@app/db/schemas";
|
||||
import { TProjects } from "@app/db/schemas/projects";
|
||||
import { TGroupDALFactory } from "@app/ee/services/group/group-dal";
|
||||
import { TUserGroupMembershipDALFactory } from "@app/ee/services/group/user-group-membership-dal";
|
||||
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
|
||||
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission";
|
||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||
@ -24,10 +32,14 @@ import { TUserAliasDALFactory } from "@app/services/user-alias/user-alias-dal";
|
||||
|
||||
import { ActorAuthMethod, ActorType, AuthMethod, AuthTokenType } from "../auth/auth-type";
|
||||
import { TAuthTokenServiceFactory } from "../auth-token/auth-token-service";
|
||||
import { TokenType } from "../auth-token/auth-token-types";
|
||||
import { TokenMetadataType, TokenType, TTokenMetadata } from "../auth-token/auth-token-types";
|
||||
import { TProjectDALFactory } from "../project/project-dal";
|
||||
import { verifyProjectVersions } from "../project/project-fns";
|
||||
import { TProjectBotDALFactory } from "../project-bot/project-bot-dal";
|
||||
import { TProjectKeyDALFactory } from "../project-key/project-key-dal";
|
||||
import { TProjectMembershipDALFactory } from "../project-membership/project-membership-dal";
|
||||
import { addMembersToProject } from "../project-membership/project-membership-fns";
|
||||
import { TProjectUserMembershipRoleDALFactory } from "../project-membership/project-user-membership-role-dal";
|
||||
import { SmtpTemplates, TSmtpService } from "../smtp/smtp-service";
|
||||
import { TUserDALFactory } from "../user/user-dal";
|
||||
import { TIncidentContactsDALFactory } from "./incident-contacts-dal";
|
||||
@ -56,8 +68,11 @@ type TOrgServiceFactoryDep = {
|
||||
userDAL: TUserDALFactory;
|
||||
groupDAL: TGroupDALFactory;
|
||||
projectDAL: TProjectDALFactory;
|
||||
projectMembershipDAL: Pick<TProjectMembershipDALFactory, "findProjectMembershipsByUserId" | "delete">;
|
||||
projectKeyDAL: Pick<TProjectKeyDALFactory, "find" | "delete">;
|
||||
projectMembershipDAL: Pick<
|
||||
TProjectMembershipDALFactory,
|
||||
"findProjectMembershipsByUserId" | "delete" | "create" | "find" | "insertMany" | "transaction"
|
||||
>;
|
||||
projectKeyDAL: Pick<TProjectKeyDALFactory, "find" | "delete" | "insertMany" | "findLatestProjectKey">;
|
||||
orgMembershipDAL: Pick<TOrgMembershipDALFactory, "findOrgMembershipById" | "findOne">;
|
||||
incidentContactDAL: TIncidentContactsDALFactory;
|
||||
samlConfigDAL: Pick<TSamlConfigDALFactory, "findOne" | "findEnforceableSamlCfg">;
|
||||
@ -69,6 +84,9 @@ type TOrgServiceFactoryDep = {
|
||||
"getPlan" | "updateSubscriptionOrgMemberCount" | "generateOrgCustomerId" | "removeOrgCustomer"
|
||||
>;
|
||||
projectUserAdditionalPrivilegeDAL: Pick<TProjectUserAdditionalPrivilegeDALFactory, "delete">;
|
||||
userGroupMembershipDAL: Pick<TUserGroupMembershipDALFactory, "findUserGroupMembershipsInProject">;
|
||||
projectBotDAL: Pick<TProjectBotDALFactory, "findOne">;
|
||||
projectUserMembershipRoleDAL: Pick<TProjectUserMembershipRoleDALFactory, "insertMany">;
|
||||
};
|
||||
|
||||
export type TOrgServiceFactory = ReturnType<typeof orgServiceFactory>;
|
||||
@ -90,7 +108,10 @@ export const orgServiceFactory = ({
|
||||
tokenService,
|
||||
orgBotDAL,
|
||||
licenseService,
|
||||
samlConfigDAL
|
||||
samlConfigDAL,
|
||||
userGroupMembershipDAL,
|
||||
projectBotDAL,
|
||||
projectUserMembershipRoleDAL
|
||||
}: TOrgServiceFactoryDep) => {
|
||||
/*
|
||||
* Get organization details by the organization id
|
||||
@ -420,10 +441,15 @@ export const orgServiceFactory = ({
|
||||
const inviteUserToOrganization = async ({
|
||||
orgId,
|
||||
userId,
|
||||
inviteeEmail,
|
||||
inviteeEmails,
|
||||
organizationRoleSlug,
|
||||
projectRoleSlug,
|
||||
projectIds,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
}: TInviteUserToOrgDTO) => {
|
||||
const appCfg = getConfig();
|
||||
|
||||
const { permission } = await permissionService.getUserOrgPermission(userId, orgId, actorAuthMethod, actorOrgId);
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Member);
|
||||
|
||||
@ -450,98 +476,203 @@ export const orgServiceFactory = ({
|
||||
});
|
||||
}
|
||||
|
||||
const invitee = await orgDAL.transaction(async (tx) => {
|
||||
const inviteeUser = await userDAL.findUserByUsername(inviteeEmail, tx);
|
||||
if (inviteeUser) {
|
||||
// if user already exist means its already part of infisical
|
||||
// Thus the signup flow is not needed anymore
|
||||
const [inviteeMembership] = await orgDAL.findMembership(
|
||||
{
|
||||
[`${TableName.OrgMembership}.orgId` as "orgId"]: orgId,
|
||||
[`${TableName.OrgMembership}.userId` as "userId"]: inviteeUser.id
|
||||
},
|
||||
{ tx }
|
||||
);
|
||||
if (inviteeMembership && inviteeMembership.status === OrgMembershipStatus.Accepted) {
|
||||
throw new BadRequestError({
|
||||
message: "Failed to invite an existing member of org",
|
||||
name: "Invite user to org"
|
||||
});
|
||||
if (projectIds?.length) {
|
||||
const projects = await projectDAL.find({
|
||||
orgId,
|
||||
$in: {
|
||||
id: projectIds
|
||||
}
|
||||
});
|
||||
|
||||
if (!inviteeMembership) {
|
||||
await orgDAL.createMembership(
|
||||
{
|
||||
userId: inviteeUser.id,
|
||||
inviteEmail: inviteeEmail,
|
||||
orgId,
|
||||
role: OrgMembershipRole.Member,
|
||||
status: OrgMembershipStatus.Invited,
|
||||
isActive: true
|
||||
},
|
||||
tx
|
||||
);
|
||||
}
|
||||
return inviteeUser;
|
||||
}
|
||||
const isEmailInvalid = await isDisposableEmail(inviteeEmail);
|
||||
if (isEmailInvalid) {
|
||||
// if its not v3, throw an error
|
||||
if (!verifyProjectVersions(projects, ProjectVersion.V3)) {
|
||||
throw new BadRequestError({
|
||||
message: "Provided a disposable email",
|
||||
name: "Org invite"
|
||||
message: "One or more selected projects are not compatible with this operation. Please upgrade your projects."
|
||||
});
|
||||
}
|
||||
// not invited before
|
||||
const user = await userDAL.create(
|
||||
{
|
||||
username: inviteeEmail,
|
||||
email: inviteeEmail,
|
||||
isAccepted: false,
|
||||
authMethods: [AuthMethod.EMAIL],
|
||||
isGhost: false
|
||||
},
|
||||
tx
|
||||
);
|
||||
await orgDAL.createMembership(
|
||||
{
|
||||
inviteEmail: inviteeEmail,
|
||||
orgId,
|
||||
userId: user.id,
|
||||
role: OrgMembershipRole.Member,
|
||||
status: OrgMembershipStatus.Invited,
|
||||
isActive: true
|
||||
},
|
||||
tx
|
||||
);
|
||||
return user;
|
||||
});
|
||||
}
|
||||
|
||||
const token = await tokenService.createTokenForUser({
|
||||
type: TokenType.TOKEN_EMAIL_ORG_INVITATION,
|
||||
userId: invitee.id,
|
||||
orgId
|
||||
const inviteeUsers = await orgDAL.transaction(async (tx) => {
|
||||
const users: Pick<
|
||||
TUsers & { orgId: string },
|
||||
"id" | "firstName" | "lastName" | "email" | "orgId" | "username"
|
||||
>[] = [];
|
||||
for await (const inviteeEmail of inviteeEmails) {
|
||||
const inviteeUser = await userDAL.findUserByUsername(inviteeEmail, tx);
|
||||
|
||||
if (inviteeUser) {
|
||||
// if user already exist means its already part of infisical
|
||||
// Thus the signup flow is not needed anymore
|
||||
const [inviteeMembership] = await orgDAL.findMembership(
|
||||
{
|
||||
[`${TableName.OrgMembership}.orgId` as "orgId"]: orgId,
|
||||
[`${TableName.OrgMembership}.userId` as "userId"]: inviteeUser.id
|
||||
},
|
||||
{ tx }
|
||||
);
|
||||
if (inviteeMembership && inviteeMembership.status === OrgMembershipStatus.Accepted) {
|
||||
throw new BadRequestError({
|
||||
message: `Failed to invite members because ${inviteeEmail} is already part of the organization`,
|
||||
name: "Invite user to org"
|
||||
});
|
||||
}
|
||||
|
||||
if (!inviteeMembership) {
|
||||
await orgDAL.createMembership(
|
||||
{
|
||||
userId: inviteeUser.id,
|
||||
inviteEmail: inviteeEmail,
|
||||
orgId,
|
||||
role: OrgMembershipRole.Member,
|
||||
status: OrgMembershipStatus.Invited,
|
||||
isActive: true
|
||||
},
|
||||
tx
|
||||
);
|
||||
|
||||
if (projectIds?.length) {
|
||||
if (
|
||||
organizationRoleSlug === OrgMembershipRole.Custom ||
|
||||
projectRoleSlug === ProjectMembershipRole.Custom
|
||||
) {
|
||||
throw new BadRequestError({
|
||||
message: "Custom roles are not supported for inviting users to projects and organizations"
|
||||
});
|
||||
}
|
||||
|
||||
if (!projectRoleSlug) {
|
||||
throw new BadRequestError({
|
||||
message: "Selecting a project role is required to invite users to projects"
|
||||
});
|
||||
}
|
||||
|
||||
await projectMembershipDAL.insertMany(
|
||||
projectIds.map((id) => ({ projectId: id, userId: inviteeUser.id })),
|
||||
tx
|
||||
);
|
||||
for await (const projectId of projectIds) {
|
||||
await addMembersToProject({
|
||||
orgDAL,
|
||||
projectDAL,
|
||||
projectMembershipDAL,
|
||||
projectKeyDAL,
|
||||
userGroupMembershipDAL,
|
||||
projectBotDAL,
|
||||
projectUserMembershipRoleDAL,
|
||||
smtpService
|
||||
}).addMembersToNonE2EEProject(
|
||||
{
|
||||
emails: [inviteeEmail],
|
||||
usernames: [],
|
||||
projectId,
|
||||
projectMembershipRole: projectRoleSlug,
|
||||
sendEmails: false
|
||||
},
|
||||
{
|
||||
tx
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
return [{ ...inviteeUser, orgId }];
|
||||
}
|
||||
const isEmailInvalid = await isDisposableEmail(inviteeEmail);
|
||||
if (isEmailInvalid) {
|
||||
throw new BadRequestError({
|
||||
message: "Provided a disposable email",
|
||||
name: "Org invite"
|
||||
});
|
||||
}
|
||||
// not invited before
|
||||
const user = await userDAL.create(
|
||||
{
|
||||
username: inviteeEmail,
|
||||
email: inviteeEmail,
|
||||
isAccepted: false,
|
||||
authMethods: [AuthMethod.EMAIL],
|
||||
isGhost: false
|
||||
},
|
||||
tx
|
||||
);
|
||||
await orgDAL.createMembership(
|
||||
{
|
||||
inviteEmail: inviteeEmail,
|
||||
orgId,
|
||||
userId: user.id,
|
||||
role: organizationRoleSlug,
|
||||
status: OrgMembershipStatus.Invited,
|
||||
isActive: true
|
||||
},
|
||||
tx
|
||||
);
|
||||
|
||||
users.push({
|
||||
...user,
|
||||
orgId
|
||||
});
|
||||
}
|
||||
return users;
|
||||
});
|
||||
|
||||
const user = await userDAL.findById(userId);
|
||||
const appCfg = getConfig();
|
||||
await smtpService.sendMail({
|
||||
template: SmtpTemplates.OrgInvite,
|
||||
subjectLine: "Infisical organization invitation",
|
||||
recipients: [inviteeEmail],
|
||||
substitutions: {
|
||||
inviterFirstName: user.firstName,
|
||||
inviterUsername: user.username,
|
||||
organizationName: org?.name,
|
||||
email: inviteeEmail,
|
||||
organizationId: org?.id.toString(),
|
||||
token,
|
||||
callback_url: `${appCfg.SITE_URL}/signupinvite`
|
||||
}
|
||||
});
|
||||
|
||||
const signupTokens: { email: string; link: string }[] = [];
|
||||
if (inviteeUsers) {
|
||||
for await (const invitee of inviteeUsers) {
|
||||
const token = await tokenService.createTokenForUser({
|
||||
type: TokenType.TOKEN_EMAIL_ORG_INVITATION,
|
||||
userId: invitee.id,
|
||||
orgId
|
||||
});
|
||||
|
||||
let inviteMetadata: string = "";
|
||||
if (projectIds && projectIds?.length > 0) {
|
||||
inviteMetadata = jwt.sign(
|
||||
{
|
||||
type: TokenMetadataType.InviteToProjects,
|
||||
payload: {
|
||||
projectIds,
|
||||
projectRoleSlug: projectRoleSlug!, // Implicitly checked inside transaction if projectRoleSlug is undefined
|
||||
userId: invitee.id,
|
||||
orgId
|
||||
}
|
||||
} satisfies TTokenMetadata,
|
||||
appCfg.AUTH_SECRET,
|
||||
{
|
||||
expiresIn: appCfg.JWT_INVITE_LIFETIME
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
signupTokens.push({
|
||||
email: invitee.email || invitee.username,
|
||||
link: `${appCfg.SITE_URL}/signupinvite?token=${token}${
|
||||
inviteMetadata ? `&metadata=${inviteMetadata}` : ""
|
||||
}&to=${invitee.email || invitee.username}&organization_id=${org?.id}`
|
||||
});
|
||||
|
||||
await smtpService.sendMail({
|
||||
template: SmtpTemplates.OrgInvite,
|
||||
subjectLine: "Infisical organization invitation",
|
||||
recipients: [invitee.email || invitee.username],
|
||||
substitutions: {
|
||||
metadata: inviteMetadata,
|
||||
inviterFirstName: user.firstName,
|
||||
inviterUsername: user.username,
|
||||
organizationName: org?.name,
|
||||
email: invitee.email || invitee.username,
|
||||
organizationId: org?.id.toString(),
|
||||
token,
|
||||
callback_url: `${appCfg.SITE_URL}/signupinvite`
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
await licenseService.updateSubscriptionOrgMemberCount(orgId);
|
||||
|
||||
if (!appCfg.isSmtpConfigured) {
|
||||
return `${appCfg.SITE_URL}/signupinvite?token=${token}&to=${inviteeEmail}&organization_id=${org?.id}`;
|
||||
return signupTokens;
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -1,3 +1,4 @@
|
||||
import { OrgMembershipRole, ProjectMembershipRole } from "@app/db/schemas";
|
||||
import { TOrgPermission } from "@app/lib/types";
|
||||
|
||||
import { ActorAuthMethod, ActorType } from "../auth/auth-type";
|
||||
@ -29,7 +30,10 @@ export type TInviteUserToOrgDTO = {
|
||||
orgId: string;
|
||||
actorOrgId: string | undefined;
|
||||
actorAuthMethod: ActorAuthMethod;
|
||||
inviteeEmail: string;
|
||||
inviteeEmails: string[];
|
||||
organizationRoleSlug: OrgMembershipRole;
|
||||
projectIds?: string[];
|
||||
projectRoleSlug?: ProjectMembershipRole;
|
||||
};
|
||||
|
||||
export type TVerifyUserToOrgDTO = {
|
||||
|
@ -0,0 +1,190 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { ProjectMembershipRole, SecretKeyEncoding, TProjectMemberships } from "@app/db/schemas";
|
||||
import { TUserGroupMembershipDALFactory } from "@app/ee/services/group/user-group-membership-dal";
|
||||
import { getConfig } from "@app/lib/config/env";
|
||||
import { infisicalSymmetricDecrypt } from "@app/lib/crypto/encryption";
|
||||
import { BadRequestError } from "@app/lib/errors";
|
||||
import { groupBy } from "@app/lib/fn";
|
||||
|
||||
import { TOrgDALFactory } from "../org/org-dal";
|
||||
import { TProjectDALFactory } from "../project/project-dal";
|
||||
import { assignWorkspaceKeysToMembers } from "../project/project-fns";
|
||||
import { TProjectBotDALFactory } from "../project-bot/project-bot-dal";
|
||||
import { TProjectKeyDALFactory } from "../project-key/project-key-dal";
|
||||
import { SmtpTemplates, TSmtpService } from "../smtp/smtp-service";
|
||||
import { TProjectMembershipDALFactory } from "./project-membership-dal";
|
||||
import { TProjectUserMembershipRoleDALFactory } from "./project-user-membership-role-dal";
|
||||
|
||||
type TAddMembersToProjectArg = {
|
||||
orgDAL: Pick<TOrgDALFactory, "findMembership" | "findOrgMembersByUsername">;
|
||||
projectMembershipDAL: Pick<TProjectMembershipDALFactory, "find" | "transaction" | "insertMany">;
|
||||
projectDAL: Pick<TProjectDALFactory, "findProjectById" | "findProjectGhostUser">;
|
||||
projectKeyDAL: Pick<TProjectKeyDALFactory, "findLatestProjectKey" | "insertMany">;
|
||||
projectBotDAL: Pick<TProjectBotDALFactory, "findOne">;
|
||||
userGroupMembershipDAL: Pick<TUserGroupMembershipDALFactory, "findUserGroupMembershipsInProject">;
|
||||
projectUserMembershipRoleDAL: Pick<TProjectUserMembershipRoleDALFactory, "insertMany">;
|
||||
smtpService: Pick<TSmtpService, "sendMail">;
|
||||
};
|
||||
|
||||
type AddMembersToNonE2EEProjectDTO = {
|
||||
emails: string[];
|
||||
usernames: string[];
|
||||
projectId: string;
|
||||
projectMembershipRole: ProjectMembershipRole;
|
||||
sendEmails?: boolean;
|
||||
};
|
||||
|
||||
type AddMembersToNonE2EEProjectOptions = {
|
||||
tx?: Knex;
|
||||
throwOnProjectNotFound?: boolean;
|
||||
};
|
||||
|
||||
export const addMembersToProject = ({
|
||||
orgDAL,
|
||||
projectDAL,
|
||||
projectMembershipDAL,
|
||||
projectKeyDAL,
|
||||
projectBotDAL,
|
||||
userGroupMembershipDAL,
|
||||
projectUserMembershipRoleDAL,
|
||||
smtpService
|
||||
}: TAddMembersToProjectArg) => {
|
||||
// Can create multiple memberships for a singular project, based on user email / username
|
||||
const addMembersToNonE2EEProject = async (
|
||||
{ emails, usernames, projectId, projectMembershipRole, sendEmails }: AddMembersToNonE2EEProjectDTO,
|
||||
options: AddMembersToNonE2EEProjectOptions = { throwOnProjectNotFound: true }
|
||||
) => {
|
||||
const processTransaction = async (tx: Knex) => {
|
||||
const usernamesAndEmails = [...emails, ...usernames];
|
||||
|
||||
const project = await projectDAL.findProjectById(projectId);
|
||||
if (!project) {
|
||||
if (options.throwOnProjectNotFound) {
|
||||
throw new BadRequestError({ message: "Project not found when attempting to add user to project" });
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
const orgMembers = await orgDAL.findOrgMembersByUsername(
|
||||
project.orgId,
|
||||
[...new Set(usernamesAndEmails.map((element) => element.toLowerCase()))],
|
||||
tx
|
||||
);
|
||||
|
||||
if (orgMembers.length !== usernamesAndEmails.length)
|
||||
throw new BadRequestError({ message: "Some users are not part of org" });
|
||||
|
||||
if (!orgMembers.length) return [];
|
||||
|
||||
const existingMembers = await projectMembershipDAL.find({
|
||||
projectId,
|
||||
$in: { userId: orgMembers.map(({ user }) => user.id).filter(Boolean) }
|
||||
});
|
||||
if (existingMembers.length) throw new BadRequestError({ message: "Some users are already part of project" });
|
||||
|
||||
const ghostUser = await projectDAL.findProjectGhostUser(projectId);
|
||||
|
||||
if (!ghostUser) {
|
||||
throw new BadRequestError({
|
||||
message: "Failed to find sudo user"
|
||||
});
|
||||
}
|
||||
|
||||
const ghostUserLatestKey = await projectKeyDAL.findLatestProjectKey(ghostUser.id, projectId);
|
||||
|
||||
if (!ghostUserLatestKey) {
|
||||
throw new BadRequestError({
|
||||
message: "Failed to find sudo user latest key"
|
||||
});
|
||||
}
|
||||
|
||||
const bot = await projectBotDAL.findOne({ projectId });
|
||||
if (!bot) {
|
||||
throw new BadRequestError({
|
||||
message: "Failed to find bot"
|
||||
});
|
||||
}
|
||||
|
||||
const botPrivateKey = infisicalSymmetricDecrypt({
|
||||
keyEncoding: bot.keyEncoding as SecretKeyEncoding,
|
||||
iv: bot.iv,
|
||||
tag: bot.tag,
|
||||
ciphertext: bot.encryptedPrivateKey
|
||||
});
|
||||
|
||||
const newWsMembers = assignWorkspaceKeysToMembers({
|
||||
decryptKey: ghostUserLatestKey,
|
||||
userPrivateKey: botPrivateKey,
|
||||
members: orgMembers.map((membership) => ({
|
||||
orgMembershipId: membership.id,
|
||||
projectMembershipRole,
|
||||
userPublicKey: membership.user.publicKey
|
||||
}))
|
||||
});
|
||||
|
||||
const members: TProjectMemberships[] = [];
|
||||
|
||||
const userIdsToExcludeForProjectKeyAddition = new Set(
|
||||
await userGroupMembershipDAL.findUserGroupMembershipsInProject(usernamesAndEmails, projectId)
|
||||
);
|
||||
const projectMemberships = await projectMembershipDAL.insertMany(
|
||||
orgMembers.map(({ user }) => ({
|
||||
projectId,
|
||||
userId: user.id
|
||||
})),
|
||||
tx
|
||||
);
|
||||
await projectUserMembershipRoleDAL.insertMany(
|
||||
projectMemberships.map(({ id }) => ({ projectMembershipId: id, role: projectMembershipRole })),
|
||||
tx
|
||||
);
|
||||
|
||||
members.push(...projectMemberships);
|
||||
|
||||
const encKeyGroupByOrgMembId = groupBy(newWsMembers, (i) => i.orgMembershipId);
|
||||
await projectKeyDAL.insertMany(
|
||||
orgMembers
|
||||
.filter(({ user }) => !userIdsToExcludeForProjectKeyAddition.has(user.id))
|
||||
.map(({ user, id }) => ({
|
||||
encryptedKey: encKeyGroupByOrgMembId[id][0].workspaceEncryptedKey,
|
||||
nonce: encKeyGroupByOrgMembId[id][0].workspaceEncryptedNonce,
|
||||
senderId: ghostUser.id,
|
||||
receiverId: user.id,
|
||||
projectId
|
||||
})),
|
||||
tx
|
||||
);
|
||||
|
||||
if (sendEmails) {
|
||||
const recipients = orgMembers.filter((i) => i.user.email).map((i) => i.user.email as string);
|
||||
|
||||
const appCfg = getConfig();
|
||||
|
||||
if (recipients.length) {
|
||||
await smtpService.sendMail({
|
||||
template: SmtpTemplates.WorkspaceInvite,
|
||||
subjectLine: "Infisical project invitation",
|
||||
recipients: orgMembers.filter((i) => i.user.email).map((i) => i.user.email as string),
|
||||
substitutions: {
|
||||
workspaceName: project.name,
|
||||
callback_url: `${appCfg.SITE_URL}/login`
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return members;
|
||||
};
|
||||
|
||||
if (options.tx) {
|
||||
return processTransaction(options.tx);
|
||||
}
|
||||
return projectMembershipDAL.transaction(processTransaction);
|
||||
};
|
||||
|
||||
return {
|
||||
addMembersToNonE2EEProject
|
||||
};
|
||||
};
|
@ -2,19 +2,12 @@
|
||||
import { ForbiddenError } from "@casl/ability";
|
||||
import ms from "ms";
|
||||
|
||||
import {
|
||||
ProjectMembershipRole,
|
||||
ProjectVersion,
|
||||
SecretKeyEncoding,
|
||||
TableName,
|
||||
TProjectMemberships
|
||||
} from "@app/db/schemas";
|
||||
import { ProjectMembershipRole, ProjectVersion, TableName } from "@app/db/schemas";
|
||||
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
|
||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
|
||||
import { TProjectUserAdditionalPrivilegeDALFactory } from "@app/ee/services/project-user-additional-privilege/project-user-additional-privilege-dal";
|
||||
import { getConfig } from "@app/lib/config/env";
|
||||
import { infisicalSymmetricDecrypt } from "@app/lib/crypto/encryption";
|
||||
import { BadRequestError } from "@app/lib/errors";
|
||||
import { groupBy } from "@app/lib/fn";
|
||||
|
||||
@ -23,13 +16,13 @@ import { ActorType } from "../auth/auth-type";
|
||||
import { TGroupProjectDALFactory } from "../group-project/group-project-dal";
|
||||
import { TOrgDALFactory } from "../org/org-dal";
|
||||
import { TProjectDALFactory } from "../project/project-dal";
|
||||
import { assignWorkspaceKeysToMembers } from "../project/project-fns";
|
||||
import { TProjectBotDALFactory } from "../project-bot/project-bot-dal";
|
||||
import { TProjectKeyDALFactory } from "../project-key/project-key-dal";
|
||||
import { TProjectRoleDALFactory } from "../project-role/project-role-dal";
|
||||
import { SmtpTemplates, TSmtpService } from "../smtp/smtp-service";
|
||||
import { TUserDALFactory } from "../user/user-dal";
|
||||
import { TProjectMembershipDALFactory } from "./project-membership-dal";
|
||||
import { addMembersToProject } from "./project-membership-fns";
|
||||
import {
|
||||
ProjectUserMembershipTemporaryMode,
|
||||
TAddUsersToWorkspaceDTO,
|
||||
@ -53,7 +46,7 @@ type TProjectMembershipServiceFactoryDep = {
|
||||
userGroupMembershipDAL: TUserGroupMembershipDALFactory;
|
||||
projectRoleDAL: Pick<TProjectRoleDALFactory, "find">;
|
||||
orgDAL: Pick<TOrgDALFactory, "findMembership" | "findOrgMembersByUsername">;
|
||||
projectDAL: Pick<TProjectDALFactory, "findById" | "findProjectGhostUser" | "transaction">;
|
||||
projectDAL: Pick<TProjectDALFactory, "findById" | "findProjectGhostUser" | "transaction" | "findProjectById">;
|
||||
projectKeyDAL: Pick<TProjectKeyDALFactory, "findLatestProjectKey" | "delete" | "insertMany">;
|
||||
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
|
||||
projectUserAdditionalPrivilegeDAL: Pick<TProjectUserAdditionalPrivilegeDALFactory, "delete">;
|
||||
@ -247,116 +240,23 @@ export const projectMembershipServiceFactory = ({
|
||||
);
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Create, ProjectPermissionSub.Member);
|
||||
|
||||
const usernamesAndEmails = [...emails, ...usernames];
|
||||
|
||||
const orgMembers = await orgDAL.findOrgMembersByUsername(project.orgId, [
|
||||
...new Set(usernamesAndEmails.map((element) => element.toLowerCase()))
|
||||
]);
|
||||
|
||||
if (orgMembers.length !== usernamesAndEmails.length)
|
||||
throw new BadRequestError({ message: "Some users are not part of org" });
|
||||
|
||||
if (!orgMembers.length) return [];
|
||||
|
||||
const existingMembers = await projectMembershipDAL.find({
|
||||
const members = await addMembersToProject({
|
||||
orgDAL,
|
||||
projectDAL,
|
||||
projectMembershipDAL,
|
||||
projectKeyDAL,
|
||||
userGroupMembershipDAL,
|
||||
projectBotDAL,
|
||||
projectUserMembershipRoleDAL,
|
||||
smtpService
|
||||
}).addMembersToNonE2EEProject({
|
||||
emails,
|
||||
usernames,
|
||||
projectId,
|
||||
$in: { userId: orgMembers.map(({ user }) => user.id).filter(Boolean) }
|
||||
});
|
||||
if (existingMembers.length) throw new BadRequestError({ message: "Some users are already part of project" });
|
||||
|
||||
const ghostUser = await projectDAL.findProjectGhostUser(projectId);
|
||||
|
||||
if (!ghostUser) {
|
||||
throw new BadRequestError({
|
||||
message: "Failed to find sudo user"
|
||||
});
|
||||
}
|
||||
|
||||
const ghostUserLatestKey = await projectKeyDAL.findLatestProjectKey(ghostUser.id, projectId);
|
||||
|
||||
if (!ghostUserLatestKey) {
|
||||
throw new BadRequestError({
|
||||
message: "Failed to find sudo user latest key"
|
||||
});
|
||||
}
|
||||
|
||||
const bot = await projectBotDAL.findOne({ projectId });
|
||||
if (!bot) {
|
||||
throw new BadRequestError({
|
||||
message: "Failed to find bot"
|
||||
});
|
||||
}
|
||||
|
||||
const botPrivateKey = infisicalSymmetricDecrypt({
|
||||
keyEncoding: bot.keyEncoding as SecretKeyEncoding,
|
||||
iv: bot.iv,
|
||||
tag: bot.tag,
|
||||
ciphertext: bot.encryptedPrivateKey
|
||||
projectMembershipRole: ProjectMembershipRole.Member,
|
||||
sendEmails
|
||||
});
|
||||
|
||||
const newWsMembers = assignWorkspaceKeysToMembers({
|
||||
decryptKey: ghostUserLatestKey,
|
||||
userPrivateKey: botPrivateKey,
|
||||
members: orgMembers.map((membership) => ({
|
||||
orgMembershipId: membership.id,
|
||||
projectMembershipRole: ProjectMembershipRole.Member,
|
||||
userPublicKey: membership.user.publicKey
|
||||
}))
|
||||
});
|
||||
|
||||
const members: TProjectMemberships[] = [];
|
||||
|
||||
const userIdsToExcludeForProjectKeyAddition = new Set(
|
||||
await userGroupMembershipDAL.findUserGroupMembershipsInProject(usernamesAndEmails, projectId)
|
||||
);
|
||||
|
||||
await projectMembershipDAL.transaction(async (tx) => {
|
||||
const projectMemberships = await projectMembershipDAL.insertMany(
|
||||
orgMembers.map(({ user }) => ({
|
||||
projectId,
|
||||
userId: user.id
|
||||
})),
|
||||
tx
|
||||
);
|
||||
await projectUserMembershipRoleDAL.insertMany(
|
||||
projectMemberships.map(({ id }) => ({ projectMembershipId: id, role: ProjectMembershipRole.Member })),
|
||||
tx
|
||||
);
|
||||
|
||||
members.push(...projectMemberships);
|
||||
|
||||
const encKeyGroupByOrgMembId = groupBy(newWsMembers, (i) => i.orgMembershipId);
|
||||
await projectKeyDAL.insertMany(
|
||||
orgMembers
|
||||
.filter(({ user }) => !userIdsToExcludeForProjectKeyAddition.has(user.id))
|
||||
.map(({ user, id }) => ({
|
||||
encryptedKey: encKeyGroupByOrgMembId[id][0].workspaceEncryptedKey,
|
||||
nonce: encKeyGroupByOrgMembId[id][0].workspaceEncryptedNonce,
|
||||
senderId: ghostUser.id,
|
||||
receiverId: user.id,
|
||||
projectId
|
||||
})),
|
||||
tx
|
||||
);
|
||||
});
|
||||
|
||||
if (sendEmails) {
|
||||
const recipients = orgMembers.filter((i) => i.user.email).map((i) => i.user.email as string);
|
||||
|
||||
const appCfg = getConfig();
|
||||
|
||||
if (recipients.length) {
|
||||
await smtpService.sendMail({
|
||||
template: SmtpTemplates.WorkspaceInvite,
|
||||
subjectLine: "Infisical project invitation",
|
||||
recipients: orgMembers.filter((i) => i.user.email).map((i) => i.user.email as string),
|
||||
substitutions: {
|
||||
workspaceName: project.name,
|
||||
callback_url: `${appCfg.SITE_URL}/login`
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
return members;
|
||||
};
|
||||
|
||||
|
52
backend/src/services/project-role/project-role-fns.ts
Normal file
@ -0,0 +1,52 @@
|
||||
import { ProjectMembershipRole } from "@app/db/schemas";
|
||||
import {
|
||||
projectAdminPermissions,
|
||||
projectMemberPermissions,
|
||||
projectNoAccessPermissions,
|
||||
projectViewerPermission
|
||||
} from "@app/ee/services/permission/project-permission";
|
||||
|
||||
export const getPredefinedRoles = (projectId: string, roleFilter?: ProjectMembershipRole) => {
|
||||
return [
|
||||
{
|
||||
id: "b11b49a9-09a9-4443-916a-4246f9ff2c69", // dummy userid
|
||||
projectId,
|
||||
name: "Admin",
|
||||
slug: ProjectMembershipRole.Admin,
|
||||
permissions: projectAdminPermissions,
|
||||
description: "Full administrative access over a project",
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date()
|
||||
},
|
||||
{
|
||||
id: "b11b49a9-09a9-4443-916a-4246f9ff2c70", // dummy user for zod validation in response
|
||||
projectId,
|
||||
name: "Developer",
|
||||
slug: ProjectMembershipRole.Member,
|
||||
permissions: projectMemberPermissions,
|
||||
description: "Limited read/write role in a project",
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date()
|
||||
},
|
||||
{
|
||||
id: "b11b49a9-09a9-4443-916a-4246f9ff2c71", // dummy user for zod validation in response
|
||||
projectId,
|
||||
name: "Viewer",
|
||||
slug: ProjectMembershipRole.Viewer,
|
||||
permissions: projectViewerPermission,
|
||||
description: "Only read role in a project",
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date()
|
||||
},
|
||||
{
|
||||
id: "b11b49a9-09a9-4443-916a-4246f9ff2c72", // dummy user for zod validation in response
|
||||
projectId,
|
||||
name: "No Access",
|
||||
slug: ProjectMembershipRole.NoAccess,
|
||||
permissions: projectNoAccessPermissions,
|
||||
description: "No access to any resources in the project",
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date()
|
||||
}
|
||||
].filter(({ slug }) => !roleFilter || roleFilter.includes(slug));
|
||||
};
|
@ -5,13 +5,9 @@ import { ProjectMembershipRole } from "@app/db/schemas";
|
||||
import { UnpackedPermissionSchema } from "@app/ee/services/identity-project-additional-privilege/identity-project-additional-privilege-service";
|
||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||
import {
|
||||
projectAdminPermissions,
|
||||
projectMemberPermissions,
|
||||
projectNoAccessPermissions,
|
||||
ProjectPermissionActions,
|
||||
ProjectPermissionSet,
|
||||
ProjectPermissionSub,
|
||||
projectViewerPermission
|
||||
ProjectPermissionSub
|
||||
} from "@app/ee/services/permission/project-permission";
|
||||
import { BadRequestError } from "@app/lib/errors";
|
||||
|
||||
@ -20,6 +16,7 @@ import { TIdentityProjectMembershipRoleDALFactory } from "../identity-project/id
|
||||
import { TProjectDALFactory } from "../project/project-dal";
|
||||
import { TProjectUserMembershipRoleDALFactory } from "../project-membership/project-user-membership-role-dal";
|
||||
import { TProjectRoleDALFactory } from "./project-role-dal";
|
||||
import { getPredefinedRoles } from "./project-role-fns";
|
||||
import { TCreateRoleDTO, TDeleteRoleDTO, TGetRoleBySlugDTO, TListRolesDTO, TUpdateRoleDTO } from "./project-role-types";
|
||||
|
||||
type TProjectRoleServiceFactoryDep = {
|
||||
@ -37,51 +34,6 @@ const unpackPermissions = (permissions: unknown) =>
|
||||
unpackRules((permissions || []) as PackRule<RawRuleOf<MongoAbility<ProjectPermissionSet>>>[])
|
||||
);
|
||||
|
||||
const getPredefinedRoles = (projectId: string, roleFilter?: ProjectMembershipRole) => {
|
||||
return [
|
||||
{
|
||||
id: "b11b49a9-09a9-4443-916a-4246f9ff2c69", // dummy userid
|
||||
projectId,
|
||||
name: "Admin",
|
||||
slug: ProjectMembershipRole.Admin,
|
||||
permissions: projectAdminPermissions,
|
||||
description: "Full administrative access over a project",
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date()
|
||||
},
|
||||
{
|
||||
id: "b11b49a9-09a9-4443-916a-4246f9ff2c70", // dummy user for zod validation in response
|
||||
projectId,
|
||||
name: "Developer",
|
||||
slug: ProjectMembershipRole.Member,
|
||||
permissions: projectMemberPermissions,
|
||||
description: "Limited read/write role in a project",
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date()
|
||||
},
|
||||
{
|
||||
id: "b11b49a9-09a9-4443-916a-4246f9ff2c71", // dummy user for zod validation in response
|
||||
projectId,
|
||||
name: "Viewer",
|
||||
slug: ProjectMembershipRole.Viewer,
|
||||
permissions: projectViewerPermission,
|
||||
description: "Only read role in a project",
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date()
|
||||
},
|
||||
{
|
||||
id: "b11b49a9-09a9-4443-916a-4246f9ff2c72", // dummy user for zod validation in response
|
||||
projectId,
|
||||
name: "No Access",
|
||||
slug: ProjectMembershipRole.NoAccess,
|
||||
permissions: projectNoAccessPermissions,
|
||||
description: "No access to any resources in the project",
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date()
|
||||
}
|
||||
].filter(({ slug }) => !roleFilter || roleFilter.includes(slug));
|
||||
};
|
||||
|
||||
export const projectRoleServiceFactory = ({
|
||||
projectRoleDAL,
|
||||
permissionService,
|
||||
|
@ -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
|
||||
};
|
||||
};
|
||||
|
@ -1,5 +1,6 @@
|
||||
import crypto from "crypto";
|
||||
|
||||
import { ProjectVersion, TProjects } from "@app/db/schemas";
|
||||
import { decryptAsymmetric, encryptAsymmetric } from "@app/lib/crypto";
|
||||
import { BadRequestError } from "@app/lib/errors";
|
||||
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
||||
@ -53,6 +54,16 @@ export const createProjectKey = ({ publicKey, privateKey, plainProjectKey }: TCr
|
||||
return { key: encryptedProjectKey, iv: encryptedProjectKeyIv };
|
||||
};
|
||||
|
||||
export const verifyProjectVersions = (projects: Pick<TProjects, "version">[], version: ProjectVersion) => {
|
||||
for (const project of projects) {
|
||||
if (project.version !== version) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
export const getProjectKmsCertificateKeyId = async ({
|
||||
projectId,
|
||||
projectDAL,
|
||||
|
@ -10,6 +10,7 @@ import { TKeyStoreFactory } from "@app/keystore/keystore";
|
||||
import { isAtLeastAsPrivileged } from "@app/lib/casl";
|
||||
import { infisicalSymmetricEncypt } from "@app/lib/crypto/encryption";
|
||||
import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
|
||||
import { groupBy } from "@app/lib/fn";
|
||||
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
||||
import { TProjectPermission } from "@app/lib/types";
|
||||
|
||||
@ -30,6 +31,8 @@ import { TProjectEnvDALFactory } from "../project-env/project-env-dal";
|
||||
import { TProjectKeyDALFactory } from "../project-key/project-key-dal";
|
||||
import { TProjectMembershipDALFactory } from "../project-membership/project-membership-dal";
|
||||
import { TProjectUserMembershipRoleDALFactory } from "../project-membership/project-user-membership-role-dal";
|
||||
import { TProjectRoleDALFactory } from "../project-role/project-role-dal";
|
||||
import { getPredefinedRoles } from "../project-role/project-role-fns";
|
||||
import { ROOT_FOLDER_NAME, TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal";
|
||||
import { TUserDALFactory } from "../user/user-dal";
|
||||
import { TProjectDALFactory } from "./project-dal";
|
||||
@ -44,6 +47,7 @@ import {
|
||||
TListProjectCasDTO,
|
||||
TListProjectCertificateTemplatesDTO,
|
||||
TListProjectCertsDTO,
|
||||
TListProjectsDTO,
|
||||
TLoadProjectKmsBackupDTO,
|
||||
TToggleProjectAutoCapitalizationDTO,
|
||||
TUpdateAuditLogsRetentionDTO,
|
||||
@ -84,6 +88,7 @@ type TProjectServiceFactoryDep = {
|
||||
orgDAL: Pick<TOrgDALFactory, "findOne">;
|
||||
keyStore: Pick<TKeyStoreFactory, "deleteItem">;
|
||||
projectBotDAL: Pick<TProjectBotDALFactory, "create">;
|
||||
projectRoleDAL: Pick<TProjectRoleDALFactory, "find">;
|
||||
kmsService: Pick<
|
||||
TKmsServiceFactory,
|
||||
| "updateProjectSecretManagerKmsKey"
|
||||
@ -112,6 +117,7 @@ export const projectServiceFactory = ({
|
||||
projectEnvDAL,
|
||||
licenseService,
|
||||
projectUserMembershipRoleDAL,
|
||||
projectRoleDAL,
|
||||
identityProjectMembershipRoleDAL,
|
||||
certificateAuthorityDAL,
|
||||
certificateDAL,
|
||||
@ -389,8 +395,34 @@ export const projectServiceFactory = ({
|
||||
return deletedProject;
|
||||
};
|
||||
|
||||
const getProjects = async (actorId: string) => {
|
||||
const getProjects = async ({ actorId, includeRoles, actorAuthMethod, actorOrgId }: TListProjectsDTO) => {
|
||||
const workspaces = await projectDAL.findAllProjects(actorId);
|
||||
|
||||
if (includeRoles) {
|
||||
const { permission } = await permissionService.getUserOrgPermission(actorId, actorOrgId, actorAuthMethod);
|
||||
|
||||
// `includeRoles` is specifically used by organization admins when inviting new users to the organizations to avoid looping redundant api calls.
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Member);
|
||||
const customRoles = await projectRoleDAL.find({
|
||||
$in: {
|
||||
projectId: workspaces.map((workspace) => workspace.id)
|
||||
}
|
||||
});
|
||||
|
||||
const workspaceMappedToRoles = groupBy(customRoles, (role) => role.projectId);
|
||||
|
||||
const workspacesWithRoles = await Promise.all(
|
||||
workspaces.map(async (workspace) => {
|
||||
return {
|
||||
...workspace,
|
||||
roles: [...(workspaceMappedToRoles[workspace.id] || []), ...getPredefinedRoles(workspace.id)]
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
return workspacesWithRoles;
|
||||
}
|
||||
|
||||
return workspaces;
|
||||
};
|
||||
|
||||
|
@ -75,6 +75,10 @@ export type TDeleteProjectDTO = {
|
||||
actorOrgId: string | undefined;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
export type TListProjectsDTO = {
|
||||
includeRoles: boolean;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
export type TUpgradeProjectDTO = {
|
||||
userPrivateKey: string;
|
||||
} & TProjectPermission;
|
||||
|
@ -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",
|
||||
|
@ -9,7 +9,7 @@
|
||||
<body>
|
||||
<h2>Join your organization on Infisical</h2>
|
||||
<p>{{inviterFirstName}} ({{inviterUsername}}) has invited you to their Infisical organization — {{organizationName}}</p>
|
||||
<a href="{{callback_url}}?token={{token}}&to={{email}}&organization_id={{organizationId}}">Join now</a>
|
||||
<a href="{{callback_url}}?token={{token}}{{#if metadata}}&metadata={{metadata}}{{/if}}&to={{email}}&organization_id={{organizationId}}">Join now</a>
|
||||
<h3>What is Infisical?</h3>
|
||||
<p>Infisical is an easy-to-use end-to-end encrypted tool that enables developers to sync and manage their secrets and configs.</p>
|
||||
</body>
|
||||
|
@ -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>
|
@ -100,7 +100,9 @@ export type TIntegrationCreatedEvent = {
|
||||
export type TUserOrgInvitedEvent = {
|
||||
event: PostHogEventTypes.UserOrgInvitation;
|
||||
properties: {
|
||||
inviteeEmail: string;
|
||||
inviteeEmails: string[];
|
||||
projectIds?: string[];
|
||||
organizationRoleSlug?: string;
|
||||
};
|
||||
};
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -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 {
|
||||
|
@ -7,6 +7,7 @@ services:
|
||||
restart: always
|
||||
ports:
|
||||
- 8080:80
|
||||
- 8443:443
|
||||
volumes:
|
||||
- ./nginx/default.dev.conf:/etc/nginx/conf.d/default.conf:ro
|
||||
depends_on:
|
||||
|
57
docs/documentation/platform/pki/est.mdx
Normal file
@ -0,0 +1,57 @@
|
||||
---
|
||||
title: "Enrollment over Secure Transport (EST)"
|
||||
sidebarTitle: "Enrollment over Secure Transport (EST)"
|
||||
description: "Learn how to manage certificate enrollment of clients using EST"
|
||||
---
|
||||
|
||||
## Concept
|
||||
|
||||
Enrollment over Secure Transport (EST) is a protocol used to automate the secure provisioning of digital certificates for devices and applications over a secure HTTPS connection. It is primarily used when a client device needs to obtain or renew a certificate from a Certificate Authority (CA) on Infisical in a secure and standardized manner. EST is commonly employed in environments requiring strong authentication and encrypted communication, such as in IoT, enterprise networks, and secure web services.
|
||||
|
||||
Infisical's EST service is based on [RFC 7030](https://datatracker.ietf.org/doc/html/rfc7030) and implements the following endpoints:
|
||||
|
||||
- **cacerts** - provides the necessary CA chain for the client to validate certificates issued by the CA.
|
||||
- **simpleenroll** - allows an EST client to request a new certificate from Infisical's EST server
|
||||
- **simplereenroll** - similar to the /simpleenroll endpoint but is used for renewing an existing certificate.
|
||||
|
||||
These endpoints are exposed on port 8443 under the .well-known/est path e.g.
|
||||
`https://app.infisical.com:8443/.well-known/est/estLabel/cacerts`
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- You need to have an existing [CA hierarchy](/documentation/platform/pki/private-ca).
|
||||
- The client devices need to have a bootstrap/pre-installed certificate.
|
||||
- The client devices must trust the server certificates used by Infisical's EST server. If the devices are new or lack existing trust configurations, you need to manually establish trust for the appropriate certificates. When using Infisical Cloud, this means establishing trust for certificates issued by AWS.
|
||||
|
||||
## Guide to configuring EST
|
||||
|
||||
1. Set up a certificate template with your selected issuing CA. This template will define the policies and parameters for certificates issued through EST. For detailed instructions on configuring a certificate template, refer to the certificate templates [documentation](/documentation/platform/pki/certificate-templates).
|
||||
|
||||
2. Proceed to the certificate template's enrollment settings
|
||||

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

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

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

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

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

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

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

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

|
||||
</Step>
|
||||
|
||||
</Steps>
|
||||
Now you have successfully integrated Infisical with Azure DevOps. Your existing and future secret changes will automatically sync to Azure DevOps.
|
||||
You can view your secrets by navigating to your Azure DevOps project and selecting the 'Library' tab under 'Pipelines' in the 'Library' section.
|
@ -109,6 +109,7 @@
|
||||
"documentation/platform/pki/private-ca",
|
||||
"documentation/platform/pki/certificates",
|
||||
"documentation/platform/pki/certificate-templates",
|
||||
"documentation/platform/pki/est",
|
||||
"documentation/platform/pki/alerting"
|
||||
]
|
||||
},
|
||||
@ -324,6 +325,7 @@
|
||||
},
|
||||
"integrations/cloud/vercel",
|
||||
"integrations/cloud/azure-key-vault",
|
||||
"integrations/cloud/azure-devops",
|
||||
"integrations/cloud/gcp-secret-manager",
|
||||
{
|
||||
"group": "Cloudflare",
|
||||
|
@ -1,10 +1,11 @@
|
||||
---
|
||||
title: "Infisical Python SDK"
|
||||
sidebarTitle: "Python"
|
||||
url: "https://github.com/Infisical/python-sdk-official?tab=readme-ov-file#infisical-python-sdk"
|
||||
icon: "python"
|
||||
---
|
||||
|
||||
If you're working with Python, the official [infisical-python](https://github.com/Infisical/sdk/edit/main/crates/infisical-py) package is the easiest way to fetch and work with secrets for your application.
|
||||
{/* If you're working with Python, the official [infisical-python](https://github.com/Infisical/sdk/edit/main/crates/infisical-py) package is the easiest way to fetch and work with secrets for your application.
|
||||
|
||||
- [PyPi Package](https://pypi.org/project/infisical-python/)
|
||||
- [Github Repository](https://github.com/Infisical/sdk/edit/main/crates/infisical-py)
|
||||
@ -529,4 +530,4 @@ decryptedString = client.decryptSymmetric(decryptOptions)
|
||||
|
||||
#### Returns (string)
|
||||
|
||||
`plaintext` (string): The decrypted plaintext.
|
||||
`plaintext` (string): The decrypted plaintext. */}
|
||||
|
@ -13,7 +13,7 @@ From local development to production, Infisical SDKs provide the easiest way for
|
||||
<Card title="Node" href="/sdks/languages/node" icon="node" color="#68a063">
|
||||
Manage secrets for your Node application on demand
|
||||
</Card>
|
||||
<Card href="/sdks/languages/python" title="Python" icon="python" color="#4c8abe">
|
||||
<Card href="https://github.com/Infisical/python-sdk-official" title="Python" icon="python" color="#4c8abe">
|
||||
Manage secrets for your Python application on demand
|
||||
</Card>
|
||||
<Card href="/sdks/languages/java" title="Java" icon="java" color="#e41f23">
|
||||
|
@ -2,7 +2,7 @@ const path = require("path");
|
||||
|
||||
const ContentSecurityPolicy = `
|
||||
default-src 'self';
|
||||
script-src 'self' https://*.posthog.com https://*.*.posthog.com https://js.stripe.com https://api.stripe.com https://widget.intercom.io https://js.intercomcdn.com https://hcaptcha.com https://*.hcaptcha.com 'unsafe-inline' 'unsafe-eval';
|
||||
script-src 'self' https://*.posthog.com https://js.stripe.com https://api.stripe.com https://widget.intercom.io https://js.intercomcdn.com https://hcaptcha.com https://*.hcaptcha.com 'unsafe-inline' 'unsafe-eval';
|
||||
style-src 'self' https://rsms.me 'unsafe-inline' https://hcaptcha.com https://*.hcaptcha.com;
|
||||
child-src https://api.stripe.com;
|
||||
frame-src https://js.stripe.com/ https://api.stripe.com https://www.youtube.com/ https://hcaptcha.com https://*.hcaptcha.com;
|
||||
|
@ -33,7 +33,8 @@ const integrationSlugNameMapping: Mapping = {
|
||||
windmill: "Windmill",
|
||||
"gcp-secret-manager": "GCP Secret Manager",
|
||||
"hasura-cloud": "Hasura Cloud",
|
||||
rundeck: "Rundeck"
|
||||
rundeck: "Rundeck",
|
||||
"azure-devops": "Azure DevOps"
|
||||
};
|
||||
|
||||
const envMapping: Mapping = {
|
||||
|
@ -1,116 +0,0 @@
|
||||
import { Fragment } from "react";
|
||||
import { Dialog, Transition } from "@headlessui/react";
|
||||
|
||||
import Button from "../buttons/Button";
|
||||
import InputField from "../InputField";
|
||||
|
||||
type Props = {
|
||||
isOpen: boolean;
|
||||
closeModal: () => void;
|
||||
submitModal: (email: string) => void;
|
||||
email: string;
|
||||
setEmail: (email: string) => void;
|
||||
orgName: string;
|
||||
};
|
||||
|
||||
const AddUserDialog = ({ isOpen, closeModal, submitModal, email, setEmail, orgName }: Props) => {
|
||||
const submit = () => {
|
||||
submitModal(email);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="z-50">
|
||||
<Transition appear show={isOpen} as={Fragment}>
|
||||
<Dialog as="div" className="relative" onClose={closeModal}>
|
||||
<Transition.Child
|
||||
as={Fragment}
|
||||
enter="ease-out duration-300"
|
||||
enterFrom="opacity-0"
|
||||
enterTo="opacity-100"
|
||||
leave="ease-in duration-200"
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
>
|
||||
<div className="fixed inset-0 bg-black bg-opacity-70" />
|
||||
</Transition.Child>
|
||||
|
||||
<div className="fixed inset-0 overflow-y-auto">
|
||||
<div className="flex min-h-full items-center justify-center p-4 text-center">
|
||||
<Transition.Child
|
||||
as={Fragment}
|
||||
enter="ease-out duration-300"
|
||||
enterFrom="opacity-0 scale-95"
|
||||
enterTo="opacity-100 scale-100"
|
||||
leave="ease-in duration-200"
|
||||
leaveFrom="opacity-100 scale-100"
|
||||
leaveTo="opacity-0 scale-95"
|
||||
>
|
||||
<Dialog.Panel className="w-full max-w-lg transform overflow-hidden rounded-md border border-gray-700 bg-bunker-800 p-6 text-left align-middle shadow-xl transition-all">
|
||||
<Dialog.Title
|
||||
as="h3"
|
||||
className="z-50 text-lg font-medium leading-6 text-gray-400"
|
||||
>
|
||||
Invite others to {orgName}
|
||||
</Dialog.Title>
|
||||
<div className="mt-2 mb-4">
|
||||
<p className="text-sm text-gray-500">
|
||||
An invite is specific to an email address and expires after 1 day. For
|
||||
security reasons, you will need to separately add members to projects.
|
||||
</p>
|
||||
</div>
|
||||
<div className="max-h-28">
|
||||
<InputField
|
||||
label="Email"
|
||||
onChangeHandler={setEmail}
|
||||
type="varName"
|
||||
value={email}
|
||||
placeholder=""
|
||||
isRequired
|
||||
/>
|
||||
</div>
|
||||
<div className="mt-4 max-w-max">
|
||||
<Button onButtonPressed={submit} color="mineshaft" text="Invite" size="md" />
|
||||
</div>
|
||||
</Dialog.Panel>
|
||||
{/* <Dialog.Panel className="w-full max-w-md transform overflow-hidden rounded-md bg-bunker-800 border border-gray-700 p-6 text-left align-middle shadow-xl transition-all">
|
||||
<Dialog.Title
|
||||
as="h3"
|
||||
className="text-xl font-medium leading-6 text-gray-300 z-50"
|
||||
>
|
||||
Unleash Infisical's Full Power
|
||||
</Dialog.Title>
|
||||
<div className="mt-4 mb-4">
|
||||
<p className="text-sm text-gray-400 mb-2">
|
||||
You have exceeded the number of members in a free organization.
|
||||
</p>
|
||||
<p className="text-sm text-gray-400">
|
||||
Upgrade now and get access to adding more members, as well as to other powerful enhancements.
|
||||
</p>
|
||||
</div>
|
||||
<div className="mt-6">
|
||||
<button
|
||||
type="button"
|
||||
className="inline-flex justify-center rounded-md border border-transparent bg-primary px-4 py-2 text-sm font-medium text-black hover:opacity-80 hover:text-semibold duration-200 focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2"
|
||||
onClick={() => router.push("/settings/billing/" + router.query.id)}
|
||||
>
|
||||
Upgrade Now
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="ml-2 inline-flex justify-center rounded-md border border-transparent bg-gray-800 px-4 py-2 text-sm font-medium text-gray-400 hover:bg-gray-500 hover:text-black hover:text-semibold duration-200 focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2"
|
||||
onClick={closeModal}
|
||||
>
|
||||
Maybe Later
|
||||
</button>
|
||||
</div>
|
||||
</Dialog.Panel> */}
|
||||
</Transition.Child>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
</Transition>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AddUserDialog;
|
@ -2,7 +2,7 @@ import React, { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
import { useAddUserToOrg } from "@app/hooks/api";
|
||||
import { useAddUsersToOrg } from "@app/hooks/api";
|
||||
import { useFetchServerStatus } from "@app/hooks/api/serverDetails";
|
||||
import { usePopUp } from "@app/hooks/usePopUp";
|
||||
|
||||
@ -17,7 +17,7 @@ export default function TeamInviteStep(): JSX.Element {
|
||||
const [emails, setEmails] = useState("");
|
||||
const { data: serverDetails } = useFetchServerStatus();
|
||||
|
||||
const { mutateAsync } = useAddUserToOrg();
|
||||
const { mutateAsync } = useAddUsersToOrg();
|
||||
const { handlePopUpToggle, popUp, handlePopUpOpen } = usePopUp(["setUpEmail"] as const);
|
||||
|
||||
// Redirect user to the getting started page
|
||||
@ -31,8 +31,9 @@ export default function TeamInviteStep(): JSX.Element {
|
||||
.map((email) => email.trim())
|
||||
.map(async (email) => {
|
||||
mutateAsync({
|
||||
inviteeEmail: email,
|
||||
organizationId: String(localStorage.getItem("orgData.id"))
|
||||
inviteeEmails: [email],
|
||||
organizationId: String(localStorage.getItem("orgData.id")),
|
||||
organizationRoleSlug: "member"
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -72,7 +72,12 @@ export const eventToNameMap: { [K in EventType]: string } = {
|
||||
[EventType.CREATE_CERTIFICATE_TEMPLATE]: "Create certificate template",
|
||||
[EventType.UPDATE_CERTIFICATE_TEMPLATE]: "Update certificate template",
|
||||
[EventType.DELETE_CERTIFICATE_TEMPLATE]: "Delete certificate template",
|
||||
[EventType.GET_CERTIFICATE_TEMPLATE]: "Get certificate template"
|
||||
[EventType.GET_CERTIFICATE_TEMPLATE]: "Get certificate template",
|
||||
[EventType.GET_CERTIFICATE_TEMPLATE_EST_CONFIG]: "Get certificate template EST configuration",
|
||||
[EventType.CREATE_CERTIFICATE_TEMPLATE_EST_CONFIG]:
|
||||
"Create certificate template EST configuration",
|
||||
[EventType.UPDATE_CERTIFICATE_TEMPLATE_EST_CONFIG]:
|
||||
"Update certificate template EST configuration"
|
||||
};
|
||||
|
||||
export const userAgentTTypeoNameMap: { [K in UserAgentType]: string } = {
|
||||
|
@ -86,5 +86,8 @@ export enum EventType {
|
||||
CREATE_CERTIFICATE_TEMPLATE = "create-certificate-template",
|
||||
UPDATE_CERTIFICATE_TEMPLATE = "update-certificate-template",
|
||||
DELETE_CERTIFICATE_TEMPLATE = "delete-certificate-template",
|
||||
GET_CERTIFICATE_TEMPLATE = "get-certificate-template"
|
||||
GET_CERTIFICATE_TEMPLATE = "get-certificate-template",
|
||||
CREATE_CERTIFICATE_TEMPLATE_EST_CONFIG = "create-certificate-template-est-config",
|
||||
UPDATE_CERTIFICATE_TEMPLATE_EST_CONFIG = "update-certificate-template-est-config",
|
||||
GET_CERTIFICATE_TEMPLATE_EST_CONFIG = "get-certificate-template-est-config"
|
||||
}
|
||||
|
@ -719,6 +719,29 @@ interface DeleteCertificateTemplate {
|
||||
};
|
||||
}
|
||||
|
||||
interface CreateCertificateTemplateEstConfig {
|
||||
type: EventType.CREATE_CERTIFICATE_TEMPLATE_EST_CONFIG;
|
||||
metadata: {
|
||||
certificateTemplateId: string;
|
||||
isEnabled: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
interface UpdateCertificateTemplateEstConfig {
|
||||
type: EventType.UPDATE_CERTIFICATE_TEMPLATE_EST_CONFIG;
|
||||
metadata: {
|
||||
certificateTemplateId: string;
|
||||
isEnabled: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
interface GetCertificateTemplateEstConfig {
|
||||
type: EventType.GET_CERTIFICATE_TEMPLATE_EST_CONFIG;
|
||||
metadata: {
|
||||
certificateTemplateId: string;
|
||||
};
|
||||
}
|
||||
|
||||
export type Event =
|
||||
| GetSecretsEvent
|
||||
| GetSecretEvent
|
||||
@ -791,7 +814,10 @@ export type Event =
|
||||
| CreateCertificateTemplate
|
||||
| UpdateCertificateTemplate
|
||||
| GetCertificateTemplate
|
||||
| DeleteCertificateTemplate;
|
||||
| DeleteCertificateTemplate
|
||||
| UpdateCertificateTemplateEstConfig
|
||||
| CreateCertificateTemplateEstConfig
|
||||
| GetCertificateTemplateEstConfig;
|
||||
|
||||
export type AuditLog = {
|
||||
id: string;
|
||||
|
@ -93,6 +93,7 @@ export type CompleteAccountDTO = {
|
||||
salt: string;
|
||||
verifier: string;
|
||||
password: string;
|
||||
tokenMetadata?: string;
|
||||
};
|
||||
|
||||
export type CompleteAccountSignupDTO = CompleteAccountDTO & {
|
||||
|
@ -8,4 +8,4 @@ export {
|
||||
useSignIntermediate,
|
||||
useUpdateCa
|
||||
} from "./mutations";
|
||||
export { useGetCaById, useGetCaCert, useGetCaCerts, useGetCaCrls,useGetCaCsr } from "./queries";
|
||||
export { useGetCaById, useGetCaCert, useGetCaCerts, useGetCaCrls, useGetCaCsr } from "./queries";
|
||||
|
@ -10,7 +10,8 @@ export const caKeys = {
|
||||
getCaCrls: (caId: string) => [{ caId }, "ca-crls"],
|
||||
getCaCert: (caId: string) => [{ caId }, "ca-cert"],
|
||||
getCaCsr: (caId: string) => [{ caId }, "ca-csr"],
|
||||
getCaCrl: (caId: string) => [{ caId }, "ca-crl"]
|
||||
getCaCrl: (caId: string) => [{ caId }, "ca-crl"],
|
||||
getCaEstConfig: (caId: string) => [{ caId }, "ca-est-config"]
|
||||
};
|
||||
|
||||
export const useGetCaById = (caId: string) => {
|
||||
|
@ -1,2 +1,8 @@
|
||||
export { useCreateCertTemplate, useDeleteCertTemplate, useUpdateCertTemplate } from "./mutations";
|
||||
export { useGetCertTemplate } from "./queries";
|
||||
export {
|
||||
useCreateCertTemplate,
|
||||
useCreateEstConfig,
|
||||
useDeleteCertTemplate,
|
||||
useUpdateCertTemplate,
|
||||
useUpdateEstConfig
|
||||
} from "./mutations";
|
||||
export { useGetCertTemplate, useGetEstConfig } from "./queries";
|
||||
|
@ -7,8 +7,10 @@ import { certTemplateKeys } from "./queries";
|
||||
import {
|
||||
TCertificateTemplate,
|
||||
TCreateCertificateTemplateDTO,
|
||||
TCreateEstConfigDTO,
|
||||
TDeleteCertificateTemplateDTO,
|
||||
TUpdateCertificateTemplateDTO
|
||||
TUpdateCertificateTemplateDTO,
|
||||
TUpdateEstConfigDTO
|
||||
} from "./types";
|
||||
|
||||
export const useCreateCertTemplate = () => {
|
||||
@ -57,3 +59,35 @@ export const useDeleteCertTemplate = () => {
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const useCreateEstConfig = () => {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation<{}, {}, TCreateEstConfigDTO>({
|
||||
mutationFn: async (body) => {
|
||||
const { data } = await apiRequest.post(
|
||||
`/api/v1/pki/certificate-templates/${body.certificateTemplateId}/est-config`,
|
||||
body
|
||||
);
|
||||
return data;
|
||||
},
|
||||
onSuccess: (_, { certificateTemplateId }) => {
|
||||
queryClient.invalidateQueries(certTemplateKeys.getEstConfig(certificateTemplateId));
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const useUpdateEstConfig = () => {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation<{}, {}, TUpdateEstConfigDTO>({
|
||||
mutationFn: async (body) => {
|
||||
const { data } = await apiRequest.patch(
|
||||
`/api/v1/pki/certificate-templates/${body.certificateTemplateId}/est-config`,
|
||||
body
|
||||
);
|
||||
return data;
|
||||
},
|
||||
onSuccess: (_, { certificateTemplateId }) => {
|
||||
queryClient.invalidateQueries(certTemplateKeys.getEstConfig(certificateTemplateId));
|
||||
}
|
||||
});
|
||||
};
|
||||
|
@ -2,10 +2,11 @@ import { useQuery } from "@tanstack/react-query";
|
||||
|
||||
import { apiRequest } from "@app/config/request";
|
||||
|
||||
import { TCertificateTemplate } from "./types";
|
||||
import { TCertificateTemplate, TEstConfig } from "./types";
|
||||
|
||||
export const certTemplateKeys = {
|
||||
getCertTemplateById: (id: string) => [{ id }, "cert-template"]
|
||||
getCertTemplateById: (id: string) => [{ id }, "cert-template"],
|
||||
getEstConfig: (id: string) => [{ id }, "cert-template-est-config"]
|
||||
};
|
||||
|
||||
export const useGetCertTemplate = (id: string) => {
|
||||
@ -20,3 +21,17 @@ export const useGetCertTemplate = (id: string) => {
|
||||
enabled: Boolean(id)
|
||||
});
|
||||
};
|
||||
|
||||
export const useGetEstConfig = (certificateTemplateId: string) => {
|
||||
return useQuery({
|
||||
queryKey: certTemplateKeys.getEstConfig(certificateTemplateId),
|
||||
queryFn: async () => {
|
||||
const { data: estConfig } = await apiRequest.get<TEstConfig>(
|
||||
`/api/v1/pki/certificate-templates/${certificateTemplateId}/est-config`
|
||||
);
|
||||
|
||||
return estConfig;
|
||||
},
|
||||
enabled: Boolean(certificateTemplateId)
|
||||
});
|
||||
};
|
||||
|
@ -35,3 +35,24 @@ export type TDeleteCertificateTemplateDTO = {
|
||||
id: string;
|
||||
projectId: string;
|
||||
};
|
||||
|
||||
export type TCreateEstConfigDTO = {
|
||||
certificateTemplateId: string;
|
||||
caChain: string;
|
||||
passphrase: string;
|
||||
isEnabled: boolean;
|
||||
};
|
||||
|
||||
export type TUpdateEstConfigDTO = {
|
||||
certificateTemplateId: string;
|
||||
caChain?: string;
|
||||
passphrase?: string;
|
||||
isEnabled?: boolean;
|
||||
};
|
||||
|
||||
export type TEstConfig = {
|
||||
id: string;
|
||||
certificateTemplateId: string;
|
||||
caChain: string;
|
||||
isEnabled: false;
|
||||
};
|
||||
|
@ -120,16 +120,22 @@ const fetchIntegrationAuthById = async (integrationAuthId: string) => {
|
||||
const fetchIntegrationAuthApps = async ({
|
||||
integrationAuthId,
|
||||
teamId,
|
||||
azureDevOpsOrgName,
|
||||
workspaceSlug
|
||||
}: {
|
||||
integrationAuthId: string;
|
||||
teamId?: string;
|
||||
azureDevOpsOrgName?: string;
|
||||
workspaceSlug?: string;
|
||||
}) => {
|
||||
const params: Record<string, string> = {};
|
||||
if (teamId) {
|
||||
params.teamId = teamId;
|
||||
}
|
||||
if (azureDevOpsOrgName) {
|
||||
params.azureDevOpsOrgName = azureDevOpsOrgName;
|
||||
}
|
||||
|
||||
if (workspaceSlug) {
|
||||
params.workspaceSlug = workspaceSlug;
|
||||
}
|
||||
@ -452,10 +458,12 @@ export const useGetIntegrationAuthById = (integrationAuthId: string) => {
|
||||
export const useGetIntegrationAuthApps = ({
|
||||
integrationAuthId,
|
||||
teamId,
|
||||
azureDevOpsOrgName,
|
||||
workspaceSlug
|
||||
}: {
|
||||
integrationAuthId: string;
|
||||
teamId?: string;
|
||||
azureDevOpsOrgName?: string;
|
||||
workspaceSlug?: string;
|
||||
}) => {
|
||||
return useQuery({
|
||||
@ -464,6 +472,7 @@ export const useGetIntegrationAuthApps = ({
|
||||
fetchIntegrationAuthApps({
|
||||
integrationAuthId,
|
||||
teamId,
|
||||
azureDevOpsOrgName,
|
||||
workspaceSlug
|
||||
}),
|
||||
enabled: true
|
||||
|
@ -47,7 +47,7 @@ export const roleQueryKeys = {
|
||||
["user-project-permissions", { workspaceId }] as const
|
||||
};
|
||||
|
||||
const getProjectRoles = async (projectId: string) => {
|
||||
export const getProjectRoles = async (projectId: string) => {
|
||||
const { data } = await apiRequest.get<{ roles: Array<Omit<TProjectRole, "permissions">> }>(
|
||||
`/api/v1/workspace/${projectId}/roles`
|
||||
);
|
||||
|
@ -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,
|
||||
|
@ -6,7 +6,7 @@ export {
|
||||
} from "./mutation";
|
||||
export {
|
||||
fetchOrgUsers,
|
||||
useAddUserToOrg,
|
||||
useAddUsersToOrg,
|
||||
useCreateAPIKey,
|
||||
useDeleteAPIKey,
|
||||
useDeleteMe,
|
||||
@ -26,4 +26,5 @@ export {
|
||||
useRevokeMySessions,
|
||||
useUpdateMfaEnabled,
|
||||
useUpdateOrgMembership,
|
||||
useUpdateUserAuthMethods} from "./queries";
|
||||
useUpdateUserAuthMethods
|
||||
} from "./queries";
|
||||
|
@ -157,12 +157,15 @@ export const useGetOrgUsers = (orgId: string) =>
|
||||
|
||||
// mutation
|
||||
// TODO(akhilmhdh): move all mutation to mutation file
|
||||
export const useAddUserToOrg = () => {
|
||||
export const useAddUsersToOrg = () => {
|
||||
const queryClient = useQueryClient();
|
||||
type Response = {
|
||||
data: {
|
||||
message: string;
|
||||
completeInviteLink: string | undefined;
|
||||
completeInviteLinks?: {
|
||||
email: string;
|
||||
link: string;
|
||||
}[];
|
||||
};
|
||||
};
|
||||
|
||||
|
@ -149,7 +149,10 @@ export type DeletOrgMembershipDTO = {
|
||||
};
|
||||
|
||||
export type AddUserToOrgDTO = {
|
||||
inviteeEmail: string;
|
||||
inviteeEmails: string[];
|
||||
projectIds?: string[];
|
||||
projectRoleSlug?: string;
|
||||
organizationRoleSlug: string;
|
||||
organizationId: string;
|
||||
};
|
||||
|
||||
|
@ -138,8 +138,12 @@ export const useGetUpgradeProjectStatus = ({
|
||||
});
|
||||
};
|
||||
|
||||
const fetchUserWorkspaces = async () => {
|
||||
const { data } = await apiRequest.get<{ workspaces: Workspace[] }>("/api/v1/workspace");
|
||||
const fetchUserWorkspaces = async (includeRoles?: boolean) => {
|
||||
const { data } = await apiRequest.get<{ workspaces: Workspace[] }>("/api/v1/workspace", {
|
||||
params: {
|
||||
includeRoles
|
||||
}
|
||||
});
|
||||
return data.workspaces;
|
||||
};
|
||||
|
||||
@ -171,8 +175,8 @@ export const useGetWorkspaceById = (
|
||||
});
|
||||
};
|
||||
|
||||
export const useGetUserWorkspaces = () =>
|
||||
useQuery(workspaceKeys.getAllUserWorkspace, fetchUserWorkspaces);
|
||||
export const useGetUserWorkspaces = (includeRoles?: boolean) =>
|
||||
useQuery(workspaceKeys.getAllUserWorkspace, () => fetchUserWorkspaces(includeRoles));
|
||||
|
||||
const fetchUserWorkspaceMemberships = async (orgId: string) => {
|
||||
const { data } = await apiRequest.get<Record<string, Workspace[]>>(
|
||||
|
@ -1,3 +1,5 @@
|
||||
import { TProjectRole } from "../roles/types";
|
||||
|
||||
export enum ProjectVersion {
|
||||
V1 = 1,
|
||||
V2 = 2,
|
||||
@ -22,6 +24,8 @@ export type Workspace = {
|
||||
auditLogsRetentionDays: number;
|
||||
slug: string;
|
||||
createdAt: string;
|
||||
|
||||
roles?: TProjectRole[];
|
||||
};
|
||||
|
||||
export type WorkspaceEnv = {
|
||||
|