Compare commits

..

97 Commits

Author SHA1 Message Date
79680b6a73 Merge pull request #2340 from Infisical/misc/added-timeout-for-hijacked-est-connection
misc: added timeout for est connection
2024-08-27 23:31:07 +08:00
58838c541f misc: added timeout for est connection 2024-08-27 23:26:56 +08:00
03cc71cfed Merge pull request #2284 from Infisical/feature/est-simpleenroll
Certificate EST protocol (simpleenroll, simplereenroll, cacerts)
2024-08-27 13:42:58 +08:00
02529106c9 Merge pull request #2336 from akhilmhdh/fix/scim-error
fix: resolved scim group update failing
2024-08-26 16:34:27 -04:00
=
d939ff289d fix: resolved scim group update failing 2024-08-27 01:50:26 +05:30
d1816c3051 Merge pull request #2334 from Infisical/daniel/azure-devops-docs
Docs: Azure DevOps Integration
2024-08-26 23:49:23 +04:00
cb350788c0 Update create.tsx 2024-08-26 23:21:56 +04:00
cd58768d6f Updated images 2024-08-26 23:20:51 +04:00
dcd6f4d55d Fix: Updated Azure DevOps integration styling 2024-08-26 23:12:00 +04:00
3c828614b8 Fix: Azure DevOps Label naming typos 2024-08-26 22:44:11 +04:00
09e7988596 Docs: Azure DevOps Integration 2024-08-26 22:43:49 +04:00
f40df19334 misc: finalized est config schema 2024-08-27 02:15:01 +08:00
76c9d3488b Merge remote-tracking branch 'origin/main' into feature/est-simpleenroll 2024-08-27 02:13:59 +08:00
0809da33e0 misc: improved docs and added support for curl clients 2024-08-27 02:05:35 +08:00
b528eec4bb Merge pull request #2333 from Infisical/daniel/secret-change-emails
Feat: Email notification on secret change requests
2024-08-26 21:50:30 +04:00
5179103680 Update SecretApprovalRequest.tsx 2024-08-26 21:46:05 +04:00
25a9e5f58a Update SecretApprovalRequest.tsx 2024-08-26 21:42:47 +04:00
8ddfe7b6e9 Update secret-approval-request-fns.ts 2024-08-26 20:53:43 +04:00
c23f21d57a Update SecretApprovalRequest.tsx 2024-08-26 20:21:05 +04:00
1242a43d98 Feat: Open approval with ID in URL 2024-08-26 20:04:06 +04:00
1655ca27d1 Fix: Creation of secret approval policies 2024-08-26 20:02:58 +04:00
2bcead03b0 Feat: Send secret change request emails to approvers 2024-08-26 19:55:04 +04:00
41ab1972ce Feat: Find project and include org dal 2024-08-26 19:54:48 +04:00
b00fff6922 Update index.ts 2024-08-26 19:54:15 +04:00
97b01ca5f8 Feat: Send secret change request emails to approvers 2024-08-26 19:54:01 +04:00
c2bd6f5ef3 Feat: Send secret change request emails to approvers 2024-08-26 19:53:49 +04:00
18efc9a6de Include more user details 2024-08-26 19:53:17 +04:00
436ccb25fb Merge pull request #2331 from Infisical/daniel/presist-selfhosting-domains
Feat: Persistent self-hosting domains on `infisical login`
2024-08-26 18:04:25 +04:00
8f08a352dd Merge pull request #2310 from Infisical/daniel/azure-devops-integration
Feat: Azure DevOps Integration
2024-08-26 18:04:04 +04:00
00f86cfd00 misc: addressed review comments 2024-08-26 21:10:29 +08:00
3944aafb11 Use slices 2024-08-26 15:18:45 +04:00
a6b852fab9 Fix: Type errors / cleanup 2024-08-26 15:13:18 +04:00
2a043afe11 Cleanup 2024-08-26 15:13:18 +04:00
df8f2cf9ab Update integration-sync-secret.ts 2024-08-26 15:13:18 +04:00
a18015b1e5 Fix: Use unique parameter for passing devops org name
Used to be teamId, now it's azureDevopsOrgName.
2024-08-26 15:13:18 +04:00
8b80622d2f Cleanup 2024-08-26 15:13:18 +04:00
c0fd0a56f3 Update integration-list.ts 2024-08-26 15:13:18 +04:00
326764dd41 Feat: Azure DevOps Integration 2024-08-26 15:13:18 +04:00
1f24d02c5e Fix: Do not save duplicate domains 2024-08-26 15:08:51 +04:00
c130fbddd9 Merge pull request #2321 from Infisical/daniel/specify-roles-and-projects
Feat: Select roles & projects when inviting members to organization
2024-08-26 15:00:39 +04:00
f560534493 Replace custom pkcs7 fns with module 2024-08-25 20:21:53 -07:00
10a97f4522 update python docs to point to new repo 2024-08-25 18:02:51 -04:00
7a2f0214f3 Feat: Persist self-hosting domains on infisical login 2024-08-24 13:18:03 +04:00
a2b994ab23 Requested changes 2024-08-24 11:00:05 +04:00
c4715124dc Merge pull request #2327 from Infisical/fix/resolve-name-null-null
fix: this pr addresses null null name issue with invited users
2024-08-23 14:01:27 -04:00
67c1cb9bf1 fix: this pr addresses null null name issue with invited users 2024-08-23 15:40:06 +08:00
28ecc37163 Update org-service.ts 2024-08-23 03:10:14 +04:00
a6a2e2bae0 Update AddOrgMemberModal.tsx 2024-08-23 02:25:15 +04:00
d8bbfacae0 UI improvements 2024-08-23 02:25:15 +04:00
58549c398f Update project-service.ts 2024-08-23 02:25:15 +04:00
842ed62bec Rename 2024-08-23 02:25:15 +04:00
06d8800ee0 Feat: Specify organization role and projects when inviting users to org 2024-08-23 02:25:15 +04:00
2ecfd1bb7e Update auth-signup-type.ts 2024-08-23 02:25:15 +04:00
783d4c7bd6 Update org-dal.ts 2024-08-23 02:25:15 +04:00
fbf3f26abd Refactored org invites to allow for multiple users and to handle project invites 2024-08-23 02:25:15 +04:00
1d09693041 Update org-types.ts 2024-08-23 02:25:15 +04:00
626e37e3d0 Moved project membership creation to project membership fns 2024-08-23 02:25:15 +04:00
07fd67b328 Add metadata to SMTP email 2024-08-23 02:25:15 +04:00
3f1f018adc Update telemetry-types.ts 2024-08-23 02:25:15 +04:00
fe04e6d20c Remove *.*.posthog.com 2024-08-23 02:25:15 +04:00
d7171a1617 Removed unused code 2024-08-23 02:25:15 +04:00
384a0daa31 Update types.ts 2024-08-23 02:25:15 +04:00
c5c949e034 Multi user org invites 2024-08-23 02:25:15 +04:00
c2c9edf156 Update types.ts 2024-08-23 02:25:15 +04:00
c8248ef4e9 Fix: Skip org selection when user only has one org 2024-08-23 02:25:15 +04:00
9f6a6a7b7c Automatic timed toggle 2024-08-23 02:25:15 +04:00
121b642d50 Added new metadata parameter for signup 2024-08-23 02:25:15 +04:00
59b16f647e Update AddOrgMemberModal.tsx 2024-08-23 02:25:15 +04:00
2ab5932693 Update OrgMembersSection.tsx 2024-08-23 02:25:15 +04:00
8dfcef3900 Seperate component for Org Invite Links 2024-08-23 02:25:15 +04:00
8ca70eec44 Refactor add users to org handlers 2024-08-23 02:25:14 +04:00
60df59c7f0 Multi-user organization invites structure 2024-08-23 02:25:14 +04:00
e231c531a6 Update index.ts 2024-08-23 02:25:14 +04:00
d48bb910fa JWT invite lifetime (1 day) 2024-08-23 02:25:14 +04:00
1317266415 Merge remote-tracking branch 'origin' into feature/est-simpleenroll 2024-08-22 14:54:34 -07:00
288d7e88ae misc: made SSL header key configurable via env 2024-08-23 01:38:12 +08:00
f88389bf9e misc: added general format 2024-08-22 21:05:34 +08:00
2e88c5e2c5 misc: improved url examples in est doc 2024-08-22 21:02:38 +08:00
73f3b8173e doc: added guide for EST usage' 2024-08-22 20:44:21 +08:00
aa5b88ff04 misc: removed enrollment options from CA page 2024-08-22 15:40:36 +08:00
b7caff88cf feat: finished up EST cacerts 2024-08-22 15:39:53 +08:00
760a1e917a feat: added simplereenroll 2024-08-20 23:56:27 +08:00
2d7ff66246 Merge branch 'feature/est-simpleenroll' of https://github.com/Infisical/infisical into feature/est-simpleenroll 2024-08-20 15:31:58 +08:00
179497e830 misc: moved est logic to service 2024-08-20 15:31:10 +08:00
4c08c80e5b Merge remote-tracking branch 'origin' into feature/est-simpleenroll 2024-08-19 14:53:04 -07:00
7d6af64904 misc: added proxy header for amazon mtls client cert 2024-08-20 01:53:47 +08:00
16519f9486 feat: added reading SANs from CSR 2024-08-20 01:39:40 +08:00
bb27d38a12 misc: ui form adjustments 2024-08-19 21:39:00 +08:00
5b26928751 misc: added audit logs 2024-08-19 20:25:07 +08:00
f425e7e48f misc: addressed alignment issue 2024-08-19 19:50:09 +08:00
4601f46afb misc: finalized variable naming 2024-08-19 19:33:46 +08:00
692bdc060c misc: updated est configuration to be binded to certificate template 2024-08-19 19:26:20 +08:00
3a4f8c2e54 Merge branch 'feature/certificate-template' into feature/est-simpleenroll 2024-08-19 17:04:22 +08:00
146c4284a2 feat: integrated to est routes 2024-08-14 20:52:21 +08:00
5ae33b9f3b misc: minor UI updates 2024-08-14 01:10:25 +08:00
1f38b92ec6 feat: finished up integration for est config management 2024-08-14 01:00:31 +08:00
f2a49a79f0 feat: initial simpleenroll setup (mvp) 2024-08-13 23:22:47 +08:00
122 changed files with 3718 additions and 870 deletions

View File

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

View File

@ -74,6 +74,7 @@
"pg-query-stream": "^4.5.3", "pg-query-stream": "^4.5.3",
"picomatch": "^3.0.1", "picomatch": "^3.0.1",
"pino": "^8.16.2", "pino": "^8.16.2",
"pkijs": "^3.2.4",
"posthog-node": "^3.6.2", "posthog-node": "^3.6.2",
"probot": "^13.0.0", "probot": "^13.0.0",
"safe-regex": "^2.1.1", "safe-regex": "^2.1.1",
@ -4457,6 +4458,17 @@
"dev": true, "dev": true,
"optional": 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": { "node_modules/@node-saml/node-saml": {
"version": "4.0.5", "version": "4.0.5",
"resolved": "https://registry.npmjs.org/@node-saml/node-saml/-/node-saml-4.0.5.tgz", "resolved": "https://registry.npmjs.org/@node-saml/node-saml/-/node-saml-4.0.5.tgz",
@ -8481,6 +8493,14 @@
"node": ">= 0.8" "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": { "node_modules/cac": {
"version": "6.7.14", "version": "6.7.14",
"resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz",
@ -14120,6 +14140,22 @@
"pathe": "^1.1.0" "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": { "node_modules/plimit-lit": {
"version": "1.6.1", "version": "1.6.1",
"resolved": "https://registry.npmjs.org/plimit-lit/-/plimit-lit-1.6.1.tgz", "resolved": "https://registry.npmjs.org/plimit-lit/-/plimit-lit-1.6.1.tgz",
@ -16268,9 +16304,9 @@
} }
}, },
"node_modules/tslib": { "node_modules/tslib": {
"version": "2.6.2", "version": "2.6.3",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.3.tgz",
"integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" "integrity": "sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ=="
}, },
"node_modules/tsup": { "node_modules/tsup": {
"version": "8.0.1", "version": "8.0.1",

View File

@ -171,6 +171,7 @@
"pg-query-stream": "^4.5.3", "pg-query-stream": "^4.5.3",
"picomatch": "^3.0.1", "picomatch": "^3.0.1",
"pino": "^8.16.2", "pino": "^8.16.2",
"pkijs": "^3.2.4",
"posthog-node": "^3.6.2", "posthog-node": "^3.6.2",
"probot": "^13.0.0", "probot": "^13.0.0",
"safe-regex": "^2.1.1", "safe-regex": "^2.1.1",

View File

@ -36,6 +36,7 @@ import { ActorAuthMethod, ActorType } from "@app/services/auth/auth-type";
import { TAuthTokenServiceFactory } from "@app/services/auth-token/auth-token-service"; import { TAuthTokenServiceFactory } from "@app/services/auth-token/auth-token-service";
import { TCertificateServiceFactory } from "@app/services/certificate/certificate-service"; import { TCertificateServiceFactory } from "@app/services/certificate/certificate-service";
import { TCertificateAuthorityServiceFactory } from "@app/services/certificate-authority/certificate-authority-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 { TCertificateTemplateServiceFactory } from "@app/services/certificate-template/certificate-template-service";
import { TGroupProjectServiceFactory } from "@app/services/group-project/group-project-service"; import { TGroupProjectServiceFactory } from "@app/services/group-project/group-project-service";
import { TIdentityServiceFactory } from "@app/services/identity/identity-service"; import { TIdentityServiceFactory } from "@app/services/identity/identity-service";
@ -160,6 +161,7 @@ declare module "fastify" {
certificateTemplate: TCertificateTemplateServiceFactory; certificateTemplate: TCertificateTemplateServiceFactory;
certificateAuthority: TCertificateAuthorityServiceFactory; certificateAuthority: TCertificateAuthorityServiceFactory;
certificateAuthorityCrl: TCertificateAuthorityCrlServiceFactory; certificateAuthorityCrl: TCertificateAuthorityCrlServiceFactory;
certificateEst: TCertificateEstServiceFactory;
pkiCollection: TPkiCollectionServiceFactory; pkiCollection: TPkiCollectionServiceFactory;
secretScanning: TSecretScanningServiceFactory; secretScanning: TSecretScanningServiceFactory;
license: TLicenseServiceFactory; license: TLicenseServiceFactory;

View File

@ -53,6 +53,9 @@ import {
TCertificateSecretsUpdate, TCertificateSecretsUpdate,
TCertificatesInsert, TCertificatesInsert,
TCertificatesUpdate, TCertificatesUpdate,
TCertificateTemplateEstConfigs,
TCertificateTemplateEstConfigsInsert,
TCertificateTemplateEstConfigsUpdate,
TCertificateTemplates, TCertificateTemplates,
TCertificateTemplatesInsert, TCertificateTemplatesInsert,
TCertificateTemplatesUpdate, TCertificateTemplatesUpdate,
@ -372,6 +375,11 @@ declare module "knex/types/tables" {
TCertificateTemplatesInsert, TCertificateTemplatesInsert,
TCertificateTemplatesUpdate TCertificateTemplatesUpdate
>; >;
[TableName.CertificateTemplateEstConfig]: KnexOriginal.CompositeTableType<
TCertificateTemplateEstConfigs,
TCertificateTemplateEstConfigsInsert,
TCertificateTemplateEstConfigsUpdate
>;
[TableName.CertificateBody]: KnexOriginal.CompositeTableType< [TableName.CertificateBody]: KnexOriginal.CompositeTableType<
TCertificateBodies, TCertificateBodies,
TCertificateBodiesInsert, TCertificateBodiesInsert,

View File

@ -0,0 +1,26 @@
import { Knex } from "knex";
import { TableName } from "../schemas";
import { createOnUpdateTrigger, dropOnUpdateTrigger } from "../utils";
export async function up(knex: Knex): Promise<void> {
const hasEstConfigTable = await knex.schema.hasTable(TableName.CertificateTemplateEstConfig);
if (!hasEstConfigTable) {
await knex.schema.createTable(TableName.CertificateTemplateEstConfig, (tb) => {
tb.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
tb.uuid("certificateTemplateId").notNullable().unique();
tb.foreign("certificateTemplateId").references("id").inTable(TableName.CertificateTemplate).onDelete("CASCADE");
tb.binary("encryptedCaChain").notNullable();
tb.string("hashedPassphrase").notNullable();
tb.boolean("isEnabled").notNullable();
tb.timestamps(true, true, true);
});
await createOnUpdateTrigger(knex, TableName.CertificateTemplateEstConfig);
}
}
export async function down(knex: Knex): Promise<void> {
await knex.schema.dropTableIfExists(TableName.CertificateTemplateEstConfig);
await dropOnUpdateTrigger(knex, TableName.CertificateTemplateEstConfig);
}

View File

@ -0,0 +1,29 @@
// Code generated by automation script, DO NOT EDIT.
// Automated by pulling database and generating zod schema
// To update. Just run npm run generate:schema
// Written by akhilmhdh.
import { z } from "zod";
import { zodBuffer } from "@app/lib/zod";
import { TImmutableDBKeys } from "./models";
export const CertificateTemplateEstConfigsSchema = z.object({
id: z.string().uuid(),
certificateTemplateId: z.string().uuid(),
encryptedCaChain: zodBuffer,
hashedPassphrase: z.string(),
isEnabled: z.boolean(),
createdAt: z.date(),
updatedAt: z.date()
});
export type TCertificateTemplateEstConfigs = z.infer<typeof CertificateTemplateEstConfigsSchema>;
export type TCertificateTemplateEstConfigsInsert = Omit<
z.input<typeof CertificateTemplateEstConfigsSchema>,
TImmutableDBKeys
>;
export type TCertificateTemplateEstConfigsUpdate = Partial<
Omit<z.input<typeof CertificateTemplateEstConfigsSchema>, TImmutableDBKeys>
>;

View File

@ -14,6 +14,7 @@ export * from "./certificate-authority-crl";
export * from "./certificate-authority-secret"; export * from "./certificate-authority-secret";
export * from "./certificate-bodies"; export * from "./certificate-bodies";
export * from "./certificate-secrets"; export * from "./certificate-secrets";
export * from "./certificate-template-est-configs";
export * from "./certificate-templates"; export * from "./certificate-templates";
export * from "./certificates"; export * from "./certificates";
export * from "./dynamic-secret-leases"; export * from "./dynamic-secret-leases";

View File

@ -3,6 +3,7 @@ import { z } from "zod";
export enum TableName { export enum TableName {
Users = "users", Users = "users",
CertificateAuthority = "certificate_authorities", CertificateAuthority = "certificate_authorities",
CertificateTemplateEstConfig = "certificate_template_est_configs",
CertificateAuthorityCert = "certificate_authority_certs", CertificateAuthorityCert = "certificate_authority_certs",
CertificateAuthoritySecret = "certificate_authority_secret", CertificateAuthoritySecret = "certificate_authority_secret",
CertificateAuthorityCrl = "certificate_authority_crl", CertificateAuthorityCrl = "certificate_authority_crl",

View File

@ -9,7 +9,10 @@ export const registerScimRouter = async (server: FastifyZodProvider) => {
server.addContentTypeParser("application/scim+json", { parseAs: "string" }, (_, body, done) => { server.addContentTypeParser("application/scim+json", { parseAs: "string" }, (_, body, done) => {
try { try {
const strBody = body instanceof Buffer ? body.toString() : body; const strBody = body instanceof Buffer ? body.toString() : body;
if (!strBody) {
done(null, undefined);
return;
}
const json: unknown = JSON.parse(strBody); const json: unknown = JSON.parse(strBody);
done(null, json); done(null, json);
} catch (err) { } catch (err) {
@ -474,18 +477,18 @@ export const registerScimRouter = async (server: FastifyZodProvider) => {
Operations: z.array( Operations: z.array(
z.union([ z.union([
z.object({ z.object({
op: z.literal("replace"), op: z.union([z.literal("replace"), z.literal("Replace")]),
value: z.object({ value: z.object({
id: z.string().trim(), id: z.string().trim(),
displayName: z.string().trim() displayName: z.string().trim()
}) })
}), }),
z.object({ z.object({
op: z.literal("remove"), op: z.union([z.literal("remove"), z.literal("Remove")]),
path: z.string().trim() path: z.string().trim()
}), }),
z.object({ z.object({
op: z.literal("add"), op: z.union([z.literal("add"), z.literal("Add")]),
path: z.string().trim(), path: z.string().trim(),
value: z.array( value: z.array(
z.object({ z.object({

View File

@ -166,7 +166,10 @@ export enum EventType {
CREATE_CERTIFICATE_TEMPLATE = "create-certificate-template", CREATE_CERTIFICATE_TEMPLATE = "create-certificate-template",
UPDATE_CERTIFICATE_TEMPLATE = "update-certificate-template", UPDATE_CERTIFICATE_TEMPLATE = "update-certificate-template",
DELETE_CERTIFICATE_TEMPLATE = "delete-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 { interface UserActorMetadata {
@ -1420,6 +1423,29 @@ interface OrgAdminAccessProjectEvent {
}; // no metadata yet }; // 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 = export type Event =
| GetSecretsEvent | GetSecretsEvent
| GetSecretEvent | GetSecretEvent
@ -1547,4 +1573,7 @@ export type Event =
| CreateCertificateTemplate | CreateCertificateTemplate
| UpdateCertificateTemplate | UpdateCertificateTemplate
| GetCertificateTemplate | GetCertificateTemplate
| DeleteCertificateTemplate; | DeleteCertificateTemplate
| CreateCertificateTemplateEstConfig
| UpdateCertificateTemplateEstConfig
| GetCertificateTemplateEstConfig;

View File

@ -98,6 +98,7 @@ export const dynamicSecretServiceFactory = ({
if (!isConnected) throw new BadRequestError({ message: "Provider connection failed" }); if (!isConnected) throw new BadRequestError({ message: "Provider connection failed" });
const encryptedInput = infisicalSymmetricEncypt(JSON.stringify(inputs)); const encryptedInput = infisicalSymmetricEncypt(JSON.stringify(inputs));
const dynamicSecretCfg = await dynamicSecretDAL.create({ const dynamicSecretCfg = await dynamicSecretDAL.create({
type: provider.type, type: provider.type,
version: 1, version: 1,

View File

@ -50,8 +50,8 @@ export const buildScimUser = ({
orgMembershipId: string; orgMembershipId: string;
username: string; username: string;
email?: string | null; email?: string | null;
firstName: string; firstName: string | null | undefined;
lastName: string; lastName: string | null | undefined;
groups?: { groups?: {
value: string; value: string;
display: string; display: string;
@ -64,9 +64,9 @@ export const buildScimUser = ({
userName: username, userName: username,
displayName: `${firstName} ${lastName}`, displayName: `${firstName} ${lastName}`,
name: { name: {
givenName: firstName, givenName: firstName || "",
middleName: null, middleName: null,
familyName: lastName familyName: lastName || ""
}, },
emails: email emails: email
? [ ? [

View File

@ -267,8 +267,8 @@ export const scimServiceFactory = ({
orgMembershipId: membership.id, orgMembershipId: membership.id,
username: membership.externalId ?? membership.username, username: membership.externalId ?? membership.username,
email: membership.email ?? "", email: membership.email ?? "",
firstName: membership.firstName as string, firstName: membership.firstName,
lastName: membership.lastName as string, lastName: membership.lastName,
active: membership.isActive, active: membership.isActive,
groups: groupMembershipsInOrg.map((group) => ({ groups: groupMembershipsInOrg.map((group) => ({
value: group.groupId, value: group.groupId,
@ -427,8 +427,8 @@ export const scimServiceFactory = ({
return buildScimUser({ return buildScimUser({
orgMembershipId: createdOrgMembership.id, orgMembershipId: createdOrgMembership.id,
username: externalId, username: externalId,
firstName: createdUser.firstName as string, firstName: createdUser.firstName,
lastName: createdUser.lastName as string, lastName: createdUser.lastName,
email: createdUser.email ?? "", email: createdUser.email ?? "",
active: createdOrgMembership.isActive active: createdOrgMembership.isActive
}); });
@ -483,8 +483,8 @@ export const scimServiceFactory = ({
orgMembershipId: membership.id, orgMembershipId: membership.id,
username: membership.externalId ?? membership.username, username: membership.externalId ?? membership.username,
email: membership.email, email: membership.email,
firstName: membership.firstName as string, firstName: membership.firstName,
lastName: membership.lastName as string, lastName: membership.lastName,
active active
}); });
}; };
@ -527,8 +527,8 @@ export const scimServiceFactory = ({
orgMembershipId: membership.id, orgMembershipId: membership.id,
username: membership.externalId ?? membership.username, username: membership.externalId ?? membership.username,
email: membership.email, email: membership.email,
firstName: membership.firstName as string, firstName: membership.firstName,
lastName: membership.lastName as string, lastName: membership.lastName,
active, active,
groups: groupMembershipsInOrg.map((group) => ({ groups: groupMembershipsInOrg.map((group) => ({
value: group.groupId, value: group.groupId,
@ -884,59 +884,50 @@ export const scimServiceFactory = ({
} }
for await (const operation of operations) { for await (const operation of operations) {
switch (operation.op) { if (operation.op === "replace" || operation.op === "Replace") {
case "replace": { group = await groupDAL.updateById(group.id, {
group = await groupDAL.updateById(group.id, { name: operation.value.displayName
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({ 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({
group, group,
userIds: [orgMembership.userId as string], userIds: orgMemberships.map((membership) => membership.userId as string),
userDAL, userDAL,
userGroupMembershipDAL, userGroupMembershipDAL,
orgDAL,
groupProjectDAL, groupProjectDAL,
projectKeyDAL projectKeyDAL,
}); projectDAL,
break; projectBotDAL
}
default: {
throw new ScimRequestError({
detail: "Invalid Operation",
status: 400
}); });
} catch {
logger.info("Repeat SCIM user-group add operation");
} }
} else if (operation.op === "remove" || operation.op === "Remove") {
const orgMembershipId = extractScimValueFromPath(operation.path);
if (!orgMembershipId) throw new ScimRequestError({ detail: "Invalid path value", status: 400 });
const orgMembership = await orgMembershipDAL.findById(orgMembershipId);
if (!orgMembership) throw new ScimRequestError({ detail: "Org Membership Not Found", status: 400 });
await removeUsersFromGroupByUserIds({
group,
userIds: [orgMembership.userId as string],
userDAL,
userGroupMembershipDAL,
groupProjectDAL,
projectKeyDAL
});
} else {
throw new ScimRequestError({
detail: "Invalid Operation",
status: 400
});
} }
} }

View File

@ -110,8 +110,10 @@ export type TUpdateScimGroupNamePatchDTO = {
operations: (TRemoveOp | TReplaceOp | TAddOp)[]; 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 = { type TReplaceOp = {
op: "replace"; op: "replace" | "Replace";
value: { value: {
id: string; id: string;
displayName: string; displayName: string;
@ -119,12 +121,12 @@ type TReplaceOp = {
}; };
type TRemoveOp = { type TRemoveOp = {
op: "remove"; op: "remove" | "Remove";
path: string; path: string;
}; };
type TAddOp = { type TAddOp = {
op: "add"; op: "add" | "Add";
path: string; path: string;
value: { value: {
value: string; value: string;

View File

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

View File

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

View File

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

View File

@ -74,6 +74,7 @@ const envSchema = z
JWT_AUTH_LIFETIME: zpStr(z.string().default("10d")), JWT_AUTH_LIFETIME: zpStr(z.string().default("10d")),
JWT_SIGNUP_LIFETIME: zpStr(z.string().default("15m")), JWT_SIGNUP_LIFETIME: zpStr(z.string().default("15m")),
JWT_REFRESH_LIFETIME: zpStr(z.string().default("90d")), 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_MFA_LIFETIME: zpStr(z.string().default("5m")),
JWT_PROVIDER_AUTH_LIFETIME: zpStr(z.string().default("15m")), JWT_PROVIDER_AUTH_LIFETIME: zpStr(z.string().default("15m")),
// Oauth // Oauth
@ -141,7 +142,8 @@ const envSchema = z
CAPTCHA_SECRET: zpStr(z.string().optional()), CAPTCHA_SECRET: zpStr(z.string().optional()),
PLAIN_API_KEY: zpStr(z.string().optional()), PLAIN_API_KEY: zpStr(z.string().optional()),
PLAIN_WISH_LABEL_IDS: 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) => ({ .transform((data) => ({
...data, ...data,

View File

@ -57,7 +57,6 @@ const extractAuth = async (req: FastifyRequest, jwtSecret: string) => {
return { authMode: AuthMode.API_KEY, token: apiKey, actor: ActorType.USER } as const; return { authMode: AuthMode.API_KEY, token: apiKey, actor: ActorType.USER } as const;
} }
const authHeader = req.headers?.authorization; const authHeader = req.headers?.authorization;
if (!authHeader) return { authMode: null, token: null }; if (!authHeader) return { authMode: null, token: null };
const authTokenValue = authHeader.slice(7); // slice of after Bearer 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.decorateRequest("auth", null);
server.addHook("onRequest", async (req) => { server.addHook("onRequest", async (req) => {
const appCfg = getConfig(); 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; return;
} }
const { authMode, token, actor } = await extractAuth(req, appCfg.AUTH_SECRET);
if (!authMode) return; if (!authMode) return;
switch (authMode) { switch (authMode) {

View File

@ -0,0 +1,173 @@
import bcrypt from "bcrypt";
import { z } from "zod";
import { getConfig } from "@app/lib/config/env";
import { BadRequestError, UnauthorizedError } from "@app/lib/errors";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
export const registerCertificateEstRouter = async (server: FastifyZodProvider) => {
const appCfg = getConfig();
// add support for CSR bodies
server.addContentTypeParser("application/pkcs10", { parseAs: "string" }, (_, body, done) => {
try {
let csrBody = body as string;
// some EST clients send CSRs in PEM format and some in base64 format
// for CSRs sent in PEM, we leave them as is
// for CSRs sent in base64, we preprocess them to remove new lines and spaces
if (!csrBody.includes("BEGIN CERTIFICATE REQUEST")) {
csrBody = csrBody.replace(/\n/g, "").replace(/ /g, "");
}
done(null, csrBody);
} catch (err) {
const error = err as Error;
done(error, undefined);
}
});
// Authenticate EST client using Passphrase
server.addHook("onRequest", async (req, res) => {
const { authorization } = req.headers;
const urlFragments = req.url.split("/");
// cacerts endpoint should not have any authentication
if (urlFragments[urlFragments.length - 1] === "cacerts") {
return;
}
if (!authorization) {
const wwwAuthenticateHeader = "WWW-Authenticate";
const errAuthRequired = "Authentication required";
await res.hijack();
// definitive connection timeout to clean-up open connections and prevent memory leak
res.raw.setTimeout(10 * 1000, () => {
res.raw.end();
});
res.raw.setHeader(wwwAuthenticateHeader, `Basic realm="infisical"`);
res.raw.setHeader("Content-Length", 0);
res.raw.statusCode = 401;
// Write the error message to the response without ending the connection
res.raw.write(errAuthRequired);
// flush headers
res.raw.flushHeaders();
return;
}
const certificateTemplateId = urlFragments.slice(-2)[0];
const estConfig = await server.services.certificateTemplate.getEstConfiguration({
isInternal: true,
certificateTemplateId
});
if (!estConfig.isEnabled) {
throw new BadRequestError({
message: "EST is disabled"
});
}
const rawCredential = authorization?.split(" ").pop();
if (!rawCredential) {
throw new UnauthorizedError({ message: "Missing HTTP credentials" });
}
// expected format is user:password
const basicCredential = atob(rawCredential);
const password = basicCredential.split(":").pop();
if (!password) {
throw new BadRequestError({
message: "No password provided"
});
}
const isPasswordValid = await bcrypt.compare(password, estConfig.hashedPassphrase);
if (!isPasswordValid) {
throw new UnauthorizedError({
message: "Invalid credentials"
});
}
});
server.route({
method: "POST",
url: "/:certificateTemplateId/simpleenroll",
config: {
rateLimit: writeLimit
},
schema: {
body: z.string().min(1),
params: z.object({
certificateTemplateId: z.string().min(1)
}),
response: {
200: z.string()
}
},
handler: async (req, res) => {
void res.header("Content-Type", "application/pkcs7-mime; smime-type=certs-only");
void res.header("Content-Transfer-Encoding", "base64");
return server.services.certificateEst.simpleEnroll({
csr: req.body,
certificateTemplateId: req.params.certificateTemplateId,
sslClientCert: req.headers[appCfg.SSL_CLIENT_CERTIFICATE_HEADER_KEY] as string
});
}
});
server.route({
method: "POST",
url: "/:certificateTemplateId/simplereenroll",
config: {
rateLimit: writeLimit
},
schema: {
body: z.string().min(1),
params: z.object({
certificateTemplateId: z.string().min(1)
}),
response: {
200: z.string()
}
},
handler: async (req, res) => {
void res.header("Content-Type", "application/pkcs7-mime; smime-type=certs-only");
void res.header("Content-Transfer-Encoding", "base64");
return server.services.certificateEst.simpleReenroll({
csr: req.body,
certificateTemplateId: req.params.certificateTemplateId,
sslClientCert: req.headers[appCfg.SSL_CLIENT_CERTIFICATE_HEADER_KEY] as string
});
}
});
server.route({
method: "GET",
url: "/:certificateTemplateId/cacerts",
config: {
rateLimit: readLimit
},
schema: {
params: z.object({
certificateTemplateId: z.string().min(1)
}),
response: {
200: z.string()
}
},
handler: async (req, res) => {
void res.header("Content-Type", "application/pkcs7-mime; smime-type=certs-only");
void res.header("Content-Transfer-Encoding", "base64");
return server.services.certificateEst.getCaCerts({
certificateTemplateId: req.params.certificateTemplateId
});
}
});
};

View File

@ -90,7 +90,9 @@ import { certificateAuthorityDALFactory } from "@app/services/certificate-author
import { certificateAuthorityQueueFactory } from "@app/services/certificate-authority/certificate-authority-queue"; import { certificateAuthorityQueueFactory } from "@app/services/certificate-authority/certificate-authority-queue";
import { certificateAuthoritySecretDALFactory } from "@app/services/certificate-authority/certificate-authority-secret-dal"; import { certificateAuthoritySecretDALFactory } from "@app/services/certificate-authority/certificate-authority-secret-dal";
import { certificateAuthorityServiceFactory } from "@app/services/certificate-authority/certificate-authority-service"; 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 { 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 { certificateTemplateServiceFactory } from "@app/services/certificate-template/certificate-template-service";
import { groupProjectDALFactory } from "@app/services/group-project/group-project-dal"; import { groupProjectDALFactory } from "@app/services/group-project/group-project-dal";
import { groupProjectMembershipRoleDALFactory } from "@app/services/group-project/group-project-membership-role-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 { injectPermission } from "../plugins/auth/inject-permission";
import { injectRateLimits } from "../plugins/inject-rate-limits"; import { injectRateLimits } from "../plugins/inject-rate-limits";
import { registerSecretScannerGhApp } from "../plugins/secret-scanner"; import { registerSecretScannerGhApp } from "../plugins/secret-scanner";
import { registerCertificateEstRouter } from "./est/certificate-est-router";
import { registerV1Routes } from "./v1"; import { registerV1Routes } from "./v1";
import { registerV2Routes } from "./v2"; import { registerV2Routes } from "./v2";
import { registerV3Routes } from "./v3"; import { registerV3Routes } from "./v3";
@ -477,9 +480,12 @@ export const registerRoutes = async (
orgRoleDAL, orgRoleDAL,
permissionService, permissionService,
orgDAL, orgDAL,
userGroupMembershipDAL,
projectBotDAL,
incidentContactDAL, incidentContactDAL,
tokenService, tokenService,
projectUserAdditionalPrivilegeDAL, projectUserAdditionalPrivilegeDAL,
projectUserMembershipRoleDAL,
projectDAL, projectDAL,
projectMembershipDAL, projectMembershipDAL,
orgMembershipDAL, orgMembershipDAL,
@ -499,6 +505,8 @@ export const registerRoutes = async (
projectDAL, projectDAL,
projectBotDAL, projectBotDAL,
groupProjectDAL, groupProjectDAL,
projectMembershipDAL,
projectUserMembershipRoleDAL,
orgDAL, orgDAL,
orgService, orgService,
licenseService licenseService
@ -595,6 +603,7 @@ export const registerRoutes = async (
const certificateAuthoritySecretDAL = certificateAuthoritySecretDALFactory(db); const certificateAuthoritySecretDAL = certificateAuthoritySecretDALFactory(db);
const certificateAuthorityCrlDAL = certificateAuthorityCrlDALFactory(db); const certificateAuthorityCrlDAL = certificateAuthorityCrlDALFactory(db);
const certificateTemplateDAL = certificateTemplateDALFactory(db); const certificateTemplateDAL = certificateTemplateDALFactory(db);
const certificateTemplateEstConfigDAL = certificateTemplateEstConfigDALFactory(db);
const certificateDAL = certificateDALFactory(db); const certificateDAL = certificateDALFactory(db);
const certificateBodyDAL = certificateBodyDALFactory(db); const certificateBodyDAL = certificateBodyDALFactory(db);
@ -652,8 +661,21 @@ export const registerRoutes = async (
const certificateTemplateService = certificateTemplateServiceFactory({ const certificateTemplateService = certificateTemplateServiceFactory({
certificateTemplateDAL, certificateTemplateDAL,
certificateTemplateEstConfigDAL,
certificateAuthorityDAL, certificateAuthorityDAL,
permissionService permissionService,
kmsService,
projectDAL
});
const certificateEstService = certificateEstServiceFactory({
certificateAuthorityService,
certificateTemplateService,
certificateTemplateDAL,
certificateAuthorityCertDAL,
certificateAuthorityDAL,
projectDAL,
kmsService
}); });
const pkiAlertService = pkiAlertServiceFactory({ const pkiAlertService = pkiAlertServiceFactory({
@ -683,6 +705,7 @@ export const registerRoutes = async (
orgDAL, orgDAL,
orgService, orgService,
projectMembershipDAL, projectMembershipDAL,
projectRoleDAL,
folderDAL, folderDAL,
licenseService, licenseService,
certificateAuthorityDAL, certificateAuthorityDAL,
@ -839,6 +862,7 @@ export const registerRoutes = async (
secretQueueService, secretQueueService,
kmsService, kmsService,
secretV2BridgeDAL, secretV2BridgeDAL,
secretApprovalPolicyDAL,
secretVersionV2BridgeDAL, secretVersionV2BridgeDAL,
secretVersionTagV2BridgeDAL, secretVersionTagV2BridgeDAL,
smtpService, smtpService,
@ -1189,6 +1213,7 @@ export const registerRoutes = async (
certificateAuthority: certificateAuthorityService, certificateAuthority: certificateAuthorityService,
certificateTemplate: certificateTemplateService, certificateTemplate: certificateTemplateService,
certificateAuthorityCrl: certificateAuthorityCrlService, certificateAuthorityCrl: certificateAuthorityCrlService,
certificateEst: certificateEstService,
pkiAlert: pkiAlertService, pkiAlert: pkiAlertService,
pkiCollection: pkiCollectionService, pkiCollection: pkiCollectionService,
secretScanning: secretScanningService, 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 // register routes for v1
await server.register( await server.register(
async (v1Server) => { async (v1Server) => {

View File

@ -669,6 +669,7 @@ export const registerCaRouter = async (server: FastifyZodProvider) => {
handler: async (req) => { handler: async (req) => {
const { certificate, certificateChain, issuingCaCertificate, serialNumber, ca } = const { certificate, certificateChain, issuingCaCertificate, serialNumber, ca } =
await server.services.certificateAuthority.signCertFromCa({ await server.services.certificateAuthority.signCertFromCa({
isInternal: false,
caId: req.params.caId, caId: req.params.caId,
actor: req.permission.type, actor: req.permission.type,
actorId: req.permission.id, actorId: req.permission.id,
@ -691,7 +692,7 @@ export const registerCaRouter = async (server: FastifyZodProvider) => {
}); });
return { return {
certificate, certificate: certificate.toString("pem"),
certificateChain, certificateChain,
issuingCaCertificate, issuingCaCertificate,
serialNumber serialNumber

View File

@ -210,6 +210,7 @@ export const registerCertRouter = async (server: FastifyZodProvider) => {
handler: async (req) => { handler: async (req) => {
const { certificate, certificateChain, issuingCaCertificate, serialNumber, ca } = const { certificate, certificateChain, issuingCaCertificate, serialNumber, ca } =
await server.services.certificateAuthority.signCertFromCa({ await server.services.certificateAuthority.signCertFromCa({
isInternal: false,
actor: req.permission.type, actor: req.permission.type,
actorId: req.permission.id, actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod, actorAuthMethod: req.permission.authMethod,
@ -231,7 +232,7 @@ export const registerCertRouter = async (server: FastifyZodProvider) => {
}); });
return { return {
certificate, certificate: certificate.toString("pem"),
certificateChain, certificateChain,
issuingCaCertificate, issuingCaCertificate,
serialNumber serialNumber

View File

@ -1,6 +1,7 @@
import ms from "ms"; import ms from "ms";
import { z } from "zod"; import { z } from "zod";
import { CertificateTemplateEstConfigsSchema } from "@app/db/schemas";
import { EventType } from "@app/ee/services/audit-log/audit-log-types"; import { EventType } from "@app/ee/services/audit-log/audit-log-types";
import { CERTIFICATE_TEMPLATES } from "@app/lib/api-docs"; import { CERTIFICATE_TEMPLATES } from "@app/lib/api-docs";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; 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 { sanitizedCertificateTemplate } from "@app/services/certificate-template/certificate-template-schema";
import { validateTemplateRegexField } from "@app/services/certificate-template/certificate-template-validators"; 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) => { export const registerCertificateTemplateRouter = async (server: FastifyZodProvider) => {
server.route({ server.route({
method: "GET", method: "GET",
@ -202,4 +209,141 @@ export const registerCertificateTemplateRouter = async (server: FastifyZodProvid
return certificateTemplate; return certificateTemplate;
} }
}); });
server.route({
method: "POST",
url: "/:certificateTemplateId/est-config",
config: {
rateLimit: writeLimit
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: {
description: "Create Certificate Template EST configuration",
params: z.object({
certificateTemplateId: z.string().trim()
}),
body: z.object({
caChain: z.string().trim().min(1),
passphrase: z.string().min(1),
isEnabled: z.boolean().default(true)
}),
response: {
200: sanitizedEstConfig
}
},
handler: async (req) => {
const estConfig = await server.services.certificateTemplate.createEstConfiguration({
certificateTemplateId: req.params.certificateTemplateId,
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
...req.body
});
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
projectId: estConfig.projectId,
event: {
type: EventType.CREATE_CERTIFICATE_TEMPLATE_EST_CONFIG,
metadata: {
certificateTemplateId: estConfig.certificateTemplateId,
isEnabled: estConfig.isEnabled as boolean
}
}
});
return estConfig;
}
});
server.route({
method: "PATCH",
url: "/:certificateTemplateId/est-config",
config: {
rateLimit: writeLimit
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: {
description: "Update Certificate Template EST configuration",
params: z.object({
certificateTemplateId: z.string().trim()
}),
body: z.object({
caChain: z.string().trim().min(1).optional(),
passphrase: z.string().min(1).optional(),
isEnabled: z.boolean().optional()
}),
response: {
200: sanitizedEstConfig
}
},
handler: async (req) => {
const estConfig = await server.services.certificateTemplate.updateEstConfiguration({
certificateTemplateId: req.params.certificateTemplateId,
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
...req.body
});
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
projectId: estConfig.projectId,
event: {
type: EventType.UPDATE_CERTIFICATE_TEMPLATE_EST_CONFIG,
metadata: {
certificateTemplateId: estConfig.certificateTemplateId,
isEnabled: estConfig.isEnabled as boolean
}
}
});
return estConfig;
}
});
server.route({
method: "GET",
url: "/:certificateTemplateId/est-config",
config: {
rateLimit: readLimit
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: {
description: "Get Certificate Template EST configuration",
params: z.object({
certificateTemplateId: z.string().trim()
}),
response: {
200: sanitizedEstConfig.extend({
caChain: z.string()
})
}
},
handler: async (req) => {
const estConfig = await server.services.certificateTemplate.getEstConfiguration({
isInternal: false,
certificateTemplateId: req.params.certificateTemplateId,
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId
});
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
projectId: estConfig.projectId,
event: {
type: EventType.GET_CERTIFICATE_TEMPLATE_EST_CONFIG,
metadata: {
certificateTemplateId: estConfig.certificateTemplateId
}
}
});
return estConfig;
}
});
}; };

View File

@ -293,6 +293,7 @@ export const registerIntegrationAuthRouter = async (server: FastifyZodProvider)
}), }),
querystring: z.object({ querystring: z.object({
teamId: z.string().trim().optional(), teamId: z.string().trim().optional(),
azureDevOpsOrgName: z.string().trim().optional(),
workspaceSlug: z.string().trim().optional() workspaceSlug: z.string().trim().optional()
}), }),
response: { response: {

View File

@ -1,6 +1,6 @@
import { z } from "zod"; 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 { inviteUserRateLimit } from "@app/server/config/rateLimiter";
import { getTelemetryDistinctId } from "@app/server/lib/telemetry"; import { getTelemetryDistinctId } from "@app/server/lib/telemetry";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
@ -16,23 +16,37 @@ export const registerInviteOrgRouter = async (server: FastifyZodProvider) => {
method: "POST", method: "POST",
schema: { schema: {
body: z.object({ body: z.object({
inviteeEmail: z.string().trim().email(), inviteeEmails: z.array(z.string().trim().email()),
organizationId: z.string().trim() organizationId: z.string().trim(),
projectIds: z.array(z.string().trim()).optional(),
projectRoleSlug: z.nativeEnum(ProjectMembershipRole).optional(),
organizationRoleSlug: z.nativeEnum(OrgMembershipRole)
}), }),
response: { response: {
200: z.object({ 200: z.object({
message: z.string(), message: z.string(),
completeInviteLink: z.string().optional() completeInviteLinks: z
.array(
z.object({
email: z.string(),
link: z.string()
})
)
.optional()
}) })
} }
}, },
onRequest: verifyAuth([AuthMode.JWT]), onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => { handler: async (req) => {
if (req.auth.actor !== ActorType.USER) return; 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, orgId: req.body.organizationId,
userId: req.permission.id, 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, actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId actorOrgId: req.permission.orgId
}); });
@ -41,14 +55,15 @@ export const registerInviteOrgRouter = async (server: FastifyZodProvider) => {
event: PostHogEventTypes.UserOrgInvitation, event: PostHogEventTypes.UserOrgInvitation,
distinctId: getTelemetryDistinctId(req), distinctId: getTelemetryDistinctId(req),
properties: { properties: {
inviteeEmail: req.body.inviteeEmail, inviteeEmails: req.body.inviteeEmails,
organizationRoleSlug: req.body.organizationRoleSlug,
...req.auditLogInfo ...req.auditLogInfo
} }
}); });
return { return {
completeInviteLink, completeInviteLinks,
message: `Send an invite link to ${req.body.inviteeEmail}` message: `Send an invite link to ${req.body.inviteeEmails.join(", ")}`
}; };
} }
}); });

View File

@ -1,6 +1,12 @@
import { z } from "zod"; 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 { PROJECTS } from "@app/lib/api-docs";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
@ -122,15 +128,31 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
rateLimit: readLimit rateLimit: readLimit
}, },
schema: { schema: {
querystring: z.object({
includeRoles: z
.enum(["true", "false"])
.default("false")
.transform((value) => value === "true")
}),
response: { response: {
200: z.object({ 200: z.object({
workspaces: projectWithEnv.array() workspaces: projectWithEnv
.extend({
roles: ProjectRolesSchema.array().optional()
})
.array()
}) })
} }
}, },
onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY]), onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY]),
handler: async (req) => { 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 }; return { workspaces };
} }
}); });

View File

@ -179,7 +179,8 @@ export const registerSignupRouter = async (server: FastifyZodProvider) => {
encryptedPrivateKeyIV: z.string().trim(), encryptedPrivateKeyIV: z.string().trim(),
encryptedPrivateKeyTag: z.string().trim(), encryptedPrivateKeyTag: z.string().trim(),
salt: z.string().trim(), salt: z.string().trim(),
verifier: z.string().trim() verifier: z.string().trim(),
tokenMetadata: z.string().optional()
}), }),
response: { response: {
200: z.object({ 200: z.object({

View File

@ -1,3 +1,5 @@
import { ProjectMembershipRole } from "@app/db/schemas";
export enum TokenType { export enum TokenType {
TOKEN_EMAIL_CONFIRMATION = "emailConfirmation", TOKEN_EMAIL_CONFIRMATION = "emailConfirmation",
TOKEN_EMAIL_VERIFICATION = "emailVerification", // unverified -> verified TOKEN_EMAIL_VERIFICATION = "emailVerification", // unverified -> verified
@ -49,3 +51,19 @@ export type TIssueAuthTokenDTO = {
ip: string; ip: string;
userAgent: 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;
};

View File

@ -583,7 +583,13 @@ export const authLoginServiceFactory = ({
} else { } else {
const isLinkingRequired = !user?.authMethods?.includes(authMethod); const isLinkingRequired = !user?.authMethods?.includes(authMethod);
if (isLinkingRequired) { 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
});
} }
} }

View File

@ -9,7 +9,7 @@ import { isAuthMethodSaml } from "@app/ee/services/permission/permission-fns";
import { getConfig } from "@app/lib/config/env"; import { getConfig } from "@app/lib/config/env";
import { infisicalSymmetricEncypt } from "@app/lib/crypto/encryption"; import { infisicalSymmetricEncypt } from "@app/lib/crypto/encryption";
import { getUserPrivateKey } from "@app/lib/crypto/srp"; 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 { isDisposableEmail } from "@app/lib/validator";
import { TGroupProjectDALFactory } from "@app/services/group-project/group-project-dal"; import { TGroupProjectDALFactory } from "@app/services/group-project/group-project-dal";
import { TProjectDALFactory } from "@app/services/project/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 { TProjectKeyDALFactory } from "@app/services/project-key/project-key-dal";
import { TAuthTokenServiceFactory } from "../auth-token/auth-token-service"; 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 { TOrgDALFactory } from "../org/org-dal";
import { TOrgServiceFactory } from "../org/org-service"; 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 { SmtpTemplates, TSmtpService } from "../smtp/smtp-service";
import { TUserDALFactory } from "../user/user-dal"; import { TUserDALFactory } from "../user/user-dal";
import { TAuthDALFactory } from "./auth-dal"; import { TAuthDALFactory } from "./auth-dal";
@ -32,10 +35,14 @@ type TAuthSignupDep = {
userDAL: TUserDALFactory; userDAL: TUserDALFactory;
userGroupMembershipDAL: Pick< userGroupMembershipDAL: Pick<
TUserGroupMembershipDALFactory, TUserGroupMembershipDALFactory,
"find" | "transaction" | "insertMany" | "deletePendingUserGroupMembershipsByUserIds" | "find"
| "transaction"
| "insertMany"
| "deletePendingUserGroupMembershipsByUserIds"
| "findUserGroupMembershipsInProject"
>; >;
projectKeyDAL: Pick<TProjectKeyDALFactory, "find" | "findLatestProjectKey" | "insertMany">; projectKeyDAL: Pick<TProjectKeyDALFactory, "find" | "findLatestProjectKey" | "insertMany">;
projectDAL: Pick<TProjectDALFactory, "findProjectGhostUser">; projectDAL: Pick<TProjectDALFactory, "findProjectGhostUser" | "findProjectById">;
projectBotDAL: Pick<TProjectBotDALFactory, "findOne">; projectBotDAL: Pick<TProjectBotDALFactory, "findOne">;
groupProjectDAL: Pick<TGroupProjectDALFactory, "find">; groupProjectDAL: Pick<TGroupProjectDALFactory, "find">;
orgService: Pick<TOrgServiceFactory, "createOrganization">; orgService: Pick<TOrgServiceFactory, "createOrganization">;
@ -43,6 +50,8 @@ type TAuthSignupDep = {
tokenService: TAuthTokenServiceFactory; tokenService: TAuthTokenServiceFactory;
smtpService: TSmtpService; smtpService: TSmtpService;
licenseService: Pick<TLicenseServiceFactory, "updateSubscriptionOrgMemberCount">; licenseService: Pick<TLicenseServiceFactory, "updateSubscriptionOrgMemberCount">;
projectMembershipDAL: Pick<TProjectMembershipDALFactory, "find" | "transaction" | "insertMany">;
projectUserMembershipRoleDAL: Pick<TProjectUserMembershipRoleDALFactory, "insertMany">;
}; };
export type TAuthSignupFactory = ReturnType<typeof authSignupServiceFactory>; export type TAuthSignupFactory = ReturnType<typeof authSignupServiceFactory>;
@ -58,6 +67,8 @@ export const authSignupServiceFactory = ({
smtpService, smtpService,
orgService, orgService,
orgDAL, orgDAL,
projectMembershipDAL,
projectUserMembershipRoleDAL,
licenseService licenseService
}: TAuthSignupDep) => { }: TAuthSignupDep) => {
// first step of signup. create user and send email // first step of signup. create user and send email
@ -301,7 +312,8 @@ export const authSignupServiceFactory = ({
encryptedPrivateKey, encryptedPrivateKey,
encryptedPrivateKeyIV, encryptedPrivateKeyIV,
encryptedPrivateKeyTag, encryptedPrivateKeyTag,
authorization authorization,
tokenMetadata
}: TCompleteAccountInviteDTO) => { }: TCompleteAccountInviteDTO) => {
const user = await userDAL.findUserByUsername(email); const user = await userDAL.findUserByUsername(email);
if (!user || (user && user.isAccepted)) { if (!user || (user && user.isAccepted)) {
@ -358,6 +370,45 @@ export const authSignupServiceFactory = ({
tx 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( const updatedMembersips = await orgDAL.updateMembership(
{ inviteEmail: email, status: OrgMembershipStatus.Invited }, { inviteEmail: email, status: OrgMembershipStatus.Invited },
{ userId: us.id, status: OrgMembershipStatus.Accepted }, { userId: us.id, status: OrgMembershipStatus.Accepted },

View File

@ -37,4 +37,5 @@ export type TCompleteAccountInviteDTO = {
ip: string; ip: string;
userAgent: string; userAgent: string;
authorization: string; authorization: string;
tokenMetadata?: string;
}; };

View File

@ -1295,24 +1295,23 @@ export const certificateAuthorityServiceFactory = ({
* Return new leaf certificate issued by CA with id [caId]. * Return new leaf certificate issued by CA with id [caId].
* Note: CSR is generated externally and submitted to Infisical. * Note: CSR is generated externally and submitted to Infisical.
*/ */
const signCertFromCa = async ({ const signCertFromCa = async (dto: TSignCertFromCaDTO) => {
caId,
certificateTemplateId,
csr,
pkiCollectionId,
friendlyName,
commonName,
altNames,
ttl,
notBefore,
notAfter,
actorId,
actorAuthMethod,
actor,
actorOrgId
}: TSignCertFromCaDTO) => {
let ca: TCertificateAuthorities | undefined; let ca: TCertificateAuthorities | undefined;
let certificateTemplate: TCertificateTemplates | undefined; let certificateTemplate: TCertificateTemplates | undefined;
const {
caId,
certificateTemplateId,
csr,
pkiCollectionId,
friendlyName,
commonName,
altNames,
ttl,
notBefore,
notAfter
} = dto;
let collectionId = pkiCollectionId; let collectionId = pkiCollectionId;
if (caId) { if (caId) {
@ -1333,15 +1332,20 @@ export const certificateAuthorityServiceFactory = ({
throw new BadRequestError({ message: "CA not found" }); throw new BadRequestError({ message: "CA not found" });
} }
const { permission } = await permissionService.getProjectPermission( if (!dto.isInternal) {
actor, const { permission } = await permissionService.getProjectPermission(
actorId, dto.actor,
ca.projectId, dto.actorId,
actorAuthMethod, ca.projectId,
actorOrgId 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.status === CaStatus.DISABLED) throw new BadRequestError({ message: "CA is disabled" });
if (!ca.activeCaCertId) throw new BadRequestError({ message: "CA does not have a certificate installed" }); 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); notAfterDate = new Date(notAfter);
} else if (ttl) { } else if (ttl) {
notAfterDate = new Date(new Date().getTime() + ms(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); const caCertNotBeforeDate = new Date(caCertObj.notBefore);
@ -1426,6 +1432,7 @@ export const certificateAuthorityServiceFactory = ({
await x509.SubjectKeyIdentifierExtension.create(csrObj.publicKey) await x509.SubjectKeyIdentifierExtension.create(csrObj.publicKey)
]; ];
let altNamesFromCsr: string = "";
let altNamesArray: { let altNamesArray: {
type: "email" | "dns"; type: "email" | "dns";
value: string; 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 // If altName is neither a valid email nor a valid hostname, throw an error or handle it accordingly
throw new Error(`Invalid altName: ${altName}`); 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); const altNamesExtension = new x509.SubjectAlternativeNameExtension(altNamesArray, false);
extensions.push(altNamesExtension); extensions.push(altNamesExtension);
} }
@ -1500,7 +1524,7 @@ export const certificateAuthorityServiceFactory = ({
status: CertStatus.ACTIVE, status: CertStatus.ACTIVE,
friendlyName: friendlyName || csrObj.subject, friendlyName: friendlyName || csrObj.subject,
commonName: cn, commonName: cn,
altNames, altNames: altNamesFromCsr || altNames,
serialNumber, serialNumber,
notBefore: notBeforeDate, notBefore: notBeforeDate,
notAfter: notAfterDate notAfter: notAfterDate
@ -1538,7 +1562,7 @@ export const certificateAuthorityServiceFactory = ({
}); });
return { return {
certificate: leafCert.toString("pem"), certificate: leafCert,
certificateChain: `${issuingCaCertificate}\n${caCertChain}`.trim(), certificateChain: `${issuingCaCertificate}\n${caCertChain}`.trim(),
issuingCaCertificate, issuingCaCertificate,
serialNumber, serialNumber,

View File

@ -97,18 +97,33 @@ export type TIssueCertFromCaDTO = {
notAfter?: string; notAfter?: string;
} & Omit<TProjectPermission, "projectId">; } & Omit<TProjectPermission, "projectId">;
export type TSignCertFromCaDTO = { export type TSignCertFromCaDTO =
caId?: string; | {
csr: string; isInternal: true;
certificateTemplateId?: string; caId?: string;
pkiCollectionId?: string; csr: string;
friendlyName?: string; certificateTemplateId?: string;
commonName?: string; pkiCollectionId?: string;
altNames: string; friendlyName?: string;
ttl: string; commonName?: string;
notBefore?: string; altNames?: string;
notAfter?: string; ttl?: string;
} & Omit<TProjectPermission, "projectId">; 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 = { export type TDNParts = {
commonName?: string; commonName?: string;

View File

@ -0,0 +1,24 @@
import { Certificate, ContentInfo, EncapsulatedContentInfo, SignedData } from "pkijs";
export const convertRawCertsToPkcs7 = (rawCertificate: ArrayBuffer[]) => {
const certs = rawCertificate.map((rawCert) => Certificate.fromBER(rawCert));
const cmsSigned = new SignedData({
encapContentInfo: new EncapsulatedContentInfo({
eContentType: "1.2.840.113549.1.7.1" // not encrypted and not compressed data
}),
certificates: certs
});
const cmsContent = new ContentInfo({
contentType: "1.2.840.113549.1.7.2", // SignedData
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
content: cmsSigned.toSchema()
});
const derBuffer = cmsContent.toSchema().toBER(false);
const base64Pkcs7 = Buffer.from(derBuffer)
.toString("base64")
.replace(/(.{64})/g, "$1\n"); // we add a linebreak for CURL clients
return base64Pkcs7;
};

View File

@ -0,0 +1,231 @@
import * as x509 from "@peculiar/x509";
import { BadRequestError, NotFoundError, UnauthorizedError } from "@app/lib/errors";
import { isCertChainValid } from "../certificate/certificate-fns";
import { TCertificateAuthorityCertDALFactory } from "../certificate-authority/certificate-authority-cert-dal";
import { TCertificateAuthorityDALFactory } from "../certificate-authority/certificate-authority-dal";
import { getCaCertChain, getCaCertChains } from "../certificate-authority/certificate-authority-fns";
import { TCertificateAuthorityServiceFactory } from "../certificate-authority/certificate-authority-service";
import { TCertificateTemplateDALFactory } from "../certificate-template/certificate-template-dal";
import { TCertificateTemplateServiceFactory } from "../certificate-template/certificate-template-service";
import { TKmsServiceFactory } from "../kms/kms-service";
import { TProjectDALFactory } from "../project/project-dal";
import { convertRawCertsToPkcs7 } from "./certificate-est-fns";
type TCertificateEstServiceFactoryDep = {
certificateAuthorityService: Pick<TCertificateAuthorityServiceFactory, "signCertFromCa">;
certificateTemplateService: Pick<TCertificateTemplateServiceFactory, "getEstConfiguration">;
certificateTemplateDAL: Pick<TCertificateTemplateDALFactory, "findById">;
certificateAuthorityDAL: Pick<TCertificateAuthorityDALFactory, "findById">;
certificateAuthorityCertDAL: Pick<TCertificateAuthorityCertDALFactory, "find" | "findById">;
projectDAL: Pick<TProjectDALFactory, "findOne" | "updateById" | "transaction">;
kmsService: Pick<TKmsServiceFactory, "decryptWithKmsKey" | "generateKmsKey">;
};
export type TCertificateEstServiceFactory = ReturnType<typeof certificateEstServiceFactory>;
export const certificateEstServiceFactory = ({
certificateAuthorityService,
certificateTemplateService,
certificateTemplateDAL,
certificateAuthorityCertDAL,
certificateAuthorityDAL,
projectDAL,
kmsService
}: TCertificateEstServiceFactoryDep) => {
const simpleReenroll = async ({
csr,
certificateTemplateId,
sslClientCert
}: {
csr: string;
certificateTemplateId: string;
sslClientCert: string;
}) => {
const estConfig = await certificateTemplateService.getEstConfiguration({
isInternal: true,
certificateTemplateId
});
if (!estConfig.isEnabled) {
throw new BadRequestError({
message: "EST is disabled"
});
}
const certTemplate = await certificateTemplateDAL.findById(certificateTemplateId);
const leafCertificate = decodeURIComponent(sslClientCert).match(
/-----BEGIN CERTIFICATE-----[\s\S]+?-----END CERTIFICATE-----/g
)?.[0];
if (!leafCertificate) {
throw new UnauthorizedError({ message: "Missing client certificate" });
}
const cert = new x509.X509Certificate(leafCertificate);
// We have to assert that the client certificate provided can be traced back to the Root CA
const caCertChains = await getCaCertChains({
caId: certTemplate.caId,
certificateAuthorityCertDAL,
certificateAuthorityDAL,
projectDAL,
kmsService
});
const verifiedChains = await Promise.all(
caCertChains.map((chain) => {
const caCert = new x509.X509Certificate(chain.certificate);
const caChain =
chain.certificateChain
.match(/-----BEGIN CERTIFICATE-----[\s\S]+?-----END CERTIFICATE-----/g)
?.map((c) => new x509.X509Certificate(c)) || [];
return isCertChainValid([cert, caCert, ...caChain]);
})
);
if (!verifiedChains.some(Boolean)) {
throw new BadRequestError({
message: "Invalid client certificate: unable to build a valid certificate chain"
});
}
// We ensure that the Subject and SubjectAltNames of the CSR and the existing certificate are exactly the same
const csrObj = new x509.Pkcs10CertificateRequest(csr);
if (csrObj.subject !== cert.subject) {
throw new BadRequestError({
message: "Subject mismatch"
});
}
let csrSanSet: Set<string> = new Set();
const csrSanExtension = csrObj.extensions.find((ext) => ext.type === "2.5.29.17");
if (csrSanExtension) {
const sanNames = new x509.GeneralNames(csrSanExtension.value);
csrSanSet = new Set([...sanNames.items.map((name) => `${name.type}-${name.value}`)]);
}
let certSanSet: Set<string> = new Set();
const certSanExtension = cert.extensions.find((ext) => ext.type === "2.5.29.17");
if (certSanExtension) {
const sanNames = new x509.GeneralNames(certSanExtension.value);
certSanSet = new Set([...sanNames.items.map((name) => `${name.type}-${name.value}`)]);
}
if (csrSanSet.size !== certSanSet.size || ![...csrSanSet].every((element) => certSanSet.has(element))) {
throw new BadRequestError({
message: "Subject alternative names mismatch"
});
}
const { certificate } = await certificateAuthorityService.signCertFromCa({
isInternal: true,
certificateTemplateId,
csr
});
return convertRawCertsToPkcs7([certificate.rawData]);
};
const simpleEnroll = async ({
csr,
certificateTemplateId,
sslClientCert
}: {
csr: string;
certificateTemplateId: string;
sslClientCert: string;
}) => {
/* We first have to assert that the client certificate provided can be traced back to the attached
CA chain in the EST configuration
*/
const estConfig = await certificateTemplateService.getEstConfiguration({
isInternal: true,
certificateTemplateId
});
if (!estConfig.isEnabled) {
throw new BadRequestError({
message: "EST is disabled"
});
}
const caCerts = estConfig.caChain
.match(/-----BEGIN CERTIFICATE-----[\s\S]+?-----END CERTIFICATE-----/g)
?.map((cert) => {
return new x509.X509Certificate(cert);
});
if (!caCerts) {
throw new BadRequestError({ message: "Failed to parse certificate chain" });
}
const leafCertificate = decodeURIComponent(sslClientCert).match(
/-----BEGIN CERTIFICATE-----[\s\S]+?-----END CERTIFICATE-----/g
)?.[0];
if (!leafCertificate) {
throw new BadRequestError({ message: "Missing client certificate" });
}
const certObj = new x509.X509Certificate(leafCertificate);
if (!(await isCertChainValid([certObj, ...caCerts]))) {
throw new BadRequestError({ message: "Invalid certificate chain" });
}
const { certificate } = await certificateAuthorityService.signCertFromCa({
isInternal: true,
certificateTemplateId,
csr
});
return convertRawCertsToPkcs7([certificate.rawData]);
};
/**
* Return the CA certificate and CA certificate chain for the CA bound to
* the certificate template with id [certificateTemplateId] as part of EST protocol
*/
const getCaCerts = async ({ certificateTemplateId }: { certificateTemplateId: string }) => {
const certTemplate = await certificateTemplateDAL.findById(certificateTemplateId);
if (!certTemplate) {
throw new NotFoundError({
message: "Certificate template not found"
});
}
const ca = await certificateAuthorityDAL.findById(certTemplate.caId);
if (!ca) {
throw new NotFoundError({
message: "Certificate Authority not found"
});
}
const { caCert, caCertChain } = await getCaCertChain({
caCertId: ca.activeCaCertId as string,
certificateAuthorityDAL,
certificateAuthorityCertDAL,
projectDAL,
kmsService
});
const certificates = caCertChain
.match(/-----BEGIN CERTIFICATE-----[\s\S]+?-----END CERTIFICATE-----/g)
?.map((cert) => new x509.X509Certificate(cert));
if (!certificates) {
throw new BadRequestError({ message: "Failed to parse certificate chain" });
}
const caCertificate = new x509.X509Certificate(caCert);
return convertRawCertsToPkcs7([caCertificate.rawData, ...certificates.map((cert) => cert.rawData)]);
};
return {
simpleEnroll,
simpleReenroll,
getCaCerts
};
};

View File

@ -0,0 +1,11 @@
import { TDbClient } from "@app/db";
import { TableName } from "@app/db/schemas";
import { ormify } from "@app/lib/knex";
export type TCertificateTemplateEstConfigDALFactory = ReturnType<typeof certificateTemplateEstConfigDALFactory>;
export const certificateTemplateEstConfigDALFactory = (db: TDbClient) => {
const certificateTemplateEstConfigOrm = ormify(db, TableName.CertificateTemplateEstConfig);
return certificateTemplateEstConfigOrm;
};

View File

@ -1,20 +1,35 @@
import { ForbiddenError } from "@casl/ability"; import { 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 { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; 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 { BadRequestError, NotFoundError } from "@app/lib/errors";
import { isCertChainValid } from "../certificate/certificate-fns";
import { TCertificateAuthorityDALFactory } from "../certificate-authority/certificate-authority-dal"; 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 { TCertificateTemplateDALFactory } from "./certificate-template-dal";
import { TCertificateTemplateEstConfigDALFactory } from "./certificate-template-est-config-dal";
import { import {
TCreateCertTemplateDTO, TCreateCertTemplateDTO,
TCreateEstConfigurationDTO,
TDeleteCertTemplateDTO, TDeleteCertTemplateDTO,
TGetCertTemplateDTO, TGetCertTemplateDTO,
TUpdateCertTemplateDTO TGetEstConfigurationDTO,
TUpdateCertTemplateDTO,
TUpdateEstConfigurationDTO
} from "./certificate-template-types"; } from "./certificate-template-types";
type TCertificateTemplateServiceFactoryDep = { type TCertificateTemplateServiceFactoryDep = {
certificateTemplateDAL: TCertificateTemplateDALFactory; certificateTemplateDAL: TCertificateTemplateDALFactory;
certificateTemplateEstConfigDAL: TCertificateTemplateEstConfigDALFactory;
projectDAL: Pick<TProjectDALFactory, "findProjectBySlug" | "findOne" | "updateById" | "findById" | "transaction">;
kmsService: Pick<TKmsServiceFactory, "generateKmsKey" | "encryptWithKmsKey" | "decryptWithKmsKey">;
certificateAuthorityDAL: Pick<TCertificateAuthorityDALFactory, "findById">; certificateAuthorityDAL: Pick<TCertificateAuthorityDALFactory, "findById">;
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">; permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
}; };
@ -23,8 +38,11 @@ export type TCertificateTemplateServiceFactory = ReturnType<typeof certificateTe
export const certificateTemplateServiceFactory = ({ export const certificateTemplateServiceFactory = ({
certificateTemplateDAL, certificateTemplateDAL,
certificateTemplateEstConfigDAL,
certificateAuthorityDAL, certificateAuthorityDAL,
permissionService permissionService,
kmsService,
projectDAL
}: TCertificateTemplateServiceFactoryDep) => { }: TCertificateTemplateServiceFactoryDep) => {
const createCertTemplate = async ({ const createCertTemplate = async ({
caId, caId,
@ -187,10 +205,228 @@ export const certificateTemplateServiceFactory = ({
return certTemplate; 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 { return {
createCertTemplate, createCertTemplate,
getCertTemplate, getCertTemplate,
deleteCertTemplate, deleteCertTemplate,
updateCertTemplate updateCertTemplate,
createEstConfiguration,
updateEstConfiguration,
getEstConfiguration
}; };
}; };

View File

@ -26,3 +26,27 @@ export type TGetCertTemplateDTO = {
export type TDeleteCertTemplateDTO = { export type TDeleteCertTemplateDTO = {
id: string; id: string;
} & Omit<TProjectPermission, "projectId">; } & Omit<TProjectPermission, "projectId">;
export type TCreateEstConfigurationDTO = {
certificateTemplateId: string;
caChain: string;
passphrase: string;
isEnabled: boolean;
} & Omit<TProjectPermission, "projectId">;
export type TUpdateEstConfigurationDTO = {
certificateTemplateId: string;
caChain?: string;
passphrase?: string;
isEnabled?: boolean;
} & Omit<TProjectPermission, "projectId">;
export type TGetEstConfigurationDTO =
| {
isInternal: true;
certificateTemplateId: string;
}
| ({
isInternal: false;
certificateTemplateId: string;
} & Omit<TProjectPermission, "projectId">);

View File

@ -24,3 +24,19 @@ export const revocationReasonToCrlCode = (crlReason: CrlReason) => {
return x509.X509CrlReason.unspecified; return x509.X509CrlReason.unspecified;
} }
}; };
export const isCertChainValid = async (certificates: x509.X509Certificate[]) => {
if (certificates.length === 1) {
return true;
}
const leafCert = certificates[0];
const chain = new x509.X509ChainBuilder({
certificates: certificates.slice(1)
});
const chainItems = await chain.build(leafCert);
// chain.build() implicitly verifies the chain
return chainItems.length === certificates.length;
};

View File

@ -1030,11 +1030,31 @@ const getAppsCloud66 = async ({ accessToken }: { accessToken: string }) => {
return apps; 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 ({ export const getApps = async ({
integration, integration,
accessToken, accessToken,
accessId, accessId,
teamId, teamId,
azureDevOpsOrgName,
workspaceSlug, workspaceSlug,
url url
}: { }: {
@ -1042,6 +1062,7 @@ export const getApps = async ({
accessToken: string; accessToken: string;
accessId?: string; accessId?: string;
teamId?: string | null; teamId?: string | null;
azureDevOpsOrgName?: string | null;
workspaceSlug?: string; workspaceSlug?: string;
url?: string | null; url?: string | null;
}): Promise<App[]> => { }): Promise<App[]> => {
@ -1184,6 +1205,12 @@ export const getApps = async ({
accessToken accessToken
}); });
case Integrations.AZURE_DEVOPS:
return getAppsAzureDevOps({
accessToken,
orgName: azureDevOpsOrgName as string
});
default: default:
throw new BadRequestError({ message: "integration not found" }); throw new BadRequestError({ message: "integration not found" });
} }

View File

@ -440,6 +440,7 @@ export const integrationAuthServiceFactory = ({
actorOrgId, actorOrgId,
actorAuthMethod, actorAuthMethod,
teamId, teamId,
azureDevOpsOrgName,
id, id,
workspaceSlug workspaceSlug
}: TIntegrationAuthAppsDTO) => { }: TIntegrationAuthAppsDTO) => {
@ -462,6 +463,7 @@ export const integrationAuthServiceFactory = ({
accessToken, accessToken,
accessId, accessId,
teamId, teamId,
azureDevOpsOrgName,
workspaceSlug, workspaceSlug,
url: integrationAuth.url url: integrationAuth.url
}); });

View File

@ -1,3 +1,4 @@
import { TIntegrations } from "@app/db/schemas";
import { TProjectPermission } from "@app/lib/types"; import { TProjectPermission } from "@app/lib/types";
export type TGetIntegrationAuthDTO = { export type TGetIntegrationAuthDTO = {
@ -28,6 +29,7 @@ export type TDeleteIntegrationAuthsDTO = TProjectPermission & {
export type TIntegrationAuthAppsDTO = { export type TIntegrationAuthAppsDTO = {
id: string; id: string;
teamId?: string; teamId?: string;
azureDevOpsOrgName?: string;
workspaceSlug?: string; workspaceSlug?: string;
} & Omit<TProjectPermission, "projectId">; } & Omit<TProjectPermission, "projectId">;
@ -163,3 +165,13 @@ export type TTeamCityBuildConfig = {
href: string; href: string;
webUrl: string; webUrl: string;
}; };
export type TIntegrationsWithEnvironment = TIntegrations & {
environment?:
| {
id?: string | null | undefined;
name?: string | null | undefined;
}
| null
| undefined;
};

View File

@ -31,7 +31,8 @@ export enum Integrations {
CLOUD_66 = "cloud-66", CLOUD_66 = "cloud-66",
NORTHFLANK = "northflank", NORTHFLANK = "northflank",
HASURA_CLOUD = "hasura-cloud", HASURA_CLOUD = "hasura-cloud",
RUNDECK = "rundeck" RUNDECK = "rundeck",
AZURE_DEVOPS = "azure-devops"
} }
export enum IntegrationType { export enum IntegrationType {
@ -88,6 +89,7 @@ export enum IntegrationUrls {
CLOUD_66_API_URL = "https://app.cloud66.com/api", CLOUD_66_API_URL = "https://app.cloud66.com/api",
NORTHFLANK_API_URL = "https://api.northflank.com", NORTHFLANK_API_URL = "https://api.northflank.com",
HASURA_CLOUD_API_URL = "https://data.pro.hasura.io/v1/graphql", 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_SERVICE_NAME = "secretmanager.googleapis.com",
GCP_SECRET_MANAGER_URL = `https://${GCP_SECRET_MANAGER_SERVICE_NAME}`, GCP_SECRET_MANAGER_URL = `https://${GCP_SECRET_MANAGER_SERVICE_NAME}`,
@ -378,6 +380,15 @@ export const getIntegrationOptions = async () => {
type: "pat", type: "pat",
clientId: "", clientId: "",
docsLink: "" docsLink: ""
},
{
name: "Azure DevOps",
slug: "azure-devops",
image: "Microsoft Azure.png",
isAvailable: true,
type: "pat",
clientId: "",
docsLink: ""
} }
]; ];

View File

@ -35,6 +35,7 @@ import { TCreateManySecretsRawFn, TUpdateManySecretsRawFn } from "@app/services/
import { TIntegrationDALFactory } from "../integration/integration-dal"; import { TIntegrationDALFactory } from "../integration/integration-dal";
import { IntegrationMetadataSchema } from "../integration/integration-schema"; import { IntegrationMetadataSchema } from "../integration/integration-schema";
import { TIntegrationsWithEnvironment } from "./integration-auth-types";
import { import {
IntegrationInitialSyncBehavior, IntegrationInitialSyncBehavior,
IntegrationMappingBehavior, 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] * Sync/push [secrets] to GitLab repo with name [integration.app]
*/ */
@ -3714,6 +3825,15 @@ export const syncIntegrationSecrets = async ({
updateManySecretsRawFn updateManySecretsRawFn
}); });
break; break;
case Integrations.AZURE_DEVOPS:
await syncSecretsAzureDevops({
integrationAuth,
integration,
secrets,
accessToken
});
break;
case Integrations.AWS_PARAMETER_STORE: case Integrations.AWS_PARAMETER_STORE:
response = await syncSecretsAWSParameterStore({ response = await syncSecretsAWSParameterStore({
integration, integration,

View File

@ -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 { try {
const members = await db const conn = tx || db;
.replicaNode()(TableName.OrgMembership) const members = await conn(TableName.OrgMembership)
// .replicaNode()(TableName.OrgMembership)
.where(`${TableName.OrgMembership}.orgId`, orgId) .where(`${TableName.OrgMembership}.orgId`, orgId)
.join(TableName.Users, `${TableName.OrgMembership}.userId`, `${TableName.Users}.id`) .join(TableName.Users, `${TableName.OrgMembership}.userId`, `${TableName.Users}.id`)
.leftJoin<TUserEncryptionKeys>( .leftJoin<TUserEncryptionKeys>(
@ -126,18 +127,18 @@ export const orgDALFactory = (db: TDbClient) => {
`${TableName.Users}.id` `${TableName.Users}.id`
) )
.select( .select(
db.ref("id").withSchema(TableName.OrgMembership), conn.ref("id").withSchema(TableName.OrgMembership),
db.ref("inviteEmail").withSchema(TableName.OrgMembership), conn.ref("inviteEmail").withSchema(TableName.OrgMembership),
db.ref("orgId").withSchema(TableName.OrgMembership), conn.ref("orgId").withSchema(TableName.OrgMembership),
db.ref("role").withSchema(TableName.OrgMembership), conn.ref("role").withSchema(TableName.OrgMembership),
db.ref("roleId").withSchema(TableName.OrgMembership), conn.ref("roleId").withSchema(TableName.OrgMembership),
db.ref("status").withSchema(TableName.OrgMembership), conn.ref("status").withSchema(TableName.OrgMembership),
db.ref("username").withSchema(TableName.Users), conn.ref("username").withSchema(TableName.Users),
db.ref("email").withSchema(TableName.Users), conn.ref("email").withSchema(TableName.Users),
db.ref("firstName").withSchema(TableName.Users), conn.ref("firstName").withSchema(TableName.Users),
db.ref("lastName").withSchema(TableName.Users), conn.ref("lastName").withSchema(TableName.Users),
db.ref("id").withSchema(TableName.Users).as("userId"), conn.ref("id").withSchema(TableName.Users).as("userId"),
db.ref("publicKey").withSchema(TableName.UserEncryptionKey) conn.ref("publicKey").withSchema(TableName.UserEncryptionKey)
) )
.where({ isGhost: false }) .where({ isGhost: false })
.whereIn("username", usernames); .whereIn("username", usernames);

View File

@ -4,9 +4,17 @@ import crypto from "crypto";
import jwt from "jsonwebtoken"; import jwt from "jsonwebtoken";
import { Knex } from "knex"; 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 { TProjects } from "@app/db/schemas/projects";
import { TGroupDALFactory } from "@app/ee/services/group/group-dal"; 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 { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission"; import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; 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 { ActorAuthMethod, ActorType, AuthMethod, AuthTokenType } from "../auth/auth-type";
import { TAuthTokenServiceFactory } from "../auth-token/auth-token-service"; 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 { 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 { TProjectKeyDALFactory } from "../project-key/project-key-dal";
import { TProjectMembershipDALFactory } from "../project-membership/project-membership-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 { SmtpTemplates, TSmtpService } from "../smtp/smtp-service";
import { TUserDALFactory } from "../user/user-dal"; import { TUserDALFactory } from "../user/user-dal";
import { TIncidentContactsDALFactory } from "./incident-contacts-dal"; import { TIncidentContactsDALFactory } from "./incident-contacts-dal";
@ -56,8 +68,11 @@ type TOrgServiceFactoryDep = {
userDAL: TUserDALFactory; userDAL: TUserDALFactory;
groupDAL: TGroupDALFactory; groupDAL: TGroupDALFactory;
projectDAL: TProjectDALFactory; projectDAL: TProjectDALFactory;
projectMembershipDAL: Pick<TProjectMembershipDALFactory, "findProjectMembershipsByUserId" | "delete">; projectMembershipDAL: Pick<
projectKeyDAL: Pick<TProjectKeyDALFactory, "find" | "delete">; TProjectMembershipDALFactory,
"findProjectMembershipsByUserId" | "delete" | "create" | "find" | "insertMany" | "transaction"
>;
projectKeyDAL: Pick<TProjectKeyDALFactory, "find" | "delete" | "insertMany" | "findLatestProjectKey">;
orgMembershipDAL: Pick<TOrgMembershipDALFactory, "findOrgMembershipById" | "findOne">; orgMembershipDAL: Pick<TOrgMembershipDALFactory, "findOrgMembershipById" | "findOne">;
incidentContactDAL: TIncidentContactsDALFactory; incidentContactDAL: TIncidentContactsDALFactory;
samlConfigDAL: Pick<TSamlConfigDALFactory, "findOne" | "findEnforceableSamlCfg">; samlConfigDAL: Pick<TSamlConfigDALFactory, "findOne" | "findEnforceableSamlCfg">;
@ -69,6 +84,9 @@ type TOrgServiceFactoryDep = {
"getPlan" | "updateSubscriptionOrgMemberCount" | "generateOrgCustomerId" | "removeOrgCustomer" "getPlan" | "updateSubscriptionOrgMemberCount" | "generateOrgCustomerId" | "removeOrgCustomer"
>; >;
projectUserAdditionalPrivilegeDAL: Pick<TProjectUserAdditionalPrivilegeDALFactory, "delete">; projectUserAdditionalPrivilegeDAL: Pick<TProjectUserAdditionalPrivilegeDALFactory, "delete">;
userGroupMembershipDAL: Pick<TUserGroupMembershipDALFactory, "findUserGroupMembershipsInProject">;
projectBotDAL: Pick<TProjectBotDALFactory, "findOne">;
projectUserMembershipRoleDAL: Pick<TProjectUserMembershipRoleDALFactory, "insertMany">;
}; };
export type TOrgServiceFactory = ReturnType<typeof orgServiceFactory>; export type TOrgServiceFactory = ReturnType<typeof orgServiceFactory>;
@ -90,7 +108,10 @@ export const orgServiceFactory = ({
tokenService, tokenService,
orgBotDAL, orgBotDAL,
licenseService, licenseService,
samlConfigDAL samlConfigDAL,
userGroupMembershipDAL,
projectBotDAL,
projectUserMembershipRoleDAL
}: TOrgServiceFactoryDep) => { }: TOrgServiceFactoryDep) => {
/* /*
* Get organization details by the organization id * Get organization details by the organization id
@ -420,10 +441,15 @@ export const orgServiceFactory = ({
const inviteUserToOrganization = async ({ const inviteUserToOrganization = async ({
orgId, orgId,
userId, userId,
inviteeEmail, inviteeEmails,
organizationRoleSlug,
projectRoleSlug,
projectIds,
actorAuthMethod, actorAuthMethod,
actorOrgId actorOrgId
}: TInviteUserToOrgDTO) => { }: TInviteUserToOrgDTO) => {
const appCfg = getConfig();
const { permission } = await permissionService.getUserOrgPermission(userId, orgId, actorAuthMethod, actorOrgId); const { permission } = await permissionService.getUserOrgPermission(userId, orgId, actorAuthMethod, actorOrgId);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Member); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Member);
@ -450,98 +476,203 @@ export const orgServiceFactory = ({
}); });
} }
const invitee = await orgDAL.transaction(async (tx) => { if (projectIds?.length) {
const inviteeUser = await userDAL.findUserByUsername(inviteeEmail, tx); const projects = await projectDAL.find({
if (inviteeUser) { orgId,
// if user already exist means its already part of infisical $in: {
// Thus the signup flow is not needed anymore id: projectIds
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 (!inviteeMembership) { // if its not v3, throw an error
await orgDAL.createMembership( if (!verifyProjectVersions(projects, ProjectVersion.V3)) {
{
userId: inviteeUser.id,
inviteEmail: inviteeEmail,
orgId,
role: OrgMembershipRole.Member,
status: OrgMembershipStatus.Invited,
isActive: true
},
tx
);
}
return inviteeUser;
}
const isEmailInvalid = await isDisposableEmail(inviteeEmail);
if (isEmailInvalid) {
throw new BadRequestError({ throw new BadRequestError({
message: "Provided a disposable email", message: "One or more selected projects are not compatible with this operation. Please upgrade your projects."
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: OrgMembershipRole.Member,
status: OrgMembershipStatus.Invited,
isActive: true
},
tx
);
return user;
});
const token = await tokenService.createTokenForUser({ const inviteeUsers = await orgDAL.transaction(async (tx) => {
type: TokenType.TOKEN_EMAIL_ORG_INVITATION, const users: Pick<
userId: invitee.id, TUsers & { orgId: string },
orgId "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 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); await licenseService.updateSubscriptionOrgMemberCount(orgId);
if (!appCfg.isSmtpConfigured) { if (!appCfg.isSmtpConfigured) {
return `${appCfg.SITE_URL}/signupinvite?token=${token}&to=${inviteeEmail}&organization_id=${org?.id}`; return signupTokens;
} }
}; };

View File

@ -1,3 +1,4 @@
import { OrgMembershipRole, ProjectMembershipRole } from "@app/db/schemas";
import { TOrgPermission } from "@app/lib/types"; import { TOrgPermission } from "@app/lib/types";
import { ActorAuthMethod, ActorType } from "../auth/auth-type"; import { ActorAuthMethod, ActorType } from "../auth/auth-type";
@ -29,7 +30,10 @@ export type TInviteUserToOrgDTO = {
orgId: string; orgId: string;
actorOrgId: string | undefined; actorOrgId: string | undefined;
actorAuthMethod: ActorAuthMethod; actorAuthMethod: ActorAuthMethod;
inviteeEmail: string; inviteeEmails: string[];
organizationRoleSlug: OrgMembershipRole;
projectIds?: string[];
projectRoleSlug?: ProjectMembershipRole;
}; };
export type TVerifyUserToOrgDTO = { export type TVerifyUserToOrgDTO = {

View File

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

View File

@ -2,19 +2,12 @@
import { ForbiddenError } from "@casl/ability"; import { ForbiddenError } from "@casl/ability";
import ms from "ms"; import ms from "ms";
import { import { ProjectMembershipRole, ProjectVersion, TableName } from "@app/db/schemas";
ProjectMembershipRole,
ProjectVersion,
SecretKeyEncoding,
TableName,
TProjectMemberships
} from "@app/db/schemas";
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; 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 { TProjectUserAdditionalPrivilegeDALFactory } from "@app/ee/services/project-user-additional-privilege/project-user-additional-privilege-dal";
import { getConfig } from "@app/lib/config/env"; import { getConfig } from "@app/lib/config/env";
import { infisicalSymmetricDecrypt } from "@app/lib/crypto/encryption";
import { BadRequestError } from "@app/lib/errors"; import { BadRequestError } from "@app/lib/errors";
import { groupBy } from "@app/lib/fn"; 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 { TGroupProjectDALFactory } from "../group-project/group-project-dal";
import { TOrgDALFactory } from "../org/org-dal"; import { TOrgDALFactory } from "../org/org-dal";
import { TProjectDALFactory } from "../project/project-dal"; import { TProjectDALFactory } from "../project/project-dal";
import { assignWorkspaceKeysToMembers } from "../project/project-fns";
import { TProjectBotDALFactory } from "../project-bot/project-bot-dal"; import { TProjectBotDALFactory } from "../project-bot/project-bot-dal";
import { TProjectKeyDALFactory } from "../project-key/project-key-dal"; import { TProjectKeyDALFactory } from "../project-key/project-key-dal";
import { TProjectRoleDALFactory } from "../project-role/project-role-dal"; import { TProjectRoleDALFactory } from "../project-role/project-role-dal";
import { SmtpTemplates, TSmtpService } from "../smtp/smtp-service"; import { SmtpTemplates, TSmtpService } from "../smtp/smtp-service";
import { TUserDALFactory } from "../user/user-dal"; import { TUserDALFactory } from "../user/user-dal";
import { TProjectMembershipDALFactory } from "./project-membership-dal"; import { TProjectMembershipDALFactory } from "./project-membership-dal";
import { addMembersToProject } from "./project-membership-fns";
import { import {
ProjectUserMembershipTemporaryMode, ProjectUserMembershipTemporaryMode,
TAddUsersToWorkspaceDTO, TAddUsersToWorkspaceDTO,
@ -53,7 +46,7 @@ type TProjectMembershipServiceFactoryDep = {
userGroupMembershipDAL: TUserGroupMembershipDALFactory; userGroupMembershipDAL: TUserGroupMembershipDALFactory;
projectRoleDAL: Pick<TProjectRoleDALFactory, "find">; projectRoleDAL: Pick<TProjectRoleDALFactory, "find">;
orgDAL: Pick<TOrgDALFactory, "findMembership" | "findOrgMembersByUsername">; orgDAL: Pick<TOrgDALFactory, "findMembership" | "findOrgMembersByUsername">;
projectDAL: Pick<TProjectDALFactory, "findById" | "findProjectGhostUser" | "transaction">; projectDAL: Pick<TProjectDALFactory, "findById" | "findProjectGhostUser" | "transaction" | "findProjectById">;
projectKeyDAL: Pick<TProjectKeyDALFactory, "findLatestProjectKey" | "delete" | "insertMany">; projectKeyDAL: Pick<TProjectKeyDALFactory, "findLatestProjectKey" | "delete" | "insertMany">;
licenseService: Pick<TLicenseServiceFactory, "getPlan">; licenseService: Pick<TLicenseServiceFactory, "getPlan">;
projectUserAdditionalPrivilegeDAL: Pick<TProjectUserAdditionalPrivilegeDALFactory, "delete">; projectUserAdditionalPrivilegeDAL: Pick<TProjectUserAdditionalPrivilegeDALFactory, "delete">;
@ -247,116 +240,23 @@ export const projectMembershipServiceFactory = ({
); );
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Create, ProjectPermissionSub.Member); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Create, ProjectPermissionSub.Member);
const usernamesAndEmails = [...emails, ...usernames]; const members = await addMembersToProject({
orgDAL,
const orgMembers = await orgDAL.findOrgMembersByUsername(project.orgId, [ projectDAL,
...new Set(usernamesAndEmails.map((element) => element.toLowerCase())) projectMembershipDAL,
]); projectKeyDAL,
userGroupMembershipDAL,
if (orgMembers.length !== usernamesAndEmails.length) projectBotDAL,
throw new BadRequestError({ message: "Some users are not part of org" }); projectUserMembershipRoleDAL,
smtpService
if (!orgMembers.length) return []; }).addMembersToNonE2EEProject({
emails,
const existingMembers = await projectMembershipDAL.find({ usernames,
projectId, projectId,
$in: { userId: orgMembers.map(({ user }) => user.id).filter(Boolean) } projectMembershipRole: ProjectMembershipRole.Member,
}); sendEmails
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: 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; return members;
}; };

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

View File

@ -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 { UnpackedPermissionSchema } from "@app/ee/services/identity-project-additional-privilege/identity-project-additional-privilege-service";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { import {
projectAdminPermissions,
projectMemberPermissions,
projectNoAccessPermissions,
ProjectPermissionActions, ProjectPermissionActions,
ProjectPermissionSet, ProjectPermissionSet,
ProjectPermissionSub, ProjectPermissionSub
projectViewerPermission
} from "@app/ee/services/permission/project-permission"; } from "@app/ee/services/permission/project-permission";
import { BadRequestError } from "@app/lib/errors"; import { BadRequestError } from "@app/lib/errors";
@ -20,6 +16,7 @@ import { TIdentityProjectMembershipRoleDALFactory } from "../identity-project/id
import { TProjectDALFactory } from "../project/project-dal"; import { TProjectDALFactory } from "../project/project-dal";
import { TProjectUserMembershipRoleDALFactory } from "../project-membership/project-user-membership-role-dal"; import { TProjectUserMembershipRoleDALFactory } from "../project-membership/project-user-membership-role-dal";
import { TProjectRoleDALFactory } from "./project-role-dal"; import { TProjectRoleDALFactory } from "./project-role-dal";
import { getPredefinedRoles } from "./project-role-fns";
import { TCreateRoleDTO, TDeleteRoleDTO, TGetRoleBySlugDTO, TListRolesDTO, TUpdateRoleDTO } from "./project-role-types"; import { TCreateRoleDTO, TDeleteRoleDTO, TGetRoleBySlugDTO, TListRolesDTO, TUpdateRoleDTO } from "./project-role-types";
type TProjectRoleServiceFactoryDep = { type TProjectRoleServiceFactoryDep = {
@ -37,51 +34,6 @@ const unpackPermissions = (permissions: unknown) =>
unpackRules((permissions || []) as PackRule<RawRuleOf<MongoAbility<ProjectPermissionSet>>>[]) 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 = ({ export const projectRoleServiceFactory = ({
projectRoleDAL, projectRoleDAL,
permissionService, permissionService,

View File

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

View File

@ -1,5 +1,6 @@
import crypto from "crypto"; import crypto from "crypto";
import { ProjectVersion, TProjects } from "@app/db/schemas";
import { decryptAsymmetric, encryptAsymmetric } from "@app/lib/crypto"; import { decryptAsymmetric, encryptAsymmetric } from "@app/lib/crypto";
import { BadRequestError } from "@app/lib/errors"; import { BadRequestError } from "@app/lib/errors";
import { TKmsServiceFactory } from "@app/services/kms/kms-service"; import { TKmsServiceFactory } from "@app/services/kms/kms-service";
@ -53,6 +54,16 @@ export const createProjectKey = ({ publicKey, privateKey, plainProjectKey }: TCr
return { key: encryptedProjectKey, iv: encryptedProjectKeyIv }; 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 ({ export const getProjectKmsCertificateKeyId = async ({
projectId, projectId,
projectDAL, projectDAL,

View File

@ -10,6 +10,7 @@ import { TKeyStoreFactory } from "@app/keystore/keystore";
import { isAtLeastAsPrivileged } from "@app/lib/casl"; import { isAtLeastAsPrivileged } from "@app/lib/casl";
import { infisicalSymmetricEncypt } from "@app/lib/crypto/encryption"; import { infisicalSymmetricEncypt } from "@app/lib/crypto/encryption";
import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors"; import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
import { groupBy } from "@app/lib/fn";
import { alphaNumericNanoId } from "@app/lib/nanoid"; import { alphaNumericNanoId } from "@app/lib/nanoid";
import { TProjectPermission } from "@app/lib/types"; 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 { TProjectKeyDALFactory } from "../project-key/project-key-dal";
import { TProjectMembershipDALFactory } from "../project-membership/project-membership-dal"; import { TProjectMembershipDALFactory } from "../project-membership/project-membership-dal";
import { TProjectUserMembershipRoleDALFactory } from "../project-membership/project-user-membership-role-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 { ROOT_FOLDER_NAME, TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal";
import { TUserDALFactory } from "../user/user-dal"; import { TUserDALFactory } from "../user/user-dal";
import { TProjectDALFactory } from "./project-dal"; import { TProjectDALFactory } from "./project-dal";
@ -44,6 +47,7 @@ import {
TListProjectCasDTO, TListProjectCasDTO,
TListProjectCertificateTemplatesDTO, TListProjectCertificateTemplatesDTO,
TListProjectCertsDTO, TListProjectCertsDTO,
TListProjectsDTO,
TLoadProjectKmsBackupDTO, TLoadProjectKmsBackupDTO,
TToggleProjectAutoCapitalizationDTO, TToggleProjectAutoCapitalizationDTO,
TUpdateAuditLogsRetentionDTO, TUpdateAuditLogsRetentionDTO,
@ -84,6 +88,7 @@ type TProjectServiceFactoryDep = {
orgDAL: Pick<TOrgDALFactory, "findOne">; orgDAL: Pick<TOrgDALFactory, "findOne">;
keyStore: Pick<TKeyStoreFactory, "deleteItem">; keyStore: Pick<TKeyStoreFactory, "deleteItem">;
projectBotDAL: Pick<TProjectBotDALFactory, "create">; projectBotDAL: Pick<TProjectBotDALFactory, "create">;
projectRoleDAL: Pick<TProjectRoleDALFactory, "find">;
kmsService: Pick< kmsService: Pick<
TKmsServiceFactory, TKmsServiceFactory,
| "updateProjectSecretManagerKmsKey" | "updateProjectSecretManagerKmsKey"
@ -112,6 +117,7 @@ export const projectServiceFactory = ({
projectEnvDAL, projectEnvDAL,
licenseService, licenseService,
projectUserMembershipRoleDAL, projectUserMembershipRoleDAL,
projectRoleDAL,
identityProjectMembershipRoleDAL, identityProjectMembershipRoleDAL,
certificateAuthorityDAL, certificateAuthorityDAL,
certificateDAL, certificateDAL,
@ -389,8 +395,34 @@ export const projectServiceFactory = ({
return deletedProject; return deletedProject;
}; };
const getProjects = async (actorId: string) => { const getProjects = async ({ actorId, includeRoles, actorAuthMethod, actorOrgId }: TListProjectsDTO) => {
const workspaces = await projectDAL.findAllProjects(actorId); 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; return workspaces;
}; };

View File

@ -75,6 +75,10 @@ export type TDeleteProjectDTO = {
actorOrgId: string | undefined; actorOrgId: string | undefined;
} & Omit<TProjectPermission, "projectId">; } & Omit<TProjectPermission, "projectId">;
export type TListProjectsDTO = {
includeRoles: boolean;
} & Omit<TProjectPermission, "projectId">;
export type TUpgradeProjectDTO = { export type TUpgradeProjectDTO = {
userPrivateKey: string; userPrivateKey: string;
} & TProjectPermission; } & TProjectPermission;

View File

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

View File

@ -9,7 +9,7 @@
<body> <body>
<h2>Join your organization on Infisical</h2> <h2>Join your organization on Infisical</h2>
<p>{{inviterFirstName}} ({{inviterUsername}}) has invited you to their Infisical organization — {{organizationName}}</p> <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> <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> <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> </body>

View File

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

View File

@ -100,7 +100,9 @@ export type TIntegrationCreatedEvent = {
export type TUserOrgInvitedEvent = { export type TUserOrgInvitedEvent = {
event: PostHogEventTypes.UserOrgInvitation; event: PostHogEventTypes.UserOrgInvitation;
properties: { properties: {
inviteeEmail: string; inviteeEmails: string[];
projectIds?: string[];
organizationRoleSlug?: string;
}; };
}; };

View File

@ -11,25 +11,12 @@ sinks:
config: config:
path: "access-token" path: "access-token"
templates: templates:
- template-content: | - source-path: my-dot-ev-secret-template
{{- with secret "202f04d7-e4cb-43d4-a292-e893712d61fc" "dev" "/" }}
{{- range . }}
{{ .Key }}={{ .Value }}
{{- end }}
{{- end }}
destination-path: my-dot-env-0.env
config:
polling-interval: 60s
execute:
command: docker-compose -f docker-compose.prod.yml down && docker-compose -f docker-compose.prod.yml up -d
- base64-template-content: e3stIHdpdGggc2VjcmV0ICIyMDJmMDRkNy1lNGNiLTQzZDQtYTI5Mi1lODkzNzEyZDYxZmMiICJkZXYiICIvIiB9fQp7ey0gcmFuZ2UgLiB9fQp7eyAuS2V5IH19PXt7IC5WYWx1ZSB9fQp7ey0gZW5kIH19Cnt7LSBlbmQgfX0=
destination-path: my-dot-env.env destination-path: my-dot-env.env
config: config:
polling-interval: 60s polling-interval: 60s
execute: execute:
command: docker-compose -f docker-compose.prod.yml down && docker-compose -f docker-compose.prod.yml up -d command: docker-compose -f docker-compose.prod.yml down && docker-compose -f docker-compose.prod.yml up -d
- source-path: my-dot-ev-secret-template1 - source-path: my-dot-ev-secret-template1
destination-path: my-dot-env-1.env destination-path: my-dot-env-1.env
config: config:

View File

@ -95,7 +95,6 @@ type Template struct {
SourcePath string `yaml:"source-path"` SourcePath string `yaml:"source-path"`
Base64TemplateContent string `yaml:"base64-template-content"` Base64TemplateContent string `yaml:"base64-template-content"`
DestinationPath string `yaml:"destination-path"` DestinationPath string `yaml:"destination-path"`
TemplateContent string `yaml:"template-content"`
Config struct { // Configurations for the template Config struct { // Configurations for the template
PollingInterval string `yaml:"polling-interval"` // How often to poll for changes in the secret PollingInterval string `yaml:"polling-interval"` // How often to poll for changes in the secret
@ -433,30 +432,6 @@ func ProcessBase64Template(templateId int, encodedTemplate string, data interfac
return &buf, nil return &buf, nil
} }
func ProcessLiteralTemplate(templateId int, templateString string, data interface{}, accessToken string, existingEtag string, currentEtag *string, dynamicSecretLeaser *DynamicSecretLeaseManager) (*bytes.Buffer, error) {
secretFunction := secretTemplateFunction(accessToken, existingEtag, currentEtag) // TODO: Fix this
dynamicSecretFunction := dynamicSecretTemplateFunction(accessToken, dynamicSecretLeaser, templateId)
funcs := template.FuncMap{
"secret": secretFunction,
"dynamic_secret": dynamicSecretFunction,
}
templateName := "literalTemplate"
tmpl, err := template.New(templateName).Funcs(funcs).Parse(templateString)
if err != nil {
return nil, err
}
var buf bytes.Buffer
if err := tmpl.Execute(&buf, data); err != nil {
return nil, err
}
return &buf, nil
}
type AgentManager struct { type AgentManager struct {
accessToken string accessToken string
accessTokenTTL time.Duration accessTokenTTL time.Duration
@ -845,8 +820,6 @@ func (tm *AgentManager) MonitorSecretChanges(secretTemplate Template, templateId
if secretTemplate.SourcePath != "" { if secretTemplate.SourcePath != "" {
processedTemplate, err = ProcessTemplate(templateId, secretTemplate.SourcePath, nil, token, existingEtag, &currentEtag, tm.dynamicSecretLeases) processedTemplate, err = ProcessTemplate(templateId, secretTemplate.SourcePath, nil, token, existingEtag, &currentEtag, tm.dynamicSecretLeases)
} else if secretTemplate.TemplateContent != "" {
processedTemplate, err = ProcessLiteralTemplate(templateId, secretTemplate.TemplateContent, nil, token, existingEtag, &currentEtag, tm.dynamicSecretLeases)
} else { } else {
processedTemplate, err = ProcessBase64Template(templateId, secretTemplate.Base64TemplateContent, nil, token, existingEtag, &currentEtag, tm.dynamicSecretLeases) processedTemplate, err = ProcessBase64Template(templateId, secretTemplate.Base64TemplateContent, nil, token, existingEtag, &currentEtag, tm.dynamicSecretLeases)
} }

View File

@ -8,6 +8,7 @@ import (
"encoding/hex" "encoding/hex"
"encoding/json" "encoding/json"
"os" "os"
"slices"
"strings" "strings"
"time" "time"
@ -152,6 +153,28 @@ var loginCmd = &cobra.Command{
DisableFlagsInUseLine: true, DisableFlagsInUseLine: true,
Run: func(cmd *cobra.Command, args []string) { 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{ infisicalClient := infisicalSdk.NewInfisicalClient(infisicalSdk.Config{
SiteUrl: config.INFISICAL_URL, SiteUrl: config.INFISICAL_URL,
UserAgent: api.USER_AGENT, UserAgent: api.USER_AGENT,
@ -464,6 +487,7 @@ func cliDefaultLogin(userCredentialsToBeStored *models.UserCredentials) {
func init() { func init() {
rootCmd.AddCommand(loginCmd) 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().BoolP("interactive", "i", false, "login via the command line")
loginCmd.Flags().String("method", "user", "login method [user, universal-auth]") loginCmd.Flags().String("method", "user", "login method [user, universal-auth]")
loginCmd.Flags().Bool("plain", false, "only output the token without any formatting") loginCmd.Flags().Bool("plain", false, "only output the token without any formatting")
@ -499,10 +523,12 @@ func DomainOverridePrompt() (bool, error) {
} }
func askForDomain() 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 ( const (
INFISICAL_CLOUD = "Infisical Cloud" INFISICAL_CLOUD = "Infisical Cloud"
SELF_HOSTING = "Self Hosting" SELF_HOSTING = "Self Hosting"
ADD_NEW_DOMAIN = "Add a new domain"
) )
options := []string{INFISICAL_CLOUD, SELF_HOSTING} options := []string{INFISICAL_CLOUD, SELF_HOSTING}
@ -524,6 +550,36 @@ func askForDomain() error {
return nil 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 { urlValidation := func(input string) error {
_, err := url.ParseRequestURI(input) _, err := url.ParseRequestURI(input)
if err != nil { if err != nil {
@ -542,12 +598,23 @@ func askForDomain() error {
if err != nil { if err != nil {
return err 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, "/") domain = strings.TrimRight(domain, "/")
//set api and login url
config.INFISICAL_URL = fmt.Sprintf("%s/api", domain) config.INFISICAL_URL = fmt.Sprintf("%s/api", domain)
config.INFISICAL_LOGIN_URL = fmt.Sprintf("%s/login", 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 return nil
} }

View File

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

View File

@ -7,6 +7,7 @@ services:
restart: always restart: always
ports: ports:
- 8080:80 - 8080:80
- 8443:443
volumes: volumes:
- ./nginx/default.dev.conf:/etc/nginx/conf.d/default.conf:ro - ./nginx/default.dev.conf:/etc/nginx/conf.d/default.conf:ro
depends_on: depends_on:

View File

@ -0,0 +1,57 @@
---
title: "Enrollment over Secure Transport (EST)"
sidebarTitle: "Enrollment over Secure Transport (EST)"
description: "Learn how to manage certificate enrollment of clients using EST"
---
## Concept
Enrollment over Secure Transport (EST) is a protocol used to automate the secure provisioning of digital certificates for devices and applications over a secure HTTPS connection. It is primarily used when a client device needs to obtain or renew a certificate from a Certificate Authority (CA) on Infisical in a secure and standardized manner. EST is commonly employed in environments requiring strong authentication and encrypted communication, such as in IoT, enterprise networks, and secure web services.
Infisical's EST service is based on [RFC 7030](https://datatracker.ietf.org/doc/html/rfc7030) and implements the following endpoints:
- **cacerts** - provides the necessary CA chain for the client to validate certificates issued by the CA.
- **simpleenroll** - allows an EST client to request a new certificate from Infisical's EST server
- **simplereenroll** - similar to the /simpleenroll endpoint but is used for renewing an existing certificate.
These endpoints are exposed on port 8443 under the .well-known/est path e.g.
`https://app.infisical.com:8443/.well-known/est/estLabel/cacerts`
## Prerequisites
- You need to have an existing [CA hierarchy](/documentation/platform/pki/private-ca).
- The client devices need to have a bootstrap/pre-installed certificate.
- The client devices must trust the server certificates used by Infisical's EST server. If the devices are new or lack existing trust configurations, you need to manually establish trust for the appropriate certificates. When using Infisical Cloud, this means establishing trust for certificates issued by AWS.
## Guide to configuring EST
1. Set up a certificate template with your selected issuing CA. This template will define the policies and parameters for certificates issued through EST. For detailed instructions on configuring a certificate template, refer to the certificate templates [documentation](/documentation/platform/pki/certificate-templates).
2. Proceed to the certificate template's enrollment settings
![est enrollment dashboard](/images/platform/pki/est/template-enroll-hover.png)
3. Select **EST** as the client enrollment method and fill up the remaining fields.
![est enrollment modal create](/images/platform/pki/est/template-enrollment-modal.png)
- **Certificate Authority Chain** - This is the certificate chain used to validate your devices' manufacturing/pre-installed certificates. This will be used to authenticate your devices with Infisical's EST server.
- **Passphrase** - This is also used to authenticate your devices with Infisical's EST server. When configuring the clients, use the value defined here as the EST password.
For security reasons, Infisical authenticates EST clients using both client certificate and passphrase.
4. Once the configuration of enrollment options is completed, a new **EST Label** field appears in the enrollment settings. This is the value to use as label in the URL when configuring the connection of EST clients to Infisical.
![est enrollment modal create](/images/platform/pki/est/template-enrollment-est-label.png)
For demonstration, the complete URL of the supported EST endpoints will look like the following:
- https://app.infisical.com:8443/.well-known/est/f110f308-9888-40ab-b228-237b12de8b96/cacerts
- https://app.infisical.com:8443/.well-known/est/f110f308-9888-40ab-b228-237b12de8b96/simpleenroll
- https://app.infisical.com:8443/.well-known/est/f110f308-9888-40ab-b228-237b12de8b96/simplereenroll
## Setting up EST clients
- To use the EST passphrase in your clients, configure it as the EST password. The EST username can be set to any arbitrary value.
- Use the appropriate client certificates for invoking the EST endpoints.
- For `simpleenroll`, use the bootstrapped/manufacturer client certificate.
- For `simplereenroll`, use a valid EST-issued client certificate.
- When configuring the PKCS#12 objects for the client certificates, only include the leaf certificate and the private key.

Binary file not shown.

After

Width:  |  Height:  |  Size: 136 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 197 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 192 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 111 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 93 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 723 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 652 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 612 KiB

View File

@ -0,0 +1,55 @@
---
title: "Azure DevOps"
description: "How to sync secrets from Infisical to Azure DevOps"
---
### Usage
Prerequisites:
- Set up and add envars to [Infisical Cloud](https://app.infisical.com).
- Create a new [Azure DevOps](https://dev.azure.com) project if you don't have one already.
#### Create a new Azure DevOps personal access token (PAT)
You'll need to create a new personal access token (PAT) in order to authenticate Infisical with Azure DevOps.
<Steps>
<Step title="Navigate to Azure DevOps">
![integrations](../../images/integrations/azure-devops/overview-page.png)
</Step>
<Step title="Create a new token">
Make sure the newly created token has Read/Write access to the Release scope.
![integrations](../../images/integrations/azure-devops/create-new-token.png)
<Note>
Please make sure that the token has access to the following scopes: Variable Groups _(read/write)_, Release _(read/write)_, Project and Team _(read)_, Service Connections _(read & query)_
</Note>
</Step>
<Step title="Copy the new access token">
Copy the newly created token as this will be used to authenticate Infisical with Azure DevOps.
![integrations](../../images/integrations/azure-devops/new-token-created.png)
</Step>
</Steps>
#### Setup the Infisical Azure DevOps integration
Navigate to your project's integrations tab and select the 'Azure DevOps' integration.
![integrations](../../images/integrations.png)
<Steps>
<Step title="Authorize Infisical for Azure DevOps">
Enter your credentials that you obtained from the previous step.
1. Azure DevOps API token is the personal access token (PAT) you created in the previous step.
2. Azure DevOps organization name is the name of your Azure DevOps organization.
![integrations](../../images/integrations/azure-devops/new-infiscial-integration-step-1.png)
</Step>
<Step title="Configure the integration">
Select Infisical project and secret path you want to sync into Azure DevOps.
Finally, press create integration to start syncing secrets to Azure DevOps.
![integrations](../../images/integrations/azure-devops/new-infiscial-integration-step-2.png)
</Step>
</Steps>
Now you have successfully integrated Infisical with Azure DevOps. Your existing and future secret changes will automatically sync to Azure DevOps.
You can view your secrets by navigating to your Azure DevOps project and selecting the 'Library' tab under 'Pipelines' in the 'Library' section.

View File

@ -1,30 +1,27 @@
--- ---
title: "Amazon ECS" title: 'Amazon ECS'
description: "Learn how to deliver secrets to Amazon Elastic Container Service." description: "Learn how to deliver secrets to Amazon Elastic Container Service."
--- ---
![ecs diagram](/images/guides/agent-with-ecs/ecs-diagram.png) ![ecs diagram](/images/guides/agent-with-ecs/ecs-diagram.png)
This guide will go over the steps needed to access secrets stored in Infisical from Amazon Elastic Container Service (ECS). This guide will go over the steps needed to access secrets stored in Infisical from Amazon Elastic Container Service (ECS).
At a high level, the steps involve setting up an ECS task with an [Infisical Agent](/infisical-agent/overview) as a sidecar container. This sidecar container uses [AWS Auth](/documentation/platform/identities/aws-auth) to authenticate with Infisical to fetch secrets/access tokens. At a high level, the steps involve setting up an ECS task with a [Infisical Agent](/infisical-agent/overview) as a sidecar container. This sidecar container uses [Universal Auth](/documentation/platform/identities/universal-auth) to authenticate with Infisical to fetch secrets/access tokens.
Once the secrets/access tokens are retrieved, they are then stored in a shared [Amazon Elastic File System](https://aws.amazon.com/efs/) (EFS) volume. This volume is then made accessible to your application and all of its replicas. Once the secrets/access tokens are retrieved, they are then stored in a shared [Amazon Elastic File System](https://aws.amazon.com/efs/) (EFS) volume. This volume is then made accessible to your application and all of its replicas.
This guide primarily focuses on integrating Infisical Cloud with Amazon ECS on AWS Fargate and Amazon EFS. This guide primarily focuses on integrating Infisical Cloud with Amazon ECS on AWS Fargate and Amazon EFS.
However, the principles and steps can be adapted for use with any instance of Infisical (on premise or cloud) and different ECS launch configurations. However, the principles and steps can be adapted for use with any instance of Infisical (on premise or cloud) and different ECS launch configurations.
## Prerequisites ## Prerequisites
This guide requires the following prerequisites: This guide requires the following prerequisites:
- Infisical account
- Infisical account
- Git installed - Git installed
- Terraform v1.0 or later installed - Terraform v1.0 or later installed
- Access to AWS credentials - Access to AWS credentials
- Understanding of [Infisical Agent](/infisical-agent/overview) - Understanding of [Infisical Agent](/infisical-agent/overview)
## What we will deploy ## What we will deploy
For this demonstration, we'll deploy the [File Browser](https://github.com/filebrowser/filebrowser) application on our ECS cluster. For this demonstration, we'll deploy the [File Browser](https://github.com/filebrowser/filebrowser) application on our ECS cluster.
Although this guide focuses on File Browser, the principles outlined here can be applied to any application of your choice. Although this guide focuses on File Browser, the principles outlined here can be applied to any application of your choice.
@ -32,20 +29,20 @@ File Browser plays a key role in this context because it enables us to view all
This feature is important for our demonstration, as it allows us to verify whether the Infisical agent is depositing the expected files into the designated file volume and if those files are accessible to the application. This feature is important for our demonstration, as it allows us to verify whether the Infisical agent is depositing the expected files into the designated file volume and if those files are accessible to the application.
<Warning> <Warning>
Volumes that contain sensitive secrets should not be publicly accessible. The Volumes that contain sensitive secrets should not be publicly accessible. The use of File Browser here is solely for demonstration and verification purposes.
use of File Browser here is solely for demonstration and verification
purposes.
</Warning> </Warning>
## Configure Authentication with Infisical
In order for the Infisical agent to fetch credentials from Infisical, we'll first need to authenticate with Infisical. Follow the documentation to configure a machine identity with AWS Auth [here](/documentation/platform/identities/aws-auth). ## Configure Authentication with Infisical
Take note of the Machine Identity ID as you will be needing this in the preceding steps. In order for the Infisical agent to fetch credentials from Infisical, we'll first need to authenticate with Infisical.
While Infisical supports various authentication methods, this guide focuses on using Universal Auth to authenticate the agent with Infisical.
Follow the documentation to configure and generate a client id and client secret with Universal auth [here](/documentation/platform/identities/universal-auth).
Make sure to save these credentials somewhere handy because you'll need them soon.
## Clone guide assets repository ## Clone guide assets repository
To help you quickly deploy the example application, please clone the guide assets from this [Github repository](https://github.com/Infisical/infisical-guides.git). To help you quickly deploy the example application, please clone the guide assets from this [Github repository](https://github.com/Infisical/infisical-guides.git).
This repository contains assets for all Infisical guides. The content for this guide can be found within a sub directory called `aws-ecs-with-agent`. This repository contains assets for all Infisical guides. The content for this guide can be found within a sub directory called `aws-ecs-with-agent`.
The guide will assume that `aws-ecs-with-agent` is your working directory going forward. The guide will assume that `aws-ecs-with-agent` is your working directory going forward.
## Deploy example application ## Deploy example application
@ -53,84 +50,95 @@ The guide will assume that `aws-ecs-with-agent` is your working directory going
Before we can deploy our full application and its related infrastructure with Terraform, we'll need to first configure our Infisical agent. Before we can deploy our full application and its related infrastructure with Terraform, we'll need to first configure our Infisical agent.
### Agent configuration overview ### Agent configuration overview
The agent config file defines what authentication method will be used when connecting with Infisical along with where the fetched secrets/access tokens should be saved to. The agent config file defines what authentication method will be used when connecting with Infisical along with where the fetched secrets/access tokens should be saved to.
Since the Infisical agent will be deployed as a sidecar, the agent configuration file will need to be encoded in base64. Since the Infisical agent will be deployed as a sidecar, the agent configuration file and any secret template files will need to be encoded in base64.
This encoding step is necessary as it allows the agent configuration file to be added into our Terraform configuration without needing to upload it first. This encoding step is necessary as it allows these files to be added into our Terraform configuration file without needing to upload them first.
#### Secret template file
The Infisical agent accepts one or more optional template files. If provided, the agent will fetch secrets using the set authentication method and format the fetched secrets according to the given template file.
For demonstration purposes, we will create the following secret template file.
This template will transform our secrets from Infisical project with the ID `62fd92aa8b63973fee23dec7`, in the `dev` environment, and secrets located in the path `/`, into a `KEY=VALUE` format.
<Tip>
Remember to update the project id, environment slug and secret path to one that exists within your Infisical project
</Tip>
```secrets.template secrets.template
{{- with secret "62fd92aa8b63973fee23dec7" "dev" "/" }}
{{- range . }}
{{ .Key }}={{ .Value }}
{{- end }}
{{- end }}
```
Next, we need encode this template file in `base64` so it can be set in the agent configuration file.
```bash
cat secrets.template | base64
Cnt7LSB3aXRoIHNlY3JldCAiMWVkMjk2MWQtNDM5NS00MmNlLTlkNzQtYjk2ZGQwYmYzMDg0IiAiZGV2IiAiLyIgfX0Ke3stIHJhbmdlIC4gfX0Ke3sgLktleSB9fT17eyAuVmFsdWUgfX0Ke3stIGVuZCB9fQp7ey0gZW5kIH19
```
#### Full agent configuration file #### Full agent configuration file
This agent config file will connect with Infisical Cloud using Universal Auth and deposit access tokens at path `/infisical-agent/access-token` and render secrets to file `/infisical-agent/secrets`.
Inside the `aws-ecs-with-agent` directory, you will find a sample `agent-config.yaml` file. This agent config file will connect with Infisical Cloud using AWS Auth and deposit access tokens at path `/infisical-agent/access-token` and render secrets to file `/infisical-agent/secrets`. You'll notice that instead of passing the path to the secret template file as we normally would, we set the base64 encoded template from the previous step under `base64-template-content` property.
```yaml agent-config.yaml ```yaml agent-config.yaml
infisical: infisical:
address: https://app.infisical.com address: https://app.infisical.com
exit-after-auth: true exit-after-auth: true
auth: auth:
type: aws-iam type: universal-auth
config:
remove_client_secret_on_read: false
sinks: sinks:
- type: file - type: file
config: config:
path: /infisical-agent/access-token path: /infisical-agent/access-token
templates: templates:
- template-content: | - base64-template-content: Cnt7LSB3aXRoIHNlY3JldCAiMWVkMjk2MWQtNDM5NS00MmNlLTlkNzQtYjk2ZGQwYmYzMDg0IiAiZGV2IiAiLyIgfX0Ke3stIHJhbmdlIC4gfX0Ke3sgLktleSB9fT17eyAuVmFsdWUgfX0Ke3stIGVuZCB9fQp7ey0gZW5kIH19
{{- with secret "202f04d7-e4cb-43d4-a292-e893712d61fc" "dev" "/" }}
{{- range . }}
{{ .Key }}={{ .Value }}
{{- end }}
{{- end }}
destination-path: /infisical-agent/secrets destination-path: /infisical-agent/secrets
``` ```
#### Secret template Again, we'll need to encode the full configuration file in `base64` so it can be easily delivered via Terraform.
The Infisical agent accepts one or more optional templates. If provided, the agent will fetch secrets using the set authentication method and format the fetched secrets according to the given template. ```bash
Typically, these templates are passed in to the agent configuration file via file reference using the `source-path` property but for simplicity we define them inline. cat agent-config.yaml | base64
aW5maXNpY2FsOgogIGFkZHJlc3M6IGh0dHBzOi8vYXBwLmluZmlzaWNhbC5jb20KICBleGl0LWFmdGVyLWF1dGg6IHRydWUKYXV0aDoKICB0eXBlOiB1bml2ZXJzYWwtYXV0aAogIGNvbmZpZzoKICAgIHJlbW92ZV9jbGllbnRfc2VjcmV0X29uX3JlYWQ6IGZhbHNlCnNpbmtzOgogIC0gdHlwZTogZmlsZQogICAgY29uZmlnOgogICAgICBwYXRoOiAvaW5maXNpY2FsLWFnZW50L2FjY2Vzcy10b2tlbgp0ZW1wbGF0ZXM6CiAgLSBiYXNlNjQtdGVtcGxhdGUtY29udGVudDogQ250N0xTQjNhWFJvSUhObFkzSmxkQ0FpTVdWa01qazJNV1F0TkRNNU5TMDBNbU5sTFRsa056UXRZamsyWkdRd1ltWXpNRGcwSWlBaVpHVjJJaUFpTHlJZ2ZYMEtlM3N0SUhKaGJtZGxJQzRnZlgwS2Uzc2dMa3RsZVNCOWZUMTdleUF1Vm1Gc2RXVWdmWDBLZTNzdElHVnVaQ0I5ZlFwN2V5MGdaVzVrSUgxOQogICAgZGVzdGluYXRpb24tcGF0aDogL2luZmlzaWNhbC1hZ2VudC9zZWNyZXRzCg==
```
In the agent configuration above, the template defined will transform the secrets from Infisical project with the ID `202f04d7-e4cb-43d4-a292-e893712d61fc`, in the `dev` environment, and secrets located in the path `/`, into a `KEY=VALUE` format. ## Add auth credentials & agent config
With the base64 encoded agent config file and Universal Auth credentials in hand, it's time to assign them as values in our Terraform config file.
<Tip> To configure these values, navigate to the `ecs.tf` file in your preferred code editor and assign values to `auth_client_id`, `auth_client_secret`, and `agent_config`.
Remember to update the project id, environment slug and secret path to one
that exists within your Infisical project
</Tip>
## Configure app on terraform
Navigate to the `ecs.tf` file in your preferred code editor. In the container_definitions section, assign the values to the `machine_identity_id` and `agent_config` properties.
The `agent_config` property expects the base64-encoded agent configuration file. In order to get this, we use the `base64encode` and `file` functions of HCL.
```hcl ecs.tf ```hcl ecs.tf
...snip... ...snip...
resource "aws_ecs_task_definition" "app" { data "template_file" "cb_app" {
family = "cb-app-task" template = file("./templates/ecs/cb_app.json.tpl")
execution_role_arn = aws_iam_role.ecs_task_execution_role.arn
task_role_arn = aws_iam_role.ecs_task_role.arn vars = {
network_mode = "awsvpc" app_image = var.app_image
requires_compatibilities = ["FARGATE"] sidecar_image = var.sidecar_image
cpu = 4096 app_port = var.app_port
memory = 8192 fargate_cpu = var.fargate_cpu
container_definitions = templatefile("./templates/ecs/cb_app.json.tpl", { fargate_memory = var.fargate_memory
app_image = var.app_image aws_region = var.aws_region
sidecar_image = var.sidecar_image auth_client_id = "<paste-client-id-string>"
app_port = var.app_port auth_client_secret = "<paset-client-secret-string>"
fargate_cpu = var.fargate_cpu agent_config = "<paste-base64-encoded-agent-config-string>"
fargate_memory = var.fargate_memory
aws_region = var.aws_region
machine_identity_id = "5655f4f5-332b-45f9-af06-8f493edff36f"
agent_config = base64encode(file("../agent-config.yaml"))
})
volume {
name = "infisical-efs"
efs_volume_configuration {
file_system_id = aws_efs_file_system.infisical_efs.id
root_directory = "/"
}
} }
} }
...snip... ...snip...
``` ```
<Warning>
To keep this guide simple, `auth_client_id`, `auth_client_secret` have been added directly into the ECS container definition.
However, in production, you should securely fetch these values from AWS Secrets Manager or AWS Parameter store and feed them directly to agent sidecar.
</Warning>
After these values have been set, they will be passed to the Infisical agent during startup through environment variables, as configured in the `infisical-sidecar` container below. After these values have been set, they will be passed to the Infisical agent during startup through environment variables, as configured in the `infisical-sidecar` container below.
```terraform templates/ecs/cb_app.json.tpl ```terraform templates/ecs/cb_app.json.tpl
@ -161,8 +169,12 @@ After these values have been set, they will be passed to the Infisical agent dur
}, },
"environment": [ "environment": [
{ {
"name": "INFISICAL_MACHINE_IDENTITY_ID", "name": "INFISICAL_UNIVERSAL_AUTH_CLIENT_ID",
"value": "${machine_identity_id}" "value": "${auth_client_id}"
},
{
"name": "INFISICAL_UNIVERSAL_CLIENT_SECRET",
"value": "${auth_client_secret}"
}, },
{ {
"name": "INFISICAL_AGENT_CONFIG_BASE64", "name": "INFISICAL_AGENT_CONFIG_BASE64",
@ -179,9 +191,9 @@ After these values have been set, they will be passed to the Infisical agent dur
] ]
``` ```
In the above container definition, you'll notice that that the Infisical agent has a `mountPoints` defined. In the above container definition, you'll notice that that the Infisical agent has a `mountPoints` defined.
This mount point is referencing to the already configured EFS volume as shown below. This mount point is referencing to the already configured EFS volume as shown below.
`containerPath` is set to `/infisical-agent` because that is that the folder we have instructed the agent to deposit the credentials to. `containerPath` is set to `/infisical-agent` because that is that the folder we have instructed the agent to deposit the credentials to.
```hcl terraform/efs.tf ```hcl terraform/efs.tf
resource "aws_efs_file_system" "infisical_efs" { resource "aws_efs_file_system" "infisical_efs" {
@ -199,9 +211,8 @@ resource "aws_efs_mount_target" "mount" {
``` ```
## Configure AWS credentials ## Configure AWS credentials
Because we'll be deploying the example file browser application to AWS via Terraform, you will need to obtain a set of `AWS Access Key` and `Secret Key`. Because we'll be deploying the example file browser application to AWS via Terraform, you will need to obtain a set of `AWS Access Key` and `Secret Key`.
Once you have generated these credentials, export them to your terminal. Once you have generated these credentials, export them to your terminal.
1. Export the AWS Access Key ID: 1. Export the AWS Access Key ID:
@ -216,31 +227,26 @@ Once you have generated these credentials, export them to your terminal.
``` ```
## Deploy terraform configuration ## Deploy terraform configuration
With the agent's sidecar configuration complete, we can now deploy our changes to AWS via Terraform. With the agent's sidecar configuration complete, we can now deploy our changes to AWS via Terraform.
1. Change your directory to `terraform` 1. Change your directory to `terraform`
```sh
```sh
cd terraform cd terraform
``` ```
2. Initialize Terraform 2. Initialize Terraform
``` ```
$ terraform init $ terraform init
``` ```
3. Preview resources that will be created 3. Preview resources that will be created
``` ```
$ terraform plan $ terraform plan
``` ```
4. Trigger resource creation 4. Trigger resource creation
```bash ```bash
$ terraform apply $ terraform apply
Do you want to perform these actions? Do you want to perform these actions?
Terraform will perform the actions described above. Terraform will perform the actions described above.
@ -258,24 +264,24 @@ Outputs:
alb_hostname = "cb-load-balancer-1675475779.us-east-1.elb.amazonaws.com:8080" alb_hostname = "cb-load-balancer-1675475779.us-east-1.elb.amazonaws.com:8080"
``` ```
Once the resources have been successfully deployed, Terraform will output the host address where the file browser application will be accessible. Once the resources have been successfully deployed, Terrafrom will output the host address where the file browser application will be accessible.
It may take a few minutes for the application to become fully ready. It may take a few minutes for the application to become fully ready.
## Verify secrets/tokens in EFS volume ## Verify secrets/tokens in EFS volume
To verify that the agent is depositing access tokens and rendering secrets to the paths specified in the agent config, navigate to the web address from the previous step. To verify that the agent is depositing access tokens and rendering secrets to the paths specified in the agent config, navigate to the web address from the previous step.
Once you visit the address, you'll be prompted to login. Enter the credentials shown below. Once you visit the address, you'll be prompted to login. Enter the credentials shown below.
![file browser main login page](/images/guides/agent-with-ecs/file_browser_main.png) ![file browser main login page](/images/guides/agent-with-ecs/file_browser_main.png)
Since our EFS volume is mounted to the path of the file browser application, we should see the access token and rendered secret file we defined via the agent config file. Since our EFS volume is mounted to the path of the file browser application, we should see the access token and rendered secret file we defined via the agent config file.
![file browswer dashbaord](/images/guides/agent-with-ecs/filebrowser_afterlogin.png) ![file browswer dashbaord](/images/guides/agent-with-ecs/filebrowser_afterlogin.png)
As expected, two files are present: `access-token` and `secrets`. As expected, two files are present: `access-token` and `secrets`.
The `access-token` file should hold a valid `Bearer` token, which can be used to make HTTP requests to Infisical. The `access-token` file should hold a valid `Bearer` token, which can be used to make HTTP requests to Infisical.
The `secrets` file should contain secrets, formatted according to the specifications in our secret template file (presented in key=value format). The `secrets` file should contain secrets, formatted according to the specifications in our secret template file (presented in key=value format).
![file browser access token deposit](/images/guides/agent-with-ecs/access-token-deposit.png) ![file browser access token deposit](/images/guides/agent-with-ecs/access-token-deposit.png)
![file browser secrets render](/images/guides/agent-with-ecs/secrets-deposit.png) ![file browser secrets render](/images/guides/agent-with-ecs/secrets-deposit.png)

View File

@ -9,60 +9,56 @@ It eliminates the need to modify application logic by enabling clients to decide
![agent diagram](/images/agent/infisical-agent-diagram.png) ![agent diagram](/images/agent/infisical-agent-diagram.png)
### Key features: ### Key features:
- Token renewal: Automatically authenticates with Infisical and deposits renewed access tokens at specified path for applications to consume - Token renewal: Automatically authenticates with Infisical and deposits renewed access tokens at specified path for applications to consume
- Templating: Renders secrets via user provided templates to desired formats for applications to consume - Templating: Renders secrets via user provided templates to desired formats for applications to consume
### Token renewal ### Token renewal
The Infisical agent can help manage the life cycle of access tokens. The token renewal process is split into two main components: a `Method`, which is the authentication process suitable for your current setup, and `Sinks`, which are the places where the agent deposits the new access token whenever it receives updates. The Infisical agent can help manage the life cycle of access tokens. The token renewal process is split into two main components: a `Method`, which is the authentication process suitable for your current setup, and `Sinks`, which are the places where the agent deposits the new access token whenever it receives updates.
When the Infisical Agent is started, it will attempt to obtain a valid access token using the authentication method you have configured. If the agent is unable to fetch a valid token, the agent will keep trying, increasing the time between each attempt. When the Infisical Agent is started, it will attempt to obtain a valid access token using the authentication method you have configured. If the agent is unable to fetch a valid token, the agent will keep trying, increasing the time between each attempt.
Once a access token is successfully fetched, the agent will make sure the access token stays valid, continuing to renew it before it expires. Once a access token is successfully fetched, the agent will make sure the access token stays valid, continuing to renew it before it expires.
Every time the agent successfully retrieves a new access token, it writes the new token to the Sinks you've configured. Every time the agent successfully retrieves a new access token, it writes the new token to the Sinks you've configured.
<Info> <Info>
Access tokens can be utilized with Infisical SDKs or directly in API requests Access tokens can be utilized with Infisical SDKs or directly in API requests to retrieve secrets from Infisical
to retrieve secrets from Infisical
</Info> </Info>
### Templating ### Templating
The Infisical agent can help deliver formatted secrets to your application in a variety of environments. To achieve this, the agent will retrieve secrets from Infisical, format them using a specified template, and then save these formatted secrets to a designated file path.
The Infisical agent can help deliver formatted secrets to your application in a variety of environments. To achieve this, the agent will retrieve secrets from Infisical, format them using a specified template, and then save these formatted secrets to a designated file path.
Templating process is done through the use of Go language's [text/template feature](https://pkg.go.dev/text/template). Multiple template definitions can be set in the agent configuration file to generate a variety of formatted secret files. Templating process is done through the use of Go language's [text/template feature](https://pkg.go.dev/text/template). Multiple template definitions can be set in the agent configuration file to generate a variety of formatted secret files.
When the agent is started and templates are defined in the agent configuration file, the agent will attempt to acquire a valid access token using the set authentication method outlined in the agent's configuration. When the agent is started and templates are defined in the agent configuration file, the agent will attempt to acquire a valid access token using the set authentication method outlined in the agent's configuration.
If this initial attempt is unsuccessful, the agent will momentarily pauses before continuing to make more attempts. If this initial attempt is unsuccessful, the agent will momentarily pauses before continuing to make more attempts.
Once the agent successfully obtains a valid access token, the agent proceeds to fetch the secrets from Infisical using it. Once the agent successfully obtains a valid access token, the agent proceeds to fetch the secrets from Infisical using it.
It then formats these secrets using the user provided templates and writes the formatted data to configured file paths. It then formats these secrets using the user provided templates and writes the formatted data to configured file paths.
## Agent configuration file ## Agent configuration file
To set up the authentication method for token renewal and to define secret templates, the Infisical agent requires a YAML configuration file containing properties defined below. To set up the authentication method for token renewal and to define secret templates, the Infisical agent requires a YAML configuration file containing properties defined below.
While specifying an authentication method is mandatory to start the agent, configuring sinks and secret templates are optional. While specifying an authentication method is mandatory to start the agent, configuring sinks and secret templates are optional.
| Field | Description | | Field | Description |
| ------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | ------------------------------------------------| ----------------------------- |
| `infisical.address` | The URL of the Infisical service. Default: `"https://app.infisical.com"`. | | `infisical.address` | The URL of the Infisical service. Default: `"https://app.infisical.com"`. |
| `auth.type` | The type of authentication method used. Available options: `universal-auth`, `kubernetes`, `azure`, `gcp-id-token`, `gcp-iam`, `aws-iam` | | `auth.type` | The type of authentication method used. Available options: `universal-auth`, `kubernetes`, `azure`, `gcp-id-token`, `gcp-iam`, `aws-iam`|
| `auth.config.identity-id` | The file path where the machine identity id is stored<br/><br/>This field is required when using any of the following auth types: `kubernetes`, `azure`, `gcp-id-token`, `gcp-iam`, or `aws-iam`. | | `auth.config.identity-id` | The file path where the machine identity id is stored<br/><br/>This field is required when using any of the following auth types: `kubernetes`, `azure`, `gcp-id-token`, `gcp-iam`, or `aws-iam`. |
| `auth.config.service-account-token` | Path to the Kubernetes service account token to use (optional)<br/><br/>Default: `/var/run/secrets/kubernetes.io/serviceaccount/token` | | `auth.config.service-account-token` | Path to the Kubernetes service account token to use (optional)<br/><br/>Default: `/var/run/secrets/kubernetes.io/serviceaccount/token` |
| `auth.config.service-account-key` | Path to your GCP service account key file. This field is required when using `gcp-iam` auth type.<br/><br/>Please note that the file should be in JSON format. | | `auth.config.service-account-key` | Path to your GCP service account key file. This field is required when using `gcp-iam` auth type.<br/><br/>Please note that the file should be in JSON format. |
| `auth.config.client-id` | The file path where the universal-auth client id is stored. | | `auth.config.client-id` | The file path where the universal-auth client id is stored. |
| `auth.config.client-secret` | The file path where the universal-auth client secret is stored. | | `auth.config.client-secret` | The file path where the universal-auth client secret is stored. |
| `auth.config.remove_client_secret_on_read` | This will instruct the agent to remove the client secret from disk. | | `auth.config.remove_client_secret_on_read` | This will instruct the agent to remove the client secret from disk. |
| `sinks[].type` | The type of sink in a list of sinks. Each item specifies a sink type. Currently, only `"file"` type is available. | | `sinks[].type` | The type of sink in a list of sinks. Each item specifies a sink type. Currently, only `"file"` type is available. |
| `sinks[].config.path` | The file path where the access token should be stored for each sink in the list. | | `sinks[].config.path` | The file path where the access token should be stored for each sink in the list. |
| `templates[].source-path` | The path to the template file that should be used to render secrets. | | `templates[].source-path` | The path to the template file that should be used to render secrets. |
| `templates[].template-content` | The inline secret template to be used for rendering the secrets. | | `templates[].destination-path` | The path where the rendered secrets from the source template will be saved to. |
| `templates[].destination-path` | The path where the rendered secrets from the source template will be saved to. | | `templates[].config.polling-interval` | How frequently to check for secret changes. Default: `5 minutes` (optional) |
| `templates[].config.polling-interval` | How frequently to check for secret changes. Default: `5 minutes` (optional) | | `templates[].config.execute.command` | The command to execute when secret change is detected (optional) |
| `templates[].config.execute.command` | The command to execute when secret change is detected (optional) | | `templates[].config.execute.timeout` | How long in seconds to wait for command to execute before timing out (optional) |
| `templates[].config.execute.timeout` | How long in seconds to wait for command to execute before timing out (optional) |
## Authentication ## Authentication
@ -72,20 +68,19 @@ The Infisical agent supports multiple authentication methods. Below are the avai
<Accordion title="Universal Auth"> <Accordion title="Universal Auth">
The Universal Auth method is a simple and secure way to authenticate with Infisical. It requires a client ID and a client secret to authenticate with Infisical. The Universal Auth method is a simple and secure way to authenticate with Infisical. It requires a client ID and a client secret to authenticate with Infisical.
<ParamField query="config" type="UniversalAuthConfig"> <ParamField query="config" type="UniversalAuthConfig">
<Expandable title="properties"> <Expandable title="properties">
<ParamField query="client-id" type="string" required> <ParamField query="client-id" type="string" required>
Path to the file containing the universal auth client ID. Path to the file containing the universal auth client ID.
</ParamField> </ParamField>
<ParamField query="client-secret" type="string" required> <ParamField query="client-secret" type="string" required>
Path to the file containing the universal auth client secret. Path to the file containing the universal auth client secret.
</ParamField> </ParamField>
<ParamField query="remove_client_secret_on_read" type="boolean" optional> <ParamField query="remove_client_secret_on_read" type="boolean" optional>
Instructs the agent to remove the client secret from disk after reading Instructs the agent to remove the client secret from disk after reading it.
it. </ParamField>
</ParamField> </Expandable>
</Expandable> </ParamField>
</ParamField>
<Steps> <Steps>
<Step title="Create a universal auth machine identity"> <Step title="Create a universal auth machine identity">
@ -103,25 +98,21 @@ The Infisical agent supports multiple authentication methods. Below are the avai
remove_client_secret_on_read: false # Optional field, instructs the agent to remove the client secret from disk after reading it remove_client_secret_on_read: false # Optional field, instructs the agent to remove the client secret from disk after reading it
``` ```
</Step> </Step>
</Steps> </Steps>
</Accordion> </Accordion>
<Accordion title="Native Kubernetes"> <Accordion title="Native Kubernetes">
The Native Kubernetes method is used to authenticate with Infisical when running in a Kubernetes environment. It requires a service account token to authenticate with Infisical. The Native Kubernetes method is used to authenticate with Infisical when running in a Kubernetes environment. It requires a service account token to authenticate with Infisical.
{" "} <ParamField query="config" type="KubernetesAuthConfig">
<Expandable title="properties">
<ParamField query="config" type="KubernetesAuthConfig"> <ParamField query="identity-id" type="string" required>
<Expandable title="properties"> Path to the file containing the machine identity ID.
<ParamField query="identity-id" type="string" required> </ParamField>
Path to the file containing the machine identity ID. <ParamField query="service-account-token" type="string" optional>
</ParamField> Path to the Kubernetes service account token to use. Default: `/var/run/secrets/kubernetes.io/serviceaccount/token`.
<ParamField query="service-account-token" type="string" optional> </ParamField>
Path to the Kubernetes service account token to use. Default: </Expandable>
`/var/run/secrets/kubernetes.io/serviceaccount/token`. </ParamField>
</ParamField>
</Expandable>
</ParamField>
<Steps> <Steps>
<Step title="Create a Kubernetes machine identity"> <Step title="Create a Kubernetes machine identity">
@ -138,7 +129,6 @@ The Infisical agent supports multiple authentication methods. Below are the avai
service-account-token: "/var/run/secrets/kubernetes.io/serviceaccount/token" # Optional field, custom path to the Kubernetes service account token to use service-account-token: "/var/run/secrets/kubernetes.io/serviceaccount/token" # Optional field, custom path to the Kubernetes service account token to use
``` ```
</Step> </Step>
</Steps> </Steps>
</Accordion> </Accordion>
@ -196,7 +186,6 @@ The Infisical agent supports multiple authentication methods. Below are the avai
``` ```
</Step> </Step>
</Steps> </Steps>
</Accordion> </Accordion>
<Accordion title="GCP IAM"> <Accordion title="GCP IAM">
The GCP IAM method is used to authenticate with Infisical with a GCP service account key. The GCP IAM method is used to authenticate with Infisical with a GCP service account key.
@ -228,7 +217,6 @@ The Infisical agent supports multiple authentication methods. Below are the avai
``` ```
</Step> </Step>
</Steps> </Steps>
</Accordion> </Accordion>
<Accordion title="Native AWS IAM"> <Accordion title="Native AWS IAM">
The AWS IAM method is used to authenticate with Infisical with an AWS IAM role while running in an AWS environment like EC2, Lambda, etc. The AWS IAM method is used to authenticate with Infisical with an AWS IAM role while running in an AWS environment like EC2, Lambda, etc.
@ -256,12 +244,10 @@ The Infisical agent supports multiple authentication methods. Below are the avai
``` ```
</Step> </Step>
</Steps> </Steps>
</Accordion> </Accordion>
</AccordionGroup> </AccordionGroup>
## Quick start Infisical Agent ## Quick start Infisical Agent
To install the Infisical agent, you must first install the [Infisical CLI](../cli/overview) in the desired environment where you'd like the agent to run. This is because the Infisical agent is a sub-command of the Infisical CLI. To install the Infisical agent, you must first install the [Infisical CLI](../cli/overview) in the desired environment where you'd like the agent to run. This is because the Infisical agent is a sub-command of the Infisical CLI.
Once you have the CLI installed, you will need to provision programmatic access for the agent via [Universal Auth](/documentation/platform/identities/universal-auth). To obtain a **Client ID** and a **Client Secret**, follow the step by step guide outlined [here](/documentation/platform/identities/universal-auth). Once you have the CLI installed, you will need to provision programmatic access for the agent via [Universal Auth](/documentation/platform/identities/universal-auth). To obtain a **Client ID** and a **Client Secret**, follow the step by step guide outlined [here](/documentation/platform/identities/universal-auth).
@ -291,8 +277,8 @@ templates:
command: ./reload-app.sh command: ./reload-app.sh
``` ```
The secret template below will be used to render the secrets with the key and the value separated by `=` sign. You'll notice that a custom function named `secret` is used to fetch the secrets. The secret template below will be used to render the secrets with the key and the value separated by `=` sign. You'll notice that a custom function named `secret` is used to fetch the secrets.
This function takes the following arguments: `secret "<project-id>" "<environment-slug>" "<secret-path>"`. This function takes the following arguments: `secret "<project-id>" "<environment-slug>" "<secret-path>"`.
```text my-dot-ev-secret-template ```text my-dot-ev-secret-template
{{- with secret "6553ccb2b7da580d7f6e7260" "dev" "/" }} {{- with secret "6553ccb2b7da580d7f6e7260" "dev" "/" }}
@ -304,12 +290,13 @@ This function takes the following arguments: `secret "<project-id>" "<environmen
After defining the agent configuration file, run the command below pointing to the path where the agent configuration file is located. After defining the agent configuration file, run the command below pointing to the path where the agent configuration file is located.
```bash
```bash
infisical agent --config example-agent-config-file.yaml infisical agent --config example-agent-config-file.yaml
``` ```
### Available secret template functions
### Available secret template functions
<Accordion title="listSecrets"> <Accordion title="listSecrets">
```bash ```bash
listSecrets "<project-id>" "environment-slug" "<secret-path>" listSecrets "<project-id>" "environment-slug" "<secret-path>"
@ -322,12 +309,11 @@ infisical agent --config example-agent-config-file.yaml
{{- end }} {{- end }}
``` ```
**Function name**: listSecrets **Function name**: listSecrets
**Description**: This function can be used to render the full list of secrets within a given project, environment and secret path. **Description**: This function can be used to render the full list of secrets within a given project, environment and secret path.
**Returns**: A single secret object with the following keys `Key, WorkspaceId, Value, Type, ID, and Comment`
**Returns**: A single secret object with the following keys `Key, WorkspaceId, Value, Type, ID, and Comment`
</Accordion> </Accordion>
<Accordion title="getSecretByName"> <Accordion title="getSecretByName">
@ -335,18 +321,18 @@ infisical agent --config example-agent-config-file.yaml
getSecretByName "<project-id>" "<environment-slug>" "<secret-path>" "<secret-name>" getSecretByName "<project-id>" "<environment-slug>" "<secret-path>" "<secret-name>"
``` ```
```bash example-template-usage ```bash example-template-usage
{{ with getSecretByName "d821f21d-aa90-453b-8448-8c78c1160a0e" "dev" "/" "POSTHOG_HOST"}} {{ with getSecretByName "d821f21d-aa90-453b-8448-8c78c1160a0e" "dev" "/" "POSTHOG_HOST"}}
{{ if .Value }} {{ if .Value }}
password = "{{ .Value }}" password = "{{ .Value }}"
{{ end }} {{ end }}
{{ end }} {{ end }}
``` ```
**Function name**: getSecretByName **Function name**: getSecretByName
**Description**: This function can be used to render a single secret by it's name. **Description**: This function can be used to render a single secret by it's name.
**Returns**: A list of secret objects with the following keys `Key, WorkspaceId, Value, Type, ID, and Comment` **Returns**: A list of secret objects with the following keys `Key, WorkspaceId, Value, Type, ID, and Comment`
</Accordion> </Accordion>

View File

@ -109,6 +109,7 @@
"documentation/platform/pki/private-ca", "documentation/platform/pki/private-ca",
"documentation/platform/pki/certificates", "documentation/platform/pki/certificates",
"documentation/platform/pki/certificate-templates", "documentation/platform/pki/certificate-templates",
"documentation/platform/pki/est",
"documentation/platform/pki/alerting" "documentation/platform/pki/alerting"
] ]
}, },
@ -324,6 +325,7 @@
}, },
"integrations/cloud/vercel", "integrations/cloud/vercel",
"integrations/cloud/azure-key-vault", "integrations/cloud/azure-key-vault",
"integrations/cloud/azure-devops",
"integrations/cloud/gcp-secret-manager", "integrations/cloud/gcp-secret-manager",
{ {
"group": "Cloudflare", "group": "Cloudflare",

View File

@ -1,10 +1,11 @@
--- ---
title: "Infisical Python SDK" title: "Infisical Python SDK"
sidebarTitle: "Python" sidebarTitle: "Python"
url: "https://github.com/Infisical/python-sdk-official?tab=readme-ov-file#infisical-python-sdk"
icon: "python" 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/) - [PyPi Package](https://pypi.org/project/infisical-python/)
- [Github Repository](https://github.com/Infisical/sdk/edit/main/crates/infisical-py) - [Github Repository](https://github.com/Infisical/sdk/edit/main/crates/infisical-py)
@ -529,4 +530,4 @@ decryptedString = client.decryptSymmetric(decryptOptions)
#### Returns (string) #### Returns (string)
`plaintext` (string): The decrypted plaintext. `plaintext` (string): The decrypted plaintext. */}

View File

@ -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"> <Card title="Node" href="/sdks/languages/node" icon="node" color="#68a063">
Manage secrets for your Node application on demand Manage secrets for your Node application on demand
</Card> </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 Manage secrets for your Python application on demand
</Card> </Card>
<Card href="/sdks/languages/java" title="Java" icon="java" color="#e41f23"> <Card href="/sdks/languages/java" title="Java" icon="java" color="#e41f23">

View File

@ -2,7 +2,7 @@ const path = require("path");
const ContentSecurityPolicy = ` const ContentSecurityPolicy = `
default-src 'self'; 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; style-src 'self' https://rsms.me 'unsafe-inline' https://hcaptcha.com https://*.hcaptcha.com;
child-src https://api.stripe.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; frame-src https://js.stripe.com/ https://api.stripe.com https://www.youtube.com/ https://hcaptcha.com https://*.hcaptcha.com;

View File

@ -33,7 +33,8 @@ const integrationSlugNameMapping: Mapping = {
windmill: "Windmill", windmill: "Windmill",
"gcp-secret-manager": "GCP Secret Manager", "gcp-secret-manager": "GCP Secret Manager",
"hasura-cloud": "Hasura Cloud", "hasura-cloud": "Hasura Cloud",
rundeck: "Rundeck" rundeck: "Rundeck",
"azure-devops": "Azure DevOps"
}; };
const envMapping: Mapping = { const envMapping: Mapping = {

View File

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

View File

@ -2,7 +2,7 @@ import React, { useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { useRouter } from "next/router"; 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 { useFetchServerStatus } from "@app/hooks/api/serverDetails";
import { usePopUp } from "@app/hooks/usePopUp"; import { usePopUp } from "@app/hooks/usePopUp";
@ -17,7 +17,7 @@ export default function TeamInviteStep(): JSX.Element {
const [emails, setEmails] = useState(""); const [emails, setEmails] = useState("");
const { data: serverDetails } = useFetchServerStatus(); const { data: serverDetails } = useFetchServerStatus();
const { mutateAsync } = useAddUserToOrg(); const { mutateAsync } = useAddUsersToOrg();
const { handlePopUpToggle, popUp, handlePopUpOpen } = usePopUp(["setUpEmail"] as const); const { handlePopUpToggle, popUp, handlePopUpOpen } = usePopUp(["setUpEmail"] as const);
// Redirect user to the getting started page // Redirect user to the getting started page
@ -31,8 +31,9 @@ export default function TeamInviteStep(): JSX.Element {
.map((email) => email.trim()) .map((email) => email.trim())
.map(async (email) => { .map(async (email) => {
mutateAsync({ mutateAsync({
inviteeEmail: email, inviteeEmails: [email],
organizationId: String(localStorage.getItem("orgData.id")) organizationId: String(localStorage.getItem("orgData.id")),
organizationRoleSlug: "member"
}); });
}); });

View File

@ -72,7 +72,12 @@ export const eventToNameMap: { [K in EventType]: string } = {
[EventType.CREATE_CERTIFICATE_TEMPLATE]: "Create certificate template", [EventType.CREATE_CERTIFICATE_TEMPLATE]: "Create certificate template",
[EventType.UPDATE_CERTIFICATE_TEMPLATE]: "Update certificate template", [EventType.UPDATE_CERTIFICATE_TEMPLATE]: "Update certificate template",
[EventType.DELETE_CERTIFICATE_TEMPLATE]: "Delete 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 } = { export const userAgentTTypeoNameMap: { [K in UserAgentType]: string } = {

View File

@ -86,5 +86,8 @@ export enum EventType {
CREATE_CERTIFICATE_TEMPLATE = "create-certificate-template", CREATE_CERTIFICATE_TEMPLATE = "create-certificate-template",
UPDATE_CERTIFICATE_TEMPLATE = "update-certificate-template", UPDATE_CERTIFICATE_TEMPLATE = "update-certificate-template",
DELETE_CERTIFICATE_TEMPLATE = "delete-certificate-template", DELETE_CERTIFICATE_TEMPLATE = "delete-certificate-template",
GET_CERTIFICATE_TEMPLATE = "get-certificate-template" GET_CERTIFICATE_TEMPLATE = "get-certificate-template",
CREATE_CERTIFICATE_TEMPLATE_EST_CONFIG = "create-certificate-template-est-config",
UPDATE_CERTIFICATE_TEMPLATE_EST_CONFIG = "update-certificate-template-est-config",
GET_CERTIFICATE_TEMPLATE_EST_CONFIG = "get-certificate-template-est-config"
} }

View File

@ -719,6 +719,29 @@ interface DeleteCertificateTemplate {
}; };
} }
interface CreateCertificateTemplateEstConfig {
type: EventType.CREATE_CERTIFICATE_TEMPLATE_EST_CONFIG;
metadata: {
certificateTemplateId: string;
isEnabled: boolean;
};
}
interface UpdateCertificateTemplateEstConfig {
type: EventType.UPDATE_CERTIFICATE_TEMPLATE_EST_CONFIG;
metadata: {
certificateTemplateId: string;
isEnabled: boolean;
};
}
interface GetCertificateTemplateEstConfig {
type: EventType.GET_CERTIFICATE_TEMPLATE_EST_CONFIG;
metadata: {
certificateTemplateId: string;
};
}
export type Event = export type Event =
| GetSecretsEvent | GetSecretsEvent
| GetSecretEvent | GetSecretEvent
@ -791,7 +814,10 @@ export type Event =
| CreateCertificateTemplate | CreateCertificateTemplate
| UpdateCertificateTemplate | UpdateCertificateTemplate
| GetCertificateTemplate | GetCertificateTemplate
| DeleteCertificateTemplate; | DeleteCertificateTemplate
| UpdateCertificateTemplateEstConfig
| CreateCertificateTemplateEstConfig
| GetCertificateTemplateEstConfig;
export type AuditLog = { export type AuditLog = {
id: string; id: string;

View File

@ -93,6 +93,7 @@ export type CompleteAccountDTO = {
salt: string; salt: string;
verifier: string; verifier: string;
password: string; password: string;
tokenMetadata?: string;
}; };
export type CompleteAccountSignupDTO = CompleteAccountDTO & { export type CompleteAccountSignupDTO = CompleteAccountDTO & {

View File

@ -8,4 +8,4 @@ export {
useSignIntermediate, useSignIntermediate,
useUpdateCa useUpdateCa
} from "./mutations"; } from "./mutations";
export { useGetCaById, useGetCaCert, useGetCaCerts, useGetCaCrls,useGetCaCsr } from "./queries"; export { useGetCaById, useGetCaCert, useGetCaCerts, useGetCaCrls, useGetCaCsr } from "./queries";

View File

@ -10,7 +10,8 @@ export const caKeys = {
getCaCrls: (caId: string) => [{ caId }, "ca-crls"], getCaCrls: (caId: string) => [{ caId }, "ca-crls"],
getCaCert: (caId: string) => [{ caId }, "ca-cert"], getCaCert: (caId: string) => [{ caId }, "ca-cert"],
getCaCsr: (caId: string) => [{ caId }, "ca-csr"], 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) => { export const useGetCaById = (caId: string) => {

View File

@ -1,2 +1,8 @@
export { useCreateCertTemplate, useDeleteCertTemplate, useUpdateCertTemplate } from "./mutations"; export {
export { useGetCertTemplate } from "./queries"; useCreateCertTemplate,
useCreateEstConfig,
useDeleteCertTemplate,
useUpdateCertTemplate,
useUpdateEstConfig
} from "./mutations";
export { useGetCertTemplate, useGetEstConfig } from "./queries";

View File

@ -7,8 +7,10 @@ import { certTemplateKeys } from "./queries";
import { import {
TCertificateTemplate, TCertificateTemplate,
TCreateCertificateTemplateDTO, TCreateCertificateTemplateDTO,
TCreateEstConfigDTO,
TDeleteCertificateTemplateDTO, TDeleteCertificateTemplateDTO,
TUpdateCertificateTemplateDTO TUpdateCertificateTemplateDTO,
TUpdateEstConfigDTO
} from "./types"; } from "./types";
export const useCreateCertTemplate = () => { export const useCreateCertTemplate = () => {
@ -57,3 +59,35 @@ export const useDeleteCertTemplate = () => {
} }
}); });
}; };
export const useCreateEstConfig = () => {
const queryClient = useQueryClient();
return useMutation<{}, {}, TCreateEstConfigDTO>({
mutationFn: async (body) => {
const { data } = await apiRequest.post(
`/api/v1/pki/certificate-templates/${body.certificateTemplateId}/est-config`,
body
);
return data;
},
onSuccess: (_, { certificateTemplateId }) => {
queryClient.invalidateQueries(certTemplateKeys.getEstConfig(certificateTemplateId));
}
});
};
export const useUpdateEstConfig = () => {
const queryClient = useQueryClient();
return useMutation<{}, {}, TUpdateEstConfigDTO>({
mutationFn: async (body) => {
const { data } = await apiRequest.patch(
`/api/v1/pki/certificate-templates/${body.certificateTemplateId}/est-config`,
body
);
return data;
},
onSuccess: (_, { certificateTemplateId }) => {
queryClient.invalidateQueries(certTemplateKeys.getEstConfig(certificateTemplateId));
}
});
};

View File

@ -2,10 +2,11 @@ import { useQuery } from "@tanstack/react-query";
import { apiRequest } from "@app/config/request"; import { apiRequest } from "@app/config/request";
import { TCertificateTemplate } from "./types"; import { TCertificateTemplate, TEstConfig } from "./types";
export const certTemplateKeys = { 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) => { export const useGetCertTemplate = (id: string) => {
@ -20,3 +21,17 @@ export const useGetCertTemplate = (id: string) => {
enabled: Boolean(id) enabled: Boolean(id)
}); });
}; };
export const useGetEstConfig = (certificateTemplateId: string) => {
return useQuery({
queryKey: certTemplateKeys.getEstConfig(certificateTemplateId),
queryFn: async () => {
const { data: estConfig } = await apiRequest.get<TEstConfig>(
`/api/v1/pki/certificate-templates/${certificateTemplateId}/est-config`
);
return estConfig;
},
enabled: Boolean(certificateTemplateId)
});
};

View File

@ -35,3 +35,24 @@ export type TDeleteCertificateTemplateDTO = {
id: string; id: string;
projectId: string; projectId: string;
}; };
export type TCreateEstConfigDTO = {
certificateTemplateId: string;
caChain: string;
passphrase: string;
isEnabled: boolean;
};
export type TUpdateEstConfigDTO = {
certificateTemplateId: string;
caChain?: string;
passphrase?: string;
isEnabled?: boolean;
};
export type TEstConfig = {
id: string;
certificateTemplateId: string;
caChain: string;
isEnabled: false;
};

View File

@ -120,16 +120,22 @@ const fetchIntegrationAuthById = async (integrationAuthId: string) => {
const fetchIntegrationAuthApps = async ({ const fetchIntegrationAuthApps = async ({
integrationAuthId, integrationAuthId,
teamId, teamId,
azureDevOpsOrgName,
workspaceSlug workspaceSlug
}: { }: {
integrationAuthId: string; integrationAuthId: string;
teamId?: string; teamId?: string;
azureDevOpsOrgName?: string;
workspaceSlug?: string; workspaceSlug?: string;
}) => { }) => {
const params: Record<string, string> = {}; const params: Record<string, string> = {};
if (teamId) { if (teamId) {
params.teamId = teamId; params.teamId = teamId;
} }
if (azureDevOpsOrgName) {
params.azureDevOpsOrgName = azureDevOpsOrgName;
}
if (workspaceSlug) { if (workspaceSlug) {
params.workspaceSlug = workspaceSlug; params.workspaceSlug = workspaceSlug;
} }
@ -452,10 +458,12 @@ export const useGetIntegrationAuthById = (integrationAuthId: string) => {
export const useGetIntegrationAuthApps = ({ export const useGetIntegrationAuthApps = ({
integrationAuthId, integrationAuthId,
teamId, teamId,
azureDevOpsOrgName,
workspaceSlug workspaceSlug
}: { }: {
integrationAuthId: string; integrationAuthId: string;
teamId?: string; teamId?: string;
azureDevOpsOrgName?: string;
workspaceSlug?: string; workspaceSlug?: string;
}) => { }) => {
return useQuery({ return useQuery({
@ -464,6 +472,7 @@ export const useGetIntegrationAuthApps = ({
fetchIntegrationAuthApps({ fetchIntegrationAuthApps({
integrationAuthId, integrationAuthId,
teamId, teamId,
azureDevOpsOrgName,
workspaceSlug workspaceSlug
}), }),
enabled: true enabled: true

View File

@ -47,7 +47,7 @@ export const roleQueryKeys = {
["user-project-permissions", { workspaceId }] as const ["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">> }>( const { data } = await apiRequest.get<{ roles: Array<Omit<TProjectRole, "permissions">> }>(
`/api/v1/workspace/${projectId}/roles` `/api/v1/workspace/${projectId}/roles`
); );

View File

@ -9,7 +9,15 @@ export const useCreateSecretApprovalPolicy = () => {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
return useMutation<{}, {}, TCreateSecretPolicyDTO>({ 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", { const { data } = await apiRequest.post("/api/v1/secret-approvals", {
environment, environment,
workspaceId, workspaceId,

View File

@ -6,7 +6,7 @@ export {
} from "./mutation"; } from "./mutation";
export { export {
fetchOrgUsers, fetchOrgUsers,
useAddUserToOrg, useAddUsersToOrg,
useCreateAPIKey, useCreateAPIKey,
useDeleteAPIKey, useDeleteAPIKey,
useDeleteMe, useDeleteMe,
@ -26,4 +26,5 @@ export {
useRevokeMySessions, useRevokeMySessions,
useUpdateMfaEnabled, useUpdateMfaEnabled,
useUpdateOrgMembership, useUpdateOrgMembership,
useUpdateUserAuthMethods} from "./queries"; useUpdateUserAuthMethods
} from "./queries";

View File

@ -157,12 +157,15 @@ export const useGetOrgUsers = (orgId: string) =>
// mutation // mutation
// TODO(akhilmhdh): move all mutation to mutation file // TODO(akhilmhdh): move all mutation to mutation file
export const useAddUserToOrg = () => { export const useAddUsersToOrg = () => {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
type Response = { type Response = {
data: { data: {
message: string; message: string;
completeInviteLink: string | undefined; completeInviteLinks?: {
email: string;
link: string;
}[];
}; };
}; };

Some files were not shown because too many files have changed in this diff Show More