diff --git a/backend/package-lock.json b/backend/package-lock.json index db116bc48..7534ac1bd 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -38,6 +38,7 @@ "@octokit/core": "^5.2.1", "@octokit/plugin-paginate-graphql": "^4.0.1", "@octokit/plugin-retry": "^5.0.5", + "@octokit/request": "8.4.1", "@octokit/rest": "^20.0.2", "@octokit/webhooks-types": "^7.3.1", "@octopusdeploy/api-client": "^3.4.1", @@ -9777,18 +9778,6 @@ "node": ">= 18" } }, - "node_modules/@octokit/auth-app/node_modules/@octokit/endpoint": { - "version": "10.1.1", - "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-10.1.1.tgz", - "integrity": "sha512-JYjh5rMOwXMJyUpj028cu0Gbp7qe/ihxfJMLc8VZBMMqSwLgOxDI1911gV4Enl1QSavAQNJcwmwBF9M0VvLh6Q==", - "dependencies": { - "@octokit/types": "^13.0.0", - "universal-user-agent": "^7.0.2" - }, - "engines": { - "node": ">= 18" - } - }, "node_modules/@octokit/auth-app/node_modules/@octokit/openapi-types": { "version": "22.2.0", "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-22.2.0.tgz", @@ -9835,11 +9824,6 @@ "node": "14 || >=16.14" } }, - "node_modules/@octokit/auth-app/node_modules/universal-user-agent": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-7.0.2.tgz", - "integrity": "sha512-0JCqzSKnStlRRQfCdowvqy3cy0Dvtlb8xecj/H8JFZuCze4rwjPZQOgvFvn0Ws/usCHQFGpyr+pB9adaGwXn4Q==" - }, "node_modules/@octokit/auth-oauth-app": { "version": "8.1.1", "resolved": "https://registry.npmjs.org/@octokit/auth-oauth-app/-/auth-oauth-app-8.1.1.tgz", @@ -9855,18 +9839,6 @@ "node": ">= 18" } }, - "node_modules/@octokit/auth-oauth-app/node_modules/@octokit/endpoint": { - "version": "10.1.1", - "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-10.1.1.tgz", - "integrity": "sha512-JYjh5rMOwXMJyUpj028cu0Gbp7qe/ihxfJMLc8VZBMMqSwLgOxDI1911gV4Enl1QSavAQNJcwmwBF9M0VvLh6Q==", - "dependencies": { - "@octokit/types": "^13.0.0", - "universal-user-agent": "^7.0.2" - }, - "engines": { - "node": ">= 18" - } - }, "node_modules/@octokit/auth-oauth-app/node_modules/@octokit/openapi-types": { "version": "22.2.0", "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-22.2.0.tgz", @@ -9905,11 +9877,6 @@ "@octokit/openapi-types": "^22.2.0" } }, - "node_modules/@octokit/auth-oauth-app/node_modules/universal-user-agent": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-7.0.2.tgz", - "integrity": "sha512-0JCqzSKnStlRRQfCdowvqy3cy0Dvtlb8xecj/H8JFZuCze4rwjPZQOgvFvn0Ws/usCHQFGpyr+pB9adaGwXn4Q==" - }, "node_modules/@octokit/auth-oauth-device": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/@octokit/auth-oauth-device/-/auth-oauth-device-7.1.1.tgz", @@ -9924,18 +9891,6 @@ "node": ">= 18" } }, - "node_modules/@octokit/auth-oauth-device/node_modules/@octokit/endpoint": { - "version": "10.1.1", - "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-10.1.1.tgz", - "integrity": "sha512-JYjh5rMOwXMJyUpj028cu0Gbp7qe/ihxfJMLc8VZBMMqSwLgOxDI1911gV4Enl1QSavAQNJcwmwBF9M0VvLh6Q==", - "dependencies": { - "@octokit/types": "^13.0.0", - "universal-user-agent": "^7.0.2" - }, - "engines": { - "node": ">= 18" - } - }, "node_modules/@octokit/auth-oauth-device/node_modules/@octokit/openapi-types": { "version": "22.2.0", "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-22.2.0.tgz", @@ -9974,11 +9929,6 @@ "@octokit/openapi-types": "^22.2.0" } }, - "node_modules/@octokit/auth-oauth-device/node_modules/universal-user-agent": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-7.0.2.tgz", - "integrity": "sha512-0JCqzSKnStlRRQfCdowvqy3cy0Dvtlb8xecj/H8JFZuCze4rwjPZQOgvFvn0Ws/usCHQFGpyr+pB9adaGwXn4Q==" - }, "node_modules/@octokit/auth-oauth-user": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/@octokit/auth-oauth-user/-/auth-oauth-user-5.1.1.tgz", @@ -9994,18 +9944,6 @@ "node": ">= 18" } }, - "node_modules/@octokit/auth-oauth-user/node_modules/@octokit/endpoint": { - "version": "10.1.1", - "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-10.1.1.tgz", - "integrity": "sha512-JYjh5rMOwXMJyUpj028cu0Gbp7qe/ihxfJMLc8VZBMMqSwLgOxDI1911gV4Enl1QSavAQNJcwmwBF9M0VvLh6Q==", - "dependencies": { - "@octokit/types": "^13.0.0", - "universal-user-agent": "^7.0.2" - }, - "engines": { - "node": ">= 18" - } - }, "node_modules/@octokit/auth-oauth-user/node_modules/@octokit/openapi-types": { "version": "22.2.0", "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-22.2.0.tgz", @@ -10044,11 +9982,6 @@ "@octokit/openapi-types": "^22.2.0" } }, - "node_modules/@octokit/auth-oauth-user/node_modules/universal-user-agent": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-7.0.2.tgz", - "integrity": "sha512-0JCqzSKnStlRRQfCdowvqy3cy0Dvtlb8xecj/H8JFZuCze4rwjPZQOgvFvn0Ws/usCHQFGpyr+pB9adaGwXn4Q==" - }, "node_modules/@octokit/auth-token": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-4.0.0.tgz", @@ -10102,32 +10035,38 @@ "@octokit/openapi-types": "^24.2.0" } }, + "node_modules/@octokit/core/node_modules/universal-user-agent": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-6.0.1.tgz", + "integrity": "sha512-yCzhz6FN2wU1NiiQRogkTQszlQSlpWaw8SvVegAc+bDxbzHgh1vX8uIe8OYyMH6DwH+sdTJsgMl36+mSMdRJIQ==", + "license": "ISC" + }, "node_modules/@octokit/endpoint": { - "version": "9.0.6", - "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-9.0.6.tgz", - "integrity": "sha512-H1fNTMA57HbkFESSt3Y9+FBICv+0jFceJFPWDePYlR/iMGrwM5ph+Dd4XRQs+8X+PUFURLQgX9ChPfhJ/1uNQw==", + "version": "10.1.4", + "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-10.1.4.tgz", + "integrity": "sha512-OlYOlZIsfEVZm5HCSR8aSg02T2lbUWOsCQoPKfTXJwDzcHQBrVBGdGXb89dv2Kw2ToZaRtudp8O3ZIYoaOjKlA==", "license": "MIT", "dependencies": { - "@octokit/types": "^13.1.0", - "universal-user-agent": "^6.0.0" + "@octokit/types": "^14.0.0", + "universal-user-agent": "^7.0.2" }, "engines": { "node": ">= 18" } }, "node_modules/@octokit/endpoint/node_modules/@octokit/openapi-types": { - "version": "24.2.0", - "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-24.2.0.tgz", - "integrity": "sha512-9sIH3nSUttelJSXUrmGzl7QUBFul0/mB8HRYl3fOlgHbIWG+WnYDXU3v/2zMtAvuzZ/ed00Ei6on975FhBfzrg==", + "version": "25.1.0", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-25.1.0.tgz", + "integrity": "sha512-idsIggNXUKkk0+BExUn1dQ92sfysJrje03Q0bv0e+KPLrvyqZF8MnBpFz8UNfYDwB3Ie7Z0TByjWfzxt7vseaA==", "license": "MIT" }, "node_modules/@octokit/endpoint/node_modules/@octokit/types": { - "version": "13.10.0", - "resolved": "https://registry.npmjs.org/@octokit/types/-/types-13.10.0.tgz", - "integrity": "sha512-ifLaO34EbbPj0Xgro4G5lP5asESjwHracYJvVaPIyXMuiuXLlhic3S47cBdTb+jfODkTE5YtGCLt3Ay3+J97sA==", + "version": "14.1.0", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-14.1.0.tgz", + "integrity": "sha512-1y6DgTy8Jomcpu33N+p5w58l6xyt55Ar2I91RPiIA0xCJBXyUAhXCcmZaDWSANiha7R9a6qJJ2CRomGPZ6f46g==", "license": "MIT", "dependencies": { - "@octokit/openapi-types": "^24.2.0" + "@octokit/openapi-types": "^25.1.0" } }, "node_modules/@octokit/graphql": { @@ -10159,6 +10098,12 @@ "@octokit/openapi-types": "^24.2.0" } }, + "node_modules/@octokit/graphql/node_modules/universal-user-agent": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-6.0.1.tgz", + "integrity": "sha512-yCzhz6FN2wU1NiiQRogkTQszlQSlpWaw8SvVegAc+bDxbzHgh1vX8uIe8OYyMH6DwH+sdTJsgMl36+mSMdRJIQ==", + "license": "ISC" + }, "node_modules/@octokit/oauth-authorization-url": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/@octokit/oauth-authorization-url/-/oauth-authorization-url-7.1.1.tgz", @@ -10181,18 +10126,6 @@ "node": ">= 18" } }, - "node_modules/@octokit/oauth-methods/node_modules/@octokit/endpoint": { - "version": "10.1.1", - "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-10.1.1.tgz", - "integrity": "sha512-JYjh5rMOwXMJyUpj028cu0Gbp7qe/ihxfJMLc8VZBMMqSwLgOxDI1911gV4Enl1QSavAQNJcwmwBF9M0VvLh6Q==", - "dependencies": { - "@octokit/types": "^13.0.0", - "universal-user-agent": "^7.0.2" - }, - "engines": { - "node": ">= 18" - } - }, "node_modules/@octokit/oauth-methods/node_modules/@octokit/openapi-types": { "version": "22.2.0", "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-22.2.0.tgz", @@ -10231,11 +10164,6 @@ "@octokit/openapi-types": "^22.2.0" } }, - "node_modules/@octokit/oauth-methods/node_modules/universal-user-agent": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-7.0.2.tgz", - "integrity": "sha512-0JCqzSKnStlRRQfCdowvqy3cy0Dvtlb8xecj/H8JFZuCze4rwjPZQOgvFvn0Ws/usCHQFGpyr+pB9adaGwXn4Q==" - }, "node_modules/@octokit/openapi-types": { "version": "19.1.0", "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-19.1.0.tgz", @@ -10376,31 +10304,54 @@ } }, "node_modules/@octokit/request-error/node_modules/@octokit/openapi-types": { - "version": "22.2.0", - "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-22.2.0.tgz", - "integrity": "sha512-QBhVjcUa9W7Wwhm6DBFu6ZZ+1/t/oYxqc2tp81Pi41YNuJinbFRx8B133qVOrAaBbF7D/m0Et6f9/pZt9Rc+tg==" + "version": "24.2.0", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-24.2.0.tgz", + "integrity": "sha512-9sIH3nSUttelJSXUrmGzl7QUBFul0/mB8HRYl3fOlgHbIWG+WnYDXU3v/2zMtAvuzZ/ed00Ei6on975FhBfzrg==", + "license": "MIT" }, "node_modules/@octokit/request-error/node_modules/@octokit/types": { - "version": "13.6.1", - "resolved": "https://registry.npmjs.org/@octokit/types/-/types-13.6.1.tgz", - "integrity": "sha512-PHZE9Z+kWXb23Ndik8MKPirBPziOc0D2/3KH1P+6jK5nGWe96kadZuE4jev2/Jq7FvIfTlT2Ltg8Fv2x1v0a5g==", + "version": "13.10.0", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-13.10.0.tgz", + "integrity": "sha512-ifLaO34EbbPj0Xgro4G5lP5asESjwHracYJvVaPIyXMuiuXLlhic3S47cBdTb+jfODkTE5YtGCLt3Ay3+J97sA==", + "license": "MIT", "dependencies": { - "@octokit/openapi-types": "^22.2.0" + "@octokit/openapi-types": "^24.2.0" + } + }, + "node_modules/@octokit/request/node_modules/@octokit/endpoint": { + "version": "9.0.6", + "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-9.0.6.tgz", + "integrity": "sha512-H1fNTMA57HbkFESSt3Y9+FBICv+0jFceJFPWDePYlR/iMGrwM5ph+Dd4XRQs+8X+PUFURLQgX9ChPfhJ/1uNQw==", + "license": "MIT", + "dependencies": { + "@octokit/types": "^13.1.0", + "universal-user-agent": "^6.0.0" + }, + "engines": { + "node": ">= 18" } }, "node_modules/@octokit/request/node_modules/@octokit/openapi-types": { - "version": "22.2.0", - "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-22.2.0.tgz", - "integrity": "sha512-QBhVjcUa9W7Wwhm6DBFu6ZZ+1/t/oYxqc2tp81Pi41YNuJinbFRx8B133qVOrAaBbF7D/m0Et6f9/pZt9Rc+tg==" + "version": "24.2.0", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-24.2.0.tgz", + "integrity": "sha512-9sIH3nSUttelJSXUrmGzl7QUBFul0/mB8HRYl3fOlgHbIWG+WnYDXU3v/2zMtAvuzZ/ed00Ei6on975FhBfzrg==", + "license": "MIT" }, "node_modules/@octokit/request/node_modules/@octokit/types": { - "version": "13.6.1", - "resolved": "https://registry.npmjs.org/@octokit/types/-/types-13.6.1.tgz", - "integrity": "sha512-PHZE9Z+kWXb23Ndik8MKPirBPziOc0D2/3KH1P+6jK5nGWe96kadZuE4jev2/Jq7FvIfTlT2Ltg8Fv2x1v0a5g==", + "version": "13.10.0", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-13.10.0.tgz", + "integrity": "sha512-ifLaO34EbbPj0Xgro4G5lP5asESjwHracYJvVaPIyXMuiuXLlhic3S47cBdTb+jfODkTE5YtGCLt3Ay3+J97sA==", + "license": "MIT", "dependencies": { - "@octokit/openapi-types": "^22.2.0" + "@octokit/openapi-types": "^24.2.0" } }, + "node_modules/@octokit/request/node_modules/universal-user-agent": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-6.0.1.tgz", + "integrity": "sha512-yCzhz6FN2wU1NiiQRogkTQszlQSlpWaw8SvVegAc+bDxbzHgh1vX8uIe8OYyMH6DwH+sdTJsgMl36+mSMdRJIQ==", + "license": "ISC" + }, "node_modules/@octokit/rest": { "version": "20.0.2", "resolved": "https://registry.npmjs.org/@octokit/rest/-/rest-20.0.2.tgz", @@ -18288,7 +18239,8 @@ "node_modules/fast-content-type-parse": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/fast-content-type-parse/-/fast-content-type-parse-1.1.0.tgz", - "integrity": "sha512-fBHHqSTFLVnR61C+gltJuE5GkVQMV0S2nqUO8TJ+5Z3qAKG8vAx4FKai1s5jq/inV1+sREynIWSuQ6HgoSXpDQ==" + "integrity": "sha512-fBHHqSTFLVnR61C+gltJuE5GkVQMV0S2nqUO8TJ+5Z3qAKG8vAx4FKai1s5jq/inV1+sREynIWSuQ6HgoSXpDQ==", + "license": "MIT" }, "node_modules/fast-copy": { "version": "3.0.1", @@ -24776,6 +24728,12 @@ "jsonwebtoken": "^9.0.2" } }, + "node_modules/octokit-auth-probot/node_modules/universal-user-agent": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-6.0.1.tgz", + "integrity": "sha512-yCzhz6FN2wU1NiiQRogkTQszlQSlpWaw8SvVegAc+bDxbzHgh1vX8uIe8OYyMH6DwH+sdTJsgMl36+mSMdRJIQ==", + "license": "ISC" + }, "node_modules/odbc": { "version": "2.4.9", "resolved": "https://registry.npmjs.org/odbc/-/odbc-2.4.9.tgz", @@ -30705,9 +30663,10 @@ "integrity": "sha512-G5o6f95b5BggDGuUfKDApKaCgNYy2x7OdHY0zSMF081O0EJobw+1130VONhrA7ezGSV2FNOGyM+KQpQZAr9bIQ==" }, "node_modules/universal-user-agent": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-6.0.1.tgz", - "integrity": "sha512-yCzhz6FN2wU1NiiQRogkTQszlQSlpWaw8SvVegAc+bDxbzHgh1vX8uIe8OYyMH6DwH+sdTJsgMl36+mSMdRJIQ==" + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-7.0.3.tgz", + "integrity": "sha512-TmnEAEAsBJVZM/AADELsK76llnwcf9vMKuPz8JflO1frO8Lchitr0fNaN9d+Ap0BjKtqWqd/J17qeDnXh8CL2A==", + "license": "ISC" }, "node_modules/universalify": { "version": "2.0.1", diff --git a/backend/package.json b/backend/package.json index ff8b832bc..f84db13fc 100644 --- a/backend/package.json +++ b/backend/package.json @@ -158,6 +158,7 @@ "@octokit/core": "^5.2.1", "@octokit/plugin-paginate-graphql": "^4.0.1", "@octokit/plugin-retry": "^5.0.5", + "@octokit/request": "8.4.1", "@octokit/rest": "^20.0.2", "@octokit/webhooks-types": "^7.3.1", "@octopusdeploy/api-client": "^3.4.1", diff --git a/backend/scripts/generate-schema-types.ts b/backend/scripts/generate-schema-types.ts index c0e18a763..111f42520 100644 --- a/backend/scripts/generate-schema-types.ts +++ b/backend/scripts/generate-schema-types.ts @@ -99,6 +99,7 @@ const main = async () => { (el) => !el.tableName.includes("_migrations") && !el.tableName.includes("audit_logs_") && + !el.tableName.includes("active_locks") && el.tableName !== "intermediate_audit_logs" ); diff --git a/backend/src/@types/fastify.d.ts b/backend/src/@types/fastify.d.ts index 15e967948..adf9489d4 100644 --- a/backend/src/@types/fastify.d.ts +++ b/backend/src/@types/fastify.d.ts @@ -18,6 +18,7 @@ import { TExternalKmsServiceFactory } from "@app/ee/services/external-kms/extern import { TGatewayServiceFactory } from "@app/ee/services/gateway/gateway-service"; import { TGithubOrgSyncServiceFactory } from "@app/ee/services/github-org-sync/github-org-sync-service"; import { TGroupServiceFactory } from "@app/ee/services/group/group-service"; +import { TIdentityAuthTemplateServiceFactory } from "@app/ee/services/identity-auth-template"; import { TIdentityProjectAdditionalPrivilegeServiceFactory } from "@app/ee/services/identity-project-additional-privilege/identity-project-additional-privilege-service"; import { TIdentityProjectAdditionalPrivilegeV2ServiceFactory } from "@app/ee/services/identity-project-additional-privilege-v2/identity-project-additional-privilege-v2-service"; import { TKmipClientDALFactory } from "@app/ee/services/kmip/kmip-client-dal"; @@ -300,6 +301,7 @@ declare module "fastify" { reminder: TReminderServiceFactory; bus: TEventBusService; sse: TServerSentEventsService; + identityAuthTemplate: TIdentityAuthTemplateServiceFactory; }; // this is exclusive use for middlewares in which we need to inject data // everywhere else access using service layer diff --git a/backend/src/@types/knex.d.ts b/backend/src/@types/knex.d.ts index 185a32356..f645cb8f2 100644 --- a/backend/src/@types/knex.d.ts +++ b/backend/src/@types/knex.d.ts @@ -494,6 +494,11 @@ import { TAccessApprovalPoliciesEnvironmentsInsert, TAccessApprovalPoliciesEnvironmentsUpdate } from "@app/db/schemas/access-approval-policies-environments"; +import { + TIdentityAuthTemplates, + TIdentityAuthTemplatesInsert, + TIdentityAuthTemplatesUpdate +} from "@app/db/schemas/identity-auth-templates"; import { TIdentityLdapAuths, TIdentityLdapAuthsInsert, @@ -878,6 +883,11 @@ declare module "knex/types/tables" { TIdentityProjectAdditionalPrivilegeInsert, TIdentityProjectAdditionalPrivilegeUpdate >; + [TableName.IdentityAuthTemplate]: KnexOriginal.CompositeTableType< + TIdentityAuthTemplates, + TIdentityAuthTemplatesInsert, + TIdentityAuthTemplatesUpdate + >; [TableName.AccessApprovalPolicy]: KnexOriginal.CompositeTableType< TAccessApprovalPolicies, diff --git a/backend/src/db/migrations/20250723220500_remove-srp.ts b/backend/src/db/migrations/20250723220500_remove-srp.ts new file mode 100644 index 000000000..4eb93e86a --- /dev/null +++ b/backend/src/db/migrations/20250723220500_remove-srp.ts @@ -0,0 +1,18 @@ +import { Knex } from "knex"; + +import { TableName } from "../schemas"; + +export async function up(knex: Knex): Promise { + await knex.schema.alterTable(TableName.UserEncryptionKey, (table) => { + table.text("encryptedPrivateKey").nullable().alter(); + table.text("publicKey").nullable().alter(); + table.text("iv").nullable().alter(); + table.text("tag").nullable().alter(); + table.text("salt").nullable().alter(); + table.text("verifier").nullable().alter(); + }); +} + +export async function down(): Promise { + // do nothing for now to avoid breaking down migrations +} diff --git a/backend/src/db/migrations/20250730162101_add-start-from-reminder.ts b/backend/src/db/migrations/20250730162101_add-start-from-reminder.ts new file mode 100644 index 000000000..427e7fb5a --- /dev/null +++ b/backend/src/db/migrations/20250730162101_add-start-from-reminder.ts @@ -0,0 +1,19 @@ +import { Knex } from "knex"; + +import { TableName } from "../schemas"; + +export async function up(knex: Knex): Promise { + if (!(await knex.schema.hasColumn(TableName.Reminder, "fromDate"))) { + await knex.schema.alterTable(TableName.Reminder, (t) => { + t.timestamp("fromDate", { useTz: true }).nullable(); + }); + } +} + +export async function down(knex: Knex): Promise { + if (await knex.schema.hasColumn(TableName.Reminder, "fromDate")) { + await knex.schema.alterTable(TableName.Reminder, (t) => { + t.dropColumn("fromDate"); + }); + } +} diff --git a/backend/src/db/migrations/20250801170240_add-identity-auth-template.ts b/backend/src/db/migrations/20250801170240_add-identity-auth-template.ts new file mode 100644 index 000000000..60974a661 --- /dev/null +++ b/backend/src/db/migrations/20250801170240_add-identity-auth-template.ts @@ -0,0 +1,36 @@ +import { Knex } from "knex"; + +import { TableName } from "../schemas"; +import { createOnUpdateTrigger, dropOnUpdateTrigger } from "../utils"; + +export async function up(knex: Knex): Promise { + if (!(await knex.schema.hasTable(TableName.IdentityAuthTemplate))) { + await knex.schema.createTable(TableName.IdentityAuthTemplate, (t) => { + t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid()); + t.binary("templateFields").notNullable(); + t.uuid("orgId").notNullable(); + t.foreign("orgId").references("id").inTable(TableName.Organization).onDelete("CASCADE"); + t.string("name", 64).notNullable(); + t.string("authMethod").notNullable(); + t.timestamps(true, true, true); + }); + await createOnUpdateTrigger(knex, TableName.IdentityAuthTemplate); + } + if (!(await knex.schema.hasColumn(TableName.IdentityLdapAuth, "templateId"))) { + await knex.schema.alterTable(TableName.IdentityLdapAuth, (t) => { + t.uuid("templateId").nullable(); + t.foreign("templateId").references("id").inTable(TableName.IdentityAuthTemplate).onDelete("SET NULL"); + }); + } +} + +export async function down(knex: Knex): Promise { + if (await knex.schema.hasColumn(TableName.IdentityLdapAuth, "templateId")) { + await knex.schema.alterTable(TableName.IdentityLdapAuth, (t) => { + t.dropForeign(["templateId"]); + t.dropColumn("templateId"); + }); + } + await knex.schema.dropTableIfExists(TableName.IdentityAuthTemplate); + await dropOnUpdateTrigger(knex, TableName.IdentityAuthTemplate); +} diff --git a/backend/src/db/schemas/identity-auth-templates.ts b/backend/src/db/schemas/identity-auth-templates.ts new file mode 100644 index 000000000..efe8ccb8c --- /dev/null +++ b/backend/src/db/schemas/identity-auth-templates.ts @@ -0,0 +1,24 @@ +// 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 IdentityAuthTemplatesSchema = z.object({ + id: z.string().uuid(), + templateFields: zodBuffer, + orgId: z.string().uuid(), + name: z.string(), + authMethod: z.string(), + createdAt: z.date(), + updatedAt: z.date() +}); + +export type TIdentityAuthTemplates = z.infer; +export type TIdentityAuthTemplatesInsert = Omit, TImmutableDBKeys>; +export type TIdentityAuthTemplatesUpdate = Partial, TImmutableDBKeys>>; diff --git a/backend/src/db/schemas/identity-ldap-auths.ts b/backend/src/db/schemas/identity-ldap-auths.ts index e8d0658d5..3e4d88649 100644 --- a/backend/src/db/schemas/identity-ldap-auths.ts +++ b/backend/src/db/schemas/identity-ldap-auths.ts @@ -25,7 +25,8 @@ export const IdentityLdapAuthsSchema = z.object({ allowedFields: z.unknown().nullable().optional(), createdAt: z.date(), updatedAt: z.date(), - accessTokenPeriod: z.coerce.number().default(0) + accessTokenPeriod: z.coerce.number().default(0), + templateId: z.string().uuid().nullable().optional() }); export type TIdentityLdapAuths = z.infer; diff --git a/backend/src/db/schemas/models.ts b/backend/src/db/schemas/models.ts index 55ec12faa..855934b28 100644 --- a/backend/src/db/schemas/models.ts +++ b/backend/src/db/schemas/models.ts @@ -91,6 +91,7 @@ export enum TableName { IdentityProjectMembership = "identity_project_memberships", IdentityProjectMembershipRole = "identity_project_membership_role", IdentityProjectAdditionalPrivilege = "identity_project_additional_privilege", + IdentityAuthTemplate = "identity_auth_templates", // used by both identity and users IdentityMetadata = "identity_metadata", ResourceMetadata = "resource_metadata", diff --git a/backend/src/db/schemas/reminders.ts b/backend/src/db/schemas/reminders.ts index 6656ad077..f9d5d8b42 100644 --- a/backend/src/db/schemas/reminders.ts +++ b/backend/src/db/schemas/reminders.ts @@ -14,7 +14,8 @@ export const RemindersSchema = z.object({ repeatDays: z.number().nullable().optional(), nextReminderDate: z.date(), createdAt: z.date(), - updatedAt: z.date() + updatedAt: z.date(), + fromDate: z.date().nullable().optional() }); export type TReminders = z.infer; diff --git a/backend/src/db/schemas/user-encryption-keys.ts b/backend/src/db/schemas/user-encryption-keys.ts index fd9d21a9d..cfe61fdc5 100644 --- a/backend/src/db/schemas/user-encryption-keys.ts +++ b/backend/src/db/schemas/user-encryption-keys.ts @@ -15,12 +15,12 @@ export const UserEncryptionKeysSchema = z.object({ protectedKey: z.string().nullable().optional(), protectedKeyIV: z.string().nullable().optional(), protectedKeyTag: z.string().nullable().optional(), - publicKey: z.string(), - encryptedPrivateKey: z.string(), - iv: z.string(), - tag: z.string(), - salt: z.string(), - verifier: z.string(), + publicKey: z.string().nullable().optional(), + encryptedPrivateKey: z.string().nullable().optional(), + iv: z.string().nullable().optional(), + tag: z.string().nullable().optional(), + salt: z.string().nullable().optional(), + verifier: z.string().nullable().optional(), userId: z.string().uuid(), hashedPassword: z.string().nullable().optional(), serverEncryptedPrivateKey: z.string().nullable().optional(), diff --git a/backend/src/db/seed-data.ts b/backend/src/db/seed-data.ts index 2aee85fb1..f1a70ca05 100644 --- a/backend/src/db/seed-data.ts +++ b/backend/src/db/seed-data.ts @@ -115,6 +115,10 @@ export const generateUserSrpKeys = async (password: string) => { }; export const getUserPrivateKey = async (password: string, user: TUserEncryptionKeys) => { + if (!user.encryptedPrivateKey || !user.iv || !user.tag || !user.salt) { + throw new Error("User encrypted private key not found"); + } + const derivedKey = await argon2.hash(password, { salt: Buffer.from(user.salt), memoryCost: 65536, diff --git a/backend/src/db/seeds/1-user.ts b/backend/src/db/seeds/1-user.ts index 5c6245382..43ce4dadf 100644 --- a/backend/src/db/seeds/1-user.ts +++ b/backend/src/db/seeds/1-user.ts @@ -1,7 +1,7 @@ import { Knex } from "knex"; -import { crypto } from "@app/lib/crypto"; -import { initLogger } from "@app/lib/logger"; +import { initEnvConfig } from "@app/lib/config/env"; +import { initLogger, logger } from "@app/lib/logger"; import { superAdminDALFactory } from "@app/services/super-admin/super-admin-dal"; import { AuthMethod } from "../../services/auth/auth-type"; @@ -17,7 +17,7 @@ export async function seed(knex: Knex): Promise { initLogger(); const superAdminDAL = superAdminDALFactory(knex); - await crypto.initialize(superAdminDAL); + await initEnvConfig(superAdminDAL, logger); await knex(TableName.SuperAdmin).insert([ // eslint-disable-next-line @@ -25,6 +25,7 @@ export async function seed(knex: Knex): Promise { { id: "00000000-0000-0000-0000-000000000000", initialized: true, allowSignUp: true } ]); // Inserts seed entries + const [user] = await knex(TableName.Users) .insert([ { diff --git a/backend/src/db/seeds/3-project.ts b/backend/src/db/seeds/3-project.ts index 26f96eafb..47a41a95c 100644 --- a/backend/src/db/seeds/3-project.ts +++ b/backend/src/db/seeds/3-project.ts @@ -1,9 +1,28 @@ import { Knex } from "knex"; +import { initEnvConfig } from "@app/lib/config/env"; import { crypto, SymmetricKeySize } from "@app/lib/crypto/cryptography"; +import { generateUserSrpKeys } from "@app/lib/crypto/srp"; +import { initLogger, logger } from "@app/lib/logger"; +import { alphaNumericNanoId } from "@app/lib/nanoid"; +import { AuthMethod } from "@app/services/auth/auth-type"; +import { assignWorkspaceKeysToMembers, createProjectKey } from "@app/services/project/project-fns"; +import { projectKeyDALFactory } from "@app/services/project-key/project-key-dal"; +import { projectMembershipDALFactory } from "@app/services/project-membership/project-membership-dal"; +import { projectUserMembershipRoleDALFactory } from "@app/services/project-membership/project-user-membership-role-dal"; +import { superAdminDALFactory } from "@app/services/super-admin/super-admin-dal"; +import { userDALFactory } from "@app/services/user/user-dal"; -import { ProjectMembershipRole, ProjectType, SecretEncryptionAlgo, SecretKeyEncoding, TableName } from "../schemas"; -import { buildUserProjectKey, getUserPrivateKey, seedData1 } from "../seed-data"; +import { + OrgMembershipRole, + OrgMembershipStatus, + ProjectMembershipRole, + ProjectType, + SecretEncryptionAlgo, + SecretKeyEncoding, + TableName +} from "../schemas"; +import { seedData1 } from "../seed-data"; export const DEFAULT_PROJECT_ENVS = [ { name: "Development", slug: "dev" }, @@ -11,12 +30,159 @@ export const DEFAULT_PROJECT_ENVS = [ { name: "Production", slug: "prod" } ]; +const createUserWithGhostUser = async ( + orgId: string, + projectId: string, + userId: string, + userOrgMembershipId: string, + knex: Knex +) => { + const projectKeyDAL = projectKeyDALFactory(knex); + const userDAL = userDALFactory(knex); + const projectMembershipDAL = projectMembershipDALFactory(knex); + const projectUserMembershipRoleDAL = projectUserMembershipRoleDALFactory(knex); + + const email = `sudo-${alphaNumericNanoId(16)}-${orgId}@infisical.com`; // We add a nanoid because the email is unique. And we have to create a new ghost user each time, so we can have access to the private key. + + const password = crypto.randomBytes(128).toString("hex"); + + const [ghostUser] = await knex(TableName.Users) + .insert({ + isGhost: true, + authMethods: [AuthMethod.EMAIL], + username: email, + email, + isAccepted: true + }) + .returning("*"); + + const encKeys = await generateUserSrpKeys(email, password); + + await knex(TableName.UserEncryptionKey) + .insert({ userId: ghostUser.id, encryptionVersion: 2, publicKey: encKeys.publicKey }) + .onConflict("userId") + .merge(); + + await knex(TableName.OrgMembership) + .insert({ + orgId, + userId: ghostUser.id, + role: OrgMembershipRole.Admin, + status: OrgMembershipStatus.Accepted, + isActive: true + }) + .returning("*"); + + const [projectMembership] = await knex(TableName.ProjectMembership) + .insert({ + userId: ghostUser.id, + projectId + }) + .returning("*"); + + await knex(TableName.ProjectUserMembershipRole).insert({ + projectMembershipId: projectMembership.id, + role: ProjectMembershipRole.Admin + }); + + const { key: encryptedProjectKey, iv: encryptedProjectKeyIv } = createProjectKey({ + publicKey: encKeys.publicKey, + privateKey: encKeys.plainPrivateKey + }); + + await knex(TableName.ProjectKeys).insert({ + projectId, + receiverId: ghostUser.id, + encryptedKey: encryptedProjectKey, + nonce: encryptedProjectKeyIv, + senderId: ghostUser.id + }); + + const { iv, tag, ciphertext, encoding, algorithm } = crypto + .encryption() + .symmetric() + .encryptWithRootEncryptionKey(encKeys.plainPrivateKey); + + await knex(TableName.ProjectBot).insert({ + name: "Infisical Bot (Ghost)", + projectId, + tag, + iv, + encryptedProjectKey, + encryptedProjectKeyNonce: encryptedProjectKeyIv, + encryptedPrivateKey: ciphertext, + isActive: true, + publicKey: encKeys.publicKey, + senderId: ghostUser.id, + algorithm, + keyEncoding: encoding + }); + + const latestKey = await projectKeyDAL.findLatestProjectKey(ghostUser.id, projectId, knex); + + if (!latestKey) { + throw new Error("Latest key not found for user"); + } + + const user = await userDAL.findUserEncKeyByUserId(userId, knex); + + if (!user || !user.publicKey) { + throw new Error("User not found"); + } + + const [projectAdmin] = assignWorkspaceKeysToMembers({ + decryptKey: latestKey, + userPrivateKey: encKeys.plainPrivateKey, + members: [ + { + userPublicKey: user.publicKey, + orgMembershipId: userOrgMembershipId + } + ] + }); + + // Create a membership for the user + const userProjectMembership = await projectMembershipDAL.create( + { + projectId, + userId: user.id + }, + knex + ); + await projectUserMembershipRoleDAL.create( + { projectMembershipId: userProjectMembership.id, role: ProjectMembershipRole.Admin }, + knex + ); + + // Create a project key for the user + await projectKeyDAL.create( + { + encryptedKey: projectAdmin.workspaceEncryptedKey, + nonce: projectAdmin.workspaceEncryptedNonce, + senderId: ghostUser.id, + receiverId: user.id, + projectId + }, + knex + ); + + return { + user: ghostUser, + keys: encKeys + }; +}; + export async function seed(knex: Knex): Promise { // Deletes ALL existing entries await knex(TableName.Project).del(); await knex(TableName.Environment).del(); await knex(TableName.SecretFolder).del(); + initLogger(); + + const superAdminDAL = superAdminDALFactory(knex); + await initEnvConfig(superAdminDAL, logger); + const [project] = await knex(TableName.Project) .insert({ name: seedData1.project.name, @@ -29,29 +195,24 @@ export async function seed(knex: Knex): Promise { }) .returning("*"); - const projectMembership = await knex(TableName.ProjectMembership) - .insert({ - projectId: project.id, + const userOrgMembership = await knex(TableName.OrgMembership) + .where({ + orgId: seedData1.organization.id, userId: seedData1.id }) - .returning("*"); - await knex(TableName.ProjectUserMembershipRole).insert({ - role: ProjectMembershipRole.Admin, - projectMembershipId: projectMembership[0].id - }); + .first(); + if (!userOrgMembership) { + throw new Error("User org membership not found"); + } const user = await knex(TableName.UserEncryptionKey).where({ userId: seedData1.id }).first(); if (!user) throw new Error("User not found"); - const userPrivateKey = await getUserPrivateKey(seedData1.password, user); - const projectKey = buildUserProjectKey(userPrivateKey, user.publicKey); - await knex(TableName.ProjectKeys).insert({ - projectId: project.id, - nonce: projectKey.nonce, - encryptedKey: projectKey.ciphertext, - receiverId: seedData1.id, - senderId: seedData1.id - }); + if (!user.publicKey) { + throw new Error("User public key not found"); + } + + await createUserWithGhostUser(seedData1.organization.id, project.id, seedData1.id, userOrgMembership.id, knex); // create default environments and default folders const envs = await knex(TableName.Environment) diff --git a/backend/src/db/seeds/5-machine-identity.ts b/backend/src/db/seeds/5-machine-identity.ts index 391f785ec..ae3d04514 100644 --- a/backend/src/db/seeds/5-machine-identity.ts +++ b/backend/src/db/seeds/5-machine-identity.ts @@ -1,6 +1,9 @@ import { Knex } from "knex"; +import { initEnvConfig } from "@app/lib/config/env"; import { crypto } from "@app/lib/crypto/cryptography"; +import { initLogger, logger } from "@app/lib/logger"; +import { superAdminDALFactory } from "@app/services/super-admin/super-admin-dal"; import { IdentityAuthMethod, OrgMembershipRole, ProjectMembershipRole, TableName } from "../schemas"; import { seedData1 } from "../seed-data"; @@ -10,6 +13,11 @@ export async function seed(knex: Knex): Promise { await knex(TableName.Identity).del(); await knex(TableName.IdentityOrgMembership).del(); + initLogger(); + + const superAdminDAL = superAdminDALFactory(knex); + await initEnvConfig(superAdminDAL, logger); + // Inserts seed entries await knex(TableName.Identity).insert([ { diff --git a/backend/src/ee/routes/v1/identity-template-router.ts b/backend/src/ee/routes/v1/identity-template-router.ts new file mode 100644 index 000000000..b30069643 --- /dev/null +++ b/backend/src/ee/routes/v1/identity-template-router.ts @@ -0,0 +1,391 @@ +import { z } from "zod"; + +import { IdentityAuthTemplatesSchema } from "@app/db/schemas/identity-auth-templates"; +import { EventType } from "@app/ee/services/audit-log/audit-log-types"; +import { + IdentityAuthTemplateMethod, + TEMPLATE_SUCCESS_MESSAGES, + TEMPLATE_VALIDATION_MESSAGES +} from "@app/ee/services/identity-auth-template/identity-auth-template-enums"; +import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; +import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; +import { AuthMode } from "@app/services/auth/auth-type"; + +const ldapTemplateFieldsSchema = z.object({ + url: z.string().min(1, TEMPLATE_VALIDATION_MESSAGES.LDAP.URL_REQUIRED), + bindDN: z.string().min(1, TEMPLATE_VALIDATION_MESSAGES.LDAP.BIND_DN_REQUIRED), + bindPass: z.string().min(1, TEMPLATE_VALIDATION_MESSAGES.LDAP.BIND_PASSWORD_REQUIRED), + searchBase: z.string().min(1, TEMPLATE_VALIDATION_MESSAGES.LDAP.SEARCH_BASE_REQUIRED), + ldapCaCertificate: z.string().trim().optional() +}); + +export const registerIdentityTemplateRouter = async (server: FastifyZodProvider) => { + server.route({ + method: "POST", + url: "/", + config: { + rateLimit: writeLimit + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + schema: { + hide: false, + description: "Create identity auth template", + security: [ + { + bearerAuth: [] + } + ], + body: z.object({ + name: z + .string() + .trim() + .min(1, TEMPLATE_VALIDATION_MESSAGES.TEMPLATE_NAME_REQUIRED) + .max(64, TEMPLATE_VALIDATION_MESSAGES.TEMPLATE_NAME_MAX_LENGTH), + authMethod: z.nativeEnum(IdentityAuthTemplateMethod), + templateFields: ldapTemplateFieldsSchema + }), + response: { + 200: IdentityAuthTemplatesSchema.extend({ + templateFields: z.record(z.string(), z.unknown()) + }) + } + }, + handler: async (req) => { + const template = await server.services.identityAuthTemplate.createTemplate({ + actor: req.permission.type, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + name: req.body.name, + authMethod: req.body.authMethod, + templateFields: req.body.templateFields + }); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + orgId: req.permission.orgId, + event: { + type: EventType.MACHINE_IDENTITY_AUTH_TEMPLATE_CREATE, + metadata: { + templateId: template.id, + name: template.name + } + } + }); + + return template; + } + }); + + server.route({ + method: "PATCH", + url: "/:templateId", + config: { + rateLimit: writeLimit + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + schema: { + hide: false, + description: "Update identity auth template", + security: [ + { + bearerAuth: [] + } + ], + params: z.object({ + templateId: z.string().min(1, TEMPLATE_VALIDATION_MESSAGES.TEMPLATE_ID_REQUIRED) + }), + body: z.object({ + name: z + .string() + .trim() + .min(1, TEMPLATE_VALIDATION_MESSAGES.TEMPLATE_NAME_REQUIRED) + .max(64, TEMPLATE_VALIDATION_MESSAGES.TEMPLATE_NAME_MAX_LENGTH) + .optional(), + templateFields: ldapTemplateFieldsSchema.partial().optional() + }), + response: { + 200: IdentityAuthTemplatesSchema.extend({ + templateFields: z.record(z.string(), z.unknown()) + }) + } + }, + handler: async (req) => { + const template = await server.services.identityAuthTemplate.updateTemplate({ + templateId: req.params.templateId, + name: req.body.name, + templateFields: req.body.templateFields, + actor: req.permission.type, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId + }); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + orgId: req.permission.orgId, + event: { + type: EventType.MACHINE_IDENTITY_AUTH_TEMPLATE_UPDATE, + metadata: { + templateId: template.id, + name: template.name + } + } + }); + + return template; + } + }); + + server.route({ + method: "DELETE", + url: "/:templateId", + config: { + rateLimit: writeLimit + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + schema: { + hide: false, + description: "Delete identity auth template", + security: [ + { + bearerAuth: [] + } + ], + params: z.object({ + templateId: z.string().min(1, TEMPLATE_VALIDATION_MESSAGES.TEMPLATE_ID_REQUIRED) + }), + response: { + 200: z.object({ + message: z.string() + }) + } + }, + handler: async (req) => { + const template = await server.services.identityAuthTemplate.deleteTemplate({ + templateId: req.params.templateId, + actor: req.permission.type, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId + }); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + orgId: req.permission.orgId, + event: { + type: EventType.MACHINE_IDENTITY_AUTH_TEMPLATE_DELETE, + metadata: { + templateId: template.id, + name: template.name + } + } + }); + + return { message: TEMPLATE_SUCCESS_MESSAGES.DELETED }; + } + }); + + server.route({ + method: "GET", + url: "/:templateId", + config: { + rateLimit: readLimit + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + schema: { + hide: false, + description: "Get identity auth template by ID", + security: [ + { + bearerAuth: [] + } + ], + params: z.object({ + templateId: z.string().min(1, TEMPLATE_VALIDATION_MESSAGES.TEMPLATE_ID_REQUIRED) + }), + response: { + 200: IdentityAuthTemplatesSchema.extend({ + templateFields: ldapTemplateFieldsSchema + }) + } + }, + handler: async (req) => { + const template = await server.services.identityAuthTemplate.getTemplate({ + templateId: req.params.templateId, + actor: req.permission.type, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId + }); + + return template; + } + }); + + server.route({ + method: "GET", + url: "/search", + config: { + rateLimit: readLimit + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + schema: { + hide: false, + description: "List identity auth templates", + security: [ + { + bearerAuth: [] + } + ], + querystring: z.object({ + limit: z.coerce.number().positive().max(100).default(5).optional(), + offset: z.coerce.number().min(0).default(0).optional(), + search: z.string().optional() + }), + response: { + 200: z.object({ + templates: IdentityAuthTemplatesSchema.extend({ + templateFields: ldapTemplateFieldsSchema + }).array(), + totalCount: z.number() + }) + } + }, + handler: async (req) => { + const { templates, totalCount } = await server.services.identityAuthTemplate.listTemplates({ + limit: req.query.limit, + offset: req.query.offset, + search: req.query.search, + actor: req.permission.type, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId + }); + + return { templates, totalCount }; + } + }); + + server.route({ + method: "GET", + url: "/", + config: { + rateLimit: readLimit + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + schema: { + hide: false, + description: "Get identity auth templates by authentication method", + security: [ + { + bearerAuth: [] + } + ], + querystring: z.object({ + authMethod: z.nativeEnum(IdentityAuthTemplateMethod) + }), + response: { + 200: IdentityAuthTemplatesSchema.extend({ + templateFields: ldapTemplateFieldsSchema + }).array() + } + }, + handler: async (req) => { + const templates = await server.services.identityAuthTemplate.getTemplatesByAuthMethod({ + authMethod: req.query.authMethod, + actor: req.permission.type, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId + }); + + return templates; + } + }); + + server.route({ + method: "GET", + url: "/:templateId/usage", + config: { + rateLimit: readLimit + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + schema: { + hide: false, + description: "Get template usage by template ID", + security: [ + { + bearerAuth: [] + } + ], + params: z.object({ + templateId: z.string() + }), + response: { + 200: z + .object({ + identityId: z.string(), + identityName: z.string() + }) + .array() + } + }, + handler: async (req) => { + const templates = await server.services.identityAuthTemplate.findTemplateUsages({ + templateId: req.params.templateId, + actor: req.permission.type, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId + }); + + return templates; + } + }); + + server.route({ + method: "POST", + url: "/:templateId/delete-usage", + config: { + rateLimit: writeLimit + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + schema: { + hide: false, + description: "Unlink identity auth template usage", + security: [ + { + bearerAuth: [] + } + ], + params: z.object({ + templateId: z.string() + }), + body: z.object({ + identityIds: z.string().array() + }), + response: { + 200: z + .object({ + authId: z.string(), + identityId: z.string(), + identityName: z.string() + }) + .array() + } + }, + handler: async (req) => { + const templates = await server.services.identityAuthTemplate.unlinkTemplateUsage({ + templateId: req.params.templateId, + identityIds: req.body.identityIds, + actor: req.permission.type, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId + }); + + return templates; + } + }); +}; diff --git a/backend/src/ee/routes/v1/index.ts b/backend/src/ee/routes/v1/index.ts index 8f3b69dfa..ab9503f58 100644 --- a/backend/src/ee/routes/v1/index.ts +++ b/backend/src/ee/routes/v1/index.ts @@ -13,6 +13,7 @@ import { registerGatewayRouter } from "./gateway-router"; import { registerGithubOrgSyncRouter } from "./github-org-sync-router"; import { registerGroupRouter } from "./group-router"; import { registerIdentityProjectAdditionalPrivilegeRouter } from "./identity-project-additional-privilege-router"; +import { registerIdentityTemplateRouter } from "./identity-template-router"; import { registerKmipRouter } from "./kmip-router"; import { registerKmipSpecRouter } from "./kmip-spec-router"; import { registerLdapRouter } from "./ldap-router"; @@ -125,6 +126,7 @@ export const registerV1EERoutes = async (server: FastifyZodProvider) => { await server.register(registerExternalKmsRouter, { prefix: "/external-kms" }); + await server.register(registerIdentityTemplateRouter, { prefix: "/identity-templates" }); await server.register(registerProjectTemplateRouter, { prefix: "/project-templates" }); diff --git a/backend/src/ee/services/audit-log/audit-log-dal.ts b/backend/src/ee/services/audit-log/audit-log-dal.ts index 2df779795..b58f58164 100644 --- a/backend/src/ee/services/audit-log/audit-log-dal.ts +++ b/backend/src/ee/services/audit-log/audit-log-dal.ts @@ -1,8 +1,10 @@ // weird commonjs-related error in the CI requires us to do the import like this import knex from "knex"; +import { v4 as uuidv4 } from "uuid"; import { TDbClient } from "@app/db"; import { TableName, TAuditLogs } from "@app/db/schemas"; +import { getConfig } from "@app/lib/config/env"; import { DatabaseError, GatewayTimeoutError } from "@app/lib/errors"; import { ormify, selectAllTableCols, TOrmify } from "@app/lib/knex"; import { logger } from "@app/lib/logger"; @@ -150,43 +152,70 @@ export const auditLogDALFactory = (db: TDbClient) => { // delete all audit log that have expired const pruneAuditLog: TAuditLogDALFactory["pruneAuditLog"] = async (tx) => { - const AUDIT_LOG_PRUNE_BATCH_SIZE = 10000; - const MAX_RETRY_ON_FAILURE = 3; + const runPrune = async (dbClient: knex.Knex) => { + const AUDIT_LOG_PRUNE_BATCH_SIZE = 10000; + const MAX_RETRY_ON_FAILURE = 3; - const today = new Date(); - let deletedAuditLogIds: { id: string }[] = []; - let numberOfRetryOnFailure = 0; - let isRetrying = false; + const today = new Date(); + let deletedAuditLogIds: { id: string }[] = []; + let numberOfRetryOnFailure = 0; + let isRetrying = false; - logger.info(`${QueueName.DailyResourceCleanUp}: audit log started`); - do { - try { - const findExpiredLogSubQuery = (tx || db)(TableName.AuditLog) - .where("expiresAt", "<", today) - .where("createdAt", "<", today) // to use audit log partition - .orderBy(`${TableName.AuditLog}.createdAt`, "desc") - .select("id") - .limit(AUDIT_LOG_PRUNE_BATCH_SIZE); + logger.info(`${QueueName.DailyResourceCleanUp}: audit log started`); + do { + try { + const findExpiredLogSubQuery = dbClient(TableName.AuditLog) + .where("expiresAt", "<", today) + .where("createdAt", "<", today) // to use audit log partition + .orderBy(`${TableName.AuditLog}.createdAt`, "desc") + .select("id") + .limit(AUDIT_LOG_PRUNE_BATCH_SIZE); - // eslint-disable-next-line no-await-in-loop - deletedAuditLogIds = await (tx || db)(TableName.AuditLog) - .whereIn("id", findExpiredLogSubQuery) - .del() - .returning("id"); - numberOfRetryOnFailure = 0; // reset - } catch (error) { - numberOfRetryOnFailure += 1; - logger.error(error, "Failed to delete audit log on pruning"); - } finally { - // eslint-disable-next-line no-await-in-loop - await new Promise((resolve) => { - setTimeout(resolve, 10); // time to breathe for db - }); - } - isRetrying = numberOfRetryOnFailure > 0; - } while (deletedAuditLogIds.length > 0 || (isRetrying && numberOfRetryOnFailure < MAX_RETRY_ON_FAILURE)); - logger.info(`${QueueName.DailyResourceCleanUp}: audit log completed`); + // eslint-disable-next-line no-await-in-loop + deletedAuditLogIds = await dbClient(TableName.AuditLog) + .whereIn("id", findExpiredLogSubQuery) + .del() + .returning("id"); + numberOfRetryOnFailure = 0; // reset + } catch (error) { + numberOfRetryOnFailure += 1; + logger.error(error, "Failed to delete audit log on pruning"); + } finally { + // eslint-disable-next-line no-await-in-loop + await new Promise((resolve) => { + setTimeout(resolve, 10); // time to breathe for db + }); + } + isRetrying = numberOfRetryOnFailure > 0; + } while (deletedAuditLogIds.length > 0 || (isRetrying && numberOfRetryOnFailure < MAX_RETRY_ON_FAILURE)); + logger.info(`${QueueName.DailyResourceCleanUp}: audit log completed`); + }; + + if (tx) { + await runPrune(tx); + } else { + const QUERY_TIMEOUT_MS = 10 * 60 * 1000; // 10 minutes + await db.transaction(async (trx) => { + await trx.raw(`SET statement_timeout = ${QUERY_TIMEOUT_MS}`); + await runPrune(trx); + }); + } }; - return { ...auditLogOrm, pruneAuditLog, find }; + const create: TAuditLogDALFactory["create"] = async (tx) => { + const config = getConfig(); + + if (config.DISABLE_AUDIT_LOG_STORAGE) { + return { + ...tx, + id: uuidv4(), + createdAt: new Date(), + updatedAt: new Date() + }; + } + + return auditLogOrm.create(tx); + }; + + return { ...auditLogOrm, create, pruneAuditLog, find }; }; diff --git a/backend/src/ee/services/audit-log/audit-log-types.ts b/backend/src/ee/services/audit-log/audit-log-types.ts index 818bb1f99..11d045eb2 100644 --- a/backend/src/ee/services/audit-log/audit-log-types.ts +++ b/backend/src/ee/services/audit-log/audit-log-types.ts @@ -161,6 +161,9 @@ export enum EventType { CREATE_IDENTITY = "create-identity", UPDATE_IDENTITY = "update-identity", DELETE_IDENTITY = "delete-identity", + MACHINE_IDENTITY_AUTH_TEMPLATE_CREATE = "machine-identity-auth-template-create", + MACHINE_IDENTITY_AUTH_TEMPLATE_UPDATE = "machine-identity-auth-template-update", + MACHINE_IDENTITY_AUTH_TEMPLATE_DELETE = "machine-identity-auth-template-delete", LOGIN_IDENTITY_UNIVERSAL_AUTH = "login-identity-universal-auth", ADD_IDENTITY_UNIVERSAL_AUTH = "add-identity-universal-auth", UPDATE_IDENTITY_UNIVERSAL_AUTH = "update-identity-universal-auth", @@ -830,6 +833,30 @@ interface LoginIdentityUniversalAuthEvent { }; } +interface MachineIdentityAuthTemplateCreateEvent { + type: EventType.MACHINE_IDENTITY_AUTH_TEMPLATE_CREATE; + metadata: { + templateId: string; + name: string; + }; +} + +interface MachineIdentityAuthTemplateUpdateEvent { + type: EventType.MACHINE_IDENTITY_AUTH_TEMPLATE_UPDATE; + metadata: { + templateId: string; + name: string; + }; +} + +interface MachineIdentityAuthTemplateDeleteEvent { + type: EventType.MACHINE_IDENTITY_AUTH_TEMPLATE_DELETE; + metadata: { + templateId: string; + name: string; + }; +} + interface AddIdentityUniversalAuthEvent { type: EventType.ADD_IDENTITY_UNIVERSAL_AUTH; metadata: { @@ -1325,6 +1352,7 @@ interface AddIdentityLdapAuthEvent { accessTokenTrustedIps?: Array; allowedFields?: TAllowedFields[]; url: string; + templateId?: string | null; }; } @@ -1338,6 +1366,7 @@ interface UpdateIdentityLdapAuthEvent { accessTokenTrustedIps?: Array; allowedFields?: TAllowedFields[]; url?: string; + templateId?: string | null; }; } @@ -3439,6 +3468,9 @@ export type Event = | UpdateIdentityEvent | DeleteIdentityEvent | LoginIdentityUniversalAuthEvent + | MachineIdentityAuthTemplateCreateEvent + | MachineIdentityAuthTemplateUpdateEvent + | MachineIdentityAuthTemplateDeleteEvent | AddIdentityUniversalAuthEvent | UpdateIdentityUniversalAuthEvent | DeleteIdentityUniversalAuthEvent diff --git a/backend/src/ee/services/group/group-fns.ts b/backend/src/ee/services/group/group-fns.ts index 436b0b79e..56f8df6c0 100644 --- a/backend/src/ee/services/group/group-fns.ts +++ b/backend/src/ee/services/group/group-fns.ts @@ -1,6 +1,6 @@ import { Knex } from "knex"; -import { SecretKeyEncoding, TableName, TUsers } from "@app/db/schemas"; +import { ProjectVersion, SecretKeyEncoding, TableName, TUsers } from "@app/db/schemas"; import { crypto } from "@app/lib/crypto/cryptography"; import { BadRequestError, ForbiddenRequestError, NotFoundError, ScimRequestError } from "@app/lib/errors"; @@ -65,6 +65,18 @@ const addAcceptedUsersToGroup = async ({ const userKeysSet = new Set(keys.map((k) => `${k.projectId}-${k.receiverId}`)); for await (const projectId of projectIds) { + const project = await projectDAL.findById(projectId, tx); + if (!project) { + throw new NotFoundError({ + message: `Failed to find project with ID '${projectId}'` + }); + } + + if (project.version !== ProjectVersion.V1 && project.version !== ProjectVersion.V2) { + // eslint-disable-next-line no-continue + continue; + } + const usersToAddProjectKeyFor = users.filter((u) => !userKeysSet.has(`${projectId}-${u.userId}`)); if (usersToAddProjectKeyFor.length) { @@ -86,6 +98,12 @@ const addAcceptedUsersToGroup = async ({ }); } + if (!ghostUserLatestKey.sender.publicKey) { + throw new NotFoundError({ + message: `Failed to find project owner's public key in project with ID '${projectId}'` + }); + } + const bot = await projectBotDAL.findOne({ projectId }, tx); if (!bot) { @@ -112,6 +130,12 @@ const addAcceptedUsersToGroup = async ({ }); const projectKeysToAdd = usersToAddProjectKeyFor.map((user) => { + if (!user.publicKey) { + throw new NotFoundError({ + message: `Failed to find user's public key in project with ID '${projectId}'` + }); + } + const { ciphertext: encryptedKey, nonce } = crypto .encryption() .asymmetric() diff --git a/backend/src/ee/services/group/group-service.ts b/backend/src/ee/services/group/group-service.ts index 55665c146..46c9831b0 100644 --- a/backend/src/ee/services/group/group-service.ts +++ b/backend/src/ee/services/group/group-service.ts @@ -41,7 +41,7 @@ type TGroupServiceFactoryDep = { TUserGroupMembershipDALFactory, "findOne" | "delete" | "filterProjectsByUserMembership" | "transaction" | "insertMany" | "find" >; - projectDAL: Pick; + projectDAL: Pick; projectBotDAL: Pick; projectKeyDAL: Pick; permissionService: Pick; diff --git a/backend/src/ee/services/group/group-types.ts b/backend/src/ee/services/group/group-types.ts index 1d7c5fc71..e91f7a47b 100644 --- a/backend/src/ee/services/group/group-types.ts +++ b/backend/src/ee/services/group/group-types.ts @@ -65,7 +65,7 @@ export type TAddUsersToGroup = { userGroupMembershipDAL: Pick; groupProjectDAL: Pick; projectKeyDAL: Pick; - projectDAL: Pick; + projectDAL: Pick; projectBotDAL: Pick; tx: Knex; }; @@ -78,7 +78,7 @@ export type TAddUsersToGroupByUserIds = { orgDAL: Pick; groupProjectDAL: Pick; projectKeyDAL: Pick; - projectDAL: Pick; + projectDAL: Pick; projectBotDAL: Pick; tx?: Knex; }; @@ -102,7 +102,7 @@ export type TConvertPendingGroupAdditionsToGroupMemberships = { >; groupProjectDAL: Pick; projectKeyDAL: Pick; - projectDAL: Pick; + projectDAL: Pick; projectBotDAL: Pick; tx?: Knex; }; diff --git a/backend/src/ee/services/identity-auth-template/identity-auth-template-dal.ts b/backend/src/ee/services/identity-auth-template/identity-auth-template-dal.ts new file mode 100644 index 000000000..736f0f033 --- /dev/null +++ b/backend/src/ee/services/identity-auth-template/identity-auth-template-dal.ts @@ -0,0 +1,83 @@ +/* eslint-disable no-case-declarations */ +import { Knex } from "knex"; + +import { TDbClient } from "@app/db"; +import { TableName } from "@app/db/schemas"; +import { buildFindFilter, ormify } from "@app/lib/knex"; + +import { IdentityAuthTemplateMethod } from "./identity-auth-template-enums"; + +export type TIdentityAuthTemplateDALFactory = ReturnType; + +export const identityAuthTemplateDALFactory = (db: TDbClient) => { + const identityAuthTemplateOrm = ormify(db, TableName.IdentityAuthTemplate); + + const findByOrgId = async ( + orgId: string, + { limit, offset, search, tx }: { limit?: number; offset?: number; search?: string; tx?: Knex } = {} + ) => { + let query = (tx || db.replicaNode())(TableName.IdentityAuthTemplate).where({ orgId }); + let countQuery = (tx || db.replicaNode())(TableName.IdentityAuthTemplate).where({ orgId }); + + if (search) { + const searchFilter = `%${search.toLowerCase()}%`; + query = query.whereRaw("LOWER(name) LIKE ?", [searchFilter]); + countQuery = countQuery.whereRaw("LOWER(name) LIKE ?", [searchFilter]); + } + + query = query.orderBy("createdAt", "desc"); + + if (limit !== undefined) { + query = query.limit(limit); + } + if (offset !== undefined) { + query = query.offset(offset); + } + + const docs = await query; + + const [{ count }] = (await countQuery.count("* as count")) as [{ count: string | number }]; + + return { docs, totalCount: Number(count) }; + }; + + const findByAuthMethod = async (authMethod: string, orgId: string, tx?: Knex) => { + const query = (tx || db.replicaNode())(TableName.IdentityAuthTemplate) + .where({ authMethod, orgId }) + .orderBy("createdAt", "desc"); + const docs = await query; + return docs; + }; + + const findTemplateUsages = async (templateId: string, authMethod: string, tx?: Knex) => { + switch (authMethod) { + case IdentityAuthTemplateMethod.LDAP: + const query = (tx || db.replicaNode())(TableName.IdentityLdapAuth) + .join(TableName.Identity, `${TableName.IdentityLdapAuth}.identityId`, `${TableName.Identity}.id`) + // eslint-disable-next-line @typescript-eslint/no-misused-promises + .where(buildFindFilter({ templateId }, TableName.IdentityLdapAuth)) + .select( + db.ref("identityId").withSchema(TableName.IdentityLdapAuth), + db.ref("name").withSchema(TableName.Identity).as("identityName") + ); + const docs = await query; + return docs; + default: + return []; + } + }; + + const findByIdAndOrgId = async (id: string, orgId: string, tx?: Knex) => { + const query = (tx || db.replicaNode())(TableName.IdentityAuthTemplate).where({ id, orgId }); + const doc = await query; + return doc?.[0]; + }; + + return { + ...identityAuthTemplateOrm, + findByOrgId, + findByAuthMethod, + findTemplateUsages, + findByIdAndOrgId + }; +}; diff --git a/backend/src/ee/services/identity-auth-template/identity-auth-template-enums.ts b/backend/src/ee/services/identity-auth-template/identity-auth-template-enums.ts new file mode 100644 index 000000000..c5b47b158 --- /dev/null +++ b/backend/src/ee/services/identity-auth-template/identity-auth-template-enums.ts @@ -0,0 +1,22 @@ +export enum IdentityAuthTemplateMethod { + LDAP = "ldap" +} + +export const TEMPLATE_VALIDATION_MESSAGES = { + TEMPLATE_NAME_REQUIRED: "Template name is required", + TEMPLATE_NAME_MAX_LENGTH: "Template name must be at most 64 characters long", + AUTH_METHOD_REQUIRED: "Auth method is required", + TEMPLATE_ID_REQUIRED: "Template ID is required", + LDAP: { + URL_REQUIRED: "LDAP URL is required", + BIND_DN_REQUIRED: "Bind DN is required", + BIND_PASSWORD_REQUIRED: "Bind password is required", + SEARCH_BASE_REQUIRED: "Search base is required" + } +} as const; + +export const TEMPLATE_SUCCESS_MESSAGES = { + CREATED: "Template created successfully", + UPDATED: "Template updated successfully", + DELETED: "Template deleted successfully" +} as const; diff --git a/backend/src/ee/services/identity-auth-template/identity-auth-template-service.ts b/backend/src/ee/services/identity-auth-template/identity-auth-template-service.ts new file mode 100644 index 000000000..ef071742d --- /dev/null +++ b/backend/src/ee/services/identity-auth-template/identity-auth-template-service.ts @@ -0,0 +1,454 @@ +import { ForbiddenError } from "@casl/ability"; + +import { EventType, TAuditLogServiceFactory } from "@app/ee/services/audit-log/audit-log-types"; +import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; +import { + OrgPermissionMachineIdentityAuthTemplateActions, + OrgPermissionSubjects +} from "@app/ee/services/permission/org-permission"; +import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types"; +import { BadRequestError, NotFoundError } from "@app/lib/errors"; +import { TOrgPermission } from "@app/lib/types"; +import { ActorType } from "@app/services/auth/auth-type"; +import { TIdentityLdapAuthDALFactory } from "@app/services/identity-ldap-auth/identity-ldap-auth-dal"; +import { TKmsServiceFactory } from "@app/services/kms/kms-service"; +import { KmsDataKey } from "@app/services/kms/kms-types"; + +import { TIdentityAuthTemplateDALFactory } from "./identity-auth-template-dal"; +import { IdentityAuthTemplateMethod } from "./identity-auth-template-enums"; +import { + TDeleteIdentityAuthTemplateDTO, + TFindTemplateUsagesDTO, + TGetIdentityAuthTemplateDTO, + TGetTemplatesByAuthMethodDTO, + TLdapTemplateFields, + TListIdentityAuthTemplatesDTO, + TUnlinkTemplateUsageDTO +} from "./identity-auth-template-types"; + +type TIdentityAuthTemplateServiceFactoryDep = { + identityAuthTemplateDAL: TIdentityAuthTemplateDALFactory; + identityLdapAuthDAL: TIdentityLdapAuthDALFactory; + permissionService: Pick; + kmsService: Pick; + licenseService: Pick; + auditLogService: Pick; +}; + +export type TIdentityAuthTemplateServiceFactory = ReturnType; + +export const identityAuthTemplateServiceFactory = ({ + identityAuthTemplateDAL, + identityLdapAuthDAL, + permissionService, + kmsService, + licenseService, + auditLogService +}: TIdentityAuthTemplateServiceFactoryDep) => { + // Plan check + const $checkPlan = async (orgId: string) => { + const plan = await licenseService.getPlan(orgId); + if (!plan.machineIdentityAuthTemplates) + throw new BadRequestError({ + message: + "Failed to use identity auth template due to plan restriction. Upgrade plan to access machine identity auth templates." + }); + }; + const createTemplate = async ({ + name, + authMethod, + templateFields, + actorId, + actorAuthMethod, + actor, + actorOrgId + }: { + name: string; + authMethod: string; + templateFields: Record; + } & Omit) => { + await $checkPlan(actorOrgId); + const { permission } = await permissionService.getOrgPermission( + actor, + actorId, + actorOrgId, + actorAuthMethod, + actorOrgId + ); + ForbiddenError.from(permission).throwUnlessCan( + OrgPermissionMachineIdentityAuthTemplateActions.CreateTemplates, + OrgPermissionSubjects.MachineIdentityAuthTemplate + ); + + const { encryptor } = await kmsService.createCipherPairWithDataKey({ + type: KmsDataKey.Organization, + orgId: actorOrgId + }); + const template = await identityAuthTemplateDAL.create({ + name, + authMethod, + templateFields: encryptor({ plainText: Buffer.from(JSON.stringify(templateFields)) }).cipherTextBlob, + orgId: actorOrgId + }); + + return { ...template, templateFields }; + }; + + const updateTemplate = async ({ + templateId, + name, + templateFields, + actorId, + actorAuthMethod, + actor, + actorOrgId + }: { + templateId: string; + name?: string; + templateFields?: Record; + } & Omit) => { + await $checkPlan(actorOrgId); + const template = await identityAuthTemplateDAL.findByIdAndOrgId(templateId, actorOrgId); + if (!template) { + throw new NotFoundError({ message: "Template not found" }); + } + + const { permission } = await permissionService.getOrgPermission( + actor, + actorId, + template.orgId, + actorAuthMethod, + actorOrgId + ); + ForbiddenError.from(permission).throwUnlessCan( + OrgPermissionMachineIdentityAuthTemplateActions.EditTemplates, + OrgPermissionSubjects.MachineIdentityAuthTemplate + ); + + const { encryptor } = await kmsService.createCipherPairWithDataKey({ + type: KmsDataKey.Organization, + orgId: template.orgId + }); + + let finalTemplateFields: Record = {}; + + const updatedTemplate = await identityAuthTemplateDAL.transaction(async (tx) => { + const authTemplate = await identityAuthTemplateDAL.updateById( + templateId, + { + name, + ...(templateFields && { + templateFields: encryptor({ plainText: Buffer.from(JSON.stringify(templateFields)) }).cipherTextBlob + }) + }, + tx + ); + + if (templateFields && template.authMethod === IdentityAuthTemplateMethod.LDAP) { + const { decryptor } = await kmsService.createCipherPairWithDataKey({ + type: KmsDataKey.Organization, + orgId: template.orgId + }); + + const currentTemplateFields = JSON.parse( + decryptor({ cipherTextBlob: template.templateFields }).toString() + ) as TLdapTemplateFields; + + const mergedTemplateFields: TLdapTemplateFields = { ...currentTemplateFields, ...templateFields }; + finalTemplateFields = mergedTemplateFields; + const ldapUpdateData: { + url?: string; + searchBase?: string; + encryptedBindDN?: Buffer; + encryptedBindPass?: Buffer; + encryptedLdapCaCertificate?: Buffer; + } = {}; + + if ("url" in templateFields) { + ldapUpdateData.url = mergedTemplateFields.url; + } + if ("searchBase" in templateFields) { + ldapUpdateData.searchBase = mergedTemplateFields.searchBase; + } + if ("bindDN" in templateFields) { + ldapUpdateData.encryptedBindDN = encryptor({ + plainText: Buffer.from(mergedTemplateFields.bindDN) + }).cipherTextBlob; + } + if ("bindPass" in templateFields) { + ldapUpdateData.encryptedBindPass = encryptor({ + plainText: Buffer.from(mergedTemplateFields.bindPass) + }).cipherTextBlob; + } + if ("ldapCaCertificate" in templateFields) { + ldapUpdateData.encryptedLdapCaCertificate = encryptor({ + plainText: Buffer.from(mergedTemplateFields.ldapCaCertificate || "") + }).cipherTextBlob; + } + + if (Object.keys(ldapUpdateData).length > 0) { + const updatedLdapAuths = await identityLdapAuthDAL.update({ templateId }, ldapUpdateData, tx); + await Promise.all( + updatedLdapAuths.map(async (updatedLdapAuth) => { + await auditLogService.createAuditLog({ + actor: { + type: ActorType.PLATFORM, + metadata: {} + }, + orgId: actorOrgId, + event: { + type: EventType.UPDATE_IDENTITY_LDAP_AUTH, + metadata: { + identityId: updatedLdapAuth.identityId, + templateId: template.id + } + } + }); + }) + ); + } + } + return authTemplate; + }); + + return { ...updatedTemplate, templateFields: finalTemplateFields }; + }; + + const deleteTemplate = async ({ + templateId, + actorId, + actorAuthMethod, + actor, + actorOrgId + }: TDeleteIdentityAuthTemplateDTO) => { + await $checkPlan(actorOrgId); + const template = await identityAuthTemplateDAL.findByIdAndOrgId(templateId, actorOrgId); + if (!template) { + throw new NotFoundError({ message: "Template not found" }); + } + + const { permission } = await permissionService.getOrgPermission( + actor, + actorId, + template.orgId, + actorAuthMethod, + actorOrgId + ); + ForbiddenError.from(permission).throwUnlessCan( + OrgPermissionMachineIdentityAuthTemplateActions.DeleteTemplates, + OrgPermissionSubjects.MachineIdentityAuthTemplate + ); + + const deletedTemplate = await identityAuthTemplateDAL.transaction(async (tx) => { + // Remove template reference from identityLdapAuth records + const updatedLdapAuths = await identityLdapAuthDAL.update({ templateId }, { templateId: null }, tx); + await Promise.all( + updatedLdapAuths.map(async (updatedLdapAuth) => { + await auditLogService.createAuditLog({ + actor: { + type: ActorType.PLATFORM, + metadata: {} + }, + orgId: actorOrgId, + event: { + type: EventType.UPDATE_IDENTITY_LDAP_AUTH, + metadata: { + identityId: updatedLdapAuth.identityId, + templateId: template.id + } + } + }); + }) + ); + + // Delete the template + const [deletedTpl] = await identityAuthTemplateDAL.delete({ id: templateId }, tx); + return deletedTpl; + }); + + return deletedTemplate; + }; + + const getTemplate = async ({ + templateId, + actorId, + actorAuthMethod, + actor, + actorOrgId + }: TGetIdentityAuthTemplateDTO) => { + await $checkPlan(actorOrgId); + const template = await identityAuthTemplateDAL.findByIdAndOrgId(templateId, actorOrgId); + if (!template) { + throw new NotFoundError({ message: "Template not found" }); + } + + const { permission } = await permissionService.getOrgPermission( + actor, + actorId, + template.orgId, + actorAuthMethod, + actorOrgId + ); + ForbiddenError.from(permission).throwUnlessCan( + OrgPermissionMachineIdentityAuthTemplateActions.ListTemplates, + OrgPermissionSubjects.MachineIdentityAuthTemplate + ); + + const { decryptor } = await kmsService.createCipherPairWithDataKey({ + type: KmsDataKey.Organization, + orgId: template.orgId + }); + const decryptedTemplateFields = decryptor({ cipherTextBlob: template.templateFields }).toString(); + return { + ...template, + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + templateFields: JSON.parse(decryptedTemplateFields) + }; + }; + + const listTemplates = async ({ + limit, + offset, + search, + actorId, + actorAuthMethod, + actor, + actorOrgId + }: TListIdentityAuthTemplatesDTO) => { + await $checkPlan(actorOrgId); + const { permission } = await permissionService.getOrgPermission( + actor, + actorId, + actorOrgId, + actorAuthMethod, + actorOrgId + ); + ForbiddenError.from(permission).throwUnlessCan( + OrgPermissionMachineIdentityAuthTemplateActions.ListTemplates, + OrgPermissionSubjects.MachineIdentityAuthTemplate + ); + + const { docs, totalCount } = await identityAuthTemplateDAL.findByOrgId(actorOrgId, { limit, offset, search }); + + const { decryptor } = await kmsService.createCipherPairWithDataKey({ + type: KmsDataKey.Organization, + orgId: actorOrgId + }); + return { + totalCount, + templates: docs.map((doc) => ({ + ...doc, + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + templateFields: JSON.parse(decryptor({ cipherTextBlob: doc.templateFields }).toString()) + })) + }; + }; + + const getTemplatesByAuthMethod = async ({ + authMethod, + actorId, + actorAuthMethod, + actor, + actorOrgId + }: TGetTemplatesByAuthMethodDTO) => { + await $checkPlan(actorOrgId); + const { permission } = await permissionService.getOrgPermission( + actor, + actorId, + actorOrgId, + actorAuthMethod, + actorOrgId + ); + ForbiddenError.from(permission).throwUnlessCan( + OrgPermissionMachineIdentityAuthTemplateActions.AttachTemplates, + OrgPermissionSubjects.MachineIdentityAuthTemplate + ); + + const docs = await identityAuthTemplateDAL.findByAuthMethod(authMethod, actorOrgId); + + const { decryptor } = await kmsService.createCipherPairWithDataKey({ + type: KmsDataKey.Organization, + orgId: actorOrgId + }); + return docs.map((doc) => ({ + ...doc, + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + templateFields: JSON.parse(decryptor({ cipherTextBlob: doc.templateFields }).toString()) + })); + }; + + const findTemplateUsages = async ({ + templateId, + actorId, + actorAuthMethod, + actor, + actorOrgId + }: TFindTemplateUsagesDTO) => { + await $checkPlan(actorOrgId); + const { permission } = await permissionService.getOrgPermission( + actor, + actorId, + actorOrgId, + actorAuthMethod, + actorOrgId + ); + ForbiddenError.from(permission).throwUnlessCan( + OrgPermissionMachineIdentityAuthTemplateActions.ListTemplates, + OrgPermissionSubjects.MachineIdentityAuthTemplate + ); + + const template = await identityAuthTemplateDAL.findByIdAndOrgId(templateId, actorOrgId); + if (!template) { + throw new NotFoundError({ message: "Template not found" }); + } + + const docs = await identityAuthTemplateDAL.findTemplateUsages(templateId, template.authMethod); + return docs; + }; + + const unlinkTemplateUsage = async ({ + templateId, + identityIds, + actorId, + actorAuthMethod, + actor, + actorOrgId + }: TUnlinkTemplateUsageDTO) => { + await $checkPlan(actorOrgId); + const { permission } = await permissionService.getOrgPermission( + actor, + actorId, + actorOrgId, + actorAuthMethod, + actorOrgId + ); + ForbiddenError.from(permission).throwUnlessCan( + OrgPermissionMachineIdentityAuthTemplateActions.UnlinkTemplates, + OrgPermissionSubjects.MachineIdentityAuthTemplate + ); + + const template = await identityAuthTemplateDAL.findByIdAndOrgId(templateId, actorOrgId); + if (!template) { + throw new NotFoundError({ message: "Template not found" }); + } + + switch (template.authMethod) { + case IdentityAuthTemplateMethod.LDAP: + await identityLdapAuthDAL.update({ $in: { identityId: identityIds }, templateId }, { templateId: null }); + break; + default: + break; + } + }; + + return { + createTemplate, + updateTemplate, + deleteTemplate, + getTemplate, + listTemplates, + getTemplatesByAuthMethod, + findTemplateUsages, + unlinkTemplateUsage + }; +}; diff --git a/backend/src/ee/services/identity-auth-template/identity-auth-template-types.ts b/backend/src/ee/services/identity-auth-template/identity-auth-template-types.ts new file mode 100644 index 000000000..8039e41c2 --- /dev/null +++ b/backend/src/ee/services/identity-auth-template/identity-auth-template-types.ts @@ -0,0 +1,61 @@ +import { TProjectPermission } from "@app/lib/types"; + +import { IdentityAuthTemplateMethod } from "./identity-auth-template-enums"; + +// Method-specific template field types +export type TLdapTemplateFields = { + url: string; + bindDN: string; + bindPass: string; + searchBase: string; + ldapCaCertificate?: string; +}; + +// Union type for all template field types +export type TTemplateFieldsByMethod = { + [IdentityAuthTemplateMethod.LDAP]: TLdapTemplateFields; +}; + +// Generic base types that use conditional types for type safety +export type TCreateIdentityAuthTemplateDTO = { + name: string; + authMethod: IdentityAuthTemplateMethod; + templateFields: TTemplateFieldsByMethod[IdentityAuthTemplateMethod]; +} & Omit; + +export type TUpdateIdentityAuthTemplateDTO = { + templateId: string; + name?: string; + templateFields?: Partial; +} & Omit; + +export type TDeleteIdentityAuthTemplateDTO = { + templateId: string; +} & Omit; + +export type TGetIdentityAuthTemplateDTO = { + templateId: string; +} & Omit; + +export type TListIdentityAuthTemplatesDTO = { + limit?: number; + offset?: number; + search?: string; +} & Omit; + +export type TGetTemplatesByAuthMethodDTO = { + authMethod: string; +} & Omit; + +export type TFindTemplateUsagesDTO = { + templateId: string; +} & Omit; + +export type TUnlinkTemplateUsageDTO = { + templateId: string; + identityIds: string[]; +} & Omit; + +// Specific LDAP types for convenience +export type TCreateLdapTemplateDTO = TCreateIdentityAuthTemplateDTO; +export type TUpdateLdapTemplateDTO = TUpdateIdentityAuthTemplateDTO; diff --git a/backend/src/ee/services/identity-auth-template/index.ts b/backend/src/ee/services/identity-auth-template/index.ts new file mode 100644 index 000000000..4358d1a27 --- /dev/null +++ b/backend/src/ee/services/identity-auth-template/index.ts @@ -0,0 +1,6 @@ +export type { TIdentityAuthTemplateDALFactory } from "./identity-auth-template-dal"; +export { identityAuthTemplateDALFactory } from "./identity-auth-template-dal"; +export * from "./identity-auth-template-enums"; +export type { TIdentityAuthTemplateServiceFactory } from "./identity-auth-template-service"; +export { identityAuthTemplateServiceFactory } from "./identity-auth-template-service"; +export type * from "./identity-auth-template-types"; diff --git a/backend/src/ee/services/ldap-config/ldap-config-service.ts b/backend/src/ee/services/ldap-config/ldap-config-service.ts index f3aac5b92..b1a10056c 100644 --- a/backend/src/ee/services/ldap-config/ldap-config-service.ts +++ b/backend/src/ee/services/ldap-config/ldap-config-service.ts @@ -55,7 +55,7 @@ type TLdapConfigServiceFactoryDep = { groupDAL: Pick; groupProjectDAL: Pick; projectKeyDAL: Pick; - projectDAL: Pick; + projectDAL: Pick; projectBotDAL: Pick; userGroupMembershipDAL: Pick< TUserGroupMembershipDALFactory, diff --git a/backend/src/ee/services/license/__mocks__/license-fns.ts b/backend/src/ee/services/license/__mocks__/license-fns.ts index 5259d4616..4c42ad7a2 100644 --- a/backend/src/ee/services/license/__mocks__/license-fns.ts +++ b/backend/src/ee/services/license/__mocks__/license-fns.ts @@ -31,7 +31,8 @@ export const getDefaultOnPremFeatures = () => { caCrl: false, sshHostGroups: false, enterpriseSecretSyncs: false, - enterpriseAppConnections: false + enterpriseAppConnections: false, + machineIdentityAuthTemplates: false }; }; diff --git a/backend/src/ee/services/license/license-fns.ts b/backend/src/ee/services/license/license-fns.ts index fecba7ba7..bd3949a7e 100644 --- a/backend/src/ee/services/license/license-fns.ts +++ b/backend/src/ee/services/license/license-fns.ts @@ -60,7 +60,8 @@ export const getDefaultOnPremFeatures = (): TFeatureSet => ({ enterpriseSecretSyncs: false, enterpriseAppConnections: false, fips: false, - eventSubscriptions: false + eventSubscriptions: false, + machineIdentityAuthTemplates: false }); export const setupLicenseRequestWithStore = ( diff --git a/backend/src/ee/services/license/license-types.ts b/backend/src/ee/services/license/license-types.ts index 84a652c46..098d00feb 100644 --- a/backend/src/ee/services/license/license-types.ts +++ b/backend/src/ee/services/license/license-types.ts @@ -75,6 +75,7 @@ export type TFeatureSet = { secretScanning: false; enterpriseSecretSyncs: false; enterpriseAppConnections: false; + machineIdentityAuthTemplates: false; fips: false; eventSubscriptions: false; }; diff --git a/backend/src/ee/services/oidc/oidc-config-service.ts b/backend/src/ee/services/oidc/oidc-config-service.ts index ca6f67235..101f744c3 100644 --- a/backend/src/ee/services/oidc/oidc-config-service.ts +++ b/backend/src/ee/services/oidc/oidc-config-service.ts @@ -79,7 +79,7 @@ type TOidcConfigServiceFactoryDep = { >; groupProjectDAL: Pick; projectKeyDAL: Pick; - projectDAL: Pick; + projectDAL: Pick; projectBotDAL: Pick; auditLogService: Pick; kmsService: Pick; diff --git a/backend/src/ee/services/permission/org-permission.ts b/backend/src/ee/services/permission/org-permission.ts index f0fe73d71..2436dae2a 100644 --- a/backend/src/ee/services/permission/org-permission.ts +++ b/backend/src/ee/services/permission/org-permission.ts @@ -28,6 +28,15 @@ export enum OrgPermissionKmipActions { Setup = "setup" } +export enum OrgPermissionMachineIdentityAuthTemplateActions { + ListTemplates = "list-templates", + EditTemplates = "edit-templates", + CreateTemplates = "create-templates", + DeleteTemplates = "delete-templates", + UnlinkTemplates = "unlink-templates", + AttachTemplates = "attach-templates" +} + export enum OrgPermissionAdminConsoleAction { AccessAllProjects = "access-all-projects" } @@ -88,6 +97,7 @@ export enum OrgPermissionSubjects { Identity = "identity", Kms = "kms", AdminConsole = "organization-admin-console", + MachineIdentityAuthTemplate = "machine-identity-auth-template", AuditLogs = "audit-logs", ProjectTemplates = "project-templates", AppConnections = "app-connections", @@ -126,6 +136,7 @@ export type OrgPermissionSet = ) ] | [OrgPermissionAdminConsoleAction, OrgPermissionSubjects.AdminConsole] + | [OrgPermissionMachineIdentityAuthTemplateActions, OrgPermissionSubjects.MachineIdentityAuthTemplate] | [OrgPermissionKmipActions, OrgPermissionSubjects.Kmip] | [OrgPermissionSecretShareAction, OrgPermissionSubjects.SecretShare]; @@ -237,6 +248,14 @@ export const OrgPermissionSchema = z.discriminatedUnion("subject", [ "Describe what action an entity can take." ) }), + z.object({ + subject: z + .literal(OrgPermissionSubjects.MachineIdentityAuthTemplate) + .describe("The entity this permission pertains to."), + action: CASL_ACTION_SCHEMA_NATIVE_ENUM(OrgPermissionMachineIdentityAuthTemplateActions).describe( + "Describe what action an entity can take." + ) + }), z.object({ subject: z.literal(OrgPermissionSubjects.Gateway).describe("The entity this permission pertains to."), action: CASL_ACTION_SCHEMA_NATIVE_ENUM(OrgPermissionGatewayActions).describe( @@ -350,6 +369,25 @@ const buildAdminPermission = () => { // the proxy assignment is temporary in order to prevent "more privilege" error during role assignment to MI can(OrgPermissionKmipActions.Proxy, OrgPermissionSubjects.Kmip); + can(OrgPermissionMachineIdentityAuthTemplateActions.ListTemplates, OrgPermissionSubjects.MachineIdentityAuthTemplate); + can(OrgPermissionMachineIdentityAuthTemplateActions.EditTemplates, OrgPermissionSubjects.MachineIdentityAuthTemplate); + can( + OrgPermissionMachineIdentityAuthTemplateActions.CreateTemplates, + OrgPermissionSubjects.MachineIdentityAuthTemplate + ); + can( + OrgPermissionMachineIdentityAuthTemplateActions.DeleteTemplates, + OrgPermissionSubjects.MachineIdentityAuthTemplate + ); + can( + OrgPermissionMachineIdentityAuthTemplateActions.UnlinkTemplates, + OrgPermissionSubjects.MachineIdentityAuthTemplate + ); + can( + OrgPermissionMachineIdentityAuthTemplateActions.AttachTemplates, + OrgPermissionSubjects.MachineIdentityAuthTemplate + ); + can(OrgPermissionSecretShareAction.ManageSettings, OrgPermissionSubjects.SecretShare); return rules; @@ -385,6 +423,16 @@ const buildMemberPermission = () => { can(OrgPermissionGatewayActions.CreateGateways, OrgPermissionSubjects.Gateway); can(OrgPermissionGatewayActions.AttachGateways, OrgPermissionSubjects.Gateway); + can(OrgPermissionMachineIdentityAuthTemplateActions.ListTemplates, OrgPermissionSubjects.MachineIdentityAuthTemplate); + can( + OrgPermissionMachineIdentityAuthTemplateActions.UnlinkTemplates, + OrgPermissionSubjects.MachineIdentityAuthTemplate + ); + can( + OrgPermissionMachineIdentityAuthTemplateActions.AttachTemplates, + OrgPermissionSubjects.MachineIdentityAuthTemplate + ); + return rules; }; diff --git a/backend/src/ee/services/scim/scim-service.ts b/backend/src/ee/services/scim/scim-service.ts index a87d67b94..9cc6e134d 100644 --- a/backend/src/ee/services/scim/scim-service.ts +++ b/backend/src/ee/services/scim/scim-service.ts @@ -59,7 +59,7 @@ type TScimServiceFactoryDep = { TOrgMembershipDALFactory, "find" | "findOne" | "create" | "updateById" | "findById" | "update" >; - projectDAL: Pick; + projectDAL: Pick; projectMembershipDAL: Pick; groupDAL: Pick< TGroupDALFactory, diff --git a/backend/src/ee/services/secret-scanning-v2/secret-scanning-v2-dal.ts b/backend/src/ee/services/secret-scanning-v2/secret-scanning-v2-dal.ts index 447ffc22a..c6ca50c5e 100644 --- a/backend/src/ee/services/secret-scanning-v2/secret-scanning-v2-dal.ts +++ b/backend/src/ee/services/secret-scanning-v2/secret-scanning-v2-dal.ts @@ -49,6 +49,7 @@ const baseSecretScanningDataSourceQuery = ({ db.ref("encryptedCredentials").withSchema(TableName.AppConnection).as("connectionEncryptedCredentials"), db.ref("description").withSchema(TableName.AppConnection).as("connectionDescription"), db.ref("version").withSchema(TableName.AppConnection).as("connectionVersion"), + db.ref("gatewayId").withSchema(TableName.AppConnection).as("connectionGatewayId"), db.ref("createdAt").withSchema(TableName.AppConnection).as("connectionCreatedAt"), db.ref("updatedAt").withSchema(TableName.AppConnection).as("connectionUpdatedAt"), db @@ -82,6 +83,7 @@ const expandSecretScanningDataSource = < connectionUpdatedAt, connectionVersion, connectionIsPlatformManagedCredentials, + connectionGatewayId, ...el } = dataSource; @@ -100,7 +102,8 @@ const expandSecretScanningDataSource = < createdAt: connectionCreatedAt, updatedAt: connectionUpdatedAt, version: connectionVersion, - isPlatformManagedCredentials: connectionIsPlatformManagedCredentials + isPlatformManagedCredentials: connectionIsPlatformManagedCredentials, + gatewayId: connectionGatewayId } : undefined }; diff --git a/backend/src/lib/api-docs/constants.ts b/backend/src/lib/api-docs/constants.ts index 83df79801..0cc272f15 100644 --- a/backend/src/lib/api-docs/constants.ts +++ b/backend/src/lib/api-docs/constants.ts @@ -18,6 +18,7 @@ import { SECRET_SYNC_CONNECTION_MAP, SECRET_SYNC_NAME_MAP } from "@app/services/ export enum ApiDocsTags { Identities = "Identities", + IdentityTemplates = "Identity Templates", TokenAuth = "Token Auth", UniversalAuth = "Universal Auth", GcpAuth = "GCP Auth", @@ -215,6 +216,7 @@ export const LDAP_AUTH = { password: "The password of the LDAP user to login." }, ATTACH: { + templateId: "The ID of the identity auth template to attach the configuration onto.", identityId: "The ID of the identity to attach the configuration onto.", url: "The URL of the LDAP server.", allowedFields: @@ -241,7 +243,8 @@ export const LDAP_AUTH = { accessTokenTTL: "The new lifetime for an access token in seconds.", accessTokenMaxTTL: "The new maximum lifetime for an access token in seconds.", accessTokenNumUsesLimit: "The new maximum number of times that an access token can be used.", - accessTokenTrustedIps: "The new IPs or CIDR ranges that access tokens can be used from." + accessTokenTrustedIps: "The new IPs or CIDR ranges that access tokens can be used from.", + templateId: "The ID of the identity auth template to update the configuration to." }, RETRIEVE: { identityId: "The ID of the identity to retrieve the configuration for." diff --git a/backend/src/lib/config/env.ts b/backend/src/lib/config/env.ts index 9ff7339c0..29883a7ad 100644 --- a/backend/src/lib/config/env.ts +++ b/backend/src/lib/config/env.ts @@ -59,6 +59,7 @@ const envSchema = z AUDIT_LOGS_DB_ROOT_CERT: zpStr( z.string().describe("Postgres database base64-encoded CA cert for Audit logs").optional() ), + DISABLE_AUDIT_LOG_STORAGE: zodStrBool.default("false").optional().describe("Disable audit log storage"), MAX_LEASE_LIMIT: z.coerce.number().default(10000), DB_ROOT_CERT: zpStr(z.string().describe("Postgres database base64-encoded CA cert").optional()), DB_HOST: zpStr(z.string().describe("Postgres database host").optional()), @@ -482,6 +483,15 @@ export const overwriteSchema: { fields: { key: keyof TEnvConfig; description?: string }[]; }; } = { + auditLogs: { + name: "Audit Logs", + fields: [ + { + key: "DISABLE_AUDIT_LOG_STORAGE", + description: "Disable audit log storage" + } + ] + }, aws: { name: "AWS", fields: [ diff --git a/backend/src/lib/crypto/secret-encryption.ts b/backend/src/lib/crypto/secret-encryption.ts index 7355d4bd6..f22c918ac 100644 --- a/backend/src/lib/crypto/secret-encryption.ts +++ b/backend/src/lib/crypto/secret-encryption.ts @@ -53,7 +53,7 @@ type DecryptedIntegrationAuths = z.infer type TLatestKey = TProjectKeys & { sender: { - publicKey: string; + publicKey?: string; }; }; @@ -91,6 +91,10 @@ const getDecryptedValues = (data: Array<{ ciphertext: string; iv: string; tag: s return results; }; export const decryptSecrets = (encryptedSecrets: TSecrets[], privateKey: string, latestKey: TLatestKey) => { + if (!latestKey.sender.publicKey) { + throw new Error("Latest key sender public key not found"); + } + const key = crypto.encryption().asymmetric().decrypt({ ciphertext: latestKey.encryptedKey, nonce: latestKey.nonce, @@ -143,6 +147,10 @@ export const decryptSecretVersions = ( privateKey: string, latestKey: TLatestKey ) => { + if (!latestKey.sender.publicKey) { + throw new Error("Latest key sender public key not found"); + } + const key = crypto.encryption().asymmetric().decrypt({ ciphertext: latestKey.encryptedKey, nonce: latestKey.nonce, @@ -195,6 +203,10 @@ export const decryptSecretApprovals = ( privateKey: string, latestKey: TLatestKey ) => { + if (!latestKey.sender.publicKey) { + throw new Error("Latest key sender public key not found"); + } + const key = crypto.encryption().asymmetric().decrypt({ ciphertext: latestKey.encryptedKey, nonce: latestKey.nonce, @@ -247,6 +259,10 @@ export const decryptIntegrationAuths = ( privateKey: string, latestKey: TLatestKey ) => { + if (!latestKey.sender.publicKey) { + throw new Error("Latest key sender public key not found"); + } + const key = crypto.encryption().asymmetric().decrypt({ ciphertext: latestKey.encryptedKey, nonce: latestKey.nonce, diff --git a/backend/src/lib/crypto/srp.ts b/backend/src/lib/crypto/srp.ts index 3f403405e..2623e5756 100644 --- a/backend/src/lib/crypto/srp.ts +++ b/backend/src/lib/crypto/srp.ts @@ -4,6 +4,7 @@ import jsrp from "jsrp"; import { TUserEncryptionKeys } from "@app/db/schemas"; import { UserEncryption } from "@app/services/user/user-types"; +import { BadRequestError } from "../errors"; import { crypto, SymmetricKeySize } from "./cryptography"; export const generateSrpServerKey = async (salt: string, verifier: string) => { @@ -127,6 +128,10 @@ export const getUserPrivateKey = async ( > ) => { if (user.encryptionVersion === UserEncryption.V1) { + if (!user.encryptedPrivateKey || !user.iv || !user.tag || !user.salt) { + throw new BadRequestError({ message: "User encrypted private key not found" }); + } + return crypto .encryption() .symmetric() @@ -138,12 +143,25 @@ export const getUserPrivateKey = async ( keySize: SymmetricKeySize.Bits128 }); } + // still used for legacy things if ( user.encryptionVersion === UserEncryption.V2 && user.protectedKey && user.protectedKeyIV && user.protectedKeyTag ) { + if ( + !user.salt || + !user.protectedKey || + !user.protectedKeyIV || + !user.protectedKeyTag || + !user.encryptedPrivateKey || + !user.iv || + !user.tag + ) { + throw new BadRequestError({ message: "User encrypted private key not found" }); + } + const derivedKey = await argon2.hash(password, { salt: Buffer.from(user.salt), memoryCost: 65536, diff --git a/backend/src/server/routes/index.ts b/backend/src/server/routes/index.ts index 256c622f9..64cfb140e 100644 --- a/backend/src/server/routes/index.ts +++ b/backend/src/server/routes/index.ts @@ -179,6 +179,8 @@ import { identityAccessTokenDALFactory } from "@app/services/identity-access-tok import { identityAccessTokenServiceFactory } from "@app/services/identity-access-token/identity-access-token-service"; import { identityAliCloudAuthDALFactory } from "@app/services/identity-alicloud-auth/identity-alicloud-auth-dal"; import { identityAliCloudAuthServiceFactory } from "@app/services/identity-alicloud-auth/identity-alicloud-auth-service"; +import { identityAuthTemplateDALFactory } from "@app/ee/services/identity-auth-template/identity-auth-template-dal"; +import { identityAuthTemplateServiceFactory } from "@app/ee/services/identity-auth-template/identity-auth-template-service"; import { identityAwsAuthDALFactory } from "@app/services/identity-aws-auth/identity-aws-auth-dal"; import { identityAwsAuthServiceFactory } from "@app/services/identity-aws-auth/identity-aws-auth-service"; import { identityAzureAuthDALFactory } from "@app/services/identity-azure-auth/identity-azure-auth-dal"; @@ -394,6 +396,7 @@ export const registerRoutes = async ( const identityProjectDAL = identityProjectDALFactory(db); const identityProjectMembershipRoleDAL = identityProjectMembershipRoleDALFactory(db); const identityProjectAdditionalPrivilegeDAL = identityProjectAdditionalPrivilegeDALFactory(db); + const identityAuthTemplateDAL = identityAuthTemplateDALFactory(db); const identityTokenAuthDAL = identityTokenAuthDALFactory(db); const identityUaDAL = identityUaDALFactory(db); @@ -772,7 +775,6 @@ export const registerRoutes = async ( orgRoleDAL, permissionService, orgDAL, - projectBotDAL, incidentContactDAL, tokenService, projectUserAdditionalPrivilegeDAL, @@ -847,7 +849,6 @@ export const registerRoutes = async ( projectDAL, permissionService, projectUserMembershipRoleDAL, - userDAL, projectBotDAL, projectKeyDAL, projectMembershipDAL @@ -1135,11 +1136,9 @@ export const registerRoutes = async ( projectBotService, identityProjectDAL, identityOrgMembershipDAL, - projectKeyDAL, userDAL, projectEnvDAL, orgDAL, - orgService, projectMembershipDAL, projectRoleDAL, folderDAL, @@ -1159,7 +1158,6 @@ export const registerRoutes = async ( identityProjectMembershipRoleDAL, keyStore, kmsService, - projectBotDAL, certificateTemplateDAL, projectSlackConfigDAL, slackIntegrationDAL, @@ -1461,6 +1459,15 @@ export const registerRoutes = async ( identityMetadataDAL }); + const identityAuthTemplateService = identityAuthTemplateServiceFactory({ + identityAuthTemplateDAL, + identityLdapAuthDAL, + permissionService, + kmsService, + licenseService, + auditLogService + }); + const identityAccessTokenService = identityAccessTokenServiceFactory({ identityAccessTokenDAL, identityOrgMembershipDAL, @@ -1604,7 +1611,8 @@ export const registerRoutes = async ( identityAccessTokenDAL, identityOrgMembershipDAL, licenseService, - identityDAL + identityDAL, + identityAuthTemplateDAL }); const dynamicSecretProviders = buildDynamicSecretProviders({ @@ -2008,6 +2016,7 @@ export const registerRoutes = async ( webhook: webhookService, serviceToken: serviceTokenService, identity: identityService, + identityAuthTemplate: identityAuthTemplateService, identityAccessToken: identityAccessTokenService, identityProject: identityProjectService, identityTokenAuth: identityTokenAuthService, @@ -2144,7 +2153,8 @@ export const registerRoutes = async ( inviteOnlySignup: z.boolean().optional(), redisConfigured: z.boolean().optional(), secretScanningConfigured: z.boolean().optional(), - samlDefaultOrgSlug: z.string().optional() + samlDefaultOrgSlug: z.string().optional(), + auditLogStorageDisabled: z.boolean().optional() }) } }, @@ -2171,7 +2181,8 @@ export const registerRoutes = async ( inviteOnlySignup: Boolean(serverCfg.allowSignUp), redisConfigured: cfg.isRedisConfigured, secretScanningConfigured: cfg.isSecretScanningConfigured, - samlDefaultOrgSlug: cfg.samlDefaultOrgSlug + samlDefaultOrgSlug: cfg.samlDefaultOrgSlug, + auditLogStorageDisabled: Boolean(cfg.DISABLE_AUDIT_LOG_STORAGE) }; } }); diff --git a/backend/src/server/routes/v1/admin-router.ts b/backend/src/server/routes/v1/admin-router.ts index 6ad368816..3f3b58b5e 100644 --- a/backend/src/server/routes/v1/admin-router.ts +++ b/backend/src/server/routes/v1/admin-router.ts @@ -464,6 +464,42 @@ export const registerAdminRouter = async (server: FastifyZodProvider) => { } }); + server.route({ + method: "DELETE", + url: "/user-management/users", + config: { + rateLimit: writeLimit + }, + schema: { + body: z.object({ + userIds: z.string().array() + }), + response: { + 200: z.object({ + users: UsersSchema.pick({ + username: true, + firstName: true, + lastName: true, + email: true, + id: true + }).array() + }) + } + }, + onRequest: (req, res, done) => { + verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN])(req, res, () => { + verifySuperAdmin(req, res, done); + }); + }, + handler: async (req) => { + const users = await server.services.superAdmin.deleteUsers(req.body.userIds); + + return { + users + }; + } + }); + server.route({ method: "PATCH", url: "/user-management/users/:userId/admin-access", diff --git a/backend/src/server/routes/v1/identity-ldap-auth-router.ts b/backend/src/server/routes/v1/identity-ldap-auth-router.ts index 3da8a425b..5d3612bf5 100644 --- a/backend/src/server/routes/v1/identity-ldap-auth-router.ts +++ b/backend/src/server/routes/v1/identity-ldap-auth-router.ts @@ -200,49 +200,104 @@ export const registerIdentityLdapAuthRouter = async (server: FastifyZodProvider) params: z.object({ identityId: z.string().trim().describe(LDAP_AUTH.ATTACH.identityId) }), - body: z - .object({ - url: z.string().trim().min(1).describe(LDAP_AUTH.ATTACH.url), - bindDN: z.string().trim().min(1).describe(LDAP_AUTH.ATTACH.bindDN), - bindPass: z.string().trim().min(1).describe(LDAP_AUTH.ATTACH.bindPass), - searchBase: z.string().trim().min(1).describe(LDAP_AUTH.ATTACH.searchBase), - searchFilter: z - .string() - .trim() - .min(1) - .default("(uid={{username}})") - .refine(isValidLdapFilter, "Invalid LDAP search filter") - .describe(LDAP_AUTH.ATTACH.searchFilter), - allowedFields: AllowedFieldsSchema.array().optional().describe(LDAP_AUTH.ATTACH.allowedFields), - ldapCaCertificate: z.string().trim().optional().describe(LDAP_AUTH.ATTACH.ldapCaCertificate), - accessTokenTrustedIps: z - .object({ - ipAddress: z.string().trim() - }) - .array() - .min(1) - .default([{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }]) - .describe(LDAP_AUTH.ATTACH.accessTokenTrustedIps), - accessTokenTTL: z - .number() - .int() - .min(0) - .max(315360000) - .default(2592000) - .describe(LDAP_AUTH.ATTACH.accessTokenTTL), - accessTokenMaxTTL: z - .number() - .int() - .min(1) - .max(315360000) - .default(2592000) - .describe(LDAP_AUTH.ATTACH.accessTokenMaxTTL), - accessTokenNumUsesLimit: z.number().int().min(0).default(0).describe(LDAP_AUTH.ATTACH.accessTokenNumUsesLimit) - }) - .refine( - (val) => val.accessTokenTTL <= val.accessTokenMaxTTL, - "Access Token TTL cannot be greater than Access Token Max TTL." - ), + body: z.union([ + // Template-based configuration + z + .object({ + templateId: z.string().trim().describe(LDAP_AUTH.ATTACH.templateId), + searchFilter: z + .string() + .trim() + .min(1) + .default("(uid={{username}})") + .refine(isValidLdapFilter, "Invalid LDAP search filter") + .describe(LDAP_AUTH.ATTACH.searchFilter), + allowedFields: AllowedFieldsSchema.array().optional().describe(LDAP_AUTH.ATTACH.allowedFields), + ldapCaCertificate: z.string().trim().optional().describe(LDAP_AUTH.ATTACH.ldapCaCertificate), + accessTokenTrustedIps: z + .object({ + ipAddress: z.string().trim() + }) + .array() + .min(1) + .default([{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }]) + .describe(LDAP_AUTH.ATTACH.accessTokenTrustedIps), + accessTokenTTL: z + .number() + .int() + .min(0) + .max(315360000) + .default(2592000) + .describe(LDAP_AUTH.ATTACH.accessTokenTTL), + accessTokenMaxTTL: z + .number() + .int() + .min(1) + .max(315360000) + .default(2592000) + .describe(LDAP_AUTH.ATTACH.accessTokenMaxTTL), + accessTokenNumUsesLimit: z + .number() + .int() + .min(0) + .default(0) + .describe(LDAP_AUTH.ATTACH.accessTokenNumUsesLimit) + }) + .refine( + (val) => val.accessTokenTTL <= val.accessTokenMaxTTL, + "Access Token TTL cannot be greater than Access Token Max TTL." + ), + + // Manual configuration + z + .object({ + url: z.string().trim().describe(LDAP_AUTH.ATTACH.url), + bindDN: z.string().trim().describe(LDAP_AUTH.ATTACH.bindDN), + bindPass: z.string().trim().describe(LDAP_AUTH.ATTACH.bindPass), + searchBase: z.string().trim().describe(LDAP_AUTH.ATTACH.searchBase), + searchFilter: z + .string() + .trim() + .min(1) + .default("(uid={{username}})") + .refine(isValidLdapFilter, "Invalid LDAP search filter") + .describe(LDAP_AUTH.ATTACH.searchFilter), + allowedFields: AllowedFieldsSchema.array().optional().describe(LDAP_AUTH.ATTACH.allowedFields), + ldapCaCertificate: z.string().trim().optional().describe(LDAP_AUTH.ATTACH.ldapCaCertificate), + accessTokenTrustedIps: z + .object({ + ipAddress: z.string().trim() + }) + .array() + .min(1) + .default([{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }]) + .describe(LDAP_AUTH.ATTACH.accessTokenTrustedIps), + accessTokenTTL: z + .number() + .int() + .min(0) + .max(315360000) + .default(2592000) + .describe(LDAP_AUTH.ATTACH.accessTokenTTL), + accessTokenMaxTTL: z + .number() + .int() + .min(1) + .max(315360000) + .default(2592000) + .describe(LDAP_AUTH.ATTACH.accessTokenMaxTTL), + accessTokenNumUsesLimit: z + .number() + .int() + .min(0) + .default(0) + .describe(LDAP_AUTH.ATTACH.accessTokenNumUsesLimit) + }) + .refine( + (val) => val.accessTokenTTL <= val.accessTokenMaxTTL, + "Access Token TTL cannot be greater than Access Token Max TTL." + ) + ]), response: { 200: z.object({ identityLdapAuth: IdentityLdapAuthsSchema.omit({ @@ -275,7 +330,8 @@ export const registerIdentityLdapAuthRouter = async (server: FastifyZodProvider) accessTokenMaxTTL: identityLdapAuth.accessTokenMaxTTL, accessTokenTTL: identityLdapAuth.accessTokenTTL, accessTokenNumUsesLimit: identityLdapAuth.accessTokenNumUsesLimit, - allowedFields: req.body.allowedFields + allowedFields: req.body.allowedFields, + templateId: identityLdapAuth.templateId } } }); @@ -309,6 +365,7 @@ export const registerIdentityLdapAuthRouter = async (server: FastifyZodProvider) bindDN: z.string().trim().min(1).optional().describe(LDAP_AUTH.UPDATE.bindDN), bindPass: z.string().trim().min(1).optional().describe(LDAP_AUTH.UPDATE.bindPass), searchBase: z.string().trim().min(1).optional().describe(LDAP_AUTH.UPDATE.searchBase), + templateId: z.string().trim().optional().describe(LDAP_AUTH.UPDATE.templateId), searchFilter: z .string() .trim() @@ -376,7 +433,8 @@ export const registerIdentityLdapAuthRouter = async (server: FastifyZodProvider) accessTokenTTL: identityLdapAuth.accessTokenTTL, accessTokenNumUsesLimit: identityLdapAuth.accessTokenNumUsesLimit, accessTokenTrustedIps: identityLdapAuth.accessTokenTrustedIps as TIdentityTrustedIp[], - allowedFields: req.body.allowedFields + allowedFields: req.body.allowedFields, + templateId: identityLdapAuth.templateId } } }); @@ -413,7 +471,8 @@ export const registerIdentityLdapAuthRouter = async (server: FastifyZodProvider) }).extend({ bindDN: z.string(), bindPass: z.string(), - ldapCaCertificate: z.string().optional() + ldapCaCertificate: z.string().optional(), + templateId: z.string().optional().nullable() }) }) } diff --git a/backend/src/server/routes/v1/organization-router.ts b/backend/src/server/routes/v1/organization-router.ts index e1669c784..323354bc1 100644 --- a/backend/src/server/routes/v1/organization-router.ts +++ b/backend/src/server/routes/v1/organization-router.ts @@ -247,7 +247,7 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => { lastName: true, id: true, superAdmin: true - }).merge(z.object({ publicKey: z.string().nullable() })) + }).merge(z.object({ publicKey: z.string().nullable().optional() })) }) ) .omit({ createdAt: true, updatedAt: true }) diff --git a/backend/src/server/routes/v1/password-router.ts b/backend/src/server/routes/v1/password-router.ts index 3396cebe9..72d20db57 100644 --- a/backend/src/server/routes/v1/password-router.ts +++ b/backend/src/server/routes/v1/password-router.ts @@ -9,73 +9,6 @@ import { ActorType, AuthMode } from "@app/services/auth/auth-type"; import { UserEncryption } from "@app/services/user/user-types"; export const registerPasswordRouter = async (server: FastifyZodProvider) => { - server.route({ - method: "POST", - url: "/srp1", - config: { - rateLimit: authRateLimit - }, - schema: { - body: z.object({ - clientPublicKey: z.string().trim() - }), - response: { - 200: z.object({ - serverPublicKey: z.string(), - salt: z.string() - }) - } - }, - onRequest: verifyAuth([AuthMode.JWT]), - handler: async (req) => { - const { salt, serverPublicKey } = await server.services.password.generateServerPubKey( - req.permission.id, - req.body.clientPublicKey - ); - return { salt, serverPublicKey }; - } - }); - - server.route({ - method: "POST", - url: "/change-password", - config: { - rateLimit: authRateLimit - }, - schema: { - body: z.object({ - clientProof: z.string().trim(), - protectedKey: z.string().trim(), - protectedKeyIV: z.string().trim(), - protectedKeyTag: z.string().trim(), - encryptedPrivateKey: z.string().trim(), - encryptedPrivateKeyIV: z.string().trim(), - encryptedPrivateKeyTag: z.string().trim(), - salt: z.string().trim(), - verifier: z.string().trim(), - password: z.string().trim() - }), - response: { - 200: z.object({ - message: z.string() - }) - } - }, - onRequest: verifyAuth([AuthMode.JWT]), - handler: async (req, res) => { - const appCfg = getConfig(); - await server.services.password.changePassword({ ...req.body, userId: req.permission.id }); - - void res.cookie("jid", "", { - httpOnly: true, - path: "/", - sameSite: "strict", - secure: appCfg.HTTPS_ENABLED - }); - return { message: "Successfully changed password" }; - } - }); - server.route({ method: "POST", url: "/email/password-reset", @@ -131,41 +64,6 @@ export const registerPasswordRouter = async (server: FastifyZodProvider) => { } }); - server.route({ - method: "POST", - url: "/backup-private-key", - config: { - rateLimit: authRateLimit - }, - onRequest: verifyAuth([AuthMode.JWT]), - schema: { - body: z.object({ - clientProof: z.string().trim(), - encryptedPrivateKey: z.string().trim(), - iv: z.string().trim(), - tag: z.string().trim(), - salt: z.string().trim(), - verifier: z.string().trim() - }), - response: { - 200: z.object({ - message: z.string(), - backupPrivateKey: BackupPrivateKeySchema.omit({ verifier: true }) - }) - } - }, - handler: async (req) => { - const token = validateSignUpAuthorization(req.headers.authorization as string, "", false)!; - const backupPrivateKey = await server.services.password.createBackupPrivateKey({ - ...req.body, - userId: token.userId - }); - if (!backupPrivateKey) throw new Error("Failed to create backup key"); - - return { message: "Successfully updated backup private key", backupPrivateKey }; - } - }); - server.route({ method: "GET", url: "/backup-private-key", @@ -257,14 +155,6 @@ export const registerPasswordRouter = async (server: FastifyZodProvider) => { }, schema: { body: z.object({ - protectedKey: z.string().trim(), - protectedKeyIV: z.string().trim(), - protectedKeyTag: z.string().trim(), - encryptedPrivateKey: z.string().trim(), - encryptedPrivateKeyIV: z.string().trim(), - encryptedPrivateKeyTag: z.string().trim(), - salt: z.string().trim(), - verifier: z.string().trim(), password: z.string().trim(), token: z.string().trim() }), diff --git a/backend/src/server/routes/v1/project-router.ts b/backend/src/server/routes/v1/project-router.ts index 6f981f61f..7a5a9341f 100644 --- a/backend/src/server/routes/v1/project-router.ts +++ b/backend/src/server/routes/v1/project-router.ts @@ -52,7 +52,7 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => { 200: z.object({ publicKeys: z .object({ - publicKey: z.string().optional(), + publicKey: z.string().nullable().optional(), userId: z.string() }) .array() diff --git a/backend/src/server/routes/v1/reminder-routers/secret-reminder-router.ts b/backend/src/server/routes/v1/reminder-routers/secret-reminder-router.ts index 4aa68197f..046607da1 100644 --- a/backend/src/server/routes/v1/reminder-routers/secret-reminder-router.ts +++ b/backend/src/server/routes/v1/reminder-routers/secret-reminder-router.ts @@ -22,6 +22,7 @@ export const registerSecretReminderRouter = async (server: FastifyZodProvider) = message: z.string().trim().max(1024).optional(), repeatDays: z.number().min(1).nullable().optional(), nextReminderDate: z.string().datetime().nullable().optional(), + fromDate: z.string().datetime().nullable().optional(), recipients: z.string().array().optional() }) .refine((data) => { @@ -45,6 +46,7 @@ export const registerSecretReminderRouter = async (server: FastifyZodProvider) = message: req.body.message, repeatDays: req.body.repeatDays, nextReminderDate: req.body.nextReminderDate, + fromDate: req.body.fromDate, recipients: req.body.recipients } }); diff --git a/backend/src/server/routes/v1/user-router.ts b/backend/src/server/routes/v1/user-router.ts index a0c3592f7..7ef2e0d33 100644 --- a/backend/src/server/routes/v1/user-router.ts +++ b/backend/src/server/routes/v1/user-router.ts @@ -1,6 +1,6 @@ import { z } from "zod"; -import { UserEncryptionKeysSchema, UsersSchema } from "@app/db/schemas"; +import { UsersSchema } from "@app/db/schemas"; import { getConfig } from "@app/lib/config/env"; import { logger } from "@app/lib/logger"; import { authRateLimit, readLimit, writeLimit } from "@app/server/config/rateLimiter"; @@ -19,23 +19,9 @@ export const registerUserRouter = async (server: FastifyZodProvider) => { schema: { response: { 200: z.object({ - user: UsersSchema.merge( - UserEncryptionKeysSchema.pick({ - clientPublicKey: true, - serverPrivateKey: true, - encryptionVersion: true, - protectedKey: true, - protectedKeyIV: true, - protectedKeyTag: true, - publicKey: true, - encryptedPrivateKey: true, - iv: true, - tag: true, - salt: true, - verifier: true, - userId: true - }) - ) + user: UsersSchema.extend({ + encryptionVersion: z.number() + }) }) } }, @@ -94,26 +80,6 @@ export const registerUserRouter = async (server: FastifyZodProvider) => { } }); - server.route({ - method: "GET", - url: "/private-key", - config: { - rateLimit: readLimit - }, - schema: { - response: { - 200: z.object({ - privateKey: z.string() - }) - } - }, - onRequest: verifyAuth([AuthMode.JWT], { requireOrg: false }), - handler: async (req) => { - const privateKey = await server.services.user.getUserPrivateKey(req.permission.id); - return { privateKey }; - } - }); - server.route({ method: "GET", url: "/:userId/unlock", diff --git a/backend/src/server/routes/v2/mfa-router.ts b/backend/src/server/routes/v2/mfa-router.ts index 59a3943f7..d8a57d29a 100644 --- a/backend/src/server/routes/v2/mfa-router.ts +++ b/backend/src/server/routes/v2/mfa-router.ts @@ -97,13 +97,13 @@ export const registerMfaRouter = async (server: FastifyZodProvider) => { response: { 200: z.object({ encryptionVersion: z.number().default(1).nullable().optional(), - protectedKey: z.string().nullable(), - protectedKeyIV: z.string().nullable(), - protectedKeyTag: z.string().nullable(), - publicKey: z.string(), - encryptedPrivateKey: z.string(), - iv: z.string(), - tag: z.string(), + protectedKey: z.string().nullish(), + protectedKeyIV: z.string().nullish(), + protectedKeyTag: z.string().nullish(), + publicKey: z.string().nullish(), + encryptedPrivateKey: z.string().nullish(), + iv: z.string().nullish(), + tag: z.string().nullish(), token: z.string() }) } diff --git a/backend/src/server/routes/v2/organization-router.ts b/backend/src/server/routes/v2/organization-router.ts index 87e1d1789..9135d2356 100644 --- a/backend/src/server/routes/v2/organization-router.ts +++ b/backend/src/server/routes/v2/organization-router.ts @@ -153,7 +153,7 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => { firstName: true, lastName: true, id: true - }).extend({ publicKey: z.string().nullable() }) + }).extend({ publicKey: z.string().nullish() }) }).omit({ createdAt: true, updatedAt: true }) }) } diff --git a/backend/src/server/routes/v2/password-router.ts b/backend/src/server/routes/v2/password-router.ts index 63b6d8aac..0f0747c3f 100644 --- a/backend/src/server/routes/v2/password-router.ts +++ b/backend/src/server/routes/v2/password-router.ts @@ -1,5 +1,6 @@ import { z } from "zod"; +import { getConfig } from "@app/lib/config/env"; import { authRateLimit } from "@app/server/config/rateLimiter"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { validatePasswordResetAuthorization } from "@app/services/auth/auth-fns"; @@ -41,13 +42,38 @@ export const registerPasswordRouter = async (server: FastifyZodProvider) => { rateLimit: authRateLimit }, onRequest: verifyAuth([AuthMode.JWT], { requireOrg: false }), - handler: async (req) => { + handler: async (req, res) => { + const appCfg = getConfig(); + await server.services.password.resetPasswordV2({ type: ResetPasswordV2Type.LoggedInReset, userId: req.permission.id, newPassword: req.body.newPassword, oldPassword: req.body.oldPassword }); + + void res.cookie("jid", "", { + httpOnly: true, + path: "/", + sameSite: "strict", + secure: appCfg.HTTPS_ENABLED + }); + + void res.cookie("infisical-project-assume-privileges", "", { + httpOnly: true, + path: "/", + sameSite: "strict", + secure: appCfg.HTTPS_ENABLED, + maxAge: 0 + }); + + void res.cookie("aod", "", { + httpOnly: false, + path: "/", + sameSite: "lax", + secure: appCfg.HTTPS_ENABLED, + maxAge: 0 + }); } }); }; diff --git a/backend/src/server/routes/v2/project-router.ts b/backend/src/server/routes/v2/project-router.ts index 2a883dcb6..b71a744fd 100644 --- a/backend/src/server/routes/v2/project-router.ts +++ b/backend/src/server/routes/v2/project-router.ts @@ -52,7 +52,7 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => { 200: ProjectKeysSchema.merge( z.object({ sender: z.object({ - publicKey: z.string() + publicKey: z.string().optional() }) }) ) diff --git a/backend/src/server/routes/v3/login-router.ts b/backend/src/server/routes/v3/login-router.ts index 3a8510f34..07923fd6c 100644 --- a/backend/src/server/routes/v3/login-router.ts +++ b/backend/src/server/routes/v3/login-router.ts @@ -20,8 +20,8 @@ export const registerLoginRouter = async (server: FastifyZodProvider) => { }), response: { 200: z.object({ - serverPublicKey: z.string(), - salt: z.string() + serverPublicKey: z.string().nullish(), + salt: z.string().nullish() }) } }, @@ -124,14 +124,14 @@ export const registerLoginRouter = async (server: FastifyZodProvider) => { }), response: { 200: z.object({ - encryptionVersion: z.number().default(1).nullable().optional(), - protectedKey: z.string().nullable(), - protectedKeyIV: z.string().nullable(), - protectedKeyTag: z.string().nullable(), - publicKey: z.string(), - encryptedPrivateKey: z.string(), - iv: z.string(), - tag: z.string(), + encryptionVersion: z.number().default(1).nullish(), + protectedKey: z.string().nullish(), + protectedKeyIV: z.string().nullish(), + protectedKeyTag: z.string().nullish(), + publicKey: z.string().nullish(), + encryptedPrivateKey: z.string().nullish(), + iv: z.string().nullish(), + tag: z.string().nullish(), token: z.string() }) } @@ -181,4 +181,59 @@ export const registerLoginRouter = async (server: FastifyZodProvider) => { } as const; } }); + + // New login route that doesn't use SRP + server.route({ + method: "POST", + url: "/login", + config: { + rateLimit: authRateLimit + }, + schema: { + body: z.object({ + email: z.string().trim(), + password: z.string().trim(), + providerAuthToken: z.string().trim().optional(), + captchaToken: z.string().trim().optional() + }), + response: { + 200: z.object({ + accessToken: z.string() + }) + } + }, + handler: async (req, res) => { + const userAgent = req.headers["user-agent"]; + if (!userAgent) throw new Error("user agent header is required"); + + const { tokens } = await server.services.login.login({ + email: req.body.email, + password: req.body.password, + ip: req.realIp, + userAgent, + providerAuthToken: req.body.providerAuthToken, + captchaToken: req.body.captchaToken + }); + const appCfg = getConfig(); + + void res.setCookie("jid", tokens.refreshToken, { + httpOnly: true, + path: "/", + sameSite: "strict", + secure: appCfg.HTTPS_ENABLED + }); + + addAuthOriginDomainCookie(res); + + void res.cookie("infisical-project-assume-privileges", "", { + httpOnly: true, + path: "/", + sameSite: "strict", + secure: appCfg.HTTPS_ENABLED, + maxAge: 0 + }); + + return { accessToken: tokens.accessToken }; + } + }); }; diff --git a/backend/src/server/routes/v3/signup-router.ts b/backend/src/server/routes/v3/signup-router.ts index 391c459a2..736bc13aa 100644 --- a/backend/src/server/routes/v3/signup-router.ts +++ b/backend/src/server/routes/v3/signup-router.ts @@ -98,15 +98,6 @@ export const registerSignupRouter = async (server: FastifyZodProvider) => { email: z.string().trim(), firstName: z.string().trim(), lastName: z.string().trim().optional(), - protectedKey: z.string().trim(), - protectedKeyIV: z.string().trim(), - protectedKeyTag: z.string().trim(), - publicKey: z.string().trim(), - encryptedPrivateKey: z.string().trim(), - encryptedPrivateKeyIV: z.string().trim(), - encryptedPrivateKeyTag: z.string().trim(), - salt: z.string().trim(), - verifier: z.string().trim(), providerAuthToken: z.string().trim().optional().nullish(), attributionSource: z.string().trim().optional(), password: z.string() @@ -189,15 +180,6 @@ export const registerSignupRouter = async (server: FastifyZodProvider) => { password: z.string(), firstName: z.string().trim(), lastName: z.string().trim().optional(), - protectedKey: z.string().trim(), - protectedKeyIV: z.string().trim(), - protectedKeyTag: z.string().trim(), - publicKey: z.string().trim(), - encryptedPrivateKey: z.string().trim(), - encryptedPrivateKeyIV: z.string().trim(), - encryptedPrivateKeyTag: z.string().trim(), - salt: z.string().trim(), - verifier: z.string().trim(), tokenMetadata: z.string().optional() }), response: { diff --git a/backend/src/services/app-connection/github/github-connection-fns.ts b/backend/src/services/app-connection/github/github-connection-fns.ts index 57d01be29..e4bd2e10d 100644 --- a/backend/src/services/app-connection/github/github-connection-fns.ts +++ b/backend/src/services/app-connection/github/github-connection-fns.ts @@ -1,4 +1,5 @@ import { createAppAuth } from "@octokit/auth-app"; +import { request } from "@octokit/request"; import { AxiosError, AxiosRequestConfig, AxiosResponse } from "axios"; import https from "https"; import RE2 from "re2"; @@ -12,7 +13,6 @@ import { GatewayProxyProtocol, withGatewayProxy } from "@app/lib/gateway"; import { logger } from "@app/lib/logger"; import { blockLocalAndPrivateIpAddresses } from "@app/lib/validator"; import { getAppConnectionMethodName } from "@app/services/app-connection/app-connection-fns"; -import { IntegrationUrls } from "@app/services/integration-auth/integration-list"; import { AppConnection } from "../app-connection-enums"; import { GitHubConnectionMethod } from "./github-connection-enums"; @@ -30,6 +30,23 @@ export const getGitHubConnectionListItem = () => { }; }; +export const getGitHubInstanceApiUrl = async (config: { + credentials: Pick; +}) => { + const host = config.credentials.host || "github.com"; + + await blockLocalAndPrivateIpAddresses(`https://${host}`); + + let apiBase: string; + if (config.credentials.instanceType === "server") { + apiBase = `${host}/api/v3`; + } else { + apiBase = `api.${host}`; + } + + return apiBase; +}; + export const requestWithGitHubGateway = async ( appConnection: { gatewayId?: string | null }, gatewayService: Pick, @@ -73,7 +90,10 @@ export const requestWithGitHubGateway = async ( return await httpRequest.request(finalRequestConfig); } catch (error) { const axiosError = error as AxiosError; - logger.error("Error during GitHub gateway request:", axiosError.message, axiosError.response?.data); + logger.error( + { message: axiosError.message, data: axiosError.response?.data }, + "Error during GitHub gateway request:" + ); throw error; } }, @@ -112,7 +132,10 @@ export const getGitHubAppAuthToken = async (appConnection: TGitHubConnection) => const appAuth = createAppAuth({ appId, privateKey: appPrivateKey, - installationId: appConnection.credentials.installationId + installationId: appConnection.credentials.installationId, + request: request.defaults({ + baseUrl: `https://${await getGitHubInstanceApiUrl(appConnection)}` + }) }); const { token } = await appAuth({ type: "installation" }); @@ -141,7 +164,7 @@ export const makePaginatedGitHubRequest = async ( const token = method === GitHubConnectionMethod.OAuth ? credentials.accessToken : await getGitHubAppAuthToken(appConnection); - let url: string | null = `https://api.${credentials.host || "github.com"}${path}`; + let url: string | null = `https://${await getGitHubInstanceApiUrl(appConnection)}${path}`; let results: T[] = []; let i = 0; @@ -325,6 +348,8 @@ export const validateGitHubConnectionCredentials = async ( }); } } catch (e: unknown) { + logger.error(e, "Unable to verify GitHub connection"); + if (e instanceof BadRequestError) { throw e; } @@ -355,7 +380,7 @@ export const validateGitHubConnectionCredentials = async ( }; }[]; }>(config, gatewayService, { - url: IntegrationUrls.GITHUB_USER_INSTALLATIONS.replace("api.github.com", `api.${host}`), + url: `https://${await getGitHubInstanceApiUrl(config)}/user/installations`, headers: { Accept: "application/json", Authorization: `Bearer ${tokenResp.data.access_token}`, @@ -377,11 +402,15 @@ export const validateGitHubConnectionCredentials = async ( switch (method) { case GitHubConnectionMethod.App: return { - installationId: credentials.installationId + installationId: credentials.installationId, + instanceType: credentials.instanceType, + host: credentials.host }; case GitHubConnectionMethod.OAuth: return { - accessToken: tokenResp.data.access_token + accessToken: tokenResp.data.access_token, + instanceType: credentials.instanceType, + host: credentials.host }; default: throw new InternalServerError({ diff --git a/backend/src/services/app-connection/github/github-connection-schemas.ts b/backend/src/services/app-connection/github/github-connection-schemas.ts index bf92ec155..1b8aa9c3f 100644 --- a/backend/src/services/app-connection/github/github-connection-schemas.ts +++ b/backend/src/services/app-connection/github/github-connection-schemas.ts @@ -10,26 +10,59 @@ import { import { GitHubConnectionMethod } from "./github-connection-enums"; -export const GitHubConnectionOAuthInputCredentialsSchema = z.object({ - code: z.string().trim().min(1, "OAuth code required"), - host: z.string().trim().optional() -}); +export const GitHubConnectionOAuthInputCredentialsSchema = z.union([ + z.object({ + code: z.string().trim().min(1, "OAuth code required"), + instanceType: z.literal("server"), + host: z.string().trim().min(1, "Host is required for server instance type") + }), + z.object({ + code: z.string().trim().min(1, "OAuth code required"), + instanceType: z.literal("cloud").optional(), + host: z.string().trim().optional() + }) +]); -export const GitHubConnectionAppInputCredentialsSchema = z.object({ - code: z.string().trim().min(1, "GitHub App code required"), - installationId: z.string().min(1, "GitHub App Installation ID required"), - host: z.string().trim().optional() -}); +export const GitHubConnectionAppInputCredentialsSchema = z.union([ + z.object({ + code: z.string().trim().min(1, "GitHub App code required"), + installationId: z.string().min(1, "GitHub App Installation ID required"), + instanceType: z.literal("server"), + host: z.string().trim().min(1, "Host is required for server instance type") + }), + z.object({ + code: z.string().trim().min(1, "GitHub App code required"), + installationId: z.string().min(1, "GitHub App Installation ID required"), + instanceType: z.literal("cloud").optional(), + host: z.string().trim().optional() + }) +]); -export const GitHubConnectionOAuthOutputCredentialsSchema = z.object({ - accessToken: z.string(), - host: z.string().trim().optional() -}); +export const GitHubConnectionOAuthOutputCredentialsSchema = z.union([ + z.object({ + accessToken: z.string(), + instanceType: z.literal("server"), + host: z.string().trim().min(1) + }), + z.object({ + accessToken: z.string(), + instanceType: z.literal("cloud").optional(), + host: z.string().trim().optional() + }) +]); -export const GitHubConnectionAppOutputCredentialsSchema = z.object({ - installationId: z.string(), - host: z.string().trim().optional() -}); +export const GitHubConnectionAppOutputCredentialsSchema = z.union([ + z.object({ + installationId: z.string(), + instanceType: z.literal("server"), + host: z.string().trim().min(1) + }), + z.object({ + installationId: z.string(), + instanceType: z.literal("cloud").optional(), + host: z.string().trim().optional() + }) +]); export const ValidateGitHubConnectionCredentialsSchema = z.discriminatedUnion("method", [ z.object({ @@ -84,11 +117,17 @@ export const GitHubConnectionSchema = z.intersection( export const SanitizedGitHubConnectionSchema = z.discriminatedUnion("method", [ BaseGitHubConnectionSchema.extend({ method: z.literal(GitHubConnectionMethod.App), - credentials: GitHubConnectionAppOutputCredentialsSchema.pick({}) + credentials: z.object({ + instanceType: z.union([z.literal("server"), z.literal("cloud")]).optional(), + host: z.string().optional() + }) }), BaseGitHubConnectionSchema.extend({ method: z.literal(GitHubConnectionMethod.OAuth), - credentials: GitHubConnectionOAuthOutputCredentialsSchema.pick({}) + credentials: z.object({ + instanceType: z.union([z.literal("server"), z.literal("cloud")]).optional(), + host: z.string().optional() + }) }) ]); diff --git a/backend/src/services/auth/auth-fns.ts b/backend/src/services/auth/auth-fns.ts index b38275c8b..d490486cb 100644 --- a/backend/src/services/auth/auth-fns.ts +++ b/backend/src/services/auth/auth-fns.ts @@ -1,8 +1,16 @@ +import { TUsers } from "@app/db/schemas"; +import { isAuthMethodSaml } from "@app/ee/services/permission/permission-fns"; import { getConfig } from "@app/lib/config/env"; +import { request } from "@app/lib/config/request"; import { crypto } from "@app/lib/crypto"; -import { ForbiddenRequestError, UnauthorizedError } from "@app/lib/errors"; +import { BadRequestError, ForbiddenRequestError, UnauthorizedError } from "@app/lib/errors"; -import { AuthModeProviderJwtTokenPayload, AuthModeProviderSignUpTokenPayload, AuthTokenType } from "./auth-type"; +import { + AuthMethod, + AuthModeProviderJwtTokenPayload, + AuthModeProviderSignUpTokenPayload, + AuthTokenType +} from "./auth-type"; export const validateProviderAuthToken = (providerToken: string, username?: string) => { if (!providerToken) throw new UnauthorizedError(); @@ -97,3 +105,50 @@ export const enforceUserLockStatus = (isLocked: boolean, temporaryLockDateEnd?: } } }; + +export const verifyCaptcha = async (user: TUsers, captchaToken?: string) => { + const appCfg = getConfig(); + if ( + user.consecutiveFailedPasswordAttempts && + user.consecutiveFailedPasswordAttempts >= 10 && + Boolean(appCfg.CAPTCHA_SECRET) + ) { + if (!captchaToken) { + throw new BadRequestError({ + name: "Captcha Required", + message: "Accomplish the required captcha by logging in via Web" + }); + } + + // validate captcha token + const response = await request.postForm<{ success: boolean }>("https://api.hcaptcha.com/siteverify", { + response: captchaToken, + secret: appCfg.CAPTCHA_SECRET + }); + + if (!response.data.success) { + throw new BadRequestError({ + name: "Invalid Captcha" + }); + } + } +}; + +export const getAuthMethodAndOrgId = (email: string, providerAuthToken?: string) => { + let authMethod = AuthMethod.EMAIL; + let organizationId: string | undefined; + + if (providerAuthToken) { + const decodedProviderToken = validateProviderAuthToken(providerAuthToken, email); + + authMethod = decodedProviderToken.authMethod; + if ( + (isAuthMethodSaml(authMethod) || [AuthMethod.LDAP, AuthMethod.OIDC].includes(authMethod)) && + decodedProviderToken.orgId + ) { + organizationId = decodedProviderToken.orgId; + } + } + + return { authMethod, organizationId }; +}; diff --git a/backend/src/services/auth/auth-login-service.ts b/backend/src/services/auth/auth-login-service.ts index aea90fe11..7dd1d3aa4 100644 --- a/backend/src/services/auth/auth-login-service.ts +++ b/backend/src/services/auth/auth-login-service.ts @@ -4,7 +4,6 @@ import { OrgMembershipRole, OrgMembershipStatus, TableName, TUsers, UserDeviceSc import { EventType, TAuditLogServiceFactory } from "@app/ee/services/audit-log/audit-log-types"; import { isAuthMethodSaml } from "@app/ee/services/permission/permission-fns"; import { getConfig } from "@app/lib/config/env"; -import { request } from "@app/lib/config/request"; import { crypto, generateSrpServerKey, srpCheckClientProof } from "@app/lib/crypto"; import { getUserPrivateKey } from "@app/lib/crypto/srp"; import { BadRequestError, DatabaseError, ForbiddenRequestError, UnauthorizedError } from "@app/lib/errors"; @@ -22,7 +21,8 @@ import { SmtpTemplates, TSmtpService } from "../smtp/smtp-service"; import { LoginMethod } from "../super-admin/super-admin-types"; import { TTotpServiceFactory } from "../totp/totp-service"; import { TUserDALFactory } from "../user/user-dal"; -import { enforceUserLockStatus, validateProviderAuthToken } from "./auth-fns"; +import { UserEncryption } from "../user/user-types"; +import { enforceUserLockStatus, getAuthMethodAndOrgId, validateProviderAuthToken, verifyCaptcha } from "./auth-fns"; import { TLoginClientProofDTO, TLoginGenServerPublicKeyDTO, @@ -208,6 +208,10 @@ export const authLoginServiceFactory = ({ throw new Error("Failed to find user"); } + if (!userEnc.salt || !userEnc.verifier) { + throw new BadRequestError({ message: "Salt or verifier not found" }); + } + if ( serverCfg.enabledLoginMethods && !serverCfg.enabledLoginMethods.includes(LoginMethod.EMAIL) && @@ -247,8 +251,6 @@ export const authLoginServiceFactory = ({ captchaToken, password }: TLoginClientProofDTO) => { - const appCfg = getConfig(); - // akhilmhdh: case sensitive email resolution const usersByUsername = await userDAL.findUserEncKeyByUsername({ username: email @@ -259,44 +261,11 @@ export const authLoginServiceFactory = ({ const user = await userDAL.findById(userEnc.userId); const cfg = getConfig(); - let authMethod = AuthMethod.EMAIL; - let organizationId: string | undefined; + const { authMethod, organizationId } = getAuthMethodAndOrgId(email, providerAuthToken); + await verifyCaptcha(user, captchaToken); - if (providerAuthToken) { - const decodedProviderToken = validateProviderAuthToken(providerAuthToken, email); - - authMethod = decodedProviderToken.authMethod; - if ( - (isAuthMethodSaml(authMethod) || [AuthMethod.LDAP, AuthMethod.OIDC].includes(authMethod)) && - decodedProviderToken.orgId - ) { - organizationId = decodedProviderToken.orgId; - } - } - - if ( - user.consecutiveFailedPasswordAttempts && - user.consecutiveFailedPasswordAttempts >= 10 && - Boolean(appCfg.CAPTCHA_SECRET) - ) { - if (!captchaToken) { - throw new BadRequestError({ - name: "Captcha Required", - message: "Accomplish the required captcha by logging in via Web" - }); - } - - // validate captcha token - const response = await request.postForm<{ success: boolean }>("https://api.hcaptcha.com/siteverify", { - response: captchaToken, - secret: appCfg.CAPTCHA_SECRET - }); - - if (!response.data.success) { - throw new BadRequestError({ - name: "Invalid Captcha" - }); - } + if (!userEnc.salt || !userEnc.verifier) { + throw new BadRequestError({ message: "Salt or verifier not found" }); } if (!userEnc.serverPrivateKey || !userEnc.clientPublicKey) throw new Error("Failed to authenticate. Try again?"); @@ -371,6 +340,80 @@ export const authLoginServiceFactory = ({ return { token, user: userEnc } as const; }; + const login = async ({ + email, + password, + ip, + userAgent, + providerAuthToken, + captchaToken + }: { + email: string; + password: string; + ip: string; + userAgent: string; + providerAuthToken?: string; + captchaToken?: string; + }) => { + const usersByUsername = await userDAL.findUserEncKeyByUsername({ + username: email + }); + const userEnc = + usersByUsername?.length > 1 ? usersByUsername.find((el) => el.username === email) : usersByUsername?.[0]; + + if (!userEnc) throw new BadRequestError({ message: "User not found" }); + + if (userEnc.encryptionVersion !== UserEncryption.V2) { + throw new BadRequestError({ message: "Legacy encryption scheme not supported", name: "LegacyEncryptionScheme" }); + } + + if (!userEnc.hashedPassword) { + if (userEnc.authMethods?.includes(AuthMethod.EMAIL)) { + throw new BadRequestError({ + message: "Legacy encryption scheme not supported", + name: "LegacyEncryptionScheme" + }); + } + + throw new BadRequestError({ message: "No password found" }); + } + + const { authMethod, organizationId } = getAuthMethodAndOrgId(email, providerAuthToken); + await verifyCaptcha(userEnc, captchaToken); + + if (!(await crypto.hashing().compareHash(password, userEnc.hashedPassword))) { + await userDAL.update( + { id: userEnc.userId }, + { + $incr: { + consecutiveFailedPasswordAttempts: 1 + } + } + ); + + throw new BadRequestError({ message: "Invalid username or email" }); + } + + const token = await generateUserTokens({ + user: { + ...userEnc, + id: userEnc.userId + }, + ip, + userAgent, + authMethod, + organizationId + }); + + return { + tokens: { + accessToken: token.access, + refreshToken: token.refresh + }, + user: userEnc + } as const; + }; + const selectOrganization = async ({ userAgent, authJwtToken, @@ -862,6 +905,7 @@ export const authLoginServiceFactory = ({ resendMfaToken, verifyMfaToken, selectOrganization, - generateUserTokens + generateUserTokens, + login }; }; diff --git a/backend/src/services/auth/auth-password-service.ts b/backend/src/services/auth/auth-password-service.ts index 0dbdfaf79..efc8b3cc0 100644 --- a/backend/src/services/auth/auth-password-service.ts +++ b/backend/src/services/auth/auth-password-service.ts @@ -1,8 +1,5 @@ -import { SecretEncryptionAlgo, SecretKeyEncoding } from "@app/db/schemas"; import { getConfig } from "@app/lib/config/env"; -import { generateSrpServerKey, srpCheckClientProof } from "@app/lib/crypto"; import { crypto } from "@app/lib/crypto/cryptography"; -import { generateUserSrpKeys } from "@app/lib/crypto/srp"; import { BadRequestError } from "@app/lib/errors"; import { logger } from "@app/lib/logger"; import { OrgServiceActor } from "@app/lib/types"; @@ -16,8 +13,6 @@ import { UserEncryption } from "../user/user-types"; import { TAuthDALFactory } from "./auth-dal"; import { ResetPasswordV2Type, - TChangePasswordDTO, - TCreateBackupPrivateKeyDTO, TResetPasswordV2DTO, TResetPasswordViaBackupKeyDTO, TSetupPasswordViaBackupKeyDTO @@ -40,79 +35,6 @@ export const authPaswordServiceFactory = ({ smtpService, totpConfigDAL }: TAuthPasswordServiceFactoryDep) => { - /* - * Pre setup for pass change with srp protocol - * Gets srp server user salt and server public key - */ - const generateServerPubKey = async (userId: string, clientPublicKey: string) => { - const userEnc = await userDAL.findUserEncKeyByUserId(userId); - if (!userEnc) throw new Error("Failed to find user"); - - const serverSrpKey = await generateSrpServerKey(userEnc.salt, userEnc.verifier); - const userEncKeys = await userDAL.updateUserEncryptionByUserId(userEnc.userId, { - clientPublicKey, - serverPrivateKey: serverSrpKey.privateKey - }); - if (!userEncKeys) throw new Error("Failed to update encryption key"); - return { salt: userEncKeys.salt, serverPublicKey: serverSrpKey.pubKey }; - }; - - /* - * Change password to new pass - * */ - const changePassword = async ({ - userId, - clientProof, - protectedKey, - protectedKeyIV, - protectedKeyTag, - encryptedPrivateKey, - encryptedPrivateKeyIV, - encryptedPrivateKeyTag, - salt, - verifier, - tokenVersionId, - password - }: TChangePasswordDTO) => { - const userEnc = await userDAL.findUserEncKeyByUserId(userId); - if (!userEnc) throw new Error("Failed to find user"); - - await userDAL.updateUserEncryptionByUserId(userEnc.userId, { - serverPrivateKey: null, - clientPublicKey: null - }); - if (!userEnc.serverPrivateKey || !userEnc.clientPublicKey) throw new Error("Failed to authenticate. Try again?"); - const isValidClientProof = await srpCheckClientProof( - userEnc.salt, - userEnc.verifier, - userEnc.serverPrivateKey, - userEnc.clientPublicKey, - clientProof - ); - if (!isValidClientProof) throw new Error("Failed to authenticate. Try again?"); - - const appCfg = getConfig(); - const hashedPassword = await crypto.hashing().createHash(password, appCfg.SALT_ROUNDS); - await userDAL.updateUserEncryptionByUserId(userId, { - encryptionVersion: 2, - protectedKey, - protectedKeyIV, - protectedKeyTag, - encryptedPrivateKey, - iv: encryptedPrivateKeyIV, - tag: encryptedPrivateKeyTag, - salt, - verifier, - serverPrivateKey: null, - clientPublicKey: null, - hashedPassword - }); - - if (tokenVersionId) { - await tokenService.clearTokenSessionById(userEnc.userId, tokenVersionId); - } - }; - /* * Email password reset flow via email. Step 1 send email */ @@ -193,6 +115,10 @@ export const authPaswordServiceFactory = ({ } if (!user.authMethods?.includes(AuthMethod.EMAIL)) { + logger.error( + { authMethods: user.authMethods }, + "Unable to reset password, no email authentication method is configured" + ); throw new BadRequestError({ message: "Unable to reset password, no email authentication method is configured" }); } @@ -211,58 +137,17 @@ export const authPaswordServiceFactory = ({ } } - const newHashedPassword = await crypto.hashing().createHash(newPassword, cfg.SALT_ROUNDS); - - // we need to get the original private key first for v2 - let privateKey: string; - if ( - user.serverEncryptedPrivateKey && - user.serverEncryptedPrivateKeyTag && - user.serverEncryptedPrivateKeyIV && - user.serverEncryptedPrivateKeyEncoding && - user.encryptionVersion === UserEncryption.V2 - ) { - privateKey = crypto - .encryption() - .symmetric() - .decryptWithRootEncryptionKey({ - iv: user.serverEncryptedPrivateKeyIV, - tag: user.serverEncryptedPrivateKeyTag, - ciphertext: user.serverEncryptedPrivateKey, - keyEncoding: user.serverEncryptedPrivateKeyEncoding as SecretKeyEncoding - }); - } else { + if (user.encryptionVersion !== UserEncryption.V2) { throw new BadRequestError({ message: "Cannot reset password without current credentials or recovery method", name: "Reset password" }); } - const encKeys = await generateUserSrpKeys(user.username, newPassword, { - publicKey: user.publicKey, - privateKey - }); - - const { tag, iv, ciphertext, encoding } = crypto.encryption().symmetric().encryptWithRootEncryptionKey(privateKey); + const newHashedPassword = await crypto.hashing().createHash(newPassword, cfg.SALT_ROUNDS); await userDAL.updateUserEncryptionByUserId(userId, { - hashedPassword: newHashedPassword, - - // srp params - salt: encKeys.salt, - verifier: encKeys.verifier, - - protectedKey: encKeys.protectedKey, - protectedKeyIV: encKeys.protectedKeyIV, - protectedKeyTag: encKeys.protectedKeyTag, - encryptedPrivateKey: encKeys.encryptedPrivateKey, - iv: encKeys.encryptedPrivateKeyIV, - tag: encKeys.encryptedPrivateKeyTag, - - serverEncryptedPrivateKey: ciphertext, - serverEncryptedPrivateKeyIV: iv, - serverEncryptedPrivateKeyTag: tag, - serverEncryptedPrivateKeyEncoding: encoding + hashedPassword: newHashedPassword }); await tokenService.revokeAllMySessions(userId); @@ -313,66 +198,6 @@ export const authPaswordServiceFactory = ({ }); }; - /* - * backup key creation to give user's their access back when lost their password - * this also needs to do the generateServerPubKey function to be executed first - * then only client proof can be verified - * */ - const createBackupPrivateKey = async ({ - clientProof, - encryptedPrivateKey, - salt, - verifier, - iv, - tag, - userId - }: TCreateBackupPrivateKeyDTO) => { - const userEnc = await userDAL.findUserEncKeyByUserId(userId); - if (!userEnc || (userEnc && !userEnc.isAccepted)) { - throw new Error("Failed to find user"); - } - - if (!userEnc.clientPublicKey || !userEnc.serverPrivateKey) throw new Error("failed to create backup key"); - const isValidClientProff = await srpCheckClientProof( - userEnc.salt, - userEnc.verifier, - userEnc.serverPrivateKey, - userEnc.clientPublicKey, - clientProof - ); - if (!isValidClientProff) throw new Error("failed to create backup key"); - const backup = await authDAL.transaction(async (tx) => { - const backupKey = await authDAL.upsertBackupKey( - userEnc.userId, - { - encryptedPrivateKey, - iv, - tag, - salt, - verifier, - algorithm: SecretEncryptionAlgo.AES_256_GCM, - keyEncoding: SecretKeyEncoding.UTF8 - }, - tx - ); - - await userDAL.updateUserEncryptionByUserId( - userEnc.userId, - { - serverPrivateKey: null, - clientPublicKey: null - }, - tx - ); - return backupKey; - }); - - return backup; - }; - - /* - * Return user back up - * */ const getBackupPrivateKeyOfUser = async (userId: string) => { const user = await userDAL.findUserEncKeyByUserId(userId); if (!user || (user && !user.isAccepted)) { @@ -416,21 +241,7 @@ export const authPaswordServiceFactory = ({ }); }; - const setupPassword = async ( - { - encryptedPrivateKey, - protectedKeyTag, - protectedKey, - protectedKeyIV, - salt, - verifier, - encryptedPrivateKeyIV, - encryptedPrivateKeyTag, - password, - token - }: TSetupPasswordViaBackupKeyDTO, - actor: OrgServiceActor - ) => { + const setupPassword = async ({ password, token }: TSetupPasswordViaBackupKeyDTO, actor: OrgServiceActor) => { try { await tokenService.validateTokenForUser({ type: TokenType.TOKEN_EMAIL_PASSWORD_SETUP, @@ -466,15 +277,7 @@ export const authPaswordServiceFactory = ({ await userDAL.updateUserEncryptionByUserId( actor.id, { - encryptionVersion: 2, - protectedKey, - protectedKeyIV, - protectedKeyTag, - encryptedPrivateKey, - iv: encryptedPrivateKeyIV, - tag: encryptedPrivateKeyTag, - salt, - verifier, + encryptionVersion: UserEncryption.V2, hashedPassword, serverPrivateKey: null, clientPublicKey: null @@ -487,12 +290,9 @@ export const authPaswordServiceFactory = ({ }; return { - generateServerPubKey, - changePassword, resetPasswordByBackupKey, sendPasswordResetEmail, verifyPasswordResetEmail, - createBackupPrivateKey, getBackupPrivateKeyOfUser, sendPasswordSetupEmail, setupPassword, diff --git a/backend/src/services/auth/auth-password-type.ts b/backend/src/services/auth/auth-password-type.ts index b3b14c3b4..ceb70d411 100644 --- a/backend/src/services/auth/auth-password-type.ts +++ b/backend/src/services/auth/auth-password-type.ts @@ -1,18 +1,3 @@ -export type TChangePasswordDTO = { - userId: string; - clientProof: string; - protectedKey: string; - protectedKeyIV: string; - protectedKeyTag: string; - encryptedPrivateKey: string; - encryptedPrivateKeyIV: string; - encryptedPrivateKeyTag: string; - salt: string; - verifier: string; - tokenVersionId?: string; - password: string; -}; - export enum ResetPasswordV2Type { Recovery = "recovery", LoggedInReset = "logged-in-reset" @@ -39,14 +24,6 @@ export type TResetPasswordViaBackupKeyDTO = { }; export type TSetupPasswordViaBackupKeyDTO = { - protectedKey: string; - protectedKeyIV: string; - protectedKeyTag: string; - encryptedPrivateKey: string; - encryptedPrivateKeyIV: string; - encryptedPrivateKeyTag: string; - salt: string; - verifier: string; password: string; token: string; }; diff --git a/backend/src/services/auth/auth-signup-service.ts b/backend/src/services/auth/auth-signup-service.ts index 96cf4121a..7bc3a5ef6 100644 --- a/backend/src/services/auth/auth-signup-service.ts +++ b/backend/src/services/auth/auth-signup-service.ts @@ -1,11 +1,10 @@ -import { OrgMembershipStatus, SecretKeyEncoding, TableName } from "@app/db/schemas"; +import { OrgMembershipStatus, TableName } from "@app/db/schemas"; import { convertPendingGroupAdditionsToGroupMemberships } from "@app/ee/services/group/group-fns"; import { TUserGroupMembershipDALFactory } from "@app/ee/services/group/user-group-membership-dal"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; import { isAuthMethodSaml } from "@app/ee/services/permission/permission-fns"; import { getConfig } from "@app/lib/config/env"; import { crypto } from "@app/lib/crypto/cryptography"; -import { generateUserSrpKeys, getUserPrivateKey } from "@app/lib/crypto/srp"; import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors"; import { getMinExpiresIn } from "@app/lib/fn"; import { isDisposableEmail } from "@app/lib/validator"; @@ -41,7 +40,7 @@ type TAuthSignupDep = { | "findUserGroupMembershipsInProject" >; projectKeyDAL: Pick; - projectDAL: Pick; + projectDAL: Pick; projectBotDAL: Pick; groupProjectDAL: Pick; orgService: Pick; @@ -147,17 +146,8 @@ export const authSignupServiceFactory = ({ firstName, lastName, providerAuthToken, - salt, - verifier, - publicKey, - protectedKey, - protectedKeyIV, - protectedKeyTag, organizationName, // attributionSource, - encryptedPrivateKey, - encryptedPrivateKeyIV, - encryptedPrivateKeyTag, ip, userAgent, authorization, @@ -191,98 +181,18 @@ export const authSignupServiceFactory = ({ } const hashedPassword = await crypto.hashing().createHash(password, appCfg.SALT_ROUNDS); - const privateKey = await getUserPrivateKey(password, { - salt, - protectedKey, - protectedKeyIV, - protectedKeyTag, - encryptedPrivateKey, - iv: encryptedPrivateKeyIV, - tag: encryptedPrivateKeyTag, - encryptionVersion: UserEncryption.V2 - }); - const { tag, encoding, ciphertext, iv } = crypto.encryption().symmetric().encryptWithRootEncryptionKey(privateKey); const updateduser = await authDAL.transaction(async (tx) => { const us = await userDAL.updateById(user.id, { firstName, lastName, isAccepted: true }, tx); if (!us) throw new Error("User not found"); - const systemGeneratedUserEncryptionKey = await userDAL.findUserEncKeyByUserId(us.id, tx); - let userEncKey; - // below condition is true means this is system generated credentials - // the private key is actually system generated password - // thus we will re-encrypt the system generated private key with the new password - // akhilmhdh: you may find this like why? The reason is simple we are moving away from e2ee and these are pieces of it - // without a dummy key in place some things will break and backward compatiability too. 2025 we will be removing all these things - if ( - systemGeneratedUserEncryptionKey && - !systemGeneratedUserEncryptionKey.hashedPassword && - systemGeneratedUserEncryptionKey.serverEncryptedPrivateKey && - systemGeneratedUserEncryptionKey.serverEncryptedPrivateKeyTag && - systemGeneratedUserEncryptionKey.serverEncryptedPrivateKeyIV && - systemGeneratedUserEncryptionKey.serverEncryptedPrivateKeyEncoding - ) { - // get server generated password - const serverGeneratedPassword = crypto - .encryption() - .symmetric() - .decryptWithRootEncryptionKey({ - iv: systemGeneratedUserEncryptionKey.serverEncryptedPrivateKeyIV, - tag: systemGeneratedUserEncryptionKey.serverEncryptedPrivateKeyTag, - ciphertext: systemGeneratedUserEncryptionKey.serverEncryptedPrivateKey, - keyEncoding: systemGeneratedUserEncryptionKey.serverEncryptedPrivateKeyEncoding as SecretKeyEncoding - }); - const serverGeneratedPrivateKey = await getUserPrivateKey(serverGeneratedPassword, { - ...systemGeneratedUserEncryptionKey - }); - const encKeys = await generateUserSrpKeys(email, password, { - publicKey: systemGeneratedUserEncryptionKey.publicKey, - privateKey: serverGeneratedPrivateKey - }); - // now reencrypt server generated key with user provided password - userEncKey = await userDAL.upsertUserEncryptionKey( - us.id, - { - encryptionVersion: UserEncryption.V2, - protectedKey: encKeys.protectedKey, - protectedKeyIV: encKeys.protectedKeyIV, - protectedKeyTag: encKeys.protectedKeyTag, - publicKey: encKeys.publicKey, - encryptedPrivateKey: encKeys.encryptedPrivateKey, - iv: encKeys.encryptedPrivateKeyIV, - tag: encKeys.encryptedPrivateKeyTag, - salt: encKeys.salt, - verifier: encKeys.verifier, - hashedPassword, - serverEncryptedPrivateKeyEncoding: encoding, - serverEncryptedPrivateKeyTag: tag, - serverEncryptedPrivateKeyIV: iv, - serverEncryptedPrivateKey: ciphertext - }, - tx - ); - } else { - userEncKey = await userDAL.upsertUserEncryptionKey( - us.id, - { - encryptionVersion: UserEncryption.V2, - salt, - verifier, - publicKey, - protectedKey, - protectedKeyIV, - protectedKeyTag, - encryptedPrivateKey, - iv: encryptedPrivateKeyIV, - tag: encryptedPrivateKeyTag, - hashedPassword, - serverEncryptedPrivateKeyEncoding: encoding, - serverEncryptedPrivateKeyTag: tag, - serverEncryptedPrivateKeyIV: iv, - serverEncryptedPrivateKey: ciphertext - }, - tx - ); - } + const userEncKey = await userDAL.upsertUserEncryptionKey( + us.id, + { + encryptionVersion: UserEncryption.V2, + hashedPassword + }, + tx + ); // If it's SAML Auth and the organization ID is present, we should check if the user has a pending invite for this org, and accept it if ( @@ -400,19 +310,10 @@ export const authSignupServiceFactory = ({ const completeAccountInvite = async ({ email, ip, - salt, password, - verifier, firstName, - publicKey, userAgent, lastName, - protectedKey, - protectedKeyIV, - protectedKeyTag, - encryptedPrivateKey, - encryptedPrivateKeyIV, - encryptedPrivateKeyTag, authorization }: TCompleteAccountInviteDTO) => { const sanitizedEmail = email.trim().toLowerCase(); @@ -437,94 +338,17 @@ export const authSignupServiceFactory = ({ const appCfg = getConfig(); const hashedPassword = await crypto.hashing().createHash(password, appCfg.SALT_ROUNDS); - const privateKey = await getUserPrivateKey(password, { - salt, - protectedKey, - protectedKeyIV, - protectedKeyTag, - encryptedPrivateKey, - iv: encryptedPrivateKeyIV, - tag: encryptedPrivateKeyTag, - encryptionVersion: 2 - }); - const { tag, encoding, ciphertext, iv } = crypto.encryption().symmetric().encryptWithRootEncryptionKey(privateKey); const updateduser = await authDAL.transaction(async (tx) => { const us = await userDAL.updateById(user.id, { firstName, lastName, isAccepted: true }, tx); if (!us) throw new Error("User not found"); - const systemGeneratedUserEncryptionKey = await userDAL.findUserEncKeyByUserId(us.id, tx); - let userEncKey; - // this means this is system generated credentials - // now replace the private key - if ( - systemGeneratedUserEncryptionKey && - !systemGeneratedUserEncryptionKey.hashedPassword && - systemGeneratedUserEncryptionKey.serverEncryptedPrivateKey && - systemGeneratedUserEncryptionKey.serverEncryptedPrivateKeyTag && - systemGeneratedUserEncryptionKey.serverEncryptedPrivateKeyIV && - systemGeneratedUserEncryptionKey.serverEncryptedPrivateKeyEncoding - ) { - // get server generated password - const serverGeneratedPassword = crypto - .encryption() - .symmetric() - .decryptWithRootEncryptionKey({ - iv: systemGeneratedUserEncryptionKey.serverEncryptedPrivateKeyIV, - tag: systemGeneratedUserEncryptionKey.serverEncryptedPrivateKeyTag, - ciphertext: systemGeneratedUserEncryptionKey.serverEncryptedPrivateKey, - keyEncoding: systemGeneratedUserEncryptionKey.serverEncryptedPrivateKeyEncoding as SecretKeyEncoding - }); - const serverGeneratedPrivateKey = await getUserPrivateKey(serverGeneratedPassword, { - ...systemGeneratedUserEncryptionKey - }); - const encKeys = await generateUserSrpKeys(sanitizedEmail, password, { - publicKey: systemGeneratedUserEncryptionKey.publicKey, - privateKey: serverGeneratedPrivateKey - }); - // now reencrypt server generated key with user provided password - userEncKey = await userDAL.upsertUserEncryptionKey( - us.id, - { - encryptionVersion: 2, - protectedKey: encKeys.protectedKey, - protectedKeyIV: encKeys.protectedKeyIV, - protectedKeyTag: encKeys.protectedKeyTag, - publicKey: encKeys.publicKey, - encryptedPrivateKey: encKeys.encryptedPrivateKey, - iv: encKeys.encryptedPrivateKeyIV, - tag: encKeys.encryptedPrivateKeyTag, - salt: encKeys.salt, - verifier: encKeys.verifier, - hashedPassword, - serverEncryptedPrivateKeyEncoding: encoding, - serverEncryptedPrivateKeyTag: tag, - serverEncryptedPrivateKeyIV: iv, - serverEncryptedPrivateKey: ciphertext - }, - tx - ); - } else { - userEncKey = await userDAL.upsertUserEncryptionKey( - us.id, - { - encryptionVersion: UserEncryption.V2, - salt, - verifier, - publicKey, - protectedKey, - protectedKeyIV, - protectedKeyTag, - encryptedPrivateKey, - iv: encryptedPrivateKeyIV, - tag: encryptedPrivateKeyTag, - hashedPassword, - serverEncryptedPrivateKeyEncoding: encoding, - serverEncryptedPrivateKeyTag: tag, - serverEncryptedPrivateKeyIV: iv, - serverEncryptedPrivateKey: ciphertext - }, - tx - ); - } + const userEncKey = await userDAL.upsertUserEncryptionKey( + us.id, + { + encryptionVersion: 2, + hashedPassword + }, + tx + ); const updatedMembersips = await orgDAL.updateMembership( { inviteEmail: sanitizedEmail, status: OrgMembershipStatus.Invited }, diff --git a/backend/src/services/auth/auth-signup-type.ts b/backend/src/services/auth/auth-signup-type.ts index 8bbf302c5..5c4fdc3c9 100644 --- a/backend/src/services/auth/auth-signup-type.ts +++ b/backend/src/services/auth/auth-signup-type.ts @@ -3,15 +3,6 @@ export type TCompleteAccountSignupDTO = { password: string; firstName: string; lastName?: string; - protectedKey: string; - protectedKeyIV: string; - protectedKeyTag: string; - publicKey: string; - encryptedPrivateKey: string; - encryptedPrivateKeyIV: string; - encryptedPrivateKeyTag: string; - salt: string; - verifier: string; organizationName?: string; providerAuthToken?: string | null; attributionSource?: string | undefined; @@ -26,15 +17,6 @@ export type TCompleteAccountInviteDTO = { password: string; firstName: string; lastName?: string; - protectedKey: string; - protectedKeyIV: string; - protectedKeyTag: string; - publicKey: string; - encryptedPrivateKey: string; - encryptedPrivateKeyIV: string; - encryptedPrivateKeyTag: string; - salt: string; - verifier: string; ip: string; userAgent: string; authorization: string; diff --git a/backend/src/services/certificate-authority/certificate-authority-validators.ts b/backend/src/services/certificate-authority/certificate-authority-validators.ts index fab62ddbf..908741bde 100644 --- a/backend/src/services/certificate-authority/certificate-authority-validators.ts +++ b/backend/src/services/certificate-authority/certificate-authority-validators.ts @@ -2,6 +2,7 @@ import { z } from "zod"; import { isValidIp } from "@app/lib/ip"; import { isFQDN } from "@app/lib/validator/validate-url"; +import { TAltNameMapping, TAltNameType } from "@app/services/certificate/certificate-types"; const isValidDate = (dateString: string) => { const date = new Date(dateString); @@ -15,10 +16,15 @@ export const validateAltNameField = z .trim() .refine( (name) => { - return isFQDN(name, { allow_wildcard: true }) || z.string().email().safeParse(name).success || isValidIp(name); + return ( + isFQDN(name, { allow_wildcard: true, require_tld: false }) || + z.string().url().safeParse(name).success || + z.string().email().safeParse(name).success || + isValidIp(name) + ); }, { - message: "SAN must be a valid hostname, email address, or IP address" + message: "SAN must be a valid hostname, email address, IP address or URL" } ); @@ -39,10 +45,31 @@ export const validateAltNamesField = z if (data === "") return true; // Split and validate each alt name return data.split(", ").every((name) => { - return isFQDN(name, { allow_wildcard: true }) || z.string().email().safeParse(name).success || isValidIp(name); + return ( + isFQDN(name, { allow_wildcard: true, require_tld: false }) || + z.string().url().safeParse(name).success || + z.string().email().safeParse(name).success || + isValidIp(name) + ); }); }, { - message: "Each alt name must be a valid hostname or email address" + message: "Each alt name must be a valid hostname, email address, IP address or URL" } ); + +export const validateAndMapAltNameType = (name: string): TAltNameMapping | null => { + if (isFQDN(name, { allow_wildcard: true, require_tld: false })) { + return { type: TAltNameType.DNS, value: name }; + } + if (z.string().url().safeParse(name).success) { + return { type: TAltNameType.URL, value: name }; + } + if (z.string().email().safeParse(name).success) { + return { type: TAltNameType.EMAIL, value: name }; + } + if (isValidIp(name)) { + return { type: TAltNameType.IP, value: name }; + } + return null; +}; diff --git a/backend/src/services/certificate-authority/internal/internal-certificate-authority-fns.ts b/backend/src/services/certificate-authority/internal/internal-certificate-authority-fns.ts index 80d2842fa..6811476f8 100644 --- a/backend/src/services/certificate-authority/internal/internal-certificate-authority-fns.ts +++ b/backend/src/services/certificate-authority/internal/internal-certificate-authority-fns.ts @@ -1,7 +1,6 @@ /* eslint-disable no-bitwise */ import * as x509 from "@peculiar/x509"; import RE2 from "re2"; -import { z } from "zod"; import { TCertificateTemplates, TPkiSubscribers } from "@app/db/schemas"; import { TCertificateAuthorityCrlDALFactory } from "@app/ee/services/certificate-authority-crl/certificate-authority-crl-dal"; @@ -9,7 +8,6 @@ import { getConfig } from "@app/lib/config/env"; import { crypto } from "@app/lib/crypto/cryptography"; import { BadRequestError } from "@app/lib/errors"; import { ms } from "@app/lib/ms"; -import { isFQDN } from "@app/lib/validator/validate-url"; import { TCertificateBodyDALFactory } from "@app/services/certificate/certificate-body-dal"; import { TCertificateDALFactory } from "@app/services/certificate/certificate-dal"; import { TCertificateSecretDALFactory } from "@app/services/certificate/certificate-secret-dal"; @@ -17,7 +15,8 @@ import { CertExtendedKeyUsage, CertKeyAlgorithm, CertKeyUsage, - CertStatus + CertStatus, + TAltNameMapping } from "@app/services/certificate/certificate-types"; import { TKmsServiceFactory } from "@app/services/kms/kms-service"; import { TProjectDALFactory } from "@app/services/project/project-dal"; @@ -34,6 +33,7 @@ import { } from "../certificate-authority-fns"; import { TCertificateAuthoritySecretDALFactory } from "../certificate-authority-secret-dal"; import { TIssueCertWithTemplateDTO } from "./internal-certificate-authority-types"; +import { validateAndMapAltNameType } from "../certificate-authority-validators"; type TInternalCertificateAuthorityFnsDeps = { certificateAuthorityDAL: Pick; @@ -152,19 +152,15 @@ export const InternalCertificateAuthorityFns = ({ extensions.push(extendedKeyUsagesExtension); } - let altNamesArray: { type: "email" | "dns"; value: string }[] = []; + let altNamesArray: TAltNameMapping[] = []; if (subscriber.subjectAlternativeNames?.length) { altNamesArray = subscriber.subjectAlternativeNames.map((altName) => { - if (z.string().email().safeParse(altName).success) { - return { type: "email", value: altName }; + const altNameType = validateAndMapAltNameType(altName); + if (!altNameType) { + throw new BadRequestError({ message: `Invalid SAN entry: ${altName}` }); } - - if (isFQDN(altName, { allow_wildcard: true })) { - return { type: "dns", value: altName }; - } - - throw new BadRequestError({ message: `Invalid SAN entry: ${altName}` }); + return altNameType; }); const altNamesExtension = new x509.SubjectAlternativeNameExtension(altNamesArray, false); @@ -418,19 +414,15 @@ export const InternalCertificateAuthorityFns = ({ ); } - let altNamesArray: { type: "email" | "dns"; value: string }[] = []; + let altNamesArray: TAltNameMapping[] = []; if (altNames) { altNamesArray = altNames.split(",").map((altName) => { - if (z.string().email().safeParse(altName).success) { - return { type: "email", value: altName }; + const altNameType = validateAndMapAltNameType(altName); + if (!altNameType) { + throw new BadRequestError({ message: `Invalid SAN entry: ${altName}` }); } - - if (isFQDN(altName, { allow_wildcard: true })) { - return { type: "dns", value: altName }; - } - - throw new BadRequestError({ message: `Invalid SAN entry: ${altName}` }); + return altNameType; }); const altNamesExtension = new x509.SubjectAlternativeNameExtension(altNamesArray, false); diff --git a/backend/src/services/certificate-authority/internal/internal-certificate-authority-service.ts b/backend/src/services/certificate-authority/internal/internal-certificate-authority-service.ts index dd30cc62e..510a160e9 100644 --- a/backend/src/services/certificate-authority/internal/internal-certificate-authority-service.ts +++ b/backend/src/services/certificate-authority/internal/internal-certificate-authority-service.ts @@ -2,7 +2,6 @@ import { ForbiddenError, subject } from "@casl/ability"; import * as x509 from "@peculiar/x509"; import slugify from "@sindresorhus/slugify"; -import { z } from "zod"; import { ActionProjectType, TableName, TCertificateAuthorities, TCertificateTemplates } from "@app/db/schemas"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types"; @@ -18,7 +17,6 @@ import { crypto } from "@app/lib/crypto/cryptography"; import { BadRequestError, NotFoundError } from "@app/lib/errors"; import { ms } from "@app/lib/ms"; import { alphaNumericNanoId } from "@app/lib/nanoid"; -import { isFQDN } from "@app/lib/validator/validate-url"; import { TCertificateBodyDALFactory } from "@app/services/certificate/certificate-body-dal"; import { TCertificateDALFactory } from "@app/services/certificate/certificate-dal"; import { TKmsServiceFactory } from "@app/services/kms/kms-service"; @@ -34,7 +32,8 @@ import { CertExtendedKeyUsageOIDToName, CertKeyAlgorithm, CertKeyUsage, - CertStatus + CertStatus, + TAltNameMapping } from "../../certificate/certificate-types"; import { TCertificateTemplateDALFactory } from "../../certificate-template/certificate-template-dal"; import { validateCertificateDetailsAgainstTemplate } from "../../certificate-template/certificate-template-fns"; @@ -69,6 +68,7 @@ import { TSignIntermediateDTO, TUpdateCaDTO } from "./internal-certificate-authority-types"; +import { validateAndMapAltNameType } from "../certificate-authority-validators"; type TInternalCertificateAuthorityServiceFactoryDep = { certificateAuthorityDAL: Pick< @@ -1364,34 +1364,18 @@ export const internalCertificateAuthorityServiceFactory = ({ ); } - let altNamesArray: { - type: "email" | "dns"; - value: string; - }[] = []; + let altNamesArray: TAltNameMapping[] = []; if (altNames) { altNamesArray = altNames .split(",") .map((name) => name.trim()) - .map((altName) => { - // check if the altName is a valid email - if (z.string().email().safeParse(altName).success) { - return { - type: "email", - value: altName - }; + .map((altName): TAltNameMapping => { + const altNameType = validateAndMapAltNameType(altName); + if (!altNameType) { + throw new Error(`Invalid altName: ${altName}`); } - - // check if the altName is a valid hostname - if (isFQDN(altName, { allow_wildcard: true })) { - return { - type: "dns", - value: altName - }; - } - - // If altName is neither a valid email nor a valid hostname, throw an error or handle it accordingly - throw new Error(`Invalid altName: ${altName}`); + return altNameType; }); const altNamesExtension = new x509.SubjectAlternativeNameExtension(altNamesArray, false); @@ -1766,34 +1750,22 @@ export const internalCertificateAuthorityServiceFactory = ({ } let altNamesFromCsr: string = ""; - let altNamesArray: { - type: "email" | "dns"; - value: string; - }[] = []; + let altNamesArray: TAltNameMapping[] = []; + if (altNames) { altNamesArray = altNames .split(",") .map((name) => name.trim()) - .map((altName) => { - // check if the altName is a valid email - if (z.string().email().safeParse(altName).success) { - return { - type: "email", - value: altName - }; + .map((altName): TAltNameMapping => { + const altNameType = validateAndMapAltNameType(altName); + if (!altNameType) { + throw new Error(`Invalid altName: ${altName}`); } - - // check if the altName is a valid hostname - if (isFQDN(altName, { allow_wildcard: true })) { - return { - type: "dns", - value: altName - }; - } - - // If altName is neither a valid email nor a valid hostname, throw an error or handle it accordingly - throw new Error(`Invalid altName: ${altName}`); + return altNameType; }); + + const altNamesExtension = new x509.SubjectAlternativeNameExtension(altNamesArray, false); + extensions.push(altNamesExtension); } else { // attempt to read from CSR if altNames is not explicitly provided const sanExtension = csrObj.extensions.find((ext) => ext.type === "2.5.29.17"); @@ -1801,11 +1773,16 @@ export const internalCertificateAuthorityServiceFactory = ({ 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 - })); + .filter( + (value) => value.type === "email" || value.type === "dns" || value.type === "url" || value.type === "ip" + ) + .map((name): TAltNameMapping => { + const altNameType = validateAndMapAltNameType(name.value); + if (!altNameType) { + throw new Error(`Invalid altName from CSR: ${name.value}`); + } + return altNameType; + }); altNamesFromCsr = sanNames.items.map((item) => item.value).join(","); } diff --git a/backend/src/services/certificate/certificate-types.ts b/backend/src/services/certificate/certificate-types.ts index f1c79a36f..527df2a39 100644 --- a/backend/src/services/certificate/certificate-types.ts +++ b/backend/src/services/certificate/certificate-types.ts @@ -104,3 +104,14 @@ export type TGetCertificateCredentialsDTO = { projectDAL: Pick; kmsService: Pick; }; + +export enum TAltNameType { + EMAIL = "email", + DNS = "dns", + IP = "ip", + URL = "url" +} +export type TAltNameMapping = { + type: TAltNameType; + value: string; +}; diff --git a/backend/src/services/group-project/group-project-service.ts b/backend/src/services/group-project/group-project-service.ts index 50a08e94f..838d89437 100644 --- a/backend/src/services/group-project/group-project-service.ts +++ b/backend/src/services/group-project/group-project-service.ts @@ -1,6 +1,6 @@ import { ForbiddenError } from "@casl/ability"; -import { ActionProjectType, ProjectMembershipRole, SecretKeyEncoding, TGroups } from "@app/db/schemas"; +import { ActionProjectType, ProjectMembershipRole, ProjectVersion, SecretKeyEncoding, TGroups } from "@app/db/schemas"; import { TListProjectGroupUsersDTO } from "@app/ee/services/group/group-types"; import { constructPermissionErrorMessage, @@ -188,7 +188,7 @@ export const groupProjectServiceFactory = ({ // other groups that are in the project const groupMembers = await userGroupMembershipDAL.findGroupMembersNotInProject(group!.id, project.id, tx); - if (groupMembers.length) { + if (groupMembers.length && (project.version === ProjectVersion.V1 || project.version === ProjectVersion.V2)) { const ghostUser = await projectDAL.findProjectGhostUser(project.id, tx); if (!ghostUser) { @@ -205,6 +205,12 @@ export const groupProjectServiceFactory = ({ }); } + if (!ghostUserLatestKey.sender.publicKey) { + throw new NotFoundError({ + message: `Failed to find project owner's latest key in project with name ${project.name}` + }); + } + const bot = await projectBotDAL.findOne({ projectId: project.id }, tx); if (!bot) { @@ -231,6 +237,12 @@ export const groupProjectServiceFactory = ({ }); const projectKeyData = groupMembers.map(({ user: { publicKey, id } }) => { + if (!publicKey) { + throw new NotFoundError({ + message: `Failed to find user's public key in project with name ${project.name}` + }); + } + const { ciphertext: encryptedKey, nonce } = crypto .encryption() .asymmetric() diff --git a/backend/src/services/identity-ldap-auth/identity-ldap-auth-service.ts b/backend/src/services/identity-ldap-auth/identity-ldap-auth-service.ts index 399ef7da9..f38f53cf0 100644 --- a/backend/src/services/identity-ldap-auth/identity-ldap-auth-service.ts +++ b/backend/src/services/identity-ldap-auth/identity-ldap-auth-service.ts @@ -2,9 +2,14 @@ import { ForbiddenError } from "@casl/ability"; import { IdentityAuthMethod } from "@app/db/schemas"; +import { TIdentityAuthTemplateDALFactory } from "@app/ee/services/identity-auth-template"; import { testLDAPConfig } from "@app/ee/services/ldap-config/ldap-fns"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; -import { OrgPermissionIdentityActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission"; +import { + OrgPermissionIdentityActions, + OrgPermissionMachineIdentityAuthTemplateActions, + OrgPermissionSubjects +} from "@app/ee/services/permission/org-permission"; import { constructPermissionErrorMessage, validatePrivilegeChangeOperation @@ -44,6 +49,7 @@ type TIdentityLdapAuthServiceFactoryDep = { permissionService: Pick; kmsService: TKmsServiceFactory; identityDAL: TIdentityDALFactory; + identityAuthTemplateDAL: TIdentityAuthTemplateDALFactory; }; export type TIdentityLdapAuthServiceFactory = ReturnType; @@ -55,7 +61,8 @@ export const identityLdapAuthServiceFactory = ({ identityOrgMembershipDAL, licenseService, permissionService, - kmsService + kmsService, + identityAuthTemplateDAL }: TIdentityLdapAuthServiceFactoryDep) => { const getLdapConfig = async (identityId: string) => { const identity = await identityDAL.findOne({ id: identityId }); @@ -173,6 +180,7 @@ export const identityLdapAuthServiceFactory = ({ const attachLdapAuth = async ({ identityId, + templateId, url, searchBase, searchFilter, @@ -213,6 +221,14 @@ export const identityLdapAuthServiceFactory = ({ actorOrgId ); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Create, OrgPermissionSubjects.Identity); + + if (templateId) { + ForbiddenError.from(permission).throwUnlessCan( + OrgPermissionMachineIdentityAuthTemplateActions.AttachTemplates, + OrgPermissionSubjects.MachineIdentityAuthTemplate + ); + } + const plan = await licenseService.getPlan(identityMembershipOrg.orgId); if (!plan.ldap) { @@ -241,33 +257,55 @@ export const identityLdapAuthServiceFactory = ({ if (allowedFields) AllowedFieldsSchema.array().parse(allowedFields); const identityLdapAuth = await identityLdapAuthDAL.transaction(async (tx) => { - const { encryptor } = await kmsService.createCipherPairWithDataKey({ + const { encryptor, decryptor } = await kmsService.createCipherPairWithDataKey({ type: KmsDataKey.Organization, orgId: identityMembershipOrg.orgId }); + const template = templateId + ? await identityAuthTemplateDAL.findByIdAndOrgId(templateId, identityMembershipOrg.orgId) + : undefined; + + let ldapConfig: { bindDN: string; bindPass: string; searchBase: string; url: string; ldapCaCertificate?: string }; + if (template) { + ldapConfig = JSON.parse(decryptor({ cipherTextBlob: template.templateFields }).toString()); + } else { + if (!bindDN || !bindPass || !searchBase || !url) { + throw new BadRequestError({ + message: "Invalid request. Missing bind DN, bind pass, search base, or URL." + }); + } + ldapConfig = { + bindDN, + bindPass, + searchBase, + url, + ldapCaCertificate + }; + } + const { cipherTextBlob: encryptedBindPass } = encryptor({ - plainText: Buffer.from(bindPass) + plainText: Buffer.from(ldapConfig.bindPass) + }); + + const { cipherTextBlob: encryptedBindDN } = encryptor({ + plainText: Buffer.from(ldapConfig.bindDN) }); let encryptedLdapCaCertificate: Buffer | undefined; - if (ldapCaCertificate) { + if (ldapConfig.ldapCaCertificate) { const { cipherTextBlob: encryptedCertificate } = encryptor({ - plainText: Buffer.from(ldapCaCertificate) + plainText: Buffer.from(ldapConfig.ldapCaCertificate) }); encryptedLdapCaCertificate = encryptedCertificate; } - const { cipherTextBlob: encryptedBindDN } = encryptor({ - plainText: Buffer.from(bindDN) - }); - const isConnected = await testLDAPConfig({ - bindDN, - bindPass, - caCert: ldapCaCertificate || "", - url + bindDN: ldapConfig.bindDN, + bindPass: ldapConfig.bindPass, + caCert: ldapConfig.ldapCaCertificate || "", + url: ldapConfig.url }); if (!isConnected) { @@ -282,15 +320,16 @@ export const identityLdapAuthServiceFactory = ({ identityId: identityMembershipOrg.identityId, encryptedBindDN, encryptedBindPass, - searchBase, + searchBase: ldapConfig.searchBase, searchFilter, - url, + url: ldapConfig.url, encryptedLdapCaCertificate, accessTokenMaxTTL, accessTokenTTL, accessTokenNumUsesLimit, accessTokenTrustedIps: JSON.stringify(reformattedAccessTokenTrustedIps), - allowedFields: allowedFields ? JSON.stringify(allowedFields) : undefined + allowedFields: allowedFields ? JSON.stringify(allowedFields) : undefined, + templateId }, tx ); @@ -301,6 +340,7 @@ export const identityLdapAuthServiceFactory = ({ const updateLdapAuth = async ({ identityId, + templateId, url, searchBase, searchFilter, @@ -344,6 +384,13 @@ export const identityLdapAuthServiceFactory = ({ ); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity); + if (templateId) { + ForbiddenError.from(permission).throwUnlessCan( + OrgPermissionMachineIdentityAuthTemplateActions.AttachTemplates, + OrgPermissionSubjects.MachineIdentityAuthTemplate + ); + } + const plan = await licenseService.getPlan(identityMembershipOrg.orgId); if (!plan.ldap) { @@ -371,33 +418,56 @@ export const identityLdapAuthServiceFactory = ({ if (allowedFields) AllowedFieldsSchema.array().parse(allowedFields); - const { encryptor } = await kmsService.createCipherPairWithDataKey({ + const { encryptor, decryptor } = await kmsService.createCipherPairWithDataKey({ type: KmsDataKey.Organization, orgId: identityMembershipOrg.orgId }); + const template = templateId + ? await identityAuthTemplateDAL.findByIdAndOrgId(templateId, identityMembershipOrg.orgId) + : undefined; + let config: { + bindDN?: string; + bindPass?: string; + searchBase?: string; + url?: string; + ldapCaCertificate?: string; + }; + + if (template) { + config = JSON.parse(decryptor({ cipherTextBlob: template.templateFields }).toString()); + } else { + config = { + bindDN, + bindPass, + searchBase, + url, + ldapCaCertificate + }; + } + let encryptedBindPass: Buffer | undefined; - if (bindPass) { + if (config.bindPass) { const { cipherTextBlob: bindPassCiphertext } = encryptor({ - plainText: Buffer.from(bindPass) + plainText: Buffer.from(config.bindPass) }); encryptedBindPass = bindPassCiphertext; } let encryptedLdapCaCertificate: Buffer | undefined; - if (ldapCaCertificate) { + if (config.ldapCaCertificate) { const { cipherTextBlob: ldapCaCertificateCiphertext } = encryptor({ - plainText: Buffer.from(ldapCaCertificate) + plainText: Buffer.from(config.ldapCaCertificate) }); encryptedLdapCaCertificate = ldapCaCertificateCiphertext; } let encryptedBindDN: Buffer | undefined; - if (bindDN) { + if (config.bindDN) { const { cipherTextBlob: bindDNCiphertext } = encryptor({ - plainText: Buffer.from(bindDN) + plainText: Buffer.from(config.bindDN) }); encryptedBindDN = bindDNCiphertext; @@ -406,10 +476,10 @@ export const identityLdapAuthServiceFactory = ({ const { ldapConfig } = await getLdapConfig(identityId); const isConnected = await testLDAPConfig({ - bindDN: bindDN || ldapConfig.bindDN, - bindPass: bindPass || ldapConfig.bindPass, - caCert: ldapCaCertificate || ldapConfig.caCert, - url: url || ldapConfig.url + bindDN: config.bindDN || ldapConfig.bindDN, + bindPass: config.bindPass || ldapConfig.bindPass, + caCert: config.ldapCaCertificate || ldapConfig.caCert, + url: config.url || ldapConfig.url }); if (!isConnected) { @@ -420,14 +490,15 @@ export const identityLdapAuthServiceFactory = ({ } const updatedLdapAuth = await identityLdapAuthDAL.updateById(identityLdapAuth.id, { - url, - searchBase, + url: config.url, + searchBase: config.searchBase, searchFilter, encryptedBindDN, encryptedBindPass, encryptedLdapCaCertificate, allowedFields: allowedFields ? JSON.stringify(allowedFields) : undefined, accessTokenMaxTTL, + templateId: template?.id || null, accessTokenTTL, accessTokenNumUsesLimit, accessTokenTrustedIps: reformattedAccessTokenTrustedIps diff --git a/backend/src/services/identity-ldap-auth/identity-ldap-auth-types.ts b/backend/src/services/identity-ldap-auth/identity-ldap-auth-types.ts index 0e6feb5fb..8629763bb 100644 --- a/backend/src/services/identity-ldap-auth/identity-ldap-auth-types.ts +++ b/backend/src/services/identity-ldap-auth/identity-ldap-auth-types.ts @@ -14,11 +14,12 @@ export type TAllowedFields = z.infer; export type TAttachLdapAuthDTO = { identityId: string; - url: string; - searchBase: string; + templateId?: string; + url?: string; + searchBase?: string; searchFilter: string; - bindDN: string; - bindPass: string; + bindDN?: string; + bindPass?: string; ldapCaCertificate?: string; allowedFields?: TAllowedFields[]; accessTokenTTL: number; @@ -30,6 +31,7 @@ export type TAttachLdapAuthDTO = { export type TUpdateLdapAuthDTO = { identityId: string; + templateId?: string; url?: string; searchBase?: string; searchFilter?: string; diff --git a/backend/src/services/org-admin/org-admin-service.ts b/backend/src/services/org-admin/org-admin-service.ts index 6640412c3..3dec0c7f8 100644 --- a/backend/src/services/org-admin/org-admin-service.ts +++ b/backend/src/services/org-admin/org-admin-service.ts @@ -1,19 +1,16 @@ import { ForbiddenError } from "@casl/ability"; -import { ProjectMembershipRole, ProjectVersion, SecretKeyEncoding } from "@app/db/schemas"; +import { ProjectMembershipRole, ProjectVersion } from "@app/db/schemas"; import { OrgPermissionAdminConsoleAction, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types"; -import { crypto } from "@app/lib/crypto/cryptography"; import { BadRequestError, NotFoundError } from "@app/lib/errors"; 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 { TProjectMembershipDALFactory } from "../project-membership/project-membership-dal"; import { TProjectUserMembershipRoleDALFactory } from "../project-membership/project-user-membership-role-dal"; import { SmtpTemplates, TSmtpService } from "../smtp/smtp-service"; -import { TUserDALFactory } from "../user/user-dal"; import { TAccessProjectDTO, TListOrgProjectsDTO } from "./org-admin-types"; type TOrgAdminServiceFactoryDep = { @@ -25,7 +22,6 @@ type TOrgAdminServiceFactoryDep = { >; projectKeyDAL: Pick; projectBotDAL: Pick; - userDAL: Pick; projectUserMembershipRoleDAL: Pick; smtpService: Pick; }; @@ -38,7 +34,6 @@ export const orgAdminServiceFactory = ({ projectMembershipDAL, projectKeyDAL, projectBotDAL, - userDAL, projectUserMembershipRoleDAL, smtpService }: TOrgAdminServiceFactoryDep) => { @@ -83,7 +78,7 @@ export const orgAdminServiceFactory = ({ actorAuthMethod, projectId }: TAccessProjectDTO) => { - const { permission, membership } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission( actor, actorId, actorOrgId, @@ -98,8 +93,10 @@ export const orgAdminServiceFactory = ({ const project = await projectDAL.findOne({ id: projectId, orgId: actorOrgId }); if (!project) throw new NotFoundError({ message: `Project with ID '${projectId}' not found` }); - if (project.version === ProjectVersion.V1) { - throw new BadRequestError({ message: "Please upgrade your project on your dashboard" }); + if (project.version === ProjectVersion.V1 || project.version === ProjectVersion.V2) { + throw new BadRequestError({ + message: `Project '${project.name}' is a legacy project and must be upgraded before accessing it through the admin console.` + }); } // check already there exist a membership if there return it @@ -144,30 +141,6 @@ export const orgAdminServiceFactory = ({ }); } - const botPrivateKey = crypto - .encryption() - .symmetric() - .decryptWithRootEncryptionKey({ - keyEncoding: bot.keyEncoding as SecretKeyEncoding, - iv: bot.iv, - tag: bot.tag, - ciphertext: bot.encryptedPrivateKey - }); - - const userEncryptionKey = await userDAL.findUserEncKeyByUserId(actorId); - if (!userEncryptionKey) - throw new NotFoundError({ message: `User encryption key for user with ID '${actorId}' not found` }); - const [newWsMember] = assignWorkspaceKeysToMembers({ - decryptKey: ghostUserLatestKey, - userPrivateKey: botPrivateKey, - members: [ - { - orgMembershipId: membership.id, - userPublicKey: userEncryptionKey.publicKey - } - ] - }); - const updatedMembership = await projectMembershipDAL.transaction(async (tx) => { const newProjectMembership = await projectMembershipDAL.create( { @@ -181,16 +154,6 @@ export const orgAdminServiceFactory = ({ tx ); - await projectKeyDAL.create( - { - encryptedKey: newWsMember.workspaceEncryptedKey, - nonce: newWsMember.workspaceEncryptedNonce, - senderId: ghostUser.id, - receiverId: actorId, - projectId - }, - tx - ); return newProjectMembership; }); diff --git a/backend/src/services/org/org-service.ts b/backend/src/services/org/org-service.ts index ca51ff57a..5779fa7d3 100644 --- a/backend/src/services/org/org-service.ts +++ b/backend/src/services/org/org-service.ts @@ -8,7 +8,6 @@ import { OrgMembershipStatus, ProjectMembershipRole, ProjectVersion, - SecretKeyEncoding, TableName, TProjectMemberships, TProjectUserMembershipRolesInsert, @@ -58,8 +57,6 @@ import { TAuthTokenServiceFactory } from "../auth-token/auth-token-service"; import { TokenType } from "../auth-token/auth-token-types"; import { TIdentityMetadataDALFactory } from "../identity/identity-metadata-dal"; import { TProjectDALFactory } from "../project/project-dal"; -import { assignWorkspaceKeysToMembers, createProjectKey } from "../project/project-fns"; -import { TProjectBotDALFactory } from "../project-bot/project-bot-dal"; import { TProjectBotServiceFactory } from "../project-bot/project-bot-service"; import { TProjectKeyDALFactory } from "../project-key/project-key-dal"; import { TProjectMembershipDALFactory } from "../project-membership/project-membership-dal"; @@ -137,7 +134,6 @@ type TOrgServiceFactoryDep = { >; projectUserAdditionalPrivilegeDAL: Pick; projectRoleDAL: Pick; - projectBotDAL: Pick; projectUserMembershipRoleDAL: Pick; projectBotService: Pick; loginService: Pick; @@ -169,7 +165,6 @@ export const orgServiceFactory = ({ projectRoleDAL, samlConfigDAL, oidcConfigDAL, - projectBotDAL, projectUserMembershipRoleDAL, identityMetadataDAL, projectBotService, @@ -287,15 +282,7 @@ export const orgServiceFactory = ({ user.id, { encryptionVersion: 2, - protectedKey: encKeys.protectedKey, - protectedKeyIV: encKeys.protectedKeyIV, - protectedKeyTag: encKeys.protectedKeyTag, - publicKey: encKeys.publicKey, - encryptedPrivateKey: encKeys.encryptedPrivateKey, - iv: encKeys.encryptedPrivateKeyIV, - tag: encKeys.encryptedPrivateKeyTag, - salt: encKeys.salt, - verifier: encKeys.verifier + publicKey: encKeys.publicKey }, tx ); @@ -885,29 +872,10 @@ export const orgServiceFactory = ({ // So what we do is we generate a random secure password and then encrypt it with a random pub-private key // Then when user sign in (as login is not possible as isAccepted is false) we rencrypt the private key with the user password if (!inviteeUser || (inviteeUser && !inviteeUser?.isAccepted && !existingEncrytionKey)) { - const serverGeneratedPassword = crypto.randomBytes(32).toString("hex"); - const { tag, encoding, ciphertext, iv } = crypto - .encryption() - .symmetric() - .encryptWithRootEncryptionKey(serverGeneratedPassword); - const encKeys = await generateUserSrpKeys(inviteeEmail, serverGeneratedPassword); await userDAL.createUserEncryption( { userId: inviteeUserId, - encryptionVersion: 2, - protectedKey: encKeys.protectedKey, - protectedKeyIV: encKeys.protectedKeyIV, - protectedKeyTag: encKeys.protectedKeyTag, - publicKey: encKeys.publicKey, - encryptedPrivateKey: encKeys.encryptedPrivateKey, - iv: encKeys.encryptedPrivateKeyIV, - tag: encKeys.encryptedPrivateKeyTag, - salt: encKeys.salt, - verifier: encKeys.verifier, - serverEncryptedPrivateKeyEncoding: encoding, - serverEncryptedPrivateKeyTag: tag, - serverEncryptedPrivateKeyIV: iv, - serverEncryptedPrivateKey: ciphertext + encryptionVersion: 2 }, tx ); @@ -1069,106 +1037,6 @@ export const orgServiceFactory = ({ const customRolesGroupBySlug = groupBy(customRoles, ({ slug }) => slug); - // this will auto generate bot - const { botKey, bot: autoGeneratedBot } = await projectBotService.getBotKey(projectId, true); - - const ghostUser = await projectDAL.findProjectGhostUser(projectId, tx); - let ghostUserId = ghostUser?.id; - - // backfill missing ghost user - if (!ghostUserId) { - const newGhostUser = await addGhostUser(project.orgId, tx); - const projectMembership = await projectMembershipDAL.create( - { - userId: newGhostUser.user.id, - projectId: project.id - }, - tx - ); - await projectUserMembershipRoleDAL.create( - { projectMembershipId: projectMembership.id, role: ProjectMembershipRole.Admin }, - tx - ); - - const { key: encryptedProjectKey, iv: encryptedProjectKeyIv } = createProjectKey({ - publicKey: newGhostUser.keys.publicKey, - privateKey: newGhostUser.keys.plainPrivateKey, - plainProjectKey: botKey - }); - - // 4. Save the project key for the ghost user. - await projectKeyDAL.create( - { - projectId: project.id, - receiverId: newGhostUser.user.id, - encryptedKey: encryptedProjectKey, - nonce: encryptedProjectKeyIv, - senderId: newGhostUser.user.id - }, - tx - ); - - const { iv, tag, ciphertext, encoding, algorithm } = crypto - .encryption() - .symmetric() - .encryptWithRootEncryptionKey(newGhostUser.keys.plainPrivateKey); - if (autoGeneratedBot) { - await projectBotDAL.updateById( - autoGeneratedBot.id, - { - tag, - iv, - encryptedProjectKey, - encryptedProjectKeyNonce: encryptedProjectKeyIv, - encryptedPrivateKey: ciphertext, - isActive: true, - publicKey: newGhostUser.keys.publicKey, - senderId: newGhostUser.user.id, - algorithm, - keyEncoding: encoding - }, - tx - ); - } - ghostUserId = newGhostUser.user.id; - } - - const bot = await projectBotDAL.findOne({ projectId }, tx); - if (!bot) { - throw new NotFoundError({ - name: "InviteUser", - message: `Failed to find project bot for project with ID '${projectId}'` - }); - } - - const ghostUserLatestKey = await projectKeyDAL.findLatestProjectKey(ghostUserId, projectId, tx); - if (!ghostUserLatestKey) { - throw new NotFoundError({ - name: "InviteUser", - message: `Failed to find project owner's latest key for project with ID '${projectId}'` - }); - } - - const botPrivateKey = crypto - .encryption() - .symmetric() - .decryptWithRootEncryptionKey({ - keyEncoding: bot.keyEncoding as SecretKeyEncoding, - iv: bot.iv, - tag: bot.tag, - ciphertext: bot.encryptedPrivateKey - }); - - const newWsMembers = assignWorkspaceKeysToMembers({ - decryptKey: ghostUserLatestKey, - userPrivateKey: botPrivateKey, - members: userWithEncryptionKeyInvitedToProject.map((userEnc) => ({ - orgMembershipId: userEnc.userId, - projectMembershipRole: ProjectMembershipRole.Admin, - userPublicKey: userEnc.publicKey - })) - }); - const projectMemberships = await projectMembershipDAL.insertMany( userWithEncryptionKeyInvitedToProject.map((userEnc) => ({ projectId, @@ -1191,16 +1059,6 @@ export const orgServiceFactory = ({ }); await projectUserMembershipRoleDAL.insertMany(sanitizedProjectMembershipRoles, tx); - await projectKeyDAL.insertMany( - newWsMembers.map((el) => ({ - encryptedKey: el.workspaceEncryptedKey, - nonce: el.workspaceEncryptedNonce, - senderId: ghostUserId, - receiverId: el.orgMembershipId, - projectId - })), - tx - ); mailsForProjectInvitation.push({ email: userWithEncryptionKeyInvitedToProject .filter((el) => !userIdsWithOrgInvitation.has(el.userId)) diff --git a/backend/src/services/project-bot/project-bot-fns.ts b/backend/src/services/project-bot/project-bot-fns.ts index d26669b86..029f320df 100644 --- a/backend/src/services/project-bot/project-bot-fns.ts +++ b/backend/src/services/project-bot/project-bot-fns.ts @@ -42,6 +42,13 @@ export const getBotKeyFnFactory = ( message: `Project bot not found for project with ID '${projectId}'. Please ask an administrator to log-in to the Infisical Console.` }); } + + if (!projectV1Keys.senderPublicKey) { + throw new NotFoundError({ + message: `Project bot not found for project with ID '${projectId}'. Please ask an administrator to log-in to the Infisical Console and upgrade the project.` + }); + } + let userPrivateKey = ""; if ( projectV1Keys?.serverEncryptedPrivateKey && diff --git a/backend/src/services/project-key/project-key-dal.ts b/backend/src/services/project-key/project-key-dal.ts index ea4ed813c..bb91b9c85 100644 --- a/backend/src/services/project-key/project-key-dal.ts +++ b/backend/src/services/project-key/project-key-dal.ts @@ -14,7 +14,7 @@ export const projectKeyDALFactory = (db: TDbClient) => { userId: string, projectId: string, tx?: Knex - ): Promise<(TProjectKeys & { sender: { publicKey: string } }) | undefined> => { + ): Promise<(TProjectKeys & { sender: { publicKey?: string } }) | undefined> => { try { const projectKey = await (tx || db.replicaNode())(TableName.ProjectKeys) .join(TableName.Users, `${TableName.ProjectKeys}.senderId`, `${TableName.Users}.id`) @@ -25,7 +25,7 @@ export const projectKeyDALFactory = (db: TDbClient) => { .select(db.ref("publicKey").withSchema(TableName.UserEncryptionKey)) .first(); if (projectKey) { - return { ...projectKey, sender: { publicKey: projectKey.publicKey } }; + return { ...projectKey, sender: { publicKey: projectKey.publicKey || undefined } }; } } catch (error) { throw new DatabaseError({ error, name: "Find latest project key" }); diff --git a/backend/src/services/project/project-fns.ts b/backend/src/services/project/project-fns.ts index f166ec04f..fca0074d7 100644 --- a/backend/src/services/project/project-fns.ts +++ b/backend/src/services/project/project-fns.ts @@ -10,6 +10,10 @@ import { TProjectDALFactory } from "@app/services/project/project-dal"; import { AddUserToWsDTO, TBootstrapSshProjectDTO } from "./project-types"; export const assignWorkspaceKeysToMembers = ({ members, decryptKey, userPrivateKey }: AddUserToWsDTO) => { + if (!decryptKey.sender.publicKey) { + throw new Error("Decrypt key sender public key not found"); + } + const plaintextProjectKey = crypto.encryption().asymmetric().decrypt({ ciphertext: decryptKey.encryptedKey, nonce: decryptKey.nonce, diff --git a/backend/src/services/project/project-queue.ts b/backend/src/services/project/project-queue.ts index 0d7c7dd55..e557b7c96 100644 --- a/backend/src/services/project/project-queue.ts +++ b/backend/src/services/project/project-queue.ts @@ -121,6 +121,10 @@ export const projectQueueFactory = ({ tag: data.encryptedPrivateKey.encryptedKeyTag }); + if (!oldProjectKey.sender.publicKey) { + throw new Error("Old project key sender public key not found"); + } + const decryptedPlainProjectKey = crypto.encryption().asymmetric().decrypt({ ciphertext: oldProjectKey.encryptedKey, nonce: oldProjectKey.nonce, @@ -290,6 +294,10 @@ export const projectQueueFactory = ({ continue; } + if (!user.publicKey) { + throw new Error(`User with ID ${key.receiverId} has no public key during upgrade.`); + } + const [newMember] = assignWorkspaceKeysToMembers({ decryptKey: ghostUserLatestKey, userPrivateKey: ghostUser.keys.plainPrivateKey, diff --git a/backend/src/services/project/project-service.ts b/backend/src/services/project/project-service.ts index 153f627fc..4261870b1 100644 --- a/backend/src/services/project/project-service.ts +++ b/backend/src/services/project/project-service.ts @@ -55,13 +55,10 @@ import { validateMicrosoftTeamsChannelsSchema } from "../microsoft-teams/microso import { TMicrosoftTeamsIntegrationDALFactory } from "../microsoft-teams/microsoft-teams-integration-dal"; import { TProjectMicrosoftTeamsConfigDALFactory } from "../microsoft-teams/project-microsoft-teams-config-dal"; import { TOrgDALFactory } from "../org/org-dal"; -import { TOrgServiceFactory } from "../org/org-service"; import { TPkiAlertDALFactory } from "../pki-alert/pki-alert-dal"; import { TPkiCollectionDALFactory } from "../pki-collection/pki-collection-dal"; -import { TProjectBotDALFactory } from "../project-bot/project-bot-dal"; import { TProjectBotServiceFactory } from "../project-bot/project-bot-service"; import { TProjectEnvDALFactory } from "../project-env/project-env-dal"; -import { TProjectKeyDALFactory } from "../project-key/project-key-dal"; import { TProjectMembershipDALFactory } from "../project-membership/project-membership-dal"; import { TProjectUserMembershipRoleDALFactory } from "../project-membership/project-user-membership-role-dal"; import { TProjectRoleDALFactory } from "../project-role/project-role-dal"; @@ -78,7 +75,7 @@ import { SmtpTemplates, TSmtpService } from "../smtp/smtp-service"; import { TUserDALFactory } from "../user/user-dal"; import { WorkflowIntegration, WorkflowIntegrationStatus } from "../workflow-integration/workflow-integration-types"; import { TProjectDALFactory } from "./project-dal"; -import { assignWorkspaceKeysToMembers, bootstrapSshProject, createProjectKey } from "./project-fns"; +import { bootstrapSshProject } from "./project-fns"; import { TProjectQueueFactory } from "./project-queue"; import { TProjectSshConfigDALFactory } from "./project-ssh-config-dal"; import { @@ -123,6 +120,7 @@ export const DEFAULT_PROJECT_ENVS = [ type TProjectServiceFactoryDep = { projectDAL: TProjectDALFactory; + identityProjectDAL: Pick; projectSshConfigDAL: Pick; projectQueue: TProjectQueueFactory; userDAL: TUserDALFactory; @@ -132,9 +130,7 @@ type TProjectServiceFactoryDep = { secretV2BridgeDAL: Pick; projectEnvDAL: Pick; identityOrgMembershipDAL: TIdentityOrgDALFactory; - identityProjectDAL: TIdentityProjectDALFactory; identityProjectMembershipRoleDAL: Pick; - projectKeyDAL: Pick; projectMembershipDAL: Pick< TProjectMembershipDALFactory, "create" | "findProjectGhostUser" | "findOne" | "delete" | "findAllProjectMembers" @@ -167,12 +163,10 @@ type TProjectServiceFactoryDep = { sshHostDAL: Pick; sshHostGroupDAL: Pick; permissionService: TPermissionServiceFactory; - orgService: Pick; licenseService: Pick; smtpService: Pick; orgDAL: Pick; keyStore: Pick; - projectBotDAL: Pick; projectRoleDAL: Pick; kmsService: Pick< TKmsServiceFactory, @@ -196,27 +190,25 @@ export const projectServiceFactory = ({ secretDAL, secretV2BridgeDAL, projectQueue, - projectKeyDAL, permissionService, projectBotService, orgDAL, userDAL, folderDAL, - orgService, - identityProjectDAL, identityOrgMembershipDAL, projectMembershipDAL, projectEnvDAL, licenseService, projectUserMembershipRoleDAL, projectRoleDAL, - identityProjectMembershipRoleDAL, certificateAuthorityDAL, certificateDAL, certificateTemplateDAL, pkiCollectionDAL, pkiAlertDAL, pkiSubscriberDAL, + identityProjectDAL, + identityProjectMembershipRoleDAL, sshCertificateAuthorityDAL, sshCertificateAuthoritySecretDAL, sshCertificateDAL, @@ -225,7 +217,6 @@ export const projectServiceFactory = ({ sshHostGroupDAL, keyStore, kmsService, - projectBotDAL, projectSlackConfigDAL, projectMicrosoftTeamsConfigDAL, slackIntegrationDAL, @@ -253,7 +244,7 @@ export const projectServiceFactory = ({ type = ProjectType.SecretManager }: TCreateProjectDTO) => { const organization = await orgDAL.findOne({ id: actorOrgId }); - const { permission, membership: orgMembership } = await permissionService.getOrgPermission( + const { permission } = await permissionService.getOrgPermission( actor, actorId, organization.id, @@ -277,7 +268,6 @@ export const projectServiceFactory = ({ message: "Failed to create workspace due to plan limit reached. Upgrade plan to add more workspaces." }); } - const ghostUser = await orgService.addGhostUser(organization.id, tx); if (kmsKeyId) { const kms = await kmsService.getKmsById(kmsKeyId, tx); @@ -329,19 +319,6 @@ export const projectServiceFactory = ({ }); } - // set ghost user as admin of project - const projectMembership = await projectMembershipDAL.create( - { - userId: ghostUser.user.id, - projectId: project.id - }, - tx - ); - await projectUserMembershipRoleDAL.create( - { projectMembershipId: projectMembership.id, role: ProjectMembershipRole.Admin }, - tx - ); - // set default environments and root folder for provided environments let envs: TProjectEnvironments[] = []; if (projectTemplate) { @@ -374,55 +351,6 @@ export const projectServiceFactory = ({ ); } - // 3. Create a random key that we'll use as the project key. - const { key: encryptedProjectKey, iv: encryptedProjectKeyIv } = createProjectKey({ - publicKey: ghostUser.keys.publicKey, - privateKey: ghostUser.keys.plainPrivateKey - }); - - // 4. Save the project key for the ghost user. - await projectKeyDAL.create( - { - projectId: project.id, - receiverId: ghostUser.user.id, - encryptedKey: encryptedProjectKey, - nonce: encryptedProjectKeyIv, - senderId: ghostUser.user.id - }, - tx - ); - - const { iv, tag, ciphertext, encoding, algorithm } = crypto - .encryption() - .symmetric() - .encryptWithRootEncryptionKey(ghostUser.keys.plainPrivateKey); - - // 5. Create & a bot for the project - await projectBotDAL.create( - { - name: "Infisical Bot (Ghost)", - projectId: project.id, - tag, - iv, - encryptedProjectKey, - encryptedProjectKeyNonce: encryptedProjectKeyIv, - encryptedPrivateKey: ciphertext, - isActive: true, - publicKey: ghostUser.keys.publicKey, - senderId: ghostUser.user.id, - algorithm, - keyEncoding: encoding - }, - tx - ); - - // Find the ghost users latest key - const latestKey = await projectKeyDAL.findLatestProjectKey(ghostUser.user.id, project.id, tx); - - if (!latestKey) { - throw new Error("Latest key not found for user"); - } - // If the project is being created by a user, add the user to the project as an admin if (actor === ActorType.USER) { // Find public key of user @@ -432,17 +360,6 @@ export const projectServiceFactory = ({ throw new Error("User not found"); } - const [projectAdmin] = assignWorkspaceKeysToMembers({ - decryptKey: latestKey, - userPrivateKey: ghostUser.keys.plainPrivateKey, - members: [ - { - userPublicKey: user.publicKey, - orgMembershipId: orgMembership.id - } - ] - }); - // Create a membership for the user const userProjectMembership = await projectMembershipDAL.create( { @@ -455,18 +372,6 @@ export const projectServiceFactory = ({ { projectMembershipId: userProjectMembership.id, role: ProjectMembershipRole.Admin }, tx ); - - // Create a project key for the user - await projectKeyDAL.create( - { - encryptedKey: projectAdmin.workspaceEncryptedKey, - nonce: projectAdmin.workspaceEncryptedNonce, - senderId: ghostUser.user.id, - receiverId: user.id, - projectId: project.id - }, - tx - ); } // If the project is being created by an identity, add the identity to the project as an admin diff --git a/backend/src/services/project/project-types.ts b/backend/src/services/project/project-types.ts index 02f89bc38..ceef78f6a 100644 --- a/backend/src/services/project/project-types.ts +++ b/backend/src/services/project/project-types.ts @@ -117,7 +117,7 @@ export type TUpgradeProjectDTO = { } & TProjectPermission; export type AddUserToWsDTO = { - decryptKey: TProjectKeys & { sender: { publicKey: string } }; + decryptKey: TProjectKeys & { sender: { publicKey?: string } }; userPrivateKey: string; members: { orgMembershipId: string; diff --git a/backend/src/services/reminder/reminder-service.ts b/backend/src/services/reminder/reminder-service.ts index a03e9cddd..181f0cdb9 100644 --- a/backend/src/services/reminder/reminder-service.ts +++ b/backend/src/services/reminder/reminder-service.ts @@ -42,7 +42,7 @@ export const reminderServiceFactory = ({ const $manageReminderRecipients = async (reminderId: string, newRecipients?: string[] | null): Promise => { if (!newRecipients || newRecipients.length === 0) { // If no recipients provided, remove all existing recipients - await reminderRecipientDAL.deleteById(reminderId); + await reminderRecipientDAL.delete({ reminderId }); return; } @@ -79,25 +79,33 @@ export const reminderServiceFactory = ({ repeatDays, nextReminderDate: nextReminderDateInput, recipients, - projectId + projectId, + fromDate: fromDateInput }: { secretId?: string; message?: string | null; repeatDays?: number | null; nextReminderDate?: string | null; recipients?: string[] | null; + fromDate?: string | null; projectId: string; }) => { if (!secretId) { throw new BadRequestError({ message: "secretId is required" }); } let nextReminderDate; + let fromDate; if (nextReminderDateInput) { nextReminderDate = new Date(nextReminderDateInput); } - if (repeatDays && repeatDays > 0) { - nextReminderDate = $addDays(repeatDays); + if (repeatDays) { + if (fromDateInput) { + fromDate = new Date(fromDateInput); + nextReminderDate = fromDate; + } else { + nextReminderDate = $addDays(repeatDays); + } } if (!nextReminderDate) { @@ -112,7 +120,8 @@ export const reminderServiceFactory = ({ await reminderDAL.updateById(existingReminder.id, { message, repeatDays, - nextReminderDate + nextReminderDate, + fromDate }); reminderId = existingReminder.id; } else { @@ -121,7 +130,8 @@ export const reminderServiceFactory = ({ secretId, message, repeatDays, - nextReminderDate + nextReminderDate, + fromDate }); reminderId = newReminder.id; } @@ -280,14 +290,28 @@ export const reminderServiceFactory = ({ } const processedReminders = remindersData.map( - ({ secretId, message, repeatDays, nextReminderDate: nextReminderDateInput, recipients, projectId }) => { + ({ + secretId, + message, + repeatDays, + nextReminderDate: nextReminderDateInput, + recipients, + projectId, + fromDate: fromDateInput + }) => { let nextReminderDate; + let fromDate; if (nextReminderDateInput) { nextReminderDate = new Date(nextReminderDateInput); } - if (repeatDays && repeatDays > 0 && !nextReminderDate) { - nextReminderDate = $addDays(repeatDays); + if (repeatDays && !nextReminderDate) { + if (fromDateInput) { + fromDate = new Date(fromDateInput); + nextReminderDate = fromDate; + } else { + nextReminderDate = $addDays(repeatDays); + } } if (!nextReminderDate) { @@ -302,17 +326,19 @@ export const reminderServiceFactory = ({ repeatDays, nextReminderDate, recipients: recipients ? [...new Set(recipients)] : [], - projectId + projectId, + fromDate }; } ); const newReminders = await reminderDAL.insertMany( - processedReminders.map(({ secretId, message, repeatDays, nextReminderDate }) => ({ + processedReminders.map(({ secretId, message, repeatDays, nextReminderDate, fromDate }) => ({ secretId, message, repeatDays, - nextReminderDate + nextReminderDate, + fromDate })), tx ); diff --git a/backend/src/services/reminder/reminder-types.ts b/backend/src/services/reminder/reminder-types.ts index 1f6a53ac7..54726ab66 100644 --- a/backend/src/services/reminder/reminder-types.ts +++ b/backend/src/services/reminder/reminder-types.ts @@ -8,6 +8,7 @@ export type TReminder = { message?: string | null; repeatDays?: number | null; nextReminderDate: Date; + fromDate?: Date | null; createdAt: Date; updatedAt: Date; }; @@ -21,6 +22,7 @@ export type TCreateReminderDTO = { secretId?: string; message?: string | null; repeatDays?: number | null; + fromDate?: string | null; nextReminderDate?: string | null; recipients?: string[] | null; }; @@ -31,6 +33,7 @@ export type TBatchCreateReminderDTO = { message?: string | null; repeatDays?: number | null; nextReminderDate?: string | Date | null; + fromDate?: Date | null; recipients?: string[] | null; projectId?: string; }[]; @@ -95,6 +98,7 @@ export interface TReminderServiceFactory { nextReminderDate?: string | null; recipients?: string[] | null; projectId: string; + fromDate?: string | null; }) => Promise<{ id: string; created: boolean; diff --git a/backend/src/services/secret-sync/github/github-sync-fns.ts b/backend/src/services/secret-sync/github/github-sync-fns.ts index b37d5e90e..e2cf8f6e8 100644 --- a/backend/src/services/secret-sync/github/github-sync-fns.ts +++ b/backend/src/services/secret-sync/github/github-sync-fns.ts @@ -3,6 +3,7 @@ import sodium from "libsodium-wrappers"; import { TGatewayServiceFactory } from "@app/ee/services/gateway/gateway-service"; import { getGitHubAppAuthToken, + getGitHubInstanceApiUrl, GitHubConnectionMethod, makePaginatedGitHubRequest, requestWithGitHubGateway @@ -73,7 +74,7 @@ const getPublicKey = async ( } const response = await requestWithGitHubGateway(connection, gatewayService, { - url: `https://api.${connection.credentials.host || "github.com"}${path}`, + url: `https://${await getGitHubInstanceApiUrl(connection)}${path}`, method: "GET", headers: { Accept: "application/vnd.github+json", @@ -111,7 +112,7 @@ const deleteSecret = async ( } await requestWithGitHubGateway(connection, gatewayService, { - url: `https://api.${connection.credentials.host || "github.com"}${path}`, + url: `https://${await getGitHubInstanceApiUrl(connection)}${path}`, method: "DELETE", headers: { Accept: "application/vnd.github+json", @@ -157,7 +158,7 @@ const putSecret = async ( } await requestWithGitHubGateway(connection, gatewayService, { - url: `https://api.${connection.credentials.host || "github.com"}${path}`, + url: `https://${await getGitHubInstanceApiUrl(connection)}${path}`, method: "PUT", headers: { Accept: "application/vnd.github+json", diff --git a/backend/src/services/secret-sync/secret-sync-dal.ts b/backend/src/services/secret-sync/secret-sync-dal.ts index 617393668..e50593f10 100644 --- a/backend/src/services/secret-sync/secret-sync-dal.ts +++ b/backend/src/services/secret-sync/secret-sync-dal.ts @@ -30,6 +30,7 @@ const baseSecretSyncQuery = ({ filter, db, tx }: { db: TDbClient; filter?: Secre db.ref("encryptedCredentials").withSchema(TableName.AppConnection).as("connectionEncryptedCredentials"), db.ref("description").withSchema(TableName.AppConnection).as("connectionDescription"), db.ref("version").withSchema(TableName.AppConnection).as("connectionVersion"), + db.ref("gatewayId").withSchema(TableName.AppConnection).as("connectionGatewayId"), db.ref("createdAt").withSchema(TableName.AppConnection).as("connectionCreatedAt"), db.ref("updatedAt").withSchema(TableName.AppConnection).as("connectionUpdatedAt"), db @@ -65,6 +66,7 @@ const expandSecretSync = ( connectionUpdatedAt, connectionVersion, connectionIsPlatformManagedCredentials, + connectionGatewayId, ...el } = secretSync; @@ -83,7 +85,8 @@ const expandSecretSync = ( createdAt: connectionCreatedAt, updatedAt: connectionUpdatedAt, version: connectionVersion, - isPlatformManagedCredentials: connectionIsPlatformManagedCredentials + isPlatformManagedCredentials: connectionIsPlatformManagedCredentials, + gatewayId: connectionGatewayId }, folder: folder ? { diff --git a/backend/src/services/smtp/emails/AccessApprovalRequestTemplate.tsx b/backend/src/services/smtp/emails/AccessApprovalRequestTemplate.tsx index fef072546..bdefc8b86 100644 --- a/backend/src/services/smtp/emails/AccessApprovalRequestTemplate.tsx +++ b/backend/src/services/smtp/emails/AccessApprovalRequestTemplate.tsx @@ -1,7 +1,9 @@ -import { Button, Heading, Link, Section, Text } from "@react-email/components"; +import { Heading, Section, Text } from "@react-email/components"; import React from "react"; +import { BaseButton } from "./BaseButton"; import { BaseEmailWrapper, BaseEmailWrapperProps } from "./BaseEmailWrapper"; +import { BaseLink } from "./BaseLink"; interface AccessApprovalRequestTemplateProps extends Omit { projectName: string; @@ -38,18 +40,15 @@ export const AccessApprovalRequestTemplate = ({ You have a new access approval request pending review for the project {projectName} -
+
- {requesterFullName} ( - - {requesterEmail} - - ) has requested {isTemporary ? "temporary" : "permanent"} access to {secretPath} in the{" "} + {requesterFullName} ({requesterEmail}) + has requested {isTemporary ? "temporary" : "permanent"} access to {secretPath} in the{" "} {environment} environment. {isTemporary && ( - + This access will expire {expiresIn} after approval. )} @@ -67,13 +66,8 @@ export const AccessApprovalRequestTemplate = ({ )}
-
- +
+ Review Request
); diff --git a/backend/src/services/smtp/emails/BaseButton.tsx b/backend/src/services/smtp/emails/BaseButton.tsx new file mode 100644 index 000000000..7d79ea581 --- /dev/null +++ b/backend/src/services/smtp/emails/BaseButton.tsx @@ -0,0 +1,18 @@ +import { Button } from "@react-email/components"; +import React from "react"; + +type Props = { + href: string; + children: string; +}; + +export const BaseButton = ({ href, children }: Props) => { + return ( + + ); +}; diff --git a/backend/src/services/smtp/emails/BaseEmailWrapper.tsx b/backend/src/services/smtp/emails/BaseEmailWrapper.tsx index 01bf779c5..3e908db18 100644 --- a/backend/src/services/smtp/emails/BaseEmailWrapper.tsx +++ b/backend/src/services/smtp/emails/BaseEmailWrapper.tsx @@ -16,23 +16,21 @@ export const BaseEmailWrapper = ({ title, preview, children, siteUrl }: BaseEmai {preview} -
-
-
- Infisical Logo -
+
+ Infisical Logo
+
{children}

Email sent via{" "} - + Infisical diff --git a/backend/src/services/smtp/emails/BaseLink.tsx b/backend/src/services/smtp/emails/BaseLink.tsx new file mode 100644 index 000000000..f2a8763e6 --- /dev/null +++ b/backend/src/services/smtp/emails/BaseLink.tsx @@ -0,0 +1,15 @@ +import { Link } from "@react-email/components"; +import React from "react"; + +type Props = { + href: string; + children: string; +}; + +export const BaseLink = ({ href, children }: Props) => { + return ( + + {children} + + ); +}; diff --git a/backend/src/services/smtp/emails/EmailMfaTemplate.tsx b/backend/src/services/smtp/emails/EmailMfaTemplate.tsx index b01e2f8af..a5d1fdf03 100644 --- a/backend/src/services/smtp/emails/EmailMfaTemplate.tsx +++ b/backend/src/services/smtp/emails/EmailMfaTemplate.tsx @@ -1,7 +1,8 @@ -import { Heading, Link, Section, Text } from "@react-email/components"; +import { Heading, Section, Text } from "@react-email/components"; import React from "react"; import { BaseEmailWrapper, BaseEmailWrapperProps } from "./BaseEmailWrapper"; +import { BaseLink } from "./BaseLink"; interface EmailMfaTemplateProps extends Omit { code: string; @@ -25,11 +26,7 @@ export const EmailMfaTemplate = ({ code, siteUrl, isCloud }: EmailMfaTemplatePro Not you?{" "} {isCloud ? ( <> - Contact us at{" "} - - support@infisical.com - {" "} - immediately + Contact us at support@infisical.com immediately ) : ( "Contact your administrator immediately" diff --git a/backend/src/services/smtp/emails/EmailVerificationTemplate.tsx b/backend/src/services/smtp/emails/EmailVerificationTemplate.tsx index fc32b01b0..313601e6a 100644 --- a/backend/src/services/smtp/emails/EmailVerificationTemplate.tsx +++ b/backend/src/services/smtp/emails/EmailVerificationTemplate.tsx @@ -1,7 +1,8 @@ -import { Heading, Link, Section, Text } from "@react-email/components"; +import { Heading, Section, Text } from "@react-email/components"; import React from "react"; import { BaseEmailWrapper, BaseEmailWrapperProps } from "./BaseEmailWrapper"; +import { BaseLink } from "./BaseLink"; interface EmailVerificationTemplateProps extends Omit { code: string; @@ -29,10 +30,7 @@ export const EmailVerificationTemplate = ({ code, siteUrl, isCloud }: EmailVerif Questions about Infisical?{" "} {isCloud ? ( <> - Email us at{" "} - - support@infisical.com - + Email us at support@infisical.com ) : ( "Contact your administrator" diff --git a/backend/src/services/smtp/emails/ExternalImportFailedTemplate.tsx b/backend/src/services/smtp/emails/ExternalImportFailedTemplate.tsx index 3cca41721..28bccc9d5 100644 --- a/backend/src/services/smtp/emails/ExternalImportFailedTemplate.tsx +++ b/backend/src/services/smtp/emails/ExternalImportFailedTemplate.tsx @@ -1,7 +1,8 @@ -import { Heading, Link, Section, Text } from "@react-email/components"; +import { Heading, Section, Text } from "@react-email/components"; import React from "react"; import { BaseEmailWrapper, BaseEmailWrapperProps } from "./BaseEmailWrapper"; +import { BaseLink } from "./BaseLink"; interface ExternalImportFailedTemplateProps extends Omit { error: string; @@ -21,12 +22,9 @@ export const ExternalImportFailedTemplate = ({ error, siteUrl, provider }: Exter If your issue persists, you can contact the Infisical team at{" "} - - support@infisical.com - - . + support@infisical.com. - + Error: "{error}"
diff --git a/backend/src/services/smtp/emails/IntegrationSyncFailedTemplate.tsx b/backend/src/services/smtp/emails/IntegrationSyncFailedTemplate.tsx index c2fb78ae0..3263d4928 100644 --- a/backend/src/services/smtp/emails/IntegrationSyncFailedTemplate.tsx +++ b/backend/src/services/smtp/emails/IntegrationSyncFailedTemplate.tsx @@ -1,6 +1,7 @@ -import { Button, Heading, Section, Text } from "@react-email/components"; +import { Heading, Section, Text } from "@react-email/components"; import React from "react"; +import { BaseButton } from "./BaseButton"; import { BaseEmailWrapper, BaseEmailWrapperProps } from "./BaseEmailWrapper"; interface IntegrationSyncFailedTemplateProps extends Omit { @@ -30,7 +31,7 @@ export const IntegrationSyncFailedTemplate = ({ {count} integration(s) failed to sync -
+
Project {projectName} Environment @@ -38,15 +39,10 @@ export const IntegrationSyncFailedTemplate = ({ Secret Path {secretPath} Failure Reason: - "{syncMessage}" + "{syncMessage}"
-
- +
+ View Integrations
); diff --git a/backend/src/services/smtp/emails/NewDeviceLoginTemplate.tsx b/backend/src/services/smtp/emails/NewDeviceLoginTemplate.tsx index 9692bc77d..da1049096 100644 --- a/backend/src/services/smtp/emails/NewDeviceLoginTemplate.tsx +++ b/backend/src/services/smtp/emails/NewDeviceLoginTemplate.tsx @@ -1,7 +1,8 @@ -import { Heading, Link, Section, Text } from "@react-email/components"; +import { Heading, Section, Text } from "@react-email/components"; import React from "react"; import { BaseEmailWrapper, BaseEmailWrapperProps } from "./BaseEmailWrapper"; +import { BaseLink } from "./BaseLink"; interface NewDeviceLoginTemplateProps extends Omit { email: string; @@ -42,9 +43,7 @@ export const NewDeviceLoginTemplate = ({ If you believe that this login is suspicious, please contact{" "} {isCloud ? ( - - support@infisical.com - + support@infisical.com ) : ( "your administrator" )}{" "} diff --git a/backend/src/services/smtp/emails/OrgAdminBreakglassAccessTemplate.tsx b/backend/src/services/smtp/emails/OrgAdminBreakglassAccessTemplate.tsx index 1d2ecc1a3..ee09574b6 100644 --- a/backend/src/services/smtp/emails/OrgAdminBreakglassAccessTemplate.tsx +++ b/backend/src/services/smtp/emails/OrgAdminBreakglassAccessTemplate.tsx @@ -1,7 +1,8 @@ -import { Heading, Link, Section, Text } from "@react-email/components"; +import { Heading, Section, Text } from "@react-email/components"; import React from "react"; import { BaseEmailWrapper, BaseEmailWrapperProps } from "./BaseEmailWrapper"; +import { BaseLink } from "./BaseLink"; interface OrgAdminBreakglassAccessTemplateProps extends Omit { email: string; @@ -35,10 +36,7 @@ export const OrgAdminBreakglassAccessTemplate = ({ {userAgent} If you'd like to disable Admin SSO Bypass, please visit{" "} - - Organization Security Settings - - . + Organization Security Settings.
diff --git a/backend/src/services/smtp/emails/OrgAdminProjectGrantAccessTemplate.tsx b/backend/src/services/smtp/emails/OrgAdminProjectGrantAccessTemplate.tsx index bec22575c..1cf875d1c 100644 --- a/backend/src/services/smtp/emails/OrgAdminProjectGrantAccessTemplate.tsx +++ b/backend/src/services/smtp/emails/OrgAdminProjectGrantAccessTemplate.tsx @@ -2,6 +2,7 @@ import { Heading, Section, Text } from "@react-email/components"; import React from "react"; import { BaseEmailWrapper, BaseEmailWrapperProps } from "./BaseEmailWrapper"; +import { BaseLink } from "./BaseLink"; interface OrgAdminProjectGrantAccessTemplateProps extends Omit { email: string; @@ -24,8 +25,8 @@ export const OrgAdminProjectGrantAccessTemplate = ({
- The organization admin {email} has self-issued direct access to the project{" "} - {projectName}. + The organization admin {email} has self-issued direct access to + the project {projectName}.
diff --git a/backend/src/services/smtp/emails/OrganizationInvitationTemplate.tsx b/backend/src/services/smtp/emails/OrganizationInvitationTemplate.tsx index 27092a843..9c57548ce 100644 --- a/backend/src/services/smtp/emails/OrganizationInvitationTemplate.tsx +++ b/backend/src/services/smtp/emails/OrganizationInvitationTemplate.tsx @@ -1,7 +1,9 @@ -import { Button, Heading, Link, Section, Text } from "@react-email/components"; +import { Heading, Section, Text } from "@react-email/components"; import React from "react"; +import { BaseButton } from "./BaseButton"; import { BaseEmailWrapper, BaseEmailWrapperProps } from "./BaseEmailWrapper"; +import { BaseLink } from "./BaseLink"; interface OrganizationInvitationTemplateProps extends Omit { metadata?: string; @@ -36,15 +38,13 @@ export const OrganizationInvitationTemplate = ({
{organizationName} on Infisical -
+
{inviterFirstName && inviterUsername ? ( <> {inviterFirstName} ( - - {inviterUsername} - - ) has invited you to collaborate on {organizationName}. + {inviterUsername}) has invited you to collaborate + on {organizationName}. ) : ( <> @@ -53,13 +53,12 @@ export const OrganizationInvitationTemplate = ({ )}
-
- +
diff --git a/backend/src/services/smtp/emails/PasswordResetTemplate.tsx b/backend/src/services/smtp/emails/PasswordResetTemplate.tsx index 7486b29d9..2cfa00508 100644 --- a/backend/src/services/smtp/emails/PasswordResetTemplate.tsx +++ b/backend/src/services/smtp/emails/PasswordResetTemplate.tsx @@ -1,7 +1,9 @@ -import { Button, Heading, Link, Section, Text } from "@react-email/components"; +import { Heading, Section, Text } from "@react-email/components"; import React from "react"; +import { BaseButton } from "./BaseButton"; import { BaseEmailWrapper, BaseEmailWrapperProps } from "./BaseEmailWrapper"; +import { BaseLink } from "./BaseLink"; interface PasswordResetTemplateProps extends Omit { email: string; @@ -20,16 +22,13 @@ export const PasswordResetTemplate = ({ email, isCloud, siteUrl, callback_url, t Account Recovery -
+
A password reset was requested for your Infisical account. If you did not initiate this request, please contact{" "} {isCloud ? ( <> - us immediately at{" "} - - support@infisical.com - + us immediately at support@infisical.com ) : ( "your administrator immediately" @@ -37,13 +36,8 @@ export const PasswordResetTemplate = ({ email, isCloud, siteUrl, callback_url, t .
-
- +
+ Reset Password
); diff --git a/backend/src/services/smtp/emails/PasswordSetupTemplate.tsx b/backend/src/services/smtp/emails/PasswordSetupTemplate.tsx index c8a986c8c..78164bed2 100644 --- a/backend/src/services/smtp/emails/PasswordSetupTemplate.tsx +++ b/backend/src/services/smtp/emails/PasswordSetupTemplate.tsx @@ -1,6 +1,8 @@ -import { Button, Heading, Link, Section, Text } from "@react-email/components"; +import { Button, Heading, Section, Text } from "@react-email/components"; import React from "react"; +import { BaseLink } from "@app/services/smtp/emails/BaseLink"; + import { BaseEmailWrapper, BaseEmailWrapperProps } from "./BaseEmailWrapper"; interface PasswordSetupTemplateProps extends Omit { @@ -16,19 +18,16 @@ export const PasswordSetupTemplate = ({ email, isCloud, siteUrl, callback_url, t Password Setup -
+
Someone requested to set up a password for your Infisical account. - + Make sure you are already logged in to Infisical in the current browser before clicking the link below. If you did not initiate this request, please contact{" "} {isCloud ? ( <> - us immediately at{" "} - - support@infisical.com - + us immediately at support@infisical.com ) : ( "your administrator immediately" @@ -36,7 +35,7 @@ export const PasswordSetupTemplate = ({ email, isCloud, siteUrl, callback_url, t .
-
+
+
+ Grant Access
); diff --git a/backend/src/services/smtp/emails/ProjectInvitationTemplate.tsx b/backend/src/services/smtp/emails/ProjectInvitationTemplate.tsx index 1745dc8e5..2105e967a 100644 --- a/backend/src/services/smtp/emails/ProjectInvitationTemplate.tsx +++ b/backend/src/services/smtp/emails/ProjectInvitationTemplate.tsx @@ -1,6 +1,7 @@ -import { Button, Heading, Section, Text } from "@react-email/components"; +import { Heading, Section, Text } from "@react-email/components"; import React from "react"; +import { BaseButton } from "./BaseButton"; import { BaseEmailWrapper, BaseEmailWrapperProps } from "./BaseEmailWrapper"; interface ProjectInvitationTemplateProps extends Omit { @@ -18,18 +19,13 @@ export const ProjectInvitationTemplate = ({ callback_url, workspaceName, siteUrl You've been invited to join a project on Infisical -
+
You've been invited to join the project {workspaceName}.
-
- +
+ Join Project
diff --git a/backend/src/services/smtp/emails/ScimUserProvisionedTemplate.tsx b/backend/src/services/smtp/emails/ScimUserProvisionedTemplate.tsx index bbd41818e..df60afba3 100644 --- a/backend/src/services/smtp/emails/ScimUserProvisionedTemplate.tsx +++ b/backend/src/services/smtp/emails/ScimUserProvisionedTemplate.tsx @@ -1,6 +1,7 @@ -import { Button, Heading, Section, Text } from "@react-email/components"; +import { Heading, Section, Text } from "@react-email/components"; import React from "react"; +import { BaseButton } from "./BaseButton"; import { BaseEmailWrapper, BaseEmailWrapperProps } from "./BaseEmailWrapper"; interface ScimUserProvisionedTemplateProps extends Omit { @@ -24,18 +25,13 @@ export const ScimUserProvisionedTemplate = ({
{organizationName} on Infisical -
+
You've been invited to collaborate on {organizationName}.
-
- +
+ Accept Invite
diff --git a/backend/src/services/smtp/emails/SecretApprovalRequestBypassedTemplate.tsx b/backend/src/services/smtp/emails/SecretApprovalRequestBypassedTemplate.tsx index a07110aa3..ebbbeda15 100644 --- a/backend/src/services/smtp/emails/SecretApprovalRequestBypassedTemplate.tsx +++ b/backend/src/services/smtp/emails/SecretApprovalRequestBypassedTemplate.tsx @@ -1,7 +1,9 @@ -import { Button, Heading, Link, Section, Text } from "@react-email/components"; +import { Heading, Section, Text } from "@react-email/components"; import React from "react"; +import { BaseButton } from "./BaseButton"; import { BaseEmailWrapper, BaseEmailWrapperProps } from "./BaseEmailWrapper"; +import { BaseLink } from "./BaseLink"; interface SecretApprovalRequestBypassedTemplateProps extends Omit { @@ -35,13 +37,10 @@ export const SecretApprovalRequestBypassedTemplate = ({ A secret approval request has been bypassed in the project {projectName} -
+
- {requesterFullName} ( - - {requesterEmail} - - ) has {requestType === "change" ? "merged" : "accessed"} a secret {requestType === "change" ? "to" : "in"}{" "} + {requesterFullName} ({requesterEmail}) + has {requestType === "change" ? "merged" : "accessed"} a secret {requestType === "change" ? "to" : "in"}{" "} {secretPath} in the {environment} environment without obtaining the required approval. @@ -50,13 +49,8 @@ export const SecretApprovalRequestBypassedTemplate = ({ {bypassReason}"
-
- +
+ Review Bypass
); diff --git a/backend/src/services/smtp/emails/SecretApprovalRequestNeedsReviewTemplate.tsx b/backend/src/services/smtp/emails/SecretApprovalRequestNeedsReviewTemplate.tsx index b4a7c306a..000614e00 100644 --- a/backend/src/services/smtp/emails/SecretApprovalRequestNeedsReviewTemplate.tsx +++ b/backend/src/services/smtp/emails/SecretApprovalRequestNeedsReviewTemplate.tsx @@ -1,6 +1,7 @@ -import { Button, Heading, Section, Text } from "@react-email/components"; +import { Heading, Section, Text } from "@react-email/components"; import React from "react"; +import { BaseButton } from "./BaseButton"; import { BaseEmailWrapper, BaseEmailWrapperProps } from "./BaseEmailWrapper"; interface SecretApprovalRequestNeedsReviewTemplateProps @@ -27,20 +28,15 @@ export const SecretApprovalRequestNeedsReviewTemplate = ({ A secret approval request for the project {projectName} requires review -
+
Hello {firstName}, You have a new secret change request pending your review for the project {projectName} in the organization {organizationName}.
-
- +
+ Review Changes
); diff --git a/backend/src/services/smtp/emails/SecretLeakIncidentTemplate.tsx b/backend/src/services/smtp/emails/SecretLeakIncidentTemplate.tsx index 631013756..fa60deb11 100644 --- a/backend/src/services/smtp/emails/SecretLeakIncidentTemplate.tsx +++ b/backend/src/services/smtp/emails/SecretLeakIncidentTemplate.tsx @@ -1,7 +1,9 @@ -import { Button, Heading, Link, Section, Text } from "@react-email/components"; +import { Heading, Section, Text } from "@react-email/components"; import React from "react"; +import { BaseButton } from "./BaseButton"; import { BaseEmailWrapper, BaseEmailWrapperProps } from "./BaseEmailWrapper"; +import { BaseLink } from "./BaseLink"; interface SecretLeakIncidentTemplateProps extends Omit { numberOfSecrets: number; @@ -24,7 +26,7 @@ export const SecretLeakIncidentTemplate = ({ Infisical has uncovered {numberOfSecrets} secret(s) from a recent commit -
+
You are receiving this notification because one or more leaked secrets have been detected in a recent commit {(pusher_email || pusher_name) && ( @@ -33,11 +35,7 @@ export const SecretLeakIncidentTemplate = ({ pushed by {pusher_name ?? "Unknown Pusher"}{" "} {pusher_email && ( <> - ( - - {pusher_email} - - ) + ({pusher_email}) )} @@ -49,24 +47,16 @@ export const SecretLeakIncidentTemplate = ({ a comment in the given programming language. This will prevent future notifications from being sent out for these secrets. - + If these are production secrets, please rotate them immediately. Once you have taken action, be sure to update the status of the risk in the{" "} - - Infisical Dashboard - - . + Infisical Dashboard.
-
- +
+ View Leaked Secrets
); diff --git a/backend/src/services/smtp/emails/SecretRequestCompletedTemplate.tsx b/backend/src/services/smtp/emails/SecretRequestCompletedTemplate.tsx index 4adeec636..37e758661 100644 --- a/backend/src/services/smtp/emails/SecretRequestCompletedTemplate.tsx +++ b/backend/src/services/smtp/emails/SecretRequestCompletedTemplate.tsx @@ -1,6 +1,7 @@ -import { Button, Heading, Section, Text } from "@react-email/components"; +import { Heading, Section, Text } from "@react-email/components"; import React from "react"; +import { BaseButton } from "./BaseButton"; import { BaseEmailWrapper, BaseEmailWrapperProps } from "./BaseEmailWrapper"; interface SecretRequestCompletedTemplateProps extends Omit { @@ -20,7 +21,7 @@ export const SecretRequestCompletedTemplate = ({ A secret has been shared with you -
+
{respondentUsername ? {respondentUsername} : "Someone"} shared a secret{" "} {name && ( @@ -31,13 +32,8 @@ export const SecretRequestCompletedTemplate = ({ with you.
-
- +
+ View Secret
); diff --git a/backend/src/services/smtp/emails/SecretRotationFailedTemplate.tsx b/backend/src/services/smtp/emails/SecretRotationFailedTemplate.tsx index 52e58986d..04cc95c56 100644 --- a/backend/src/services/smtp/emails/SecretRotationFailedTemplate.tsx +++ b/backend/src/services/smtp/emails/SecretRotationFailedTemplate.tsx @@ -1,6 +1,7 @@ -import { Button, Heading, Section, Text } from "@react-email/components"; +import { Heading, Section, Text } from "@react-email/components"; import React from "react"; +import { BaseButton } from "./BaseButton"; import { BaseEmailWrapper, BaseEmailWrapperProps } from "./BaseEmailWrapper"; interface SecretRotationFailedTemplateProps extends Omit { @@ -28,7 +29,7 @@ export const SecretRotationFailedTemplate = ({ Your {rotationType} rotation {rotationName} failed to rotate -
+
Name {rotationName} Type @@ -40,15 +41,12 @@ export const SecretRotationFailedTemplate = ({ Secret Path {secretPath} Reason: - {content} + {content}
-
- +
); diff --git a/backend/src/services/smtp/emails/SecretScanningScanFailedTemplate.tsx b/backend/src/services/smtp/emails/SecretScanningScanFailedTemplate.tsx index 2e212cb82..5b56b574a 100644 --- a/backend/src/services/smtp/emails/SecretScanningScanFailedTemplate.tsx +++ b/backend/src/services/smtp/emails/SecretScanningScanFailedTemplate.tsx @@ -1,6 +1,7 @@ -import { Button, Heading, Section, Text } from "@react-email/components"; +import { Heading, Section, Text } from "@react-email/components"; import React from "react"; +import { BaseButton } from "./BaseButton"; import { BaseEmailWrapper, BaseEmailWrapperProps } from "./BaseEmailWrapper"; interface SecretScanningScanFailedTemplateProps extends Omit { @@ -30,7 +31,7 @@ export const SecretScanningScanFailedTemplate = ({ Infisical encountered an error while attempting to scan the resource {resourceName} -
+
Resource {resourceName} Data Source @@ -40,15 +41,10 @@ export const SecretScanningScanFailedTemplate = ({ Timestamp {timestamp} Error - {errorMessage} + {errorMessage}
-
- +
+ View in Infisical
); diff --git a/backend/src/services/smtp/emails/SecretScanningSecretsDetectedTemplate.tsx b/backend/src/services/smtp/emails/SecretScanningSecretsDetectedTemplate.tsx index b7c0d8a14..6bd4e8aa7 100644 --- a/backend/src/services/smtp/emails/SecretScanningSecretsDetectedTemplate.tsx +++ b/backend/src/services/smtp/emails/SecretScanningSecretsDetectedTemplate.tsx @@ -1,7 +1,9 @@ -import { Button, Heading, Link, Section, Text } from "@react-email/components"; +import { Heading, Section, Text } from "@react-email/components"; import React from "react"; +import { BaseButton } from "./BaseButton"; import { BaseEmailWrapper, BaseEmailWrapperProps } from "./BaseEmailWrapper"; +import { BaseLink } from "./BaseLink"; interface SecretScanningSecretsDetectedTemplateProps extends Omit { @@ -32,7 +34,7 @@ export const SecretScanningSecretsDetectedTemplate = ({ Infisical has uncovered {numberOfSecrets} secret(s) {isDiffScan ? " from a recent commit to" : " in"} {resourceName} -
+
You are receiving this notification because one or more leaked secrets have been detected {isDiffScan && " in a recent commit"} @@ -43,11 +45,7 @@ export const SecretScanningSecretsDetectedTemplate = ({ pushed by {authorName ?? "Unknown Pusher"}{" "} {authorEmail && ( <> - ( - - {authorEmail} - - ) + ({authorEmail}) )} @@ -65,24 +63,16 @@ export const SecretScanningSecretsDetectedTemplate = ({ a comment in the given programming language. This will prevent future notifications from being sent out for these secrets. - + If these are production secrets, please rotate them immediately. Once you have taken action, be sure to update the finding status in the{" "} - - Infisical Dashboard - - . + Infisical Dashboard.
-
- +
+ View Leaked Secrets
); diff --git a/backend/src/services/smtp/emails/SecretSyncFailedTemplate.tsx b/backend/src/services/smtp/emails/SecretSyncFailedTemplate.tsx index 01d909219..4abbf8f99 100644 --- a/backend/src/services/smtp/emails/SecretSyncFailedTemplate.tsx +++ b/backend/src/services/smtp/emails/SecretSyncFailedTemplate.tsx @@ -1,6 +1,7 @@ -import { Button, Heading, Section, Text } from "@react-email/components"; +import { Heading, Section, Text } from "@react-email/components"; import React from "react"; +import { BaseButton } from "./BaseButton"; import { BaseEmailWrapper, BaseEmailWrapperProps } from "./BaseEmailWrapper"; interface SecretSyncFailedTemplateProps extends Omit { @@ -28,7 +29,7 @@ export const SecretSyncFailedTemplate = ({ Your {syncDestination} sync {syncName} failed to complete -
+
Name {syncName} Destination @@ -50,17 +51,12 @@ export const SecretSyncFailedTemplate = ({ {failureMessage && ( <> Reason: - {failureMessage} + {failureMessage} )}
-
- +
+ View in Infisical
); diff --git a/backend/src/services/smtp/emails/ServiceTokenExpiryNoticeTemplate.tsx b/backend/src/services/smtp/emails/ServiceTokenExpiryNoticeTemplate.tsx index 8f8deb63a..a4c171f86 100644 --- a/backend/src/services/smtp/emails/ServiceTokenExpiryNoticeTemplate.tsx +++ b/backend/src/services/smtp/emails/ServiceTokenExpiryNoticeTemplate.tsx @@ -1,6 +1,7 @@ -import { Button, Heading, Section, Text } from "@react-email/components"; +import { Heading, Section, Text } from "@react-email/components"; import React from "react"; +import { BaseButton } from "./BaseButton"; import { BaseEmailWrapper, BaseEmailWrapperProps } from "./BaseEmailWrapper"; interface ServiceTokenExpiryNoticeTemplateProps extends Omit { @@ -24,20 +25,15 @@ export const ServiceTokenExpiryNoticeTemplate = ({ Service token expiry notice -
+
Your service token {tokenName} for the project {projectName} will expire within 24 hours. If this token is still needed for your workflow, please create a new one before it expires.
-
- +
+ Create New Token
); diff --git a/backend/src/services/smtp/emails/SignupEmailVerificationTemplate.tsx b/backend/src/services/smtp/emails/SignupEmailVerificationTemplate.tsx index 0a3da75f9..4c0ab133f 100644 --- a/backend/src/services/smtp/emails/SignupEmailVerificationTemplate.tsx +++ b/backend/src/services/smtp/emails/SignupEmailVerificationTemplate.tsx @@ -1,7 +1,8 @@ -import { Heading, Link, Section, Text } from "@react-email/components"; +import { Heading, Section, Text } from "@react-email/components"; import React from "react"; import { BaseEmailWrapper, BaseEmailWrapperProps } from "./BaseEmailWrapper"; +import { BaseLink } from "./BaseLink"; interface SignupEmailVerificationTemplateProps extends Omit { code: string; @@ -29,10 +30,7 @@ export const SignupEmailVerificationTemplate = ({ code, siteUrl, isCloud }: Sign Questions about setting up Infisical?{" "} {isCloud ? ( <> - Email us at{" "} - - support@infisical.com - + Email us at support@infisical.com ) : ( "Contact your administrator" diff --git a/backend/src/services/smtp/emails/UnlockAccountTemplate.tsx b/backend/src/services/smtp/emails/UnlockAccountTemplate.tsx index b0b4f2086..7ec1be3f4 100644 --- a/backend/src/services/smtp/emails/UnlockAccountTemplate.tsx +++ b/backend/src/services/smtp/emails/UnlockAccountTemplate.tsx @@ -1,6 +1,7 @@ -import { Button, Heading, Section, Text } from "@react-email/components"; +import { Heading, Section, Text } from "@react-email/components"; import React from "react"; +import { BaseButton } from "./BaseButton"; import { BaseEmailWrapper, BaseEmailWrapperProps } from "./BaseEmailWrapper"; interface UnlockAccountTemplateProps extends Omit { @@ -18,19 +19,14 @@ export const UnlockAccountTemplate = ({ token, siteUrl, callback_url }: UnlockAc Unlock your Infisical account -
+
Your account has been temporarily locked due to multiple failed login attempts. If these attempts were not made by you, reset your password immediately.
-
- +
+ Unlock Account
); diff --git a/backend/src/services/super-admin/super-admin-service.ts b/backend/src/services/super-admin/super-admin-service.ts index ff47718f1..93ac5cadb 100644 --- a/backend/src/services/super-admin/super-admin-service.ts +++ b/backend/src/services/super-admin/super-admin-service.ts @@ -704,10 +704,39 @@ export const superAdminServiceFactory = ({ }; const deleteUser = async (userId: string) => { + const superAdmins = await userDAL.find({ + superAdmin: true + }); + + if (superAdmins.length === 1 && superAdmins[0].id === userId) { + throw new BadRequestError({ + message: "Cannot delete the only server admin on this instance. Add another server admin to delete this user." + }); + } + const user = await userDAL.deleteById(userId); return user; }; + const deleteUsers = async (userIds: string[]) => { + const superAdmins = await userDAL.find({ + superAdmin: true + }); + + if (superAdmins.every((superAdmin) => userIds.includes(superAdmin.id))) { + throw new BadRequestError({ + message: "Instance must have at least one server admin. Add another server admin to delete these users." + }); + } + + const users = await userDAL.delete({ + $in: { + id: userIds + } + }); + return users; + }; + const deleteIdentitySuperAdminAccess = async (identityId: string, actorId: string) => { const identity = await identityDAL.findById(identityId); if (!identity) { @@ -730,6 +759,17 @@ export const superAdminServiceFactory = ({ throw new NotFoundError({ name: "User", message: "User not found" }); } + const superAdmins = await userDAL.find({ + superAdmin: true + }); + + if (superAdmins.length === 1 && superAdmins[0].id === userId) { + throw new BadRequestError({ + message: + "Cannot remove the only server admin on this instance. Add another server admin to remove status for this user." + }); + } + const updatedUser = userDAL.updateById(userId, { superAdmin: false }); return updatedUser; @@ -913,6 +953,7 @@ export const superAdminServiceFactory = ({ initializeAdminIntegrationConfigSync, initializeEnvConfigSync, getEnvOverrides, - getEnvOverridesOrganized + getEnvOverridesOrganized, + deleteUsers }; }; diff --git a/backend/src/services/user/user-service.ts b/backend/src/services/user/user-service.ts index b2258447e..30bf750c3 100644 --- a/backend/src/services/user/user-service.ts +++ b/backend/src/services/user/user-service.ts @@ -1,9 +1,7 @@ import { ForbiddenError } from "@casl/ability"; -import { SecretKeyEncoding } from "@app/db/schemas"; import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types"; -import { crypto } from "@app/lib/crypto/cryptography"; import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors"; import { logger } from "@app/lib/logger"; import { TAuthTokenServiceFactory } from "@app/services/auth-token/auth-token-service"; @@ -175,7 +173,11 @@ export const userServiceFactory = ({ const getMe = async (userId: string) => { const user = await userDAL.findUserEncKeyByUserId(userId); if (!user) throw new NotFoundError({ message: `User with ID '${userId}' not found`, name: "GetMe" }); - return user; + + return { + ...user, + encryptionVersion: user.encryptionVersion! + }; }; const deleteUser = async (userId: string) => { @@ -212,25 +214,6 @@ export const userServiceFactory = ({ ); }; - const getUserPrivateKey = async (userId: string) => { - const user = await userDAL.findUserEncKeyByUserId(userId); - if (!user?.serverEncryptedPrivateKey || !user.serverEncryptedPrivateKeyIV || !user.serverEncryptedPrivateKeyTag) { - throw new NotFoundError({ message: `Private key for user with ID '${userId}' not found` }); - } - - const privateKey = crypto - .encryption() - .symmetric() - .decryptWithRootEncryptionKey({ - ciphertext: user.serverEncryptedPrivateKey, - tag: user.serverEncryptedPrivateKeyTag, - iv: user.serverEncryptedPrivateKeyIV, - keyEncoding: user.serverEncryptedPrivateKeyEncoding as SecretKeyEncoding - }); - - return privateKey; - }; - const getUserProjectFavorites = async (userId: string, orgId: string) => { const orgMembership = await orgMembershipDAL.findOne({ userId, @@ -311,7 +294,6 @@ export const userServiceFactory = ({ listUserGroups, getUserAction, unlockUser, - getUserPrivateKey, getAllMyAccounts, getUserProjectFavorites, removeMyDuplicateAccounts, diff --git a/docs/docs.json b/docs/docs.json index 5ca2cbea2..960c41727 100644 --- a/docs/docs.json +++ b/docs/docs.json @@ -14,94 +14,51 @@ "navigation": { "tabs": [ { - "tab": "Documentation", + "tab": "Platform", "groups": [ { "group": "Getting Started", "pages": [ + "documentation/getting-started/overview", "documentation/getting-started/introduction", { - "group": "Quickstart", - "pages": ["documentation/guides/local-development"] - }, - { - "group": "Guides", + "group": "Concepts", "pages": [ - "documentation/guides/introduction", - "documentation/guides/node", - "documentation/guides/python", - "documentation/guides/nextjs-vercel", - "documentation/guides/microsoft-power-apps", - "documentation/guides/organization-structure" + "documentation/getting-started/concepts/deployment-models", + "documentation/getting-started/concepts/platform-hierarchy", + "documentation/getting-started/concepts/platform-iam", + "documentation/getting-started/concepts/client-integrations", + "documentation/getting-started/concepts/audit-logs" ] }, { - "group": "Setup", - "pages": ["documentation/setup/networking"] + "group": "Guides", + "pages": ["documentation/guides/organization-structure"] } ] }, { - "group": "Platform", + "group": "Platform Reference", "pages": [ "documentation/platform/organization", - "documentation/platform/project", "documentation/platform/event-subscriptions", "documentation/platform/folder", { - "group": "Secrets", + "group": "Projects", "pages": [ - "documentation/platform/secret-versioning", - "documentation/platform/pit-recovery", - "documentation/platform/secret-reference", - "documentation/platform/webhooks" - ] - }, - { - "group": "Internal PKI", - "pages": [ - "documentation/platform/pki/overview", - "documentation/platform/pki/private-ca", - "documentation/platform/pki/external-ca", - "documentation/platform/pki/subscribers", - "documentation/platform/pki/certificates", - "documentation/platform/pki/acme-ca", - "documentation/platform/pki/est", - "documentation/platform/pki/alerting", + "documentation/platform/project", + "documentation/platform/project-templates", { - "group": "Integrations", + "group": "KMS Configuration", "pages": [ - "documentation/platform/pki/pki-issuer", - "documentation/platform/pki/integration-guides/gloo-mesh" + "documentation/platform/kms-configuration/overview", + "documentation/platform/kms-configuration/aws-kms", + "documentation/platform/kms-configuration/aws-hsm", + "documentation/platform/kms-configuration/gcp-kms" ] } ] }, - { - "group": "Infisical SSH", - "pages": [ - "documentation/platform/ssh/overview", - "documentation/platform/ssh/host-groups" - ] - }, - { - "group": "Key Management (KMS)", - "pages": [ - "documentation/platform/kms/overview", - "documentation/platform/kms/hsm-integration", - "documentation/platform/kms/kubernetes-encryption", - "documentation/platform/kms/kmip" - ] - }, - { - "group": "KMS Configuration", - "pages": [ - "documentation/platform/kms-configuration/overview", - "documentation/platform/kms-configuration/aws-kms", - "documentation/platform/kms-configuration/aws-hsm", - "documentation/platform/kms-configuration/gcp-kms" - ] - }, { "group": "Identities", "pages": [ @@ -141,57 +98,53 @@ ] }, { - "group": "Secret Rotation", + "group": "App Connections", "pages": [ - "documentation/platform/secret-rotation/overview", - "documentation/platform/secret-rotation/auth0-client-secret", - "documentation/platform/secret-rotation/aws-iam-user-secret", - "documentation/platform/secret-rotation/azure-client-secret", - "documentation/platform/secret-rotation/ldap-password", - "documentation/platform/secret-rotation/mssql-credentials", - "documentation/platform/secret-rotation/mysql-credentials", - "documentation/platform/secret-rotation/okta-client-secret", - "documentation/platform/secret-rotation/oracledb-credentials", - "documentation/platform/secret-rotation/postgres-credentials" + "integrations/app-connections/overview", + { + "group": "Connections", + "pages": [ + "integrations/app-connections/1password", + "integrations/app-connections/auth0", + "integrations/app-connections/aws", + "integrations/app-connections/azure-app-configuration", + "integrations/app-connections/azure-client-secrets", + "integrations/app-connections/azure-devops", + "integrations/app-connections/azure-key-vault", + "integrations/app-connections/bitbucket", + "integrations/app-connections/camunda", + "integrations/app-connections/checkly", + "integrations/app-connections/cloudflare", + "integrations/app-connections/databricks", + "integrations/app-connections/digital-ocean", + "integrations/app-connections/flyio", + "integrations/app-connections/gcp", + "integrations/app-connections/github", + "integrations/app-connections/github-radar", + "integrations/app-connections/gitlab", + "integrations/app-connections/hashicorp-vault", + "integrations/app-connections/heroku", + "integrations/app-connections/humanitec", + "integrations/app-connections/ldap", + "integrations/app-connections/mssql", + "integrations/app-connections/mysql", + "integrations/app-connections/netlify", + "integrations/app-connections/oci", + "integrations/app-connections/okta", + "integrations/app-connections/oracledb", + "integrations/app-connections/postgres", + "integrations/app-connections/railway", + "integrations/app-connections/render", + "integrations/app-connections/supabase", + "integrations/app-connections/teamcity", + "integrations/app-connections/terraform-cloud", + "integrations/app-connections/vercel", + "integrations/app-connections/windmill", + "integrations/app-connections/zabbix" + ] + } ] }, - { - "group": "Dynamic Secrets", - "pages": [ - "documentation/platform/dynamic-secrets/overview", - "documentation/platform/dynamic-secrets/aws-elasticache", - "documentation/platform/dynamic-secrets/aws-iam", - "documentation/platform/dynamic-secrets/azure-entra-id", - "documentation/platform/dynamic-secrets/cassandra", - "documentation/platform/dynamic-secrets/elastic-search", - "documentation/platform/dynamic-secrets/gcp-iam", - "documentation/platform/dynamic-secrets/github", - "documentation/platform/dynamic-secrets/ldap", - "documentation/platform/dynamic-secrets/mongo-atlas", - "documentation/platform/dynamic-secrets/mongo-db", - "documentation/platform/dynamic-secrets/mssql", - "documentation/platform/dynamic-secrets/mysql", - "documentation/platform/dynamic-secrets/oracle", - "documentation/platform/dynamic-secrets/postgresql", - "documentation/platform/dynamic-secrets/rabbit-mq", - "documentation/platform/dynamic-secrets/redis", - "documentation/platform/dynamic-secrets/sap-ase", - "documentation/platform/dynamic-secrets/sap-hana", - "documentation/platform/dynamic-secrets/snowflake", - "documentation/platform/dynamic-secrets/totp", - "documentation/platform/dynamic-secrets/kubernetes", - "documentation/platform/dynamic-secrets/vertica" - ] - }, - { - "group": "Gateway", - "pages": [ - "documentation/platform/gateways/overview", - "documentation/platform/gateways/gateway-security", - "documentation/platform/gateways/networking" - ] - }, - "documentation/platform/project-templates", { "group": "Workflow Integrations", "pages": [ @@ -207,22 +160,20 @@ "documentation/platform/external-migrations/vault" ] }, + "documentation/platform/admin-panel/server-admin", + "documentation/platform/secret-sharing" + ] + }, + { + "group": "Connectivity", + "pages": [ + "documentation/setup/networking", { - "group": "Admin Consoles", + "group": "Gateway", "pages": [ - "documentation/platform/admin-panel/overview", - "documentation/platform/admin-panel/server-admin", - "documentation/platform/admin-panel/org-admin-console" - ] - }, - "documentation/platform/secret-sharing", - { - "group": "Secret Scanning", - "pages": [ - "documentation/platform/secret-scanning/overview", - "documentation/platform/secret-scanning/bitbucket", - "documentation/platform/secret-scanning/github", - "documentation/platform/secret-scanning/gitlab" + "documentation/platform/gateways/overview", + "documentation/platform/gateways/gateway-security", + "documentation/platform/gateways/networking" ] } ] @@ -323,6 +274,7 @@ } ] }, + "documentation/platform/identities/auth-templates", "documentation/platform/token", "documentation/platform/mfa", "documentation/platform/github-org-sync" @@ -427,18 +379,80 @@ ] }, { - "tab": "Integrations", - "groups": [ + "tab": "Products", + "menu": [ { - "group": "Infrastructure Integrations", - "pages": [ - "integrations/platforms/ansible", - "integrations/platforms/apache-airflow", + "item": "Secrets Management", + "groups": [ { - "group": "Container orchestrators", + "group": "Secrets Management", "pages": [ + "documentation/platform/secrets-mgmt/overview", + "documentation/platform/secrets-mgmt/project", + "documentation/platform/folder", { - "group": "Kubernetes", + "group": "Secret Rotation", + "pages": [ + "documentation/platform/secret-rotation/overview", + "documentation/platform/secret-rotation/auth0-client-secret", + "documentation/platform/secret-rotation/aws-iam-user-secret", + "documentation/platform/secret-rotation/azure-client-secret", + "documentation/platform/secret-rotation/ldap-password", + "documentation/platform/secret-rotation/mssql-credentials", + "documentation/platform/secret-rotation/mysql-credentials", + "documentation/platform/secret-rotation/okta-client-secret", + "documentation/platform/secret-rotation/oracledb-credentials", + "documentation/platform/secret-rotation/postgres-credentials" + ] + }, + { + "group": "Dynamic Secrets", + "pages": [ + "documentation/platform/dynamic-secrets/overview", + "documentation/platform/dynamic-secrets/aws-elasticache", + "documentation/platform/dynamic-secrets/aws-iam", + "documentation/platform/dynamic-secrets/azure-entra-id", + "documentation/platform/dynamic-secrets/cassandra", + "documentation/platform/dynamic-secrets/elastic-search", + "documentation/platform/dynamic-secrets/gcp-iam", + "documentation/platform/dynamic-secrets/github", + "documentation/platform/dynamic-secrets/ldap", + "documentation/platform/dynamic-secrets/mongo-atlas", + "documentation/platform/dynamic-secrets/mongo-db", + "documentation/platform/dynamic-secrets/mssql", + "documentation/platform/dynamic-secrets/mysql", + "documentation/platform/dynamic-secrets/oracle", + "documentation/platform/dynamic-secrets/postgresql", + "documentation/platform/dynamic-secrets/rabbit-mq", + "documentation/platform/dynamic-secrets/redis", + "documentation/platform/dynamic-secrets/sap-ase", + "documentation/platform/dynamic-secrets/sap-hana", + "documentation/platform/dynamic-secrets/snowflake", + "documentation/platform/dynamic-secrets/totp", + "documentation/platform/dynamic-secrets/kubernetes", + "documentation/platform/dynamic-secrets/vertica" + ] + }, + { + "group": "Guides", + "pages": [ + "documentation/guides/introduction", + "documentation/guides/local-development", + "documentation/guides/node", + "documentation/guides/python", + "documentation/guides/nextjs-vercel", + "documentation/guides/microsoft-power-apps" + ] + } + ] + }, + { + "group": "Infrastructure Integrations", + "pages": [ + "integrations/platforms/ansible", + "integrations/platforms/apache-airflow", + { + "group": "Kubernetes Operator", "pages": [ "integrations/platforms/kubernetes/overview", "integrations/platforms/kubernetes/infisical-secret-crd", @@ -448,222 +462,249 @@ }, "integrations/platforms/kubernetes-injector", "integrations/platforms/kubernetes-csi", - "integrations/platforms/docker-swarm-with-agent", - "integrations/platforms/ecs-with-agent" + { + "group": "Agent", + "pages": [ + "integrations/platforms/infisical-agent", + "integrations/platforms/docker-swarm-with-agent", + "integrations/platforms/ecs-with-agent" + ] + }, + { + "group": "Docker", + "pages": [ + "integrations/platforms/docker-intro", + "integrations/platforms/docker", + "integrations/platforms/docker-pass-envs", + "integrations/platforms/docker-compose" + ] + }, + "integrations/frameworks/packer", + "integrations/frameworks/pulumi", + "integrations/frameworks/terraform" ] }, { - "group": "Docker", + "group": "Secret Syncs", "pages": [ - "integrations/platforms/docker-intro", - "integrations/platforms/docker", - "integrations/platforms/docker-pass-envs", - "integrations/platforms/docker-compose" + "integrations/secret-syncs/overview", + { + "group": "Syncs", + "pages": [ + "integrations/secret-syncs/1password", + "integrations/secret-syncs/aws-parameter-store", + "integrations/secret-syncs/aws-secrets-manager", + "integrations/secret-syncs/azure-app-configuration", + "integrations/secret-syncs/azure-devops", + "integrations/secret-syncs/azure-key-vault", + "integrations/secret-syncs/bitbucket", + "integrations/secret-syncs/camunda", + "integrations/secret-syncs/checkly", + "integrations/secret-syncs/cloudflare-pages", + "integrations/secret-syncs/cloudflare-workers", + "integrations/secret-syncs/databricks", + "integrations/secret-syncs/digital-ocean-app-platform", + "integrations/secret-syncs/flyio", + "integrations/secret-syncs/gcp-secret-manager", + "integrations/secret-syncs/github", + "integrations/secret-syncs/gitlab", + "integrations/secret-syncs/hashicorp-vault", + "integrations/secret-syncs/heroku", + "integrations/secret-syncs/humanitec", + "integrations/secret-syncs/netlify", + "integrations/secret-syncs/oci-vault", + "integrations/secret-syncs/railway", + "integrations/secret-syncs/render", + "integrations/secret-syncs/supabase", + "integrations/secret-syncs/teamcity", + "integrations/secret-syncs/terraform-cloud", + "integrations/secret-syncs/vercel", + "integrations/secret-syncs/windmill", + "integrations/secret-syncs/zabbix" + ] + } ] }, - "integrations/platforms/infisical-agent", - "integrations/frameworks/packer", - "integrations/frameworks/pulumi", - "integrations/frameworks/terraform" - ] - }, - { - "group": "App Connections", - "pages": [ - "integrations/app-connections/overview", { - "group": "Connections", + "group": "Native Integrations", "pages": [ - "integrations/app-connections/1password", - "integrations/app-connections/auth0", - "integrations/app-connections/aws", - "integrations/app-connections/azure-app-configuration", - "integrations/app-connections/azure-client-secrets", - "integrations/app-connections/azure-devops", - "integrations/app-connections/azure-key-vault", - "integrations/app-connections/bitbucket", - "integrations/app-connections/camunda", - "integrations/app-connections/checkly", - "integrations/app-connections/cloudflare", - "integrations/app-connections/databricks", - "integrations/app-connections/digital-ocean", - "integrations/app-connections/flyio", - "integrations/app-connections/gcp", - "integrations/app-connections/github", - "integrations/app-connections/github-radar", - "integrations/app-connections/gitlab", - "integrations/app-connections/hashicorp-vault", - "integrations/app-connections/heroku", - "integrations/app-connections/humanitec", - "integrations/app-connections/ldap", - "integrations/app-connections/mssql", - "integrations/app-connections/mysql", - "integrations/app-connections/netlify", - "integrations/app-connections/oci", - "integrations/app-connections/okta", - "integrations/app-connections/oracledb", - "integrations/app-connections/postgres", - "integrations/app-connections/railway", - "integrations/app-connections/render", - "integrations/app-connections/supabase", - "integrations/app-connections/teamcity", - "integrations/app-connections/terraform-cloud", - "integrations/app-connections/vercel", - "integrations/app-connections/windmill", - "integrations/app-connections/zabbix" - ] - } - ] - }, - { - "group": "Secret Syncs", - "pages": [ - "integrations/secret-syncs/overview", - { - "group": "Syncs", - "pages": [ - "integrations/secret-syncs/1password", - "integrations/secret-syncs/aws-parameter-store", - "integrations/secret-syncs/aws-secrets-manager", - "integrations/secret-syncs/azure-app-configuration", - "integrations/secret-syncs/azure-devops", - "integrations/secret-syncs/azure-key-vault", - "integrations/secret-syncs/bitbucket", - "integrations/secret-syncs/camunda", - "integrations/secret-syncs/checkly", - "integrations/secret-syncs/cloudflare-pages", - "integrations/secret-syncs/cloudflare-workers", - "integrations/secret-syncs/databricks", - "integrations/secret-syncs/digital-ocean-app-platform", - "integrations/secret-syncs/flyio", - "integrations/secret-syncs/gcp-secret-manager", - "integrations/secret-syncs/github", - "integrations/secret-syncs/gitlab", - "integrations/secret-syncs/hashicorp-vault", - "integrations/secret-syncs/heroku", - "integrations/secret-syncs/humanitec", - "integrations/secret-syncs/netlify", - "integrations/secret-syncs/oci-vault", - "integrations/secret-syncs/railway", - "integrations/secret-syncs/render", - "integrations/secret-syncs/supabase", - "integrations/secret-syncs/teamcity", - "integrations/secret-syncs/terraform-cloud", - "integrations/secret-syncs/vercel", - "integrations/secret-syncs/windmill", - "integrations/secret-syncs/zabbix" - ] - } - ] - }, - { - "group": "Native Integrations", - "pages": [ - { - "group": "AWS", - "pages": [ - "integrations/cloud/aws-parameter-store", - "integrations/cloud/aws-secret-manager", - "integrations/cloud/aws-amplify" + { + "group": "AWS", + "pages": [ + "integrations/cloud/aws-parameter-store", + "integrations/cloud/aws-secret-manager", + "integrations/cloud/aws-amplify" + ] + }, + "integrations/cloud/vercel", + "integrations/cloud/azure-key-vault", + "integrations/cloud/azure-app-configuration", + "integrations/cloud/azure-devops", + "integrations/cloud/gcp-secret-manager", + { + "group": "Cloudflare", + "pages": [ + "integrations/cloud/cloudflare-pages", + "integrations/cloud/cloudflare-workers" + ] + }, + "integrations/cloud/terraform-cloud", + "integrations/cloud/databricks", + { + "group": "View more", + "pages": [ + "integrations/cloud/digital-ocean-app-platform", + "integrations/cloud/heroku", + "integrations/cloud/netlify", + "integrations/cloud/flyio", + "integrations/cloud/railway", + "integrations/cloud/render", + "integrations/cloud/laravel-forge", + "integrations/cloud/supabase", + "integrations/cloud/northflank", + "integrations/cloud/hasura-cloud", + "integrations/cloud/qovery", + "integrations/cloud/hashicorp-vault", + "integrations/cloud/cloud-66", + "integrations/cloud/windmill" + ] + } ] }, - "integrations/cloud/vercel", - "integrations/cloud/azure-key-vault", - "integrations/cloud/azure-app-configuration", - "integrations/cloud/azure-devops", - "integrations/cloud/gcp-secret-manager", { - "group": "Cloudflare", + "group": "CI/CD Integrations", "pages": [ - "integrations/cloud/cloudflare-pages", - "integrations/cloud/cloudflare-workers" + "integrations/cicd/jenkins", + "integrations/cicd/githubactions", + "integrations/cicd/gitlab", + "integrations/cicd/bitbucket", + "integrations/cloud/teamcity", + { + "group": "View more", + "pages": [ + "integrations/cicd/circleci", + "integrations/cicd/travisci", + "integrations/cicd/rundeck", + "integrations/cicd/codefresh", + "integrations/cloud/checkly", + "integrations/cicd/octopus-deploy" + ] + } ] }, - "integrations/cloud/terraform-cloud", - "integrations/cloud/databricks", { - "group": "View more", + "group": "Framework Integrations", "pages": [ - "integrations/cloud/digital-ocean-app-platform", - "integrations/cloud/heroku", - "integrations/cloud/netlify", - "integrations/cloud/flyio", - "integrations/cloud/railway", - "integrations/cloud/render", - "integrations/cloud/laravel-forge", - "integrations/cloud/supabase", - "integrations/cloud/northflank", - "integrations/cloud/hasura-cloud", - "integrations/cloud/qovery", - "integrations/cloud/hashicorp-vault", - "integrations/cloud/cloud-66", - "integrations/cloud/windmill" + "integrations/frameworks/spring-boot-maven", + "integrations/frameworks/react", + "integrations/frameworks/vue", + "integrations/frameworks/express", + { + "group": "View more", + "pages": [ + "integrations/frameworks/nextjs", + "integrations/frameworks/nestjs", + "integrations/frameworks/sveltekit", + "integrations/frameworks/nuxt", + "integrations/frameworks/gatsby", + "integrations/frameworks/remix", + "integrations/frameworks/vite", + "integrations/frameworks/fiber", + "integrations/frameworks/django", + "integrations/frameworks/flask", + "integrations/frameworks/laravel", + "integrations/frameworks/rails", + "integrations/frameworks/dotnet", + "integrations/platforms/pm2", + "integrations/frameworks/ab-initio" + ] + } + ] + }, + { + "group": "Build Tool Integrations", + "pages": ["integrations/build-tools/gradle"] + }, + { + "group": "Others", + "pages": ["integrations/external/backstage"] + } + ] + }, + { + "item": "Secrets Scanning", + "groups": [ + { + "group": "Secret Scanning", + "pages": [ + "documentation/platform/secret-scanning/overview" + ] + }, + { + "group": "Datasources", + "pages": [ + "documentation/platform/secret-scanning/bitbucket", + "documentation/platform/secret-scanning/github", + "documentation/platform/secret-scanning/gitlab" ] } ] }, { - "group": "CI/CD Integrations", - "pages": [ - "integrations/cicd/jenkins", - "integrations/cicd/githubactions", - "integrations/cicd/gitlab", - "integrations/cicd/bitbucket", - "integrations/cloud/teamcity", + "item": "Infisical PKI", + "groups": [ { - "group": "View more", + "group": "Infisical PKI", "pages": [ - "integrations/cicd/circleci", - "integrations/cicd/travisci", - "integrations/cicd/rundeck", - "integrations/cicd/codefresh", - "integrations/cloud/checkly", - "integrations/cicd/octopus-deploy" + "documentation/platform/pki/overview", + "documentation/platform/pki/private-ca", + "documentation/platform/pki/external-ca", + "documentation/platform/pki/subscribers", + "documentation/platform/pki/certificates", + "documentation/platform/pki/acme-ca", + "documentation/platform/pki/est", + "documentation/platform/pki/alerting", + { + "group": "Integrations", + "pages": [ + "documentation/platform/pki/pki-issuer", + "documentation/platform/pki/integration-guides/gloo-mesh" + ] + } ] } ] }, { - "group": "Framework Integrations", - "pages": [ - "integrations/frameworks/spring-boot-maven", - "integrations/frameworks/react", - "integrations/frameworks/vue", - "integrations/frameworks/express", + "item": "Infisical SSH", + "groups": [ { - "group": "View more", + "group": "Infisical SSH", "pages": [ - "integrations/frameworks/nextjs", - "integrations/frameworks/nestjs", - "integrations/frameworks/sveltekit", - "integrations/frameworks/nuxt", - "integrations/frameworks/gatsby", - "integrations/frameworks/remix", - "integrations/frameworks/vite", - "integrations/frameworks/fiber", - "integrations/frameworks/django", - "integrations/frameworks/flask", - "integrations/frameworks/laravel", - "integrations/frameworks/rails", - "integrations/frameworks/dotnet", - "integrations/platforms/pm2", - "integrations/frameworks/ab-initio" + "documentation/platform/ssh/overview", + "documentation/platform/ssh/host-groups" ] } ] }, { - "group": "Build Tool Integrations", - "pages": ["integrations/build-tools/gradle"] - }, - { - "group": "Others", - "pages": ["integrations/external/backstage"] + "item": "Infisical KMS", + "groups": [ + { + "group": "Infisical KMS", + "pages": [ + "documentation/platform/kms/overview", + "documentation/platform/kms/hsm-integration", + "documentation/platform/kms/kubernetes-encryption", + "documentation/platform/kms/kmip" + ] + } + ] } ] }, { - "tab": "CLI", + "tab": "CLI Reference", "groups": [ { "group": "Command line", diff --git a/docs/documentation/getting-started/concepts/audit-logs.mdx b/docs/documentation/getting-started/concepts/audit-logs.mdx new file mode 100644 index 000000000..e67f06f5a --- /dev/null +++ b/docs/documentation/getting-started/concepts/audit-logs.mdx @@ -0,0 +1,40 @@ +--- +title: "Audit Logs" +sidebarTitle: "Audit Logs" +description: "Understand how Infisical logs activity and supports external audit streaming." +--- + +Infisical records a detailed audit trail of actions across the platform — providing deep visibility into access, changes, and usage for security and compliance purposes. + +Every interaction with Infisical resources generates an audit event. These events are immutable and include metadata such as the actor, event type, affected resources, timestamp, IP address, and client source. + +Audit logs enable teams to: + +- Monitor access and changes to secrets, certificates, and infrastructure. +- Investigate incidents with full context around who did what, when, and how. +- Meet compliance and governance requirements with structured activity records. + +To learn more, refer to the [audit logs documentation](/documentation/platform/audit-logs). + +## Log Coverage + +Infisical tracks dozens of event types across the platform — including secret access, permission changes, certificate issuance, SSH session activity, and identity management. + +Each audit entry includes structured fields that make it easy to search, filter, and correlate across systems. For example: + +- Event Type: Action that occurred (e.g., `create-secret`, `issue-ssh-cert`). +- Actor: Who performed the action (user or machine identity). +- Resource: What was affected (e.g., project, secret, certificate). +- Context: IP address, user agent, permissions, and more. + +## External Log Streaming + +For centralized monitoring and long-term retention, Infisical supports [audit log streaming](/documentation/platform/audit-log-streams/audit-log-streams) to external systems. + +You can forward logs to SIEM platforms, storage buckets, or observability stacks using JSON-based collectors. Infisical integrates well with tools like [Fluent Bit](/documentation/platform/audit-log-streams/audit-log-streams-with-fluentbit#deploy-fluent-bit), enabling teams to route logs to destinations such as: + +- AWS S3 +- Elasticsearch +- Splunk +- Datadog +- Cloud-native log pipelines diff --git a/docs/documentation/getting-started/concepts/client-integrations.mdx b/docs/documentation/getting-started/concepts/client-integrations.mdx new file mode 100644 index 000000000..bcd935830 --- /dev/null +++ b/docs/documentation/getting-started/concepts/client-integrations.mdx @@ -0,0 +1,31 @@ +--- +title: "Client Ecosystem" +sidebarTitle: "Client Ecosystem" +description: "Get an overview of the CLI, SDKs, agents, APIs, and integrations that interact with Infisical." +--- + +Infisical provides a flexible interface for integrating into development workflows and infrastructure. Around it is a rich ecosystem of clients and integrations that allow users and systems to interact with Infisical across any environment. + +These clients enable access to secrets, certificates, and other resources from wherever they’re needed—whether that’s a developer’s terminal, a CI/CD pipeline, or a running Kubernetes workload. + +## Available Clients and Interfaces + +Infisical offers a non-exhaustive set of clients and interfaces to support a wide range of use cases: + +- [CLI](/cli/overview): A powerful command-line interface for developers and operators to interact with Infisical from local or automated environments. Commonly used for secret access, SSH credential issuance, and more. + +- [SDKs](/sdks/overview): Official client libraries for languages like Go, Node.js, and Python make it easy to integrate Infisical directly into applications and internal tooling. + +- [HTTP API](/api-reference/overview/introduction): A fully documented RESTful API powers all core functionality and enables advanced or custom integrations. + +- [Agents](/integrations/platforms/infisical-agent): Lightweight background processes that can fetch and sync secrets or credentials into local environments, containers, or file systems. + +- [Kubernetes Operator](/integrations/platforms/kubernetes/overview): A native controller that syncs Infisical secrets into Kubernetes as native Secrets, and supports secure workload integration. + +- [External Secrets Operator (ESO)](https://external-secrets.io/latest/provider/infisical): Allows Infisical to act as a backend provider for syncing secrets into Kubernetes `Secret` objects using the widely adopted External Secrets Operator. + +- [Kubernetes PKI Issuer](/documentation/platform/pki/pki-issuer): A controller that issues X.509 certificates from Infisical PKI using the cert-manager Issuer and Certificate CRDs. + +- [Secret Syncs](/integrations/secret-syncs/overview): Native integrations to forward secrets to services like GitHub, GitLab, AWS Secrets Manager, Vercel, and more. + +This modular ecosystem lets teams use Infisical alongside their existing stack—without requiring opinionated workflows or lock-in. diff --git a/docs/documentation/getting-started/concepts/deployment-models.mdx b/docs/documentation/getting-started/concepts/deployment-models.mdx new file mode 100644 index 000000000..39078a142 --- /dev/null +++ b/docs/documentation/getting-started/concepts/deployment-models.mdx @@ -0,0 +1,52 @@ +--- +title: "Using Infisical: Cloud or Self-Hosted" +sidebarTitle: "Cloud vs. Self-Host" +description: "Choose between Infisical Cloud or a self-managed deployment" +--- + +Infisical can be used in two ways: via [Infisical Cloud](https://app.infisical.com), a managed offering, or through a self-hosted deployment within your own infrastructure. + +Both options provide the same core platform capabilities. The decision depends on your operational model, trust boundaries, and compliance requirements. While Infisical Cloud comes with built-in security and operational guarantees, a self-hosted deployment gives you full control—but also full responsibility for securing and maintaining the system. + +## Infisical Cloud + +Infisical Cloud is our managed service found at [app.infisical.com](https://app.infisical.com). It includes automated updates, availability guarantees, and secure infrastructure operations. + +For most teams, Infisical Cloud is the recommended way to get started. It simplifies adoption by removing the need to manage deployment, scaling, or maintenance internally. + +Use this if: + +- You prefer not to operate infrastructure or handle upgrades +- You require a secure, production-grade hosted service +- You want to adopt Infisical with minimal operational overhead + + +

+ By default, Infisical Cloud is a secure, multi-tenant service. For + enterprises with stricter isolation or regulatory needs, dedicated cloud + instances are available. +

+

Contact sales@infisical.com to learn more.

+
+ +## Self-Hosted Infisical + +Infisical can also be deployed and managed within your own infrastructure. This approach provides full control over platform configuration, data storage, and operational security. In this model, your team is responsible for maintaining uptime, monitoring, patching, and integrations. + +Use this if: + +- You require complete control over data, deployment, and security posture +- Your compliance model mandates self-managed or on-premise systems +- You need to tightly integrate with internal tooling and infrastructure + +Infisical supports multiple deployment methods, including [Docker](/self-hosting/deployment-options/standalone-infisical), [Docker Compose](/self-hosting/deployment-options/docker-compose), [Kubernetes](/self-hosting/deployment-options/kubernetes-helm), and [Linux package](/self-hosting/deployment-options/native/linux-package/installation). + +To learn more, refer to the [self-hosting documentation](/self-hosting/overview). + + +

+ The open-source core is available under the MIT license. Additional + enterprise features and support are available with a commercial license. +

+

Contact sales@infisical.com to learn more.

+
diff --git a/docs/documentation/getting-started/concepts/platform-hierarchy.mdx b/docs/documentation/getting-started/concepts/platform-hierarchy.mdx new file mode 100644 index 000000000..7697f935b --- /dev/null +++ b/docs/documentation/getting-started/concepts/platform-hierarchy.mdx @@ -0,0 +1,41 @@ +--- +title: "Platform Hierarchy" +sidebarTitle: "Platform Hierarchy" +description: "Understand how organizations and projects are structured in Infisical." +--- + +Infisical is structured around organizations and projects, allowing teams to manage multiple products, access scopes, and use cases within a single account while keeping boundaries and responsibilities clearly defined. + +## Organizations + +An [organization](/documentation/platform/organization) typically represents a company or high-level entity (e.g. Acme Corp). It acts as the umbrella for all projects, members, and billing settings. + +[Users](/documentation/platform/identities/user-identities) are invited to an organization and assigned [organization-level roles](/documentation/platform/access-controls/role-based-access-controls#organization-level-access-controls) that determine what they can manage—such as members, machine identities, and billing details. + +![organization](/images/platform/organization/organization.png) + +## Projects + +A [project](/documentation/platform/project) belongs to an organization and defines a specific scope of work. Each project has a product type such as Secrets Management, SSH, or PKI that determines what features are available in that project. + +For example: + +- A Secrets Management project manages application secrets across environments. + +- An SSH project enables certificate-based access to infrastructure. + +- A PKI project manages certificate authorities and X.509 certificate workflows. + +Users are added to a project and assigned [project-level roles](/documentation/platform/access-controls/role-based-access-controls#project-level-access-controls) that determine what they can manage—such as secrets, access policies, or certificate authorities. A user can have different roles across projects, allowing for flexible and fine-grained access control that reflects how teams operate in practice. + +![organization projects](/images/platform/organization/organization-projects.png) + +## Key Characteristics + +- Projects are isolated in terms of configuration, permissions, and product workflows. + +- Access is managed independently at both the organization and project level. + +- All projects within an organization share the same billing and user directory. + +Teams can adopt Infisical incrementally—starting with one product and expanding as needed. diff --git a/docs/documentation/getting-started/concepts/platform-iam.mdx b/docs/documentation/getting-started/concepts/platform-iam.mdx new file mode 100644 index 000000000..87c343269 --- /dev/null +++ b/docs/documentation/getting-started/concepts/platform-iam.mdx @@ -0,0 +1,29 @@ +--- +title: "Platform Identity and Access Management" +sidebarTitle: "Platform IAM" +description: "Understand how users, machine identities, roles, and permissions are managed." +--- + +Infisical uses identity-based access control to govern how users and systems interact with secrets, certificates, infrastructure, and other resources on the platform. + +There are two types of identities: + +- [User identities](/documentation/platform/identities/user-identities): Represent individuals such as developers or administrators that typically access the platform via browser. + +- [Machine identities](/documentation/platform/identities/machine-identities): Represent systems such as CI pipelines or applications that programmatically interact with the platform. + +Each identity is granted access based on its assigned roles and permissions and must authenticate with the platform in order to access any resources. + +To learn more, refer to the [identities documentation](/documentation/platform/identities/overview). + +## Roles and Access + +Infisical provides a robust and flexible access control system. The primary authorization mechanism is [role-based access control (RBAC)](/documentation/platform/access-controls/role-based-access-controls), where identities are assigned roles at two access control levels: + +- [Organization-level access control](/documentation/platform/access-controls/role-based-access-controls#organization-level-access-controls): Control billing, member management, and platform-wide settings + +- [Project-level access control](/documentation/platform/access-controls/role-based-access-controls#project-level-access-controls): Control access to specific product resources like secrets, SSH hosts, or certificates + +Beyond RBAC, Infisical also supports additional project-level permissioning features, [including attribute-based access control (ABAC)](/documentation/platform/access-controls/abac/overview), [temporary access grants](/documentation/platform/access-controls/temporary-access), and [additional privileges](/documentation/platform/access-controls/additional-privileges) for select project types. + +To learn more, refer to the [access control documentation](/documentation/platform/access-controls/overview). diff --git a/docs/documentation/getting-started/introduction-new.mdx b/docs/documentation/getting-started/introduction-new.mdx deleted file mode 100644 index c8eee8739..000000000 --- a/docs/documentation/getting-started/introduction-new.mdx +++ /dev/null @@ -1,107 +0,0 @@ ---- -mode: 'custom' ---- - -export function openSearch() { - document.getElementById('search-bar-entry').click(); -} - -
-
- -
-
-
- Infisical Documentation -
-

- What can we help you build? -

- -
-
- -
- -
- Choose a topic below or simply{' '} - get started -
- - - - Practical guides and best practices to get you up and running quickly. - - - Comprehensive details about the Infisical API. - - - Learn more about Infisical's architecture and underlying security. - - - Read self-hosting instruction for Infisical. - - - Infisical's growing number of third-party integrations. - - - News about features and changes in Pinecone and related tools. - - - -
\ No newline at end of file diff --git a/docs/documentation/getting-started/introduction.mdx b/docs/documentation/getting-started/introduction.mdx index d73c28ab5..f773019ec 100644 --- a/docs/documentation/getting-started/introduction.mdx +++ b/docs/documentation/getting-started/introduction.mdx @@ -1,106 +1,40 @@ --- title: "What is Infisical?" sidebarTitle: "What is Infisical?" -description: "An Introduction to the Infisical secret management platform." +description: "The open source platform for managing secrets, certificates, and secure infrastructure access." --- -**[Infisical](https://infisical.com)** is the open source secret management platform that developers use to centralize their application configuration and secrets like API keys and database credentials as well as manage their internal PKI. Additionally, developers use Infisical to prevent secrets leaks to git and securely share secrets amongst engineers. +## What is Infisical? + +[Infisical](https://infisical.com) is the [open source](https://github.com/Infisical/infisical), all-in-one platform for secrets, certificates, and privileged access management. + +It provides modern security workflows — including secrets rotation, dynamic credentials, access approvals, and SSH certificate-based access — all within one platform designed for developers, infrastructure, and security teams. Start managing secrets securely with [Infisical Cloud](https://app.infisical.com) or learn how to [host Infisical](/self-hosting/overview) yourself. - - - Get started with Infisical Cloud in just a few minutes. - - - Self-host Infisical on your own infrastructure. - - +## Why use Infisical? -## Why Infisical? +Managing secrets, credentials, and infrastructure access is a critical concern for engineering teams. As infrastructure scales and environments become more complex, [secrets start to sprawl](https://infisical.com/blog/what-is-secret-sprawl) — across codebases, CI/CD pipelines, configuration files, and cloud services. This makes them difficult to track, rotate, and secure. -Infisical helps developers achieve secure centralized secret management and provides all the tools to easily manage secrets in various environments and infrastructure components. In particular, here are some of the most common points that developers mention after adopting Infisical: +Without proper management, secret sprawl turns into risk: hardcoded credentials, unrotated keys, fragmented access controls that attackers can exploit amongst other things. -- Streamlined **local development** processes (switching .env files to [Infisical CLI](/cli/commands/run) and removing secrets from developer machines). -- **Best-in-class developer experience** with an easy-to-use [Web Dashboard](/documentation/platform/project). -- Simple secret management inside **[CI/CD pipelines](/integrations/cicd/githubactions)** and staging environments. -- Secure and compliant secret management practices in **[production environments](/sdks/overview)**. -- **Facilitated workflows** around [secret change management](/documentation/platform/pr-workflows), [access requests](/documentation/platform/access-controls/access-requests), [temporary access provisioning](/documentation/platform/access-controls/temporary-access), and more. -- **Improved security posture** thanks to [secret scanning](/cli/scanning-overview), [granular access control policies](/documentation/platform/access-controls/overview), [automated secret rotation](https://infisical.com/docs/documentation/platform/secret-rotation/overview), and [dynamic secrets](/documentation/platform/dynamic-secrets/overview) capabilities. +Infisical addresses this challenge by providing an all-in-one platform and workflows to: -## How does Infisical work? +- Securely store and manage application secrets from development to production. +- Scan code and pipelines for exposed credentials. +- Automate X.509 certificate issuance and renewal. +- Manage SSH access using short-lived, policy-driven certificates. +- Encrypt and decrypt sensitive data with centralized key control. +- Audit every access, credential use, and change. -To make secret management effortless and secure, Infisical follows a certain structure for enabling secret management workflows as defined below. +Infisical is designed to integrate cleanly into your stack—improving security without adding complexity. -**Identities** in Infisical are users or machine which have a certain set of roles and permissions assigned to them. Such identities are able to manage secrets in various **Clients** throughout the entire infrastructure. To do that, identities have to verify themselves through one of the available **Authentication Methods**. +## What does Infisical include? -As a result, the 3 main concepts that are important to understand are: +Infisical consists of several tightly integrated products, each designed to solve a specific part of the infrastructure security surface: -- **[Identities](/documentation/platform/identities/overview)**: users or machines with a set permissions assigned to them. -- **[Clients](/integrations/platforms/kubernetes)**: Infisical-developed tools for managing secrets in various infrastructure components (e.g., [Kubernetes Operator](/integrations/platforms/kubernetes), [Infisical Agent](/integrations/platforms/infisical-agent), [CLI](/cli/usage), [SDKs](/sdks/overview), [API](/api-reference/overview/introduction), [Web Dashboard](/documentation/platform/organization)). -- **[Authentication Methods](/documentation/platform/identities/universal-auth)**: ways for Identities to authenticate inside different clients (e.g., SAML SSO for Web Dashboard, Universal Auth for Infisical Agent, AWS Auth etc.). - -## How to get started with Infisical? - -Depending on your use case, it might be helpful to look into some of the resources and guides provided below. - - - - Inject secrets into any application process/environment. - - - Fetch secrets with any programming language on demand. - - - Inject secrets into Docker containers. - - - Fetch and save secrets as native Kubernetes secrets. - - - Fetch secrets via HTTP request. - - - Explore integrations for GitHub, Vercel, AWS, and more. - - +- [Secrets Management](/documentation/platform/secrets-mgmt/overview): Securely store, access, and distribute secrets across environments with fine-grained controls, automatic rotation, and audit logging. +- [Secrets Scanning](/documentation/platform/secret-scanning/overview): Detect hardcoded secrets in code, CI pipelines, and infrastructure—integrated with GitHub, GitLab, Bitbucket, and more. +- [Infisical PKI](/documentation/platform/pki/overview): Issue and manage X.509 certificates using protocols like EST, with support for internal and external CAs. +- [Infisical SSH](/documentation/platform/ssh/overview): Provide short-lived SSH access to servers using certificate-based authentication, replacing static keys with policy-driven, time-bound control. +- [Infisical KMS](/documentation/platform/kms/overview): Encrypt and decrypt data using centrally managed keys with enforced access policies and full audit visibility. diff --git a/docs/documentation/getting-started/overview.mdx b/docs/documentation/getting-started/overview.mdx new file mode 100644 index 000000000..769990987 --- /dev/null +++ b/docs/documentation/getting-started/overview.mdx @@ -0,0 +1,77 @@ +--- +title: "Overview" +sidebarTitle: "Overview" +description: "The open source platform for managing secrets, certificates, and secure infrastructure access." +--- + + + Learn what Infisical is and how it can help you manage secrets, certificates, + and secure access across your infrastructure. + + +## Products + + + + Securely store, manage, and control access to sensitive application secrets across your environments. + + + + Automatically detect and alert on hardcoded secrets in source code, CI pipelines, and infrastructure. + + + Automate the issuance and management of X.509 certificates across your infrastructure using modern protocols like EST. + + + Replace static SSH keys with short-lived SSH certificates to simplify access and improve security. + + + + + + Encrypt and decrypt sensitive data using a centralized key management system. + + + +## Resources + + + + Explore Infisical’s command-line interface for managing secrets, + certificates, and system operations via terminal. + + + Browse Infisical’s API documentation to programmatically interact with + secrets, access controls, and certificate workflows. + + + + + Learn how to deploy and operate Infisical on your own infrastructure with full + control and data ownership. + + diff --git a/docs/documentation/platform/event-subscriptions.mdx b/docs/documentation/platform/event-subscriptions.mdx index 1d36e5aa2..ca6ceebd1 100644 --- a/docs/documentation/platform/event-subscriptions.mdx +++ b/docs/documentation/platform/event-subscriptions.mdx @@ -5,21 +5,26 @@ description: "Subscribe to events in Infisical for real-time updates" --- - **Note:** Event Subscriptions is a paid feature. - - **Infisical Cloud users:** Event Subscriptions is available under the **Enterprise Tier**. - - **Self-Hosted Infisical:** Please contact [sales@infisical.com](mailto:sales@infisical.com) to purchase an enterprise license. + **Note:** Event Subscriptions is a paid feature. - **Infisical Cloud users:** Event Subscriptions is available under + the **Enterprise Tier**. - **Self-Hosted Infisical:** Please contact [sales@infisical.com](mailto:sales@infisical.com) + to purchase an enterprise license. -Event Subscriptions in Infisical allow you to receive real-time notifications when specific actions occur within your account or organization. These notifications include changes to secrets, users, teams, and other important resources. +Event Subscriptions in Infisical allow you to receive real-time notifications when specific actions occur within your account or organization. These notifications include changes to secrets, users, teams, and many more **coming soon**. ## How It Works -Event Subscriptions enable real-time communication and state synchronization across multiple Infisical server instances through a distributed messaging architecture. The system leverages Redis as the central messaging backbone, utilizing Redis Pub/Sub to orchestrate event distribution in a fan-out pattern across all connected services. All server instances subscribe to a shared Redis topic named `infisical::core-servers`, ensuring consistent event delivery throughout the system. +- Server receives message over pubsub connection indicating changes have occurred +- Server processes the change notification +- Updated data is synchronized across all connected Infisical instances +- Client applications receive real-time updates through [Server-Sent Events (SSE)](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events) +- All servers maintain consistent state without manual intervention -Event messages are structured according to the [CloudEvents specification](https://github.com/cloudevents/spec), providing a standardized format for cross-service communication. For client-side delivery, the system employs Server-Sent Events (SSE) streams, which offer efficient unidirectional communication without requiring specialized client libraries. This lightweight approach ensures seamless real-time updates while maintaining broad compatibility across different client environments. +This ensures your infrastructure stays up-to-date automatically, without requiring restarts or manual synchronization. - Event Subscriptions are designed for real-time communication and do not include persistence or replay capabilities—events are delivered once and are not stored for future retrieval. + Event Subscriptions are designed for real-time communication and do not include persistence or replay + capabilities—events are delivered once and are not stored for future retrieval. ## Supported Resources @@ -27,9 +32,9 @@ Event messages are structured according to the [CloudEvents specification](https You can currently subscribe to notifications for the following resources and event types: - **Secrets** - - `secret:created`: Triggered when a secret is created - - `secret:updated`: Triggered when a secret is updated - - `secret:deleted`: Triggered when a secret is deleted + - `secret:created`: Triggered when a secret is created + - `secret:updated`: Triggered when a secret is updated + - `secret:deleted`: Triggered when a secret is deleted ## Permissions Setup @@ -40,45 +45,48 @@ Follow these steps to set up the necessary permissions: ![Select Project](/images/platform/events/select-project.png) - - On your project page, open **Project Settings** from the sidebar. - - In the Project name section, click **Copy Project ID** to copy your Project ID, or extract it from the URL: - `https://app.infisical.com/project//settings` + +On your project page, open **Project Settings** from the sidebar. + +In the Project name section, click **Copy Project ID** to copy your Project ID, or extract it from the URL: +`https://app.infisical.com/project//settings` + - - ![Project Detail](/images/platform/events/project-detail.png) - ![Project Access](/images/platform/events/project-access.png) - - Navigate to **Access Management**, then select **Project Roles**. - + + ![Project Detail](/images/platform/events/project-detail.png) ![Project + Access](/images/platform/events/project-access.png) Navigate to **Access Management**, then select **Project Roles**. + - - ![Project Role](/images/platform/events/project-role.png) - - You can either edit an existing role or create a new role for event subscriptions. - + + ![Project Role](/images/platform/events/project-role.png) You can either edit an existing role or create a new role + for event subscriptions. + - - ![Role Detail](/images/platform/events/role-detail.png) - - Select the specific resources that the role should have access to. - - ![Add policy](/images/platform/events/add-policy.png) - + + ![Role Detail](/images/platform/events/role-detail.png) Select the specific resources that the role should have access + to. ![Add policy](/images/platform/events/add-policy.png) + ![Policy setting](/images/platform/events/policy-setting.png) - - Ensure the **Subscribe** action is selected for the relevant resources and events. - + + Ensure the **Subscribe** action is selected for the relevant resources and events. + + ## Conditions + + By default, the role will have access to all events for the selected resources in this project. + - - By default, the role will have access to all events for the selected resources in this project. If needed, you can add filters to limit the scope of accessible events. - - ![Policy setting](/images/platform/events/add-conditions.png) + + ![Policy setting](/images/platform/events/access-full.png) + + ![Policy setting](/images/platform/events/access-path.png) + + + ![Policy setting](/images/platform/events/access-dev.png) + @@ -87,61 +95,24 @@ Follow these steps to set up the necessary permissions: Currently, events are only available via [API](/api-reference/endpoints/events) but will soon be available in our SDKs, Kubernetes Operator, and more. -### API Authentication - - - - ![Org ACL page](/images/platform/events/org-access-control.png) - - Navigate to the **Organization Access Management** tab under Project access control settings. - - ![Select Identity](/images/platform/events/identity-selector.png) - - Select or create an identity you want to create the token for. - - - - ![Add identity role](/images/platform/events/identity-add-role.png) - - Select a project and the role you previously configured in the [permissions setup](#permissions-setup) section. - - ![Add identity role](/images/platform/events/identity-add-project.png) - - - - ![Add Auth Method](/images/platform/events/add-auth-method.png) - ![Add Auth Method Form](/images/platform/events/add-auth-method-form.png) - - - - ![Generate auth token](/images/platform/events/generate-auth-token.png) - ![Generate auth token add](/images/platform/events/generate-auth-token-add.png) - ![Generate auth token add](/images/platform/events/generate-auth-token-create.png) - - Enter a descriptive name and click **Create**. - - ![Copy token](/images/platform/events/copy-generated-token.png) - - Click to copy the generated token and save it for later use. - - - ### API Usage +You need an auth token to use this API. To get an authentication token, follow the authentication guide for one of our supported auth methods from the [machine identities documentation](/documentation/platform/identities/machine-identities#authentication-methods). + #### Creating a Subscription ![Postman Subscription](/images/platform/events/postman-subscribe.png) **Request Parameters:** + - `projectId`: Project whose events you want to subscribe to - `register`: List of event filters - - `conditions`: Conditions to filter events on - - `environmentSlug`: Project environment - - `secretPath`: Path of the secrets - - `recursive`: If `true`, gets events from all nested paths under `secretPath` + - `conditions`: Conditions to filter events on + - `environmentSlug`: Project environment + - `secretPath`: Path of the secrets ![Postman Subscription Response](/images/platform/events/postman-sse-response.png) The subscribe endpoint responds with a `text/event-stream` content type to initiate SSE streaming. -For more specific details, please refer to our [API Reference](/api-reference/endpoints/events). \ No newline at end of file +For more specific details, please refer to our [API Reference](/api-reference/endpoints/events). diff --git a/docs/documentation/platform/identities/auth-templates.mdx b/docs/documentation/platform/identities/auth-templates.mdx new file mode 100644 index 000000000..5c389376f --- /dev/null +++ b/docs/documentation/platform/identities/auth-templates.mdx @@ -0,0 +1,96 @@ +--- +title: "Machine Identity Auth Templates" +description: "Learn how to use auth templates to standardize authentication configurations for machine identities." +--- + +## Concept + +Machine Identity Auth Templates allow you to create reusable authentication configurations that can be applied across multiple machine identities. This feature helps standardize authentication setups, reduces configuration drift, and simplifies identity management at scale. + +Instead of manually configuring authentication settings for each identity, you can create templates with predefined authentication parameters and apply them to multiple identities. This ensures consistency and reduces the likelihood of configuration errors. + +Key Benefits: + +- **Standardization**: Ensure consistent authentication configurations across identities +- **Efficiency**: Reduce time spent configuring individual identities +- **Governance**: Centrally manage and update authentication parameters +- **Scalability**: Easily apply proven configurations to new identities + +## Managing Auth Templates + +Auth templates are managed in **Organization Settings > Access Control > Identities** under the **Identity Auth Templates** section. + +![Identity Auth Templates Section](/images/platform/identities/auth-templates/templates-section.png) + +### Creating a Template + + + + In your organization settings, go to **Access Control > Identities** and scroll down to the **Identity Auth Templates** section. + + + + Click **Create Template** to open the template creation modal. + + ![Create Template Button](/images/platform/identities/auth-templates/create-template-button.png) + + Select the authentication method you want to create a template for (currently supports LDAP Auth). + + + + Fill in the template configuration based on your chosen authentication method. + + + + **For LDAP Auth templates**, configure the following fields: + + ![LDAP Auth Template](/images/platform/identities/auth-templates/ldap-template.png) + + - **Template Name**: A descriptive name for your template + - **URL**: The LDAP server to connect to such as `ldap://ldap.your-org.com`, `ldaps://ldap.myorg.com:636` _(for connection over SSL/TLS)_, etc. + - **Bind DN**: The DN to bind to the LDAP server with. + - **Bind Pass**: The password to bind to the LDAP server with. + - **Search Base / DN**: Base DN under which to perform user search such as `ou=Users,dc=acme,dc=com`. + - **CA Certificate**: The CA certificate to use when verifying the LDAP server certificate. This field is optional but recommended. + + + You can read more about LDAP Auth configuration in the [LDAP Auth documentation](/documentation/platform/identities/ldap-auth/general). + + + + + + +### Using Templates + +Once created, templates can be applied when configuring authentication methods for machine identities. When adding an auth method to an identity, you'll have the option to select from available templates or configure manually. + +![Attach Template](/images/platform/identities/auth-templates/machine-identity-page.png) +![Attach Template Form](/images/platform/identities/auth-templates/attach-template-form.png) + +### Managing Template Usage + +You can view which identities are using a specific template by clicking **View Usages** in the template's dropdown menu. + +![Template Usages](/images/platform/identities/auth-templates/template-usages.png) +![Template Usages Modal](/images/platform/identities/auth-templates/template-usages-modal.png) + +## FAQ + + + + Yes, you can edit existing templates. After editing a template, changes to templates will automatically update identities that are already using them. + + + + If you delete a template that's currently being used by identities, those identities will continue to function with their existing configuration. However, the link to the template will be broken, and you won't be able to use the template for new identities. + + + + Yes, click **View Usages** in the template's dropdown menu to see all identities currently using that template. + + + + Currently, auth templates support LDAP Auth. Support for additional authentication methods will be added in future releases. + + \ No newline at end of file diff --git a/docs/documentation/platform/identities/ldap-auth/general.mdx b/docs/documentation/platform/identities/ldap-auth/general.mdx index 7fb2798c7..01395b68c 100644 --- a/docs/documentation/platform/identities/ldap-auth/general.mdx +++ b/docs/documentation/platform/identities/ldap-auth/general.mdx @@ -5,6 +5,12 @@ description: "Learn how to authenticate with Infisical using LDAP." **LDAP Auth** is an LDAP based authentication method that allows you to authenticate with Infisical using a machine identity configured with an [LDAP](https://en.wikipedia.org/wiki/Lightweight_Directory_Access_Protocol) directory. +## Templates + +You can create reusable LDAP authentication templates to standardize configurations across multiple machine identities. Templates help ensure consistency, reduce configuration errors, and simplify identity management at scale. + +To create and manage LDAP auth templates, see our [Machine Identity Auth Templates documentation](/documentation/platform/identities/auth-templates). Once you've created a template, you can apply it when configuring LDAP auth for your identities in the guide below. + ## Guide diff --git a/docs/documentation/platform/identities/user-identities.mdx b/docs/documentation/platform/identities/user-identities.mdx index bcb470a3e..31e4bf242 100644 --- a/docs/documentation/platform/identities/user-identities.mdx +++ b/docs/documentation/platform/identities/user-identities.mdx @@ -5,18 +5,19 @@ description: "Read more about the concept of user identities in Infisical." ## Concept -A **user identity** (also known as **user**) represents a developer, admin, or any other human entity interacting with resources in Infisical. +A **user identity** (also known as **user**) represents a developer, admin, or any other human entity interacting with resources in Infisical. -Users can be added manually (through Web UI) or programmatically (e.g., API) to [organizations](../organization) and [projects](../projects). +Users can be added manually (through Web UI) or programmatically (e.g., API) to [organizations](../organization) and [projects](../projects). -Upon being added to an organization and projects, users assume a certain set of roles and permissions that represents their identity. +Upon being added to an organization and projects, users assume a certain set of roles and permissions that represents their identity. -![organization members](../../../images/platform/organization/organization-members.png) +![organization users](/images/platform/organization/organization-users.png) ## Authentication methods -To interact with various resources in Infisical, users are able to utilize a number of authentication methods: -- **Email & Password**: the most common authentication method that is used for authentication into Web Dashboard and Infisical CLI. It is recommended to utilize [Multi-factor Authentication](/documentation/platform/mfa) in addition to it. -- **SSO**: Infisical natively integrates with a number of SSO identity providers like [Google](/documentation/platform/sso/google), [GitHub](/documentation/platform/sso/github), and [GitLab](/documentation/platform/sso/gitlab). -- **SAML SSO**: It is also possible to set up SAML SSO integration with identity providers like [Okta](/documentation/platform/sso/okta), [Microsoft Entra ID](/documentation/platform/sso/azure) (formerly known as Azure AD), [JumpCloud](/documentation/platform/sso/jumpcloud), [Google](/documentation/platform/sso/google-saml), and more. +To interact with various resources in Infisical, users are able to utilize a number of authentication methods: + +- **Email & Password**: the most common authentication method that is used for authentication into Web Dashboard and Infisical CLI. It is recommended to utilize [Multi-factor Authentication](/documentation/platform/mfa) in addition to it. +- **SSO**: Infisical natively integrates with a number of SSO identity providers like [Google](/documentation/platform/sso/google), [GitHub](/documentation/platform/sso/github), and [GitLab](/documentation/platform/sso/gitlab). +- **SAML SSO**: It is also possible to set up SAML SSO integration with identity providers like [Okta](/documentation/platform/sso/okta), [Microsoft Entra ID](/documentation/platform/sso/azure) (formerly known as Azure AD), [JumpCloud](/documentation/platform/sso/jumpcloud), [Google](/documentation/platform/sso/google-saml), and more. - **LDAP**: For organizations with more advanced needs, Infisical also provides user authentication with [LDAP](/documentation/platform/ldap/overview) that includes a number of LDAP providers. diff --git a/docs/documentation/platform/organization.mdx b/docs/documentation/platform/organization.mdx index 6c3b218ab..aea842d3f 100644 --- a/docs/documentation/platform/organization.mdx +++ b/docs/documentation/platform/organization.mdx @@ -3,74 +3,94 @@ title: "Organizations" description: "Learn more and understand the concept of Infisical organizations." --- -An Infisical organization is a set of [projects](./project) that use the same billing. Organizations allow one or more users to control billing and project permissions for all of the projects belonging to the organization. Each project belongs to an organization. +Infisical is structured around organizations and [projects](/documentation/platform/project). + +## Organizations + +An organization represents a company or high-level entity (e.g. Acme Corp) and acts as the root scope for managing members and machine identities, projects, usage and billing, global integrations and configuration (such as single sign-on, provisioning, etc), and more. + +Within an organization, you can create any number of projects—each tied to a specific product type such as Secrets Management or PKI that determines the functionality available. + +![organization](/images/platform/organization/organization.png) ## Projects -The **Projects** page is where you can view the projects that you have access to within your organization -as well as create a new project. +The _Projects_ tab shows a list of projects that you have access to. -![organization](../../images/platform/organization/organization-projects.png) +If you're an organization admin, you also have the option to view _All Projects_—a complete view of every project within the organization, including those you are not currently a member of— and gain access to any project. -## Settings +Admins can gain access to any project in the organization by opening the options menu (⋮) next to a project and selecting Access. This will add you to the project as an admin and allow full visibility and control. -The **Settings** page lets you manage information about your organization including: +![organization projects](/images/platform/organization/organization-projects.png) -- **Name**: The name of your organization. -- **Slug**: The slug of your organization. -- **Default Organization Member Role**: The role assigned to users when joining your organization unless otherwise specified. -- **Incident Contacts**: Emails that should be alerted if anything abnormal is detected within the organization. -- **Enabled Products**: Products which are enabled for your organization. This setting strictly affects the sidebar UI; disabling a product does not disable its API or routes. +## Roles and Access Control -![organization settings general](../../images/platform/organization/organization-settings-general.png) +The _Access Control_ tab lets you view and manage roles and permissions for users, machine identities, and groups across your organization. -- Security and Authentication: A set of setting to enforce or manage [SAML](/documentation/platform/sso/overview), [OIDC](/documentation/platform/sso/overview), [SCIM](/documentation/platform/scim/overview), [LDAP](/documentation/platform/ldap/overview), and other authentication configurations. +Users are invited to an organization and assigned organization-level roles such as `Admin` or `Member`. You can also define [custom roles](/documentation/platform/access-controls/role-based-access-controls#creating-custom-roles) at the organization level to fit your permission model. -![organization settings auth](../../images/platform/organization/organization-settings-auth.png) +![organization users](/images/platform/organization/organization-users.png) - - You can adjust the maximum time a user token will remain valid for your organization. After this period, users will be required to re-authenticate. This helps improve security by enforcing regular sign-ins. - +Infisical supports [user identities](/documentation/platform/identities/user-identities) (representing people) and [machine identities](/documentation/platform/identities/machine-identities) (representing services, CI/CD pipelines, or agents). The same roles and permissions can be applied to either type of identity. -## Access Control +To manage access at scale, Infisical also supports [user groups](/documentation/platform/groups) — roles assigned to a group apply to all of its members automatically. -The **Access Control** page is where you can manage identities (both people and machines) that are part of your organization. -You can add or remove additional members as well as modify their permissions. +Note that Infisical distinguishes between organization-level and project-level access control: -![organization members](../../images/platform/organization/organization-members.png) -![organization identities](../../images/platform/organization/organization-machine-identities.png) +- [Organization-level access control](/documentation/platform/access-controls/role-based-access-controls#organization-level-access-controls): Roles and permissions governing access to organization-level resources and controls such as billing, member management, and identity provider configuration. +- [Project-level access control](/documentation/platform/access-controls/role-based-access-controls#project-level-access-controls): Roles and permissions governing access to resources and workflows within a specific project (e.g., secrets, certificates, SSH hosts). -In the **Organization Roles** tab, you can edit current or create new custom roles for members within the organization. +![organization roles](/images/platform/organization/organization-roles.png) + +To learn more about how permissions work in detail, refer to the [access control documentation](/documentation/platform/access-controls/overview). - Note that Role-Based Access Management (RBAC) is partly a paid feature. - - Infisical provides immutable roles like `admin`, `member`, etc. - at the organization and project level for free. +Infisical provides immutable roles such as `admin` and `member` for free. If you're using Infisical Cloud, the ability to create custom roles is available under the **Pro Tier**. + If you're self-hosting Infisical, then you should contact sales@infisical.com to purchase an enterprise license to use it. -![organization roles](../../images/platform/organization/organization-members-roles.png) - -As you can see next, Infisical supports granular permissions that you can tailor to each role. -If you need certain members to only be able to access billing details, for example, then you can -assign them that permission only. - -![organization role permissions](../../images/platform/organization/organization-members-roles-add-perm.png) - ## Usage & Billing -The **Usage & Billing** page applies only to [Infisical Cloud](https://app.infisical.com) and is where you can -manage your plan and billing information. +The _Usage & Billing_ tab provides an overview of your organization's billing information and platform usage. -This includes the following items: +Infisical calculates usage at the organization level—aggregating activity across all projects and product types (e.g., Secrets Management, SSH, PKI). From this tab, you can track usage, view billing details, and manage your Infisical Cloud subscription. -- Current plan: The current plan information such as what tier your organization is on and what features/limits apply to this tier. -- Licenses: The license keys for self-hosted instances of Infisical (if applicable). -- Receipts: The receipts of monthly/annual invoices. -- Billing: The billing details of your organization including payment methods on file, tax IDs (if applicable), etc. +![organization billing](/images/platform/organization/organization-billing.png) -![organization usage and billing](../../images/platform/organization/organization-usage-billing.png) +## Audit Logs + +Infisical provides a unified view of [audit logs](/documentation/platform/audit-logs) at the organization level. All platform activity—including secret access, certificate issuance, platform logins across the organization —is recorded and searchable in a central log view. + +Audit logs are also viewable at the project level, where they are scoped to show only events relevant to that specific project. This allows project administrators to monitor activity and investigate changes without requiring organization-wide access. + +## App Connections + +Infisical supports [app connections](/integrations/app-connections/overview) — integrations configured at the organization level with third-party platforms such as AWS, GCP, GitHub, and many others. + +Once configured, these connections can be reused across multiple projects as part of any feature that requires third-party integrations—such as [secret syncing](/integrations/secret-syncs/overview) or [dynamic credential generation](/documentation/platform/dynamic-secrets/overview). + +![organization app connections](/images/platform/organization/organization-app-connections.png) + +To learn more, refer to the [app connections documentation](/integrations/app-connections/overview). + +## Organization Settings + +The _Organization Settings_ tab lets you configure global behavior and security controls for the organization. + +Key configuration areas include: + +- General: Manage the organization’s name, slug, and default role for newly invited members. +- Single Sign-On (SSO): Enable [SAML](/documentation/platform/sso/overview), [LDAP](/documentation/platform/ldap/overview), or [OIDC-based](/documentation/platform/sso/general-oidc/overview) authentication for user login. +- Provisioning: Enable [SCIM](/documentation/platform/scim/overview) to automatically provision and deprovision users and groups from an identity provider. +- Security Policies: Enforce MFA and configure session duration limits. +- Encryption: Integrate with external KMS systems or bring your own encryption keys (BYOK). +- [Audit Log Streaming](/documentation/platform/audit-log-streams/audit-log-streams): Forward audit events to third-party logging tools like SIEMs or cloud storage. +- Workflow Integrations: Trigger [Slack](/documentation/platform/workflow-integrations/slack-integration) or [Microsoft Teams](/documentation/platform/workflow-integrations/microsoft-teams-integration) notifications for events like access requests. +- [Project Templates](/documentation/platform/project-templates): Define default environments, roles, and settings to standardize project creation. +- KMIP (Enterprise): Connect to KMIP-compatible HSMs for hardware-backed key storage and operations. + +![organization settings](/images/platform/organization/organization-settings.png) diff --git a/docs/documentation/platform/pki/overview.mdx b/docs/documentation/platform/pki/overview.mdx index 8ee9b113d..b2351813c 100644 --- a/docs/documentation/platform/pki/overview.mdx +++ b/docs/documentation/platform/pki/overview.mdx @@ -1,13 +1,13 @@ --- -title: "Internal PKI" +title: "Infisical PKI" sidebarTitle: "Overview" description: "Learn how to create a Private CA hierarchy and issue X.509 certificates." --- -Infisical can be used to create a Private Certificate Authority (CA) hierarchy and issue X.509 certificates for internal use. This allows you to manage your own PKI infrastructure and issue digital certificates for subscribers such as services, applications, and devices. +Infisical can be used to create and manage Certificate Authorities (CAs) and issue X.509 certificates. This allows you to manage PKI infrastructure and issue digital certificates for subscribers such as services, applications, and devices. Infisical's PKI offering is split into three components: -- [Certificate Authorities](/documentation/platform/pki/private-ca): Create and manage private CAs, including root and intermediate CAs. +- [Certificate Authorities](/documentation/platform/pki/private-ca): Create and manage CAs, including root and intermediate CAs. - [Subscribers](/documentation/platform/pki/subscribers): Define and manage entities that will request X.509 certificates from CAs. This module provides a centralized view of all subscribers, enabling you to issue certificates and monitor their status. - [Certificates](/documentation/platform/pki/certificates): Track and monitor issued X.509 certificates, maintaining a comprehensive inventory of all active and expired certificates. diff --git a/docs/documentation/platform/project.mdx b/docs/documentation/platform/project.mdx index dcf447169..7d0df2e22 100644 --- a/docs/documentation/platform/project.mdx +++ b/docs/documentation/platform/project.mdx @@ -1,116 +1,51 @@ --- -title: "Projects" +title: "Overview" description: "Learn more and understand the concept of Infisical projects." --- -A project in Infisical belongs to an [organization](./organization) and contains a number of environments, folders, and secrets. -Only users and machine identities who belong to a project can access resources inside of it according to predefined permissions. +## Projects -Infisical also allows users to request project access. Refer to the [project access request section](./access-controls/project-access-requests) +A project defines a specific scope of work for a given product line in Infisical. -## Project environments +Projects are created within an [organization](/documentation/platform/organization), and an organization can contain multiple projects across different product types. -For both visual and organizational structure, Infisical allows splitting up secrets into environments (e.g., development, staging, production). In project settings, such environments can be -customized depending on the intended use case. +## Project Types -![project secrets overview](../../images/platform/project/project-environments.png) +Infisical supports project types, each representing a different security product with its own dashboard, workflows, and capabilities. -## Secrets Overview +![project types](/images/platform/project/project-types.png) -The **Secrets Overview** page captures a birds-eye-view of secrets and [folders](./folder) across environments. -This is useful for comparing secrets, identifying if anything is missing, and making quick changes. +The supported project types are: -![project secrets overview](../../images/platform/project/project-secrets-overview-open.png) +- [Secrets Management](/documentation/platform/secrets-mgmt/overview): Securely store, access, and distribute secrets across environments with fine-grained controls, automatic rotation, and audit logging. +- [Secrets Scanning](/documentation/platform/secret-scanning/overview): Detect hardcoded secrets in code, CI pipelines, and infrastructure—integrated with GitHub, GitLab, Bitbucket, and more. +- [Infisical PKI](/documentation/platform/pki/overview): Issue and manage X.509 certificates using protocols like EST, with support for internal and external CAs. +- [Infisical SSH](/documentation/platform/ssh/overview): Provide short-lived SSH access to servers using certificate-based authentication, replacing static keys with policy-driven, time-bound control. +- [Infisical KMS](/documentation/platform/kms/overview): Encrypt and decrypt data using centrally managed keys with enforced access policies and full audit visibility. -## Secrets Dashboard +## Roles and Access Control -The **Secrets Dashboard** page appears when you press to manage the secrets of a specific environment. +[Users](/documentation/platform/identities/user-identities) and [machine identities](/documentation/platform/identities/machine-identities) must be added to a project to access its resources. Each identity is assigned a [project-level role](/documentation/platform/access-controls/role-based-access-controls#project-level-access-controls) that defines what they can manage—such as secrets, certificates, or SSH access. These roles apply to both individuals and [user groups](/documentation/platform/groups), enabling scalable access across teams and environments. -![project dashboard](../../images/dashboard.png) +Project access is strictly scoped: only members of a project can view or manage its resources. If someone needs access but isn’t part of the project, they can submit an access request. -### Secrets +Each project in Infisical has its own [access control model](/documentation/platform/access-controls/role-based-access-controls#project-level-access-controls), distinct from [organization-level access control](/documentation/platform/access-controls/role-based-access-controls#organization-level-access-controls). While organization roles govern broader administrative access, project-level roles control what users, groups, and machine identities can do within the boundaries of a specific project—such as managing secrets, issuing certificates, or configuring SSH access. -To add a secret, press **Add Secret** button at the top of the dashboard. +Depending on the project type (e.g. Secrets Management, PKI, SSH), project-level access control supports advanced features like [temporary access](/documentation/platform/access-controls/temporary-access), [access requests](/documentation/platform/access-controls/access-requests), and [additional privileges](/documentation/platform/access-controls/additional-privileges). -![project add secret](../../images/platform/project/project-secrets-add.png) +![project roles](/images/platform/project/project-roles.png) -For a new project, it can be convenient to populate the dashboard by dropping a `.env` file into the provided pane as shown below: +To learn more about how permissions work in detail, refer to the [access control documentation](/documentation/platform/access-controls/overview). -![project drop env file](../../images/platform/project/project-secrets-drop-env.png) +## Audit Logs -To delete a secret, hover over it and press the **X** button that appears on the right side. +Infisical provides [audit logging](/documentation/platform/audit-logs) at the project level to help teams monitor activity and maintain accountability within a specific project. These logs capture all relevant events—such as secret access, certificate issuance, and SSH activity—that occur within the boundaries of that project. -![project delete secret](../../images/platform/project/project-secrets-delete.png) +Unlike the organization-level audit view, which aggregates logs across all projects in one centralized interface, the project-level audit view is scoped to a single project. This enables relevant project admins and contributors to review activity relevant to their work without having broader access to audit logs in other projects that they are not part of. -To delete multiple secrets at once, hover over and select the secrets you'd like to delete -and press the **Delete** button that appears at the top. +## Project Settings -![project delete secret batch](../../images/platform/project/project-secrets-delete-batch.png) +Each project has its own settings panel, with options that vary depending on the selected product type. These may include +setup and configuration for environments, tags, behaviors, encryption strategies, and other options. -### Search - -To search for specific secrets by their key name, you can use the search bar. - -![project search](../../images/platform/project/project-secrets-search.png) - -To assist you with finding secrets, you can also group them by similar prefixes and filter them by tags (if applicable). - -![project filter](../../images/platform/project/project-secrets-filter.png) - -### Hide/Un-hide - -To view/hide all secrets at once, toggle the hide or un-hide button. - -![project filter](../../images/platform/project/project-secrets-unhide.png) - -### Download as .env - -To download/export secrets back into a `.env` file, press the download button. - -![project download back env](../../images/platform/project/project-secrets-download-env.png) - -### Tags - -To better organize similar secrets, hover over them and label them with a tag. - -![project tag secret](../../images/platform/project/project-secrets-tag.png) - -### Comments - -To provide more context about a given secret, especially for your team, hover over it and press the comment button. - -![project comment secret](../../images/platform/project/project-secrets-comment.png) - -### Personal overrides - -Infisical employs the concept of **shared** and **personal** secrets to address the need -for common and custom secret values, or branching, amongst members of a team during software development. -To provide a helpful analogy: A shared value is to a `main` branch as a personal value is to a custom branch. - -Consider: - -- A team with users A, B, user C. -- A project with an environment containing a shared secret called D with the value E. - -Suppose user A overrides the value of secret D with the value F. - -Then: - -- If user A fetches the secret D back, they get the value F. -- If users B and C fetch the secret D back, they both get the value E. - - - Please keep in mind that secret reminders won't work with personal overrides. - - -![project override secret](../../images/platform/project/project-secrets-override.png) - -### Drawer - -To view the full details of each secret, you can hover over it and press on the ellipses button. - -![project secrets ellipses](../../images/platform/project/project-secrets-ellipses.png) - -This opens up a side-drawer: - -![project secrets drawer](../../images/platform/project/project-secrets-drawer.png) +Project settings are fully independent and reflect the capabilities of the associated product. diff --git a/docs/documentation/platform/secret-rotation/azure-client-secret.mdx b/docs/documentation/platform/secret-rotation/azure-client-secret.mdx index 046b772f6..c2da0a6a1 100644 --- a/docs/documentation/platform/secret-rotation/azure-client-secret.mdx +++ b/docs/documentation/platform/secret-rotation/azure-client-secret.mdx @@ -6,6 +6,7 @@ description: "Learn how to automatically rotate Azure Client Secrets." ## Prerequisites - Create an [Azure Client Secret Connection](/integrations/app-connections/azure-client-secrets). +- Ensure your network security policies allow incoming requests from Infisical to this rotation provider, if network restrictions apply. ## Create an Azure Client Secret Rotation in Infisical diff --git a/docs/documentation/platform/secret-rotation/ldap-password.mdx b/docs/documentation/platform/secret-rotation/ldap-password.mdx index feb3a664d..ba75e0af7 100644 --- a/docs/documentation/platform/secret-rotation/ldap-password.mdx +++ b/docs/documentation/platform/secret-rotation/ldap-password.mdx @@ -14,6 +14,7 @@ description: "Learn how to automatically rotate LDAP passwords." ## Prerequisites - Create an [LDAP Connection](/integrations/app-connections/ldap) with the **Secret Rotation** requirements +- Ensure your network security policies allow incoming requests from Infisical to this rotation provider, if network restrictions apply. ## Create an LDAP Password Rotation in Infisical diff --git a/docs/documentation/platform/secret-rotation/mssql-credentials.mdx b/docs/documentation/platform/secret-rotation/mssql-credentials.mdx index c20622f26..f609d11c5 100644 --- a/docs/documentation/platform/secret-rotation/mssql-credentials.mdx +++ b/docs/documentation/platform/secret-rotation/mssql-credentials.mdx @@ -30,6 +30,7 @@ An example creation statement might look like: To learn more about Microsoft SQL Server's permission system, please visit their [documentation](https://learn.microsoft.com/en-us/sql/t-sql/statements/grant-transact-sql?view=sql-server-ver16). +3. Ensure your network security policies allow incoming requests from Infisical to this rotation provider, if network restrictions apply. ## Create a Microsoft SQL Server Credentials Rotation in Infisical diff --git a/docs/documentation/platform/secret-rotation/mysql-credentials.mdx b/docs/documentation/platform/secret-rotation/mysql-credentials.mdx index d0088a29e..349b5b893 100644 --- a/docs/documentation/platform/secret-rotation/mysql-credentials.mdx +++ b/docs/documentation/platform/secret-rotation/mysql-credentials.mdx @@ -25,7 +25,7 @@ description: "Learn how to automatically rotate MySQL credentials." To learn more about the MySQL permission system, please visit their [documentation](https://dev.mysql.com/doc/refman/8.4/en/grant.html). - +3. Ensure your network security policies allow incoming requests from Infisical to this rotation provider, if network restrictions apply. ## Create a MySQL Credentials Rotation in Infisical diff --git a/docs/documentation/platform/secret-rotation/oracledb-credentials.mdx b/docs/documentation/platform/secret-rotation/oracledb-credentials.mdx index fb0887d33..0f72d62fa 100644 --- a/docs/documentation/platform/secret-rotation/oracledb-credentials.mdx +++ b/docs/documentation/platform/secret-rotation/oracledb-credentials.mdx @@ -31,6 +31,7 @@ description: "Learn how to automatically rotate Oracle Database credentials." To learn more about the Oracle Database permission system, please visit their [documentation](https://docs.oracle.com/en/database/oracle/oracle-database/19/dbseg/configuring-privilege-and-role-authorization.html). +3. Ensure your network security policies allow incoming requests from Infisical to this rotation provider, if network restrictions apply. ## Create an Oracle Database Credentials Rotation in Infisical diff --git a/docs/documentation/platform/secret-rotation/postgres-credentials.mdx b/docs/documentation/platform/secret-rotation/postgres-credentials.mdx index 55175d967..e8cddb0f4 100644 --- a/docs/documentation/platform/secret-rotation/postgres-credentials.mdx +++ b/docs/documentation/platform/secret-rotation/postgres-credentials.mdx @@ -27,6 +27,7 @@ description: "Learn how to automatically rotate PostgreSQL credentials." To learn more about PostgreSQL's permission system, please visit their [documentation](https://www.postgresql.org/docs/current/sql-grant.html). +3. Ensure your network security policies allow incoming requests from Infisical to this rotation provider, if network restrictions apply. ## Create a PostgreSQL Credentials Rotation in Infisical diff --git a/docs/documentation/platform/secrets-mgmt/overview.mdx b/docs/documentation/platform/secrets-mgmt/overview.mdx new file mode 100644 index 000000000..e00eba358 --- /dev/null +++ b/docs/documentation/platform/secrets-mgmt/overview.mdx @@ -0,0 +1,17 @@ +--- +title: "Secrets Management" +sidebarTitle: "Overview" +description: "Learn how to securely store, access, and manage sensitive application secrets." +--- + +Infisical provides a flexible platform for managing application secrets — such as API keys, database credentials, application configuration, and more — across every stage of the development lifecycle from local development to production. + +It helps teams eliminate hardcoded secrets, enforce access controls, and adopt secure workflows like secret rotation, dynamic secrets, and secrets syncs to external platforms. + +Core capabilities include: + +- Secret Stores: Secure, versioned storage scoped by [project](/documentation/platform/secrets-mgmt/project), [environment](/documentation/platform/secrets-mgmt/project#project-environments), and [path](/documentation/platform/folder). +- [Access Control](/documentation/platform/access-controls/overview): Fine-grained, identity-aware permissions for users and machines +- Secret Delivery: Access secrets via [CLI](/cli/overview), [SDKs](/sdks/overview) (Go, Node.js, Python, etc.), [HTTP API](/api-reference/overview/introduction), [agents](/integrations/platforms/infisical-agent), [Kubernetes Operator](/integrations/platforms/kubernetes/overview), [External Secrets Operator (ESO)](https://external-secrets.io/latest/provider/infisical), and more. +- Lifecycle Automation: Automate [secret rotation](/documentation/platform/secret-rotation/overview), generate [dynamic secrets](/documentation/platform/dynamic-secrets/overview), and enforce [approval-based workflows](/documentation/platform/pr-workflows). +- [Secrets Syncs](/integrations/secret-syncs/overview): Push secrets to external services like [GitHub](/integrations/secret-syncs/github), [GitLab](/integrations/secret-syncs/gitlab), [AWS Secrets Manager](/integrations/secret-syncs/aws-secrets-manager), [Vercel](/integrations/secret-syncs/vercel), and more. diff --git a/docs/documentation/platform/secrets-mgmt/project.mdx b/docs/documentation/platform/secrets-mgmt/project.mdx new file mode 100644 index 000000000..3e7c7cbc7 --- /dev/null +++ b/docs/documentation/platform/secrets-mgmt/project.mdx @@ -0,0 +1,115 @@ +--- +title: "Projects" +description: "Learn more and understand the concept of Infisical projects." +--- + +A secrets management project in Infisical is a dedicated workspace for managing application secrets such as API keys, database credentials, configuration, etc. used by your applications. + +Secrets are organized into a clear hierarchy of environments, folders, and individual secrets, making it easy to manage values across different stages of your development lifecycle (e.g., development, staging, production). + +## Project environments + +For both visual and organizational structure, Infisical allows splitting up secrets into environments (e.g., development, staging, production). In project settings, such environments can be +customized depending on the intended use case. + +![project secrets overview](/images/platform/project/project-environments.png) + +## Secrets Overview + +The **Secrets Overview** page captures a birds-eye-view of secrets and [folders](./folder) across environments. +This is useful for comparing secrets, identifying if anything is missing, and making quick changes. + +![project secrets overview](/images/platform/project/project-secrets-overview-open.png) + +## Secrets Dashboard + +The **Secrets Dashboard** page appears when you press to manage the secrets of a specific environment. + +![project dashboard](/images/dashboard.png) + +### Secrets + +To add a secret, press **Add Secret** button at the top of the dashboard. + +![project add secret](/images/platform/project/project-secrets-add.png) + +For a new project, it can be convenient to populate the dashboard by dropping a `.env` file into the provided pane as shown below: + +![project drop env file](/images/platform/project/project-secrets-drop-env.png) + +To delete a secret, hover over it and press the **X** button that appears on the right side. + +![project delete secret](/images/platform/project/project-secrets-delete.png) + +To delete multiple secrets at once, hover over and select the secrets you'd like to delete +and press the **Delete** button that appears at the top. + +![project delete secret batch](/images/platform/project/project-secrets-delete-batch.png) + +### Search + +To search for specific secrets by their key name, you can use the search bar. + +![project search](/images/platform/project/project-secrets-search.png) + +To assist you with finding secrets, you can also group them by similar prefixes and filter them by tags (if applicable). + +![project filter](/images/platform/project/project-secrets-filter.png) + +### Hide/Un-hide + +To view/hide all secrets at once, toggle the hide or un-hide button. + +![project filter](/images/platform/project/project-secrets-unhide.png) + +### Download as .env + +To download/export secrets back into a `.env` file, press the download button. + +![project download back env](/images/platform/project/project-secrets-download-env.png) + +### Tags + +To better organize similar secrets, hover over them and label them with a tag. + +![project tag secret](/images/platform/project/project-secrets-tag.png) + +### Comments + +To provide more context about a given secret, especially for your team, hover over it and press the comment button. + +![project comment secret](/images/platform/project/project-secrets-comment.png) + +### Personal overrides + +Infisical employs the concept of **shared** and **personal** secrets to address the need +for common and custom secret values, or branching, amongst members of a team during software development. +To provide a helpful analogy: A shared value is to a `main` branch as a personal value is to a custom branch. + +Consider: + +- A team with users A, B, user C. +- A project with an environment containing a shared secret called D with the value E. + +Suppose user A overrides the value of secret D with the value F. + +Then: + +- If user A fetches the secret D back, they get the value F. +- If users B and C fetch the secret D back, they both get the value E. + + + Please keep in mind that secret reminders won't work with personal overrides. + + +![project override secret](/images/platform/project/project-secrets-override.png) + +### Drawer + +To view the full details of each secret, you can hover over it and press on the ellipses button. + +![project secrets ellipses](/images/platform/project/project-secrets-ellipses.png) + +This opens up a side-drawer: + +![project secrets drawer](/images/platform/project/project-secrets-drawer.png) diff --git a/docs/documentation/platform/ssh/host-groups.mdx b/docs/documentation/platform/ssh/host-groups.mdx index 877291183..373141e05 100644 --- a/docs/documentation/platform/ssh/host-groups.mdx +++ b/docs/documentation/platform/ssh/host-groups.mdx @@ -1,5 +1,5 @@ --- -title: "Infisical SSH" +title: "Host Groups" sidebarTitle: "Host Groups" description: "Learn how to organize SSH hosts into groups and manage access policies at scale." --- diff --git a/docs/documentation/platform/ssh/overview.mdx b/docs/documentation/platform/ssh/overview.mdx index a252e1f71..d9606214d 100644 --- a/docs/documentation/platform/ssh/overview.mdx +++ b/docs/documentation/platform/ssh/overview.mdx @@ -1,5 +1,5 @@ --- -title: "Infisical SSH" +title: "Overview" sidebarTitle: "Overview" description: "Learn how to securely provision user SSH access to your infrastructure using SSH certificates." --- diff --git a/docs/images/platform/events/access-dev.png b/docs/images/platform/events/access-dev.png new file mode 100644 index 000000000..3f44fc51b Binary files /dev/null and b/docs/images/platform/events/access-dev.png differ diff --git a/docs/images/platform/events/access-full.png b/docs/images/platform/events/access-full.png new file mode 100644 index 000000000..d41260c17 Binary files /dev/null and b/docs/images/platform/events/access-full.png differ diff --git a/docs/images/platform/events/access-path.png b/docs/images/platform/events/access-path.png new file mode 100644 index 000000000..3bfa9b28b Binary files /dev/null and b/docs/images/platform/events/access-path.png differ diff --git a/docs/images/platform/identities/auth-templates/attach-template-form.png b/docs/images/platform/identities/auth-templates/attach-template-form.png new file mode 100644 index 000000000..7ac01d797 Binary files /dev/null and b/docs/images/platform/identities/auth-templates/attach-template-form.png differ diff --git a/docs/images/platform/identities/auth-templates/create-template-button.png b/docs/images/platform/identities/auth-templates/create-template-button.png new file mode 100644 index 000000000..859031ea3 Binary files /dev/null and b/docs/images/platform/identities/auth-templates/create-template-button.png differ diff --git a/docs/images/platform/identities/auth-templates/ldap-template.png b/docs/images/platform/identities/auth-templates/ldap-template.png new file mode 100644 index 000000000..04e9c5a1a Binary files /dev/null and b/docs/images/platform/identities/auth-templates/ldap-template.png differ diff --git a/docs/images/platform/identities/auth-templates/machine-identity-page.png b/docs/images/platform/identities/auth-templates/machine-identity-page.png new file mode 100644 index 000000000..4574afa3f Binary files /dev/null and b/docs/images/platform/identities/auth-templates/machine-identity-page.png differ diff --git a/docs/images/platform/identities/auth-templates/template-usages-modal.png b/docs/images/platform/identities/auth-templates/template-usages-modal.png new file mode 100644 index 000000000..7f53c7a45 Binary files /dev/null and b/docs/images/platform/identities/auth-templates/template-usages-modal.png differ diff --git a/docs/images/platform/identities/auth-templates/template-usages.png b/docs/images/platform/identities/auth-templates/template-usages.png new file mode 100644 index 000000000..67e71b53a Binary files /dev/null and b/docs/images/platform/identities/auth-templates/template-usages.png differ diff --git a/docs/images/platform/identities/auth-templates/templates-section.png b/docs/images/platform/identities/auth-templates/templates-section.png new file mode 100644 index 000000000..a26ebd820 Binary files /dev/null and b/docs/images/platform/identities/auth-templates/templates-section.png differ diff --git a/docs/images/platform/organization/organization-app-connections.png b/docs/images/platform/organization/organization-app-connections.png new file mode 100644 index 000000000..7da9221fd Binary files /dev/null and b/docs/images/platform/organization/organization-app-connections.png differ diff --git a/docs/images/platform/organization/organization-billing.png b/docs/images/platform/organization/organization-billing.png new file mode 100644 index 000000000..498812938 Binary files /dev/null and b/docs/images/platform/organization/organization-billing.png differ diff --git a/docs/images/platform/organization/organization-machine-identities.png b/docs/images/platform/organization/organization-machine-identities.png index 4400b9f62..85657e137 100644 Binary files a/docs/images/platform/organization/organization-machine-identities.png and b/docs/images/platform/organization/organization-machine-identities.png differ diff --git a/docs/images/platform/organization/organization-members-roles-add-perm.png b/docs/images/platform/organization/organization-members-roles-add-perm.png deleted file mode 100644 index 0e87e3e5f..000000000 Binary files a/docs/images/platform/organization/organization-members-roles-add-perm.png and /dev/null differ diff --git a/docs/images/platform/organization/organization-members-roles.png b/docs/images/platform/organization/organization-members-roles.png deleted file mode 100644 index 08c2d1e90..000000000 Binary files a/docs/images/platform/organization/organization-members-roles.png and /dev/null differ diff --git a/docs/images/platform/organization/organization-members.png b/docs/images/platform/organization/organization-members.png deleted file mode 100644 index a79d3bbe0..000000000 Binary files a/docs/images/platform/organization/organization-members.png and /dev/null differ diff --git a/docs/images/platform/organization/organization-projects.png b/docs/images/platform/organization/organization-projects.png index 82c0aee57..b8a294632 100644 Binary files a/docs/images/platform/organization/organization-projects.png and b/docs/images/platform/organization/organization-projects.png differ diff --git a/docs/images/platform/organization/organization-roles.png b/docs/images/platform/organization/organization-roles.png new file mode 100644 index 000000000..04fd0043d Binary files /dev/null and b/docs/images/platform/organization/organization-roles.png differ diff --git a/docs/images/platform/organization/organization-settings-auth.png b/docs/images/platform/organization/organization-settings-auth.png deleted file mode 100644 index fd7a946e0..000000000 Binary files a/docs/images/platform/organization/organization-settings-auth.png and /dev/null differ diff --git a/docs/images/platform/organization/organization-settings-general.png b/docs/images/platform/organization/organization-settings-general.png deleted file mode 100644 index 9467b6005..000000000 Binary files a/docs/images/platform/organization/organization-settings-general.png and /dev/null differ diff --git a/docs/images/platform/organization/organization-settings.png b/docs/images/platform/organization/organization-settings.png new file mode 100644 index 000000000..873852b44 Binary files /dev/null and b/docs/images/platform/organization/organization-settings.png differ diff --git a/docs/images/platform/organization/organization-usage-billing.png b/docs/images/platform/organization/organization-usage-billing.png deleted file mode 100644 index 345a437f7..000000000 Binary files a/docs/images/platform/organization/organization-usage-billing.png and /dev/null differ diff --git a/docs/images/platform/organization/organization-users.png b/docs/images/platform/organization/organization-users.png new file mode 100644 index 000000000..489fd2346 Binary files /dev/null and b/docs/images/platform/organization/organization-users.png differ diff --git a/docs/images/platform/organization/organization.png b/docs/images/platform/organization/organization.png new file mode 100644 index 000000000..f72c53286 Binary files /dev/null and b/docs/images/platform/organization/organization.png differ diff --git a/docs/images/platform/project/project-roles.png b/docs/images/platform/project/project-roles.png new file mode 100644 index 000000000..c7ae3f7f3 Binary files /dev/null and b/docs/images/platform/project/project-roles.png differ diff --git a/docs/images/platform/project/project-types.png b/docs/images/platform/project/project-types.png new file mode 100644 index 000000000..8156094f9 Binary files /dev/null and b/docs/images/platform/project/project-types.png differ diff --git a/docs/integrations/platforms/kubernetes-injector.mdx b/docs/integrations/platforms/kubernetes-injector.mdx index aacdcfbbb..7fea3dc92 100644 --- a/docs/integrations/platforms/kubernetes-injector.mdx +++ b/docs/integrations/platforms/kubernetes-injector.mdx @@ -105,44 +105,93 @@ The templates hold an array of templates that will be rendered and injected into ### Authentication -The Infisical Agent Injector only supports Machine Identity [Kubernetes Auth](/documentation/platform/identities/kubernetes-auth) authentication at the moment. +The Infisical Agent Injector supports Machine Identity [Kubernetes Auth](/documentation/platform/identities/kubernetes-auth) and [LDAP Auth](/documentation/platform/identities/ldap-auth) authentication. -To configure Kubernetes Auth, you need to set the `auth.type` field to `kubernetes` and set the `auth.config.identity-id` to the ID of the machine identity you wish to use for authentication. -```yaml -auth: - type: "kubernetes" - config: - identity-id: "" -``` + + -### Example ConfigMap -```yaml config-map.yaml -apiVersion: v1 -kind: ConfigMap -metadata: - name: demo-config-map -data: - config.yaml: | - infisical: - address: "https://app.infisical.com" - auth: - type: "kubernetes" - config: - identity-id: "" - templates: - - destination-path: "/path/to/save/secrets/file.txt" - template-content: | - {{- with secret "" "dev" "/" }} - {{- range . }} - {{ .Key }}={{ .Value }} - {{- end }} - {{- end }} -``` + To configure Kubernetes Auth, you need to set the `auth.type` field to `kubernetes` and set the `auth.config.identity-id` to the ID of the machine identity you wish to use for authentication. + ```yaml + auth: + type: "kubernetes" + config: + identity-id: "" + ``` -```bash -kubectl apply -f config-map.yaml -``` + ### Example ConfigMap + ```yaml config-map.yaml + apiVersion: v1 + kind: ConfigMap + metadata: + name: demo-config-map + data: + config.yaml: | + infisical: + address: "https://app.infisical.com" + auth: + type: "kubernetes" + config: + identity-id: "" + templates: + - destination-path: "/path/to/save/secrets/file.txt" + template-content: | + {{- with secret "" "dev" "/" }} + {{- range . }} + {{ .Key }}={{ .Value }} + {{- end }} + {{- end }} + ``` + + ```bash + kubectl apply -f config-map.yaml + ``` + + + + To configure LDAP Auth, you need to set the `auth.type` field to `ldap-auth` and set the `auth.config.identity-id` to the ID of the machine identity you wish to use for authentication. Configure the `auth.config.username` and `auth.config.password` to the username and password of the LDAP user to authenticate with. + + ```yaml + auth: + type: "ldap-auth" + config: + identity-id: "" + username: "" + password: "" + ``` + + ### Example ConfigMap + ```yaml config-map.yaml + apiVersion: v1 + kind: ConfigMap + metadata: + name: demo-config-map + data: + config.yaml: | + infisical: + address: "https://app.infisical.com" + auth: + type: "ldap-auth" + config: + identity-id: "" + username: "" + password: "" + templates: + - destination-path: "/path/to/save/secrets/file.txt" + template-content: | + {{- with secret "" "dev" "/" }} + {{- range . }} + {{ .Key }}={{ .Value }} + {{- end }} + {{- end }} + ``` + + ```bash + kubectl apply -f config-map.yaml + ``` + + + To use the config map in your pod, you will need to add the `org.infisical.com/agent-config-map` annotation to your pod's deployment. The value of the annotation is the name of the config map you created above. ```yaml diff --git a/docs/integrations/platforms/kubernetes/infisical-dynamic-secret-crd.mdx b/docs/integrations/platforms/kubernetes/infisical-dynamic-secret-crd.mdx index 5962e4c10..76dd24cf7 100644 --- a/docs/integrations/platforms/kubernetes/infisical-dynamic-secret-crd.mdx +++ b/docs/integrations/platforms/kubernetes/infisical-dynamic-secret-crd.mdx @@ -68,6 +68,11 @@ spec: serviceAccountKeyFilePath: gcpIdTokenAuth: identityId: + ldapAuth: + identityId: + credentialsRef: + secretName: # ldap-auth-credentials + secretNamespace: # default kubernetesAuth: identityId: serviceAccountRef: @@ -105,15 +110,15 @@ kind: Secret ### InfisicalDynamicSecret CRD properties - If you are fetching secrets from a self-hosted instance of Infisical set the value of `hostAPI` to - ` https://your-self-hosted-instace.com/api` + If you are fetching secrets from a self-hosted instance of Infisical set the value of `hostAPI` to + `https://your-self-hosted-instace.com/api` When `hostAPI` is not defined the operator fetches secrets from Infisical Cloud. - If you have installed your Infisical instance within the same cluster as the Infisical operator, you can optionally access the Infisical backend's service directly without having to route through the public internet. + If you have installed your Infisical instance within the same cluster as the Infisical operator, you can optionally access the Infisical backend's service directly without having to route through the public internet. To achieve this, use the following address for the hostAPI field: - + ``` bash http://..svc.cluster.local:4000/api ``` @@ -126,18 +131,19 @@ When `hostAPI` is not defined the operator fetches secrets from Infisical Cloud. The `leaseTTL` is a string-formatted duration that defines the time the lease should last for the dynamic secret. - The format of the field is `[duration][unit]` where `duration` is a number and `unit` is a string representing the unit of time. +The format of the field is `[duration][unit]` where `duration` is a number and `unit` is a string representing the unit of time. - The following units are supported: +The following units are supported: - - `s` for seconds (must be at least 5 seconds) - - `m` for minutes - - `h` for hours - - `d` for days +- `s` for seconds (must be at least 5 seconds) +- `m` for minutes +- `h` for hours +- `d` for days - - The lease duration at most be 1 day (24 hours). And the TTL must be less than the max TTL defined on the dynamic secret. - + + The lease duration at most be 1 day (24 hours). And the TTL must be less than the max TTL defined on the dynamic secret. + + @@ -212,7 +218,7 @@ spec: The `dynamicSecret` field is used to specify which dynamic secret to create leases for. The required fields are `secretName`, `projectId`, `secretsPath`, and `environmentSlug`. - + ```yaml spec: dynamicSecret: @@ -300,7 +306,6 @@ The available authentication methods are `universalAuth`, `kubernetesAuth`, `aws - `autoCreateServiceAccountToken`: If set to `true`, the operator will automatically create a short-lived service account token on-demand for the service account. Defaults to `false`. - `serviceAccountTokenAudiences`: Optionally specify audience for the service account token. This field is only relevant if you have set `autoCreateServiceAccountToken` to `true`. No audience is specified by default. - Example: ```yaml @@ -316,7 +321,40 @@ The available authentication methods are `universalAuth`, `kubernetesAuth`, `aws ``` + + The LDAP machine identity authentication method is used to authenticate with a configured LDAP directory. [Read more about LDAP Auth](/documentation/platform/identities/ldap-auth). + Valid fields: + - `identityId`: The identity ID of the machine identity you created. + - `credentialsRef`: The name and namespace of the Kubernetes secret that stores the LDAP credentials. + - `credentialsRef.secretName`: The name of the Kubernetes secret. + - `credentialsRef.secretNamespace`: The namespace of the Kubernetes secret. + + Example: + + ```yaml + # infisical-push-secret.yaml + spec: + ldapAuth: + identityId: + credentialsRef: + secretName: + secretNamespace: + ``` + + ```yaml + # machine-identity-credentials.yaml + apiVersion: v1 + kind: Secret + metadata: + name: ldap-auth-credentials + type: Opaque + stringData: + username: + password: + ``` + + The AWS IAM machine identity authentication method is used to authenticate with Infisical. [Read more about AWS IAM Auth](/documentation/platform/identities/aws-auth). @@ -391,7 +429,7 @@ The available authentication methods are `universalAuth`, `kubernetesAuth`, `aws This block defines the TLS settings to use for connecting to the Infisical instance. - + Fields: This block defines the reference to the CA certificate to use for connecting to the Infisical instance with SSL/TLS. @@ -444,7 +482,7 @@ metadata: name: nginx-deployment labels: app: nginx - annotations: + annotations: secrets.infisical.com/auto-reload: "true" # <- redeployment annotation spec: replicas: 1 @@ -467,7 +505,7 @@ spec: ``` - #### How it works - When the lease changes, the operator will check to see which deployments are using the operator-managed Kubernetes secret that received the update. + #### How it works + When the lease changes, the operator will check to see which deployments are using the operator-managed Kubernetes secret that received the update. Then, for each deployment that has this annotation present, a rolling update will be triggered. A redeployment won't happen if the lease is renewed, only if it's recreated. diff --git a/docs/integrations/platforms/kubernetes/infisical-push-secret-crd.mdx b/docs/integrations/platforms/kubernetes/infisical-push-secret-crd.mdx index d87648bbf..d5a9c1ef4 100644 --- a/docs/integrations/platforms/kubernetes/infisical-push-secret-crd.mdx +++ b/docs/integrations/platforms/kubernetes/infisical-push-secret-crd.mdx @@ -5,9 +5,9 @@ description: "Learn how to use the InfisicalPushSecret CRD to push and manage se --- -## Overview +## Overview -The **InfisicalPushSecret** CRD allows you to create secrets in your Kubernetes cluster and push them to Infisical. +The **InfisicalPushSecret** CRD allows you to create secrets in your Kubernetes cluster and push them to Infisical. This CRD offers the following features: @@ -70,6 +70,11 @@ Before applying the InfisicalPushSecret CRD, you need to create a Kubernetes sec serviceAccountRef: name: namespace: + ldapAuth: + identityId: + credentialsRef: + secretName: # ldap-auth-credentials + secretNamespace: # default universalAuth: credentialsRef: secretName: # universal-auth-credentials @@ -104,15 +109,15 @@ After applying the InfisicalPushSecret CRD, you should notice that the secrets y ## InfisicalPushSecret CRD properties - If you are fetching secrets from a self-hosted instance of Infisical set the value of `hostAPI` to - ` https://your-self-hosted-instace.com/api` + If you are fetching secrets from a self-hosted instance of Infisical set the value of `hostAPI` to + `https://your-self-hosted-instace.com/api` When `hostAPI` is not defined the operator fetches secrets from Infisical Cloud. - If you have installed your Infisical instance within the same cluster as the Infisical operator, you can optionally access the Infisical backend's service directly without having to route through the public internet. + If you have installed your Infisical instance within the same cluster as the Infisical operator, you can optionally access the Infisical backend's service directly without having to route through the public internet. To achieve this, use the following address for the hostAPI field: - + ``` bash http://..svc.cluster.local:4000/api ``` @@ -187,7 +192,7 @@ After applying the InfisicalPushSecret CRD, you should notice that the secrets y The `destination` field is used to specify where you want to create the secrets in Infisical. The required fields are `projectId`, `environmentSlug`, and `secretsPath`. - + ```yaml spec: destination: @@ -212,7 +217,7 @@ After applying the InfisicalPushSecret CRD, you should notice that the secrets y The `push` field is used to define what you want to push to Infisical. Currently the operator only supports pushing Kubernetes secrets to Infisical. An example of the `push` field is shown below. - + @@ -220,7 +225,7 @@ After applying the InfisicalPushSecret CRD, you should notice that the secrets y - Example usage of the `push.secret` field: + Example usage of the `push.secret` field: ```yaml infisical-push-secret.yaml push: @@ -282,7 +287,7 @@ After applying the InfisicalPushSecret CRD, you should notice that the secrets y spec: universalAuth: credentialsRef: - secretName: + secretName: secretNamespace: ``` @@ -324,7 +329,39 @@ After applying the InfisicalPushSecret CRD, you should notice that the secrets y namespace: ``` + + The LDAP machine identity authentication method is used to authenticate with a configured LDAP directory. [Read more about LDAP Auth](/documentation/platform/identities/ldap-auth). + Valid fields: + - `identityId`: The identity ID of the machine identity you created. + - `credentialsRef`: The name and namespace of the Kubernetes secret that stores the LDAP credentials. + - `credentialsRef.secretName`: The name of the Kubernetes secret. + - `credentialsRef.secretNamespace`: The namespace of the Kubernetes secret. + + Example: + + ```yaml + # infisical-push-secret.yaml + spec: + ldapAuth: + identityId: + credentialsRef: + secretName: + secretNamespace: + ``` + + ```yaml + # machine-identity-credentials.yaml + apiVersion: v1 + kind: Secret + metadata: + name: ldap-auth-credentials + type: Opaque + stringData: + username: + password: + ``` + The AWS IAM machine identity authentication method is used to authenticate with Infisical. [Read more about AWS IAM Auth](/documentation/platform/identities/aws-auth). @@ -398,7 +435,7 @@ After applying the InfisicalPushSecret CRD, you should notice that the secrets y This block defines the TLS settings to use for connecting to the Infisical instance. - + Fields: This block defines the reference to the CA certificate to use for connecting to the Infisical instance with SSL/TLS. @@ -438,7 +475,7 @@ Using Go templates, you can format, combine, and create new key-value pairs of s Use this option when you would like to push **only** a subset of secrets from the Kubernetes secret to Infisical. - Define secret keys and their corresponding templates. + Define secret keys and their corresponding templates. Each data value uses a Golang template with access to all secrets defined in the `push.secret.secretName` Kubernetes secret. Secrets are structured as follows: @@ -483,7 +520,7 @@ A generator is defined as a custom resource (`ClusterGenerator`) within the clus Because of this behavior, you may want to disable automatic syncing for the `InfisicalPushSecret` resource to avoid continuous regeneration of secrets. This can be done by omitting the `resyncInterval` field from the InfisicalPushSecret CRD. -### Example usage +### Example usage ```yaml push: secret: @@ -625,4 +662,4 @@ After applying, you should notice that the secrets have been pushed to Infisical ```bash kubectl apply -f source-push-secret.yaml # The secret that you're referencing in the InfisicalPushSecret CRD push.secret field kubectl apply -f example-infisical-push-secret-crd.yaml # The InfisicalPushSecret CRD itself -``` \ No newline at end of file +``` diff --git a/docs/integrations/platforms/kubernetes/infisical-secret-crd.mdx b/docs/integrations/platforms/kubernetes/infisical-secret-crd.mdx index 145737e96..d0e403b79 100644 --- a/docs/integrations/platforms/kubernetes/infisical-secret-crd.mdx +++ b/docs/integrations/platforms/kubernetes/infisical-secret-crd.mdx @@ -44,15 +44,15 @@ spec: The following properties help define what instance of Infisical the operator will interact with, the interval it will sync secrets and any CA certificates that may be required to connect. - If you are fetching secrets from a self-hosted instance of Infisical set the value of `hostAPI` to + If you are fetching secrets from a self-hosted instance of Infisical set the value of `hostAPI` to ` https://your-self-hosted-instace.com/api` When `hostAPI` is not defined the operator fetches secrets from Infisical Cloud. - If you have installed your Infisical instance within the same cluster as the Infisical operator, you can optionally access the Infisical backend's service directly without having to route through the public internet. + If you have installed your Infisical instance within the same cluster as the Infisical operator, you can optionally access the Infisical backend's service directly without having to route through the public internet. To achieve this, use the following address for the hostAPI field: - + ``` bash http://..svc.cluster.local:4000/api ``` @@ -110,7 +110,7 @@ The list of available authentication methods are shown below. Once you have created your machine identity and added it to your project(s), you will need to create a Kubernetes secret containing the identity credentials. To quickly create a Kubernetes secret containing the identity credentials, you can run the command below. - + Make sure you replace `` with the identity client ID and `` with the identity client secret. ``` bash @@ -525,49 +525,6 @@ spec: ... ``` - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -747,6 +704,59 @@ spec: + + The LDAP machine identity authentication method is used to authenticate with Infisical using the configured LDAP directory. The username and password needs to be stored in a Kubernetes secret. This block defines the reference to the name and namespace of secret that stores these credentials. + + + + You need to create a machine identity, and give it access to the project(s) you want to interact with. You can [read more about machine identities here](/documentation/platform/identities/universal-auth). + + + Once you have created your machine identity and added it to your project(s), you will need to create a Kubernetes secret containing the identity credentials. + To quickly create a Kubernetes secret containing the identity credentials, you can run the command below. + + Make sure you replace `` with the identity LDAP username and `` with the identity LDAP password. + + ``` bash + kubectl create secret generic ldap-auth-credentials --from-literal=username="" --from-literal=password="" + ``` + + + + Once the secret is created, add the `secretName` and `secretNamespace` of the secret that was just created under `authentication.ldapAuth.credentialsRef` field in the InfisicalSecret resource. + + + + + + Make sure to also populate the `secretsScope` field with the project slug + _`projectSlug`_, environment slug _`envSlug`_, and secrets path + _`secretsPath`_ that you want to fetch secrets from. Please see the example + below. + + +## Example + +```yaml +apiVersion: secrets.infisical.com/v1alpha1 +kind: InfisicalSecret +metadata: + name: infisicalsecret-sample-crd +spec: + authentication: + ldapAuth: + secretsScope: + projectSlug: # <-- project slug + envSlug: # "dev", "staging", "prod", etc.. + secretsPath: "" # Root is "/" + identityId: + credentialsRef: + secretName: ldap-auth-credentials # <-- name of the Kubernetes secret that stores our machine identity credentials + secretNamespace: default # <-- namespace of the Kubernetes secret that stores our machine identity credentials +``` + + + The service token required to authenticate with Infisical needs to be stored in a Kubernetes secret. This block defines the reference to the name and namespace of secret that stores this service token. @@ -826,7 +836,7 @@ managedKubeSecretReferences: The name of the managed Kubernetes secret to be created -The namespace of the managed Kubernetes secret to be created. +The namespace of the managed Kubernetes secret to be created. Override the default Opaque type for managed secrets with this field. Useful for creating kubernetes.io/dockerconfigjson secrets. @@ -855,8 +865,8 @@ Using Go templates, you can format, combine, and create new key-value pairs from - This property controls what secrets are included in your managed secret when using templates. - When set to `true`, all secrets fetched from your Infisical project will be added into your managed Kubernetes secret resource. + This property controls what secrets are included in your managed secret when using templates. + When set to `true`, all secrets fetched from your Infisical project will be added into your managed Kubernetes secret resource. **Use this option when you would like to sync all secrets from Infisical to Kubernetes but want to template a subset of them.** When set to `false`, only secrets defined in the `managedKubeSecretReferences[].template.data` field of the template will be included in the managed secret. @@ -864,7 +874,7 @@ Use this option when you would like to sync **only** a subset of secrets from In -Define secret keys and their corresponding templates. +Define secret keys and their corresponding templates. Each data value uses a Golang template with access to all secrets retrieved from the specified scope. Secrets are structured as follows: @@ -928,7 +938,9 @@ The properties includes defining the name and namespace of the Kubernetes config The Infisical operator will automatically create the Kubernetes config map in the specified name/namespace and ensure it stays up-to-date. If a config map already exists in the specified namespace, the operator will update the existing config map with the new data. - The usage of config maps is only intended for storing non-sensitive data. If you are looking to store sensitive data, please use the [managed secret](#operator-managed-secrets) property instead. + The usage of config maps is only intended for storing non-sensitive data. If + you are looking to store sensitive data, please use the [managed + secret](#operator-managed-secrets) property instead. @@ -937,25 +949,24 @@ The Infisical operator will automatically create the Kubernetes config map in th The name of the managed Kubernetes config map that your Infisical data will be stored in. - The namespace of the managed Kubernetes config map that your Infisical data will be stored in. + The namespace of the managed Kubernetes config map that your Infisical data will be stored in. Creation policies allow you to control whether or not owner references should be added to the managed Kubernetes config map that is generated by the Infisical operator. This is useful for tools such as ArgoCD, where every resource requires an owner reference; otherwise, it will be pruned automatically. - #### Available options +#### Available options - - `Orphan` (default) - - `Owner` +- `Orphan` (default) +- `Owner` - - When creation policy is set to `Owner`, the `InfisicalSecret` CRD must be in - the same namespace as where the managed kubernetes config map. - + + When creation policy is set to `Owner`, the `InfisicalSecret` CRD must be in + the same namespace as where the managed kubernetes config map. + - #### Managed ConfigMap Templating Fetching secrets from Infisical as is via the operator may not be enough. This is where templating functionality may be helpful. @@ -964,67 +975,68 @@ Using Go templates, you can format, combine, and create new key-value pairs from - This property controls what secrets are included in your managed config map when using templates. - When set to `true`, all secrets fetched from your Infisical project will be added into your managed Kubernetes config map resource. + This property controls what secrets are included in your managed config map when using templates. + When set to `true`, all secrets fetched from your Infisical project will be added into your managed Kubernetes config map resource. **Use this option when you would like to sync all secrets from Infisical to Kubernetes but want to template a subset of them.** - When set to `false`, only secrets defined in the `managedKubeConfigMapReferences[].template.data` field of the template will be included in the managed config map. - Use this option when you would like to sync **only** a subset of secrets from Infisical to Kubernetes. +When set to `false`, only secrets defined in the `managedKubeConfigMapReferences[].template.data` field of the template will be included in the managed config map. +Use this option when you would like to sync **only** a subset of secrets from Infisical to Kubernetes. - Define secret keys and their corresponding templates. + Define secret keys and their corresponding templates. Each data value uses a Golang template with access to all secrets retrieved from the specified scope. - Secrets are structured as follows: +Secrets are structured as follows: - ```golang - type TemplateSecret struct { - Value string `json:"value"` - SecretPath string `json:"secretPath"` - } - ``` +```golang +type TemplateSecret struct { + Value string `json:"value"` + SecretPath string `json:"secretPath"` +} +``` - #### Example template configuration: +#### Example template configuration: - ```yaml - managedKubeConfigMapReferences: - - configMapName: managed-configmap - configMapNamespace: default - template: - includeAllSecrets: true - data: - # Create new key that doesn't exist in your Infisical project using values of other secrets - SITE_URL: "{{ .SITE_URL.Value }}" - # Override an existing key in Infisical project with a new value using values of other secrets - API_URL: "https://api.{{.SITE_URL.Value}}.{{.REGION.Value}}.com" - ``` +```yaml +managedKubeConfigMapReferences: + - configMapName: managed-configmap + configMapNamespace: default + template: + includeAllSecrets: true + data: + # Create new key that doesn't exist in your Infisical project using values of other secrets + SITE_URL: "{{ .SITE_URL.Value }}" + # Override an existing key in Infisical project with a new value using values of other secrets + API_URL: "https://api.{{.SITE_URL.Value}}.{{.REGION.Value}}.com" +``` - For this example, let's assume the following secrets exist in your Infisical project: +For this example, let's assume the following secrets exist in your Infisical project: - ``` - SITE_URL = "https://example.com" - REGION = "us-east-1" - API_URL = "old-url" # This will be overridden - ``` +``` +SITE_URL = "https://example.com" +REGION = "us-east-1" +API_URL = "old-url" # This will be overridden +``` - The resulting managed Kubernetes config map will then contain: +The resulting managed Kubernetes config map will then contain: - ``` - # Original config map data (from includeAllSecrets: true) - SITE_URL = "https://example.com" - REGION = "us-east-1" +``` +# Original config map data (from includeAllSecrets: true) +SITE_URL = "https://example.com" +REGION = "us-east-1" - # New and overridden config map data - SITE_URL = "https://example.com" - API_URL = "https://api.example.com.us-east-1.com" # Existing secret overridden by template - ``` +# New and overridden config map data +SITE_URL = "https://example.com" +API_URL = "https://api.example.com.us-east-1.com" # Existing secret overridden by template +``` - To help transform your config map data further, the operator provides a set of built-in functions that you can use in your templates. +To help transform your config map data further, the operator provides a set of built-in functions that you can use in your templates. + +### Available templating functions + +Please refer to the [templating functions documentation](/integrations/platforms/kubernetes/overview#available-helper-functions) for more information. - ### Available templating functions - - Please refer to the [templating functions documentation](/integrations/platforms/kubernetes/overview#available-helper-functions) for more information. ## Applying CRD @@ -1061,8 +1073,6 @@ To verify that the operator has successfully created the managed secret, you can - - ## Using Managed Secret In Your Deployment To make use of the managed secret created by the operator into your deployment can be achieved through several methods. @@ -1071,45 +1081,45 @@ Here, we will highlight three of the most common ways to utilize it. Learn more This will take all the secrets from your managed secret and expose them to your container - ````yaml - envFrom: - - secretRef: - name: managed-secret # managed secret name - ``` +````yaml + envFrom: + - secretRef: + name: managed-secret # managed secret name + ``` - Example usage in a deployment - ```yaml - apiVersion: apps/v1 - kind: Deployment - metadata: - name: nginx-deployment - labels: + Example usage in a deployment + ```yaml + apiVersion: apps/v1 +kind: Deployment +metadata: + name: nginx-deployment + labels: + app: nginx +spec: + replicas: 1 + selector: + matchLabels: app: nginx - spec: - replicas: 1 - selector: - matchLabels: + template: + metadata: + labels: app: nginx - template: - metadata: - labels: - app: nginx - spec: - containers: - - name: nginx - image: nginx:1.14.2 - envFrom: - - secretRef: - name: managed-secret # <- name of managed secret - ports: - - containerPort: 80 - ```` + spec: + containers: + - name: nginx + image: nginx:1.14.2 + envFrom: + - secretRef: + name: managed-secret # <- name of managed secret + ports: + - containerPort: 80 +```` - - This will allow you to select individual secrets by key name from your managed secret and expose them to your container - + + This will allow you to select individual secrets by key name from your managed secret and expose them to your container + ```yaml env: - name: SECRET_NAME # The environment variable's name which is made available in the container @@ -1119,37 +1129,38 @@ Here, we will highlight three of the most common ways to utilize it. Learn more key: SOME_SECRET_KEY # The name of the key which exists in the managed secret ``` - Example usage in a deployment +Example usage in a deployment - ```yaml - apiVersion: apps/v1 - kind: Deployment - metadata: - name: nginx-deployment - labels: +```yaml +apiVersion: apps/v1 +kind: Deployment +metadata: + name: nginx-deployment + labels: + app: nginx +spec: + replicas: 1 + selector: + matchLabels: app: nginx - spec: - replicas: 1 - selector: - matchLabels: + template: + metadata: + labels: app: nginx - template: - metadata: - labels: - app: nginx - spec: - containers: - - name: nginx - image: nginx:1.14.2 - env: - - name: STRIPE_API_SECRET - valueFrom: - secretKeyRef: - name: managed-secret # <- name of managed secret - key: STRIPE_API_SECRET - ports: - - containerPort: 80 - ``` + spec: + containers: + - name: nginx + image: nginx:1.14.2 + env: + - name: STRIPE_API_SECRET + valueFrom: + secretKeyRef: + name: managed-secret # <- name of managed secret + key: STRIPE_API_SECRET + ports: + - containerPort: 80 +``` + @@ -1161,48 +1172,48 @@ Here, we will highlight three of the most common ways to utilize it. Learn more secretName: managed-secret # managed secret name ```` - You can then mount this volume to the container's filesystem so that your deployment can access the files containing the managed secrets +You can then mount this volume to the container's filesystem so that your deployment can access the files containing the managed secrets - ```yaml - volumeMounts: - - name: secrets-volume-name - mountPath: /etc/secrets - readOnly: true - ``` +```yaml +volumeMounts: + - name: secrets-volume-name + mountPath: /etc/secrets + readOnly: true +``` - Example usage in a deployment +Example usage in a deployment - ```yaml - apiVersion: apps/v1 - kind: Deployment - metadata: - name: nginx-deployment - labels: +```yaml +apiVersion: apps/v1 +kind: Deployment +metadata: + name: nginx-deployment + labels: + app: nginx +spec: + replicas: 1 + selector: + matchLabels: app: nginx - spec: - replicas: 1 - selector: - matchLabels: + template: + metadata: + labels: app: nginx - template: - metadata: - labels: - app: nginx - spec: - containers: - - name: nginx - image: nginx:1.14.2 - volumeMounts: - - name: secrets-volume-name - mountPath: /etc/secrets - readOnly: true - ports: - - containerPort: 80 - volumes: - - name: secrets-volume-name - secret: - secretName: managed-secret # <- managed secrets - ``` + spec: + containers: + - name: nginx + image: nginx:1.14.2 + volumeMounts: + - name: secrets-volume-name + mountPath: /etc/secrets + readOnly: true + ports: + - containerPort: 80 + volumes: + - name: secrets-volume-name + secret: + secretName: managed-secret # <- managed secrets +``` @@ -1244,7 +1255,7 @@ secrets.infisical.com/auto-reload: "true" name: nginx-deployment labels: app: nginx - annotations: + annotations: secrets.infisical.com/auto-reload: "true" # <- redeployment annotation spec: replicas: 1 @@ -1339,9 +1350,11 @@ secrets.infisical.com/auto-reload: "true" - #### How it works - When a managed secret is updated, the operator checks for any Deployments, DaemonSets, or StatefulSets that consume the updated secret and have the annotation - `secrets.infisical.com/auto-reload: "true"`. For each matching workload, the operator triggers a rolling restart to ensure it picks up the latest secret values. + #### How it works When a managed secret is updated, the operator checks for + any Deployments, DaemonSets, or StatefulSets that consume the updated secret + and have the annotation `secrets.infisical.com/auto-reload: "true"`. For each + matching workload, the operator triggers a rolling restart to ensure it picks + up the latest secret values. ## Using Managed ConfigMap In Your Deployment @@ -1350,52 +1363,52 @@ To make use of the managed ConfigMap created by the operator into your deploymen Here, we will highlight three of the most common ways to utilize it. Learn more about Kubernetes ConfigMaps [here](https://kubernetes.io/docs/concepts/configuration/configmap/) - Automatic redeployment of deployments using managed ConfigMaps is not yet supported. + Automatic redeployment of deployments using managed ConfigMaps is not yet + supported. - This will take all the secrets from your managed ConfigMap and expose them to your container - ````yaml - envFrom: - - configMapRef: - name: managed-configmap # managed configmap name - ``` +````yaml + envFrom: + - configMapRef: + name: managed-configmap # managed configmap name + ``` - Example usage in a deployment - ```yaml - apiVersion: apps/v1 - kind: Deployment - metadata: - name: nginx-deployment - labels: + Example usage in a deployment + ```yaml + apiVersion: apps/v1 +kind: Deployment +metadata: + name: nginx-deployment + labels: + app: nginx +spec: + replicas: 1 + selector: + matchLabels: app: nginx - spec: - replicas: 1 - selector: - matchLabels: + template: + metadata: + labels: app: nginx - template: - metadata: - labels: - app: nginx - spec: - containers: - - name: nginx - image: nginx:1.14.2 - envFrom: - - configMapRef: - name: managed-configmap # <- name of managed configmap - ports: - - containerPort: 80 - ```` + spec: + containers: + - name: nginx + image: nginx:1.14.2 + envFrom: + - configMapRef: + name: managed-configmap # <- name of managed configmap + ports: + - containerPort: 80 +```` - - This will allow you to select individual secrets by key name from your managed ConfigMap and expose them to your container - + + This will allow you to select individual secrets by key name from your managed ConfigMap and expose them to your container + ```yaml env: - name: CONFIG_NAME # The environment variable's name which is made available in the container @@ -1405,37 +1418,37 @@ Here, we will highlight three of the most common ways to utilize it. Learn more key: SOME_CONFIG_KEY # The name of the key which exists in the managed configmap ``` - Example usage in a deployment +Example usage in a deployment - ```yaml - apiVersion: apps/v1 - kind: Deployment - metadata: - name: nginx-deployment - labels: +```yaml +apiVersion: apps/v1 +kind: Deployment +metadata: + name: nginx-deployment + labels: + app: nginx +spec: + replicas: 1 + selector: + matchLabels: app: nginx - spec: - replicas: 1 - selector: - matchLabels: + template: + metadata: + labels: app: nginx - template: - metadata: - labels: - app: nginx - spec: - containers: - - name: nginx - image: nginx:1.14.2 - env: - - name: STRIPE_API_SECRET - valueFrom: - configMapKeyRef: - name: managed-configmap # <- name of managed configmap - key: STRIPE_API_SECRET - ports: - - containerPort: 80 - ``` + spec: + containers: + - name: nginx + image: nginx:1.14.2 + env: + - name: STRIPE_API_SECRET + valueFrom: + configMapKeyRef: + name: managed-configmap # <- name of managed configmap + key: STRIPE_API_SECRET + ports: + - containerPort: 80 +``` @@ -1448,48 +1461,49 @@ Here, we will highlight three of the most common ways to utilize it. Learn more name: managed-configmap # managed configmap name ```` - You can then mount this volume to the container's filesystem so that your deployment can access the files containing the managed secrets +You can then mount this volume to the container's filesystem so that your deployment can access the files containing the managed secrets - ```yaml - volumeMounts: - - name: configmaps-volume-name - mountPath: /etc/config - readOnly: true - ``` +```yaml +volumeMounts: + - name: configmaps-volume-name + mountPath: /etc/config + readOnly: true +``` - Example usage in a deployment +Example usage in a deployment - ```yaml - apiVersion: apps/v1 - kind: Deployment - metadata: - name: nginx-deployment - labels: +```yaml +apiVersion: apps/v1 +kind: Deployment +metadata: + name: nginx-deployment + labels: + app: nginx +spec: + replicas: 1 + selector: + matchLabels: app: nginx - spec: - replicas: 1 - selector: - matchLabels: + template: + metadata: + labels: app: nginx - template: - metadata: - labels: - app: nginx - spec: - containers: - - name: nginx - image: nginx:1.14.2 - volumeMounts: - - name: configmaps-volume-name - mountPath: /etc/config - readOnly: true - ports: - - containerPort: 80 - volumes: - - name: configmaps-volume-name - configMap: - name: managed-configmap # <- managed configmap - ``` + spec: + containers: + - name: nginx + image: nginx:1.14.2 + volumeMounts: + - name: configmaps-volume-name + mountPath: /etc/config + readOnly: true + ports: + - containerPort: 80 + volumes: + - name: configmaps-volume-name + configMap: + name: managed-configmap # <- managed configmap +``` + The definition file of the Kubernetes secret for the CA certificate can be structured like the following: @@ -1532,20 +1546,21 @@ Thus, if a specific label is required on the resulting secret, it can be applied ... ``` - This would result in the following managed secret to be created: +This would result in the following managed secret to be created: + +```yaml +apiVersion: v1 +data: ... +kind: Secret +metadata: + annotations: + example.com/annotation-to-be-passed-to-managed-secret: sample-value + secrets.infisical.com/version: W/"3f1-ZyOSsrCLGSkAhhCkY2USPu2ivRw" + labels: + label-to-be-passed-to-managed-secret: sample-value + name: managed-token + namespace: default +type: Opaque +``` - ```yaml - apiVersion: v1 - data: ... - kind: Secret - metadata: - annotations: - example.com/annotation-to-be-passed-to-managed-secret: sample-value - secrets.infisical.com/version: W/"3f1-ZyOSsrCLGSkAhhCkY2USPu2ivRw" - labels: - label-to-be-passed-to-managed-secret: sample-value - name: managed-token - namespace: default - type: Opaque - ``` diff --git a/docs/integrations/secret-syncs/aws-parameter-store.mdx b/docs/integrations/secret-syncs/aws-parameter-store.mdx index 32fd0c12f..0a2d52ce1 100644 --- a/docs/integrations/secret-syncs/aws-parameter-store.mdx +++ b/docs/integrations/secret-syncs/aws-parameter-store.mdx @@ -7,6 +7,7 @@ description: "Learn how to configure an AWS Parameter Store Sync for Infisical." - Set up and add secrets to [Infisical Cloud](https://app.infisical.com) - Create an [AWS Connection](/integrations/app-connections/aws) with the required **Secret Sync** permissions + - Ensure your network security policies allow incoming requests from Infisical to this secret sync provider, if network restrictions apply. diff --git a/docs/integrations/secret-syncs/aws-secrets-manager.mdx b/docs/integrations/secret-syncs/aws-secrets-manager.mdx index 91c606b0a..241c0350a 100644 --- a/docs/integrations/secret-syncs/aws-secrets-manager.mdx +++ b/docs/integrations/secret-syncs/aws-secrets-manager.mdx @@ -7,6 +7,7 @@ description: "Learn how to configure an AWS Secrets Manager Sync for Infisical." - Set up and add secrets to [Infisical Cloud](https://app.infisical.com) - Create an [AWS Connection](/integrations/app-connections/aws) with the required **Secret Sync** permissions + - Ensure your network security policies allow incoming requests from Infisical to this secret sync provider, if network restrictions apply. diff --git a/docs/integrations/secret-syncs/azure-app-configuration.mdx b/docs/integrations/secret-syncs/azure-app-configuration.mdx index f4aaa7edd..34a8eb9a8 100644 --- a/docs/integrations/secret-syncs/azure-app-configuration.mdx +++ b/docs/integrations/secret-syncs/azure-app-configuration.mdx @@ -7,6 +7,7 @@ description: "Learn how to configure an Azure App Configuration Sync for Infisic - Set up and add secrets to [Infisical Cloud](https://app.infisical.com) - Create an [Azure App Configuration Connection](/integrations/app-connections/azure-app-configuration) + - Ensure your network security policies allow incoming requests from Infisical to this secret sync provider, if network restrictions apply. The Azure App Configuration Secret Sync requires the following permissions to be set on the user / service principal diff --git a/docs/integrations/secret-syncs/azure-devops.mdx b/docs/integrations/secret-syncs/azure-devops.mdx index 2f99fe128..b8c0331f8 100644 --- a/docs/integrations/secret-syncs/azure-devops.mdx +++ b/docs/integrations/secret-syncs/azure-devops.mdx @@ -7,6 +7,7 @@ description: "Learn how to configure a Azure DevOps Sync for Infisical." - Set up and add secrets to [Infisical Cloud](https://app.infisical.com) - Create an [Azure DevOps Connection](/integrations/app-connections/azure-devops) +- Ensure your network security policies allow incoming requests from Infisical to this secret sync provider, if network restrictions apply. diff --git a/docs/integrations/secret-syncs/azure-key-vault.mdx b/docs/integrations/secret-syncs/azure-key-vault.mdx index d19a0162e..e4cb82a04 100644 --- a/docs/integrations/secret-syncs/azure-key-vault.mdx +++ b/docs/integrations/secret-syncs/azure-key-vault.mdx @@ -7,6 +7,7 @@ description: "Learn how to configure a Azure Key Vault Sync for Infisical." - Set up and add secrets to [Infisical Cloud](https://app.infisical.com) - Create an [Azure Key Vault Connection](/integrations/app-connections/azure-key-vault) + - Ensure your network security policies allow incoming requests from Infisical to this secret sync provider, if network restrictions apply. The Azure Key Vault Secret Sync requires the following secrets permissions to be set on the user / service principal diff --git a/docs/integrations/secret-syncs/gcp-secret-manager.mdx b/docs/integrations/secret-syncs/gcp-secret-manager.mdx index afce51a0c..1ef31c997 100644 --- a/docs/integrations/secret-syncs/gcp-secret-manager.mdx +++ b/docs/integrations/secret-syncs/gcp-secret-manager.mdx @@ -11,6 +11,7 @@ description: "Learn how to configure a GCP Secret Manager Sync for Infisical." ![Secret Syncs Tab](/images/secret-syncs/gcp-secret-manager/enable-resource-manager-api.png) ![Secret Syncs Tab](/images/secret-syncs/gcp-secret-manager/enable-secret-manager-api.png) ![Secret Syncs Tab](/images/secret-syncs/gcp-secret-manager/enable-service-usage-api.png) + - Ensure your network security policies allow incoming requests from Infisical to this secret sync provider, if network restrictions apply. diff --git a/docs/integrations/secret-syncs/github.mdx b/docs/integrations/secret-syncs/github.mdx index 14b2d9a7f..ed76c5edb 100644 --- a/docs/integrations/secret-syncs/github.mdx +++ b/docs/integrations/secret-syncs/github.mdx @@ -7,6 +7,7 @@ description: "Learn how to configure a GitHub Sync for Infisical." - Set up and add secrets to [Infisical Cloud](https://app.infisical.com) - Create a [GitHub Connection](/integrations/app-connections/github) + - Ensure your network security policies allow incoming requests from Infisical to this secret sync provider, if network restrictions apply. diff --git a/docs/integrations/secret-syncs/gitlab.mdx b/docs/integrations/secret-syncs/gitlab.mdx index 5e6cacffa..3f468ac0e 100644 --- a/docs/integrations/secret-syncs/gitlab.mdx +++ b/docs/integrations/secret-syncs/gitlab.mdx @@ -7,6 +7,7 @@ description: "Learn how to configure a GitLab Sync for Infisical." - Set up and add secrets to [Infisical Cloud](https://app.infisical.com) - Create a [GitLab Connection](/integrations/app-connections/gitlab) + - Ensure your network security policies allow incoming requests from Infisical to this secret sync provider, if network restrictions apply. diff --git a/docs/integrations/secret-syncs/oci-vault.mdx b/docs/integrations/secret-syncs/oci-vault.mdx index 396b4d13f..105d42395 100644 --- a/docs/integrations/secret-syncs/oci-vault.mdx +++ b/docs/integrations/secret-syncs/oci-vault.mdx @@ -14,6 +14,7 @@ description: "Learn how to configure an Oracle Cloud Infrastructure Vault Sync f - Create an [OCI Connection](/integrations/app-connections/oci) with the required **Secret Sync** permissions - [Create](https://docs.oracle.com/en-us/iaas/Content/Identity/compartments/To_create_a_compartment.htm) or use an existing OCI Compartment (which the OCI Connection is authorized to access) - [Create](https://docs.oracle.com/en-us/iaas/Content/KeyManagement/Tasks/managingvaults_topic-To_create_a_new_vault.htm#createnewvault) or use an existing OCI Vault +- Ensure your network security policies allow incoming requests from Infisical to this secret sync provider, if network restrictions apply. diff --git a/docs/internals/permissions/organization-permissions.mdx b/docs/internals/permissions/organization-permissions.mdx index 80c843851..5f5fc962f 100644 --- a/docs/internals/permissions/organization-permissions.mdx +++ b/docs/internals/permissions/organization-permissions.mdx @@ -217,3 +217,14 @@ Supports conditions and permission inversion | `edit-gateways` | Modify existing gateway settings | | `delete-gateways` | Remove gateways from organization | | `attach-gateways` | Attach gateways to resources | + +#### Subject: `machine-identity-auth-template` + +| Action | Description | +| ------------------ | ---------------------------------------------- | +| `list-templates` | View identity auth templates | +| `create-templates` | Create new identity auth templates | +| `edit-templates` | Modify existing identity auth templates | +| `delete-templates` | Remove identity auth templates | +| `unlink-templates` | Unlink identity auth templates from identities | +| `attach-templates` | Attach identity auth templates to identities | diff --git a/docs/self-hosting/configuration/envars.mdx b/docs/self-hosting/configuration/envars.mdx index 1e050ff14..53adf95a4 100644 --- a/docs/self-hosting/configuration/envars.mdx +++ b/docs/self-hosting/configuration/envars.mdx @@ -65,7 +65,14 @@ Example values: default="false" optional > - Determines whether your Infisical instance can automatically read the service account token of the pod it's running on. Used for features such as the IRSA auth method. + Determines whether your Infisical instance can automatically read the service + account token of the pod it's running on. Used for features such as the IRSA + auth method. + + + + Disable storing audit logs in the database. This is useful if you're using + audit log streams and don't want to store them in the database. ## CORS diff --git a/frontend/src/components/auth/UserInfoStep.tsx b/frontend/src/components/auth/UserInfoStep.tsx index 2a1f8dca1..2eb6c65f6 100644 --- a/frontend/src/components/auth/UserInfoStep.tsx +++ b/frontend/src/components/auth/UserInfoStep.tsx @@ -1,12 +1,8 @@ -import crypto from "crypto"; - import { useState } from "react"; import { useTranslation } from "react-i18next"; import { faXmark } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import jsrp from "jsrp"; -import { useServerConfig } from "@app/context"; import { initProjectHelper } from "@app/helpers/project"; import { completeAccountSignup, useSelectOrganization } from "@app/hooks/api/auth/queries"; import { fetchOrganizations } from "@app/hooks/api/organization/queries"; @@ -14,15 +10,9 @@ import { onRequestError } from "@app/hooks/api/reactQuery"; import InputField from "../basic/InputField"; import checkPassword from "../utilities/checks/password/checkPassword"; -import Aes256Gcm from "../utilities/cryptography/aes-256-gcm"; -import { deriveArgonKey, generateKeyPair } from "../utilities/cryptography/crypto"; -import { saveTokenToLocalStorage } from "../utilities/saveTokenToLocalStorage"; import SecurityClient from "../utilities/SecurityClient"; import { Button, Input } from "../v2"; -// eslint-disable-next-line new-cap -const client = new jsrp.client(); - interface UserInfoStepProps { incrementStep: () => void; email: string; @@ -76,7 +66,6 @@ export default function UserInfoStep({ }: UserInfoStepProps): JSX.Element { const [nameError, setNameError] = useState(false); const [organizationNameError, setOrganizationNameError] = useState(false); - const { config } = useServerConfig(); const [errors, setErrors] = useState({}); @@ -108,109 +97,41 @@ export default function UserInfoStep({ }); if (!errorCheck) { - // Generate a random pair of a public and a private key - const pair = await generateKeyPair(config.fipsEnabled); + try { + const response = await completeAccountSignup({ + email, + password, + firstName: name.split(" ")[0], + lastName: name.split(" ").slice(1).join(" "), + providerAuthToken, + organizationName, + attributionSource + }); - localStorage.setItem("PRIVATE_KEY", pair.privateKey); + // unset signup JWT token and set JWT token + SecurityClient.setSignupToken(""); + SecurityClient.setToken(response.token); + SecurityClient.setProviderAuthToken(""); - client.init( - { - username: email, - password - }, - async () => { - client.createVerifier(async (_err: any, result: { salt: string; verifier: string }) => { - try { - // TODO: moduralize into KeyService - const derivedKey = await deriveArgonKey({ - password, - salt: result.salt, - mem: 65536, - time: 3, - parallelism: 1, - hashLen: 32 - }); - - if (!derivedKey) throw new Error("Failed to derive key from password"); - - const key = crypto.randomBytes(32); - - // create encrypted private key by encrypting the private - // key with the symmetric key [key] - const { - ciphertext: encryptedPrivateKey, - iv: encryptedPrivateKeyIV, - tag: encryptedPrivateKeyTag - } = Aes256Gcm.encrypt({ - text: pair.privateKey, - secret: key - }); - - // create the protected key by encrypting the symmetric key - // [key] with the derived key - const { - ciphertext: protectedKey, - iv: protectedKeyIV, - tag: protectedKeyTag - } = Aes256Gcm.encrypt({ - text: key.toString("hex"), - secret: Buffer.from(derivedKey.hash) - }); - - const response = await completeAccountSignup({ - email, - password, - firstName: name.split(" ")[0], - lastName: name.split(" ").slice(1).join(" "), - protectedKey, - protectedKeyIV, - protectedKeyTag, - publicKey: pair.publicKey, - encryptedPrivateKey, - encryptedPrivateKeyIV, - encryptedPrivateKeyTag, - providerAuthToken, - salt: result.salt, - verifier: result.verifier, - organizationName, - attributionSource - }); - - // unset signup JWT token and set JWT token - SecurityClient.setSignupToken(""); - SecurityClient.setToken(response.token); - SecurityClient.setProviderAuthToken(""); - - if (response.organizationId) { - await selectOrganization({ organizationId: response.organizationId }); - } - - saveTokenToLocalStorage({ - publicKey: pair.publicKey, - encryptedPrivateKey, - iv: encryptedPrivateKeyIV, - tag: encryptedPrivateKeyTag, - privateKey: pair.privateKey - }); - - const userOrgs = await fetchOrganizations(); - - const orgId = userOrgs[0]?.id; - await initProjectHelper({ - projectName: "Example Project" - }); - - localStorage.setItem("orgData.id", orgId); - - incrementStep(); - } catch (error) { - onRequestError(error); - setIsLoading(false); - console.error(error); - } - }); + if (response.organizationId) { + await selectOrganization({ organizationId: response.organizationId }); } - ); + + const userOrgs = await fetchOrganizations(); + + const orgId = userOrgs[0]?.id; + await initProjectHelper({ + projectName: "Example Project" + }); + + localStorage.setItem("orgData.id", orgId); + + incrementStep(); + } catch (error) { + onRequestError(error); + setIsLoading(false); + console.error(error); + } } else { setIsLoading(false); } diff --git a/frontend/src/components/utilities/attemptChangePassword.ts b/frontend/src/components/utilities/attemptChangePassword.ts deleted file mode 100644 index e51168525..000000000 --- a/frontend/src/components/utilities/attemptChangePassword.ts +++ /dev/null @@ -1,108 +0,0 @@ -/* eslint-disable new-cap */ -import crypto from "crypto"; - -import jsrp from "jsrp"; - -import { changePassword, srp1 } from "@app/hooks/api/auth/queries"; - -import Aes256Gcm from "./cryptography/aes-256-gcm"; -import { deriveArgonKey } from "./cryptography/crypto"; -import { saveTokenToLocalStorage } from "./saveTokenToLocalStorage"; - -const clientOldPassword = new jsrp.client(); -const clientNewPassword = new jsrp.client(); - -type Params = { - email: string; - currentPassword: string; - newPassword: string; -}; - -const attemptChangePassword = ({ email, currentPassword, newPassword }: Params): Promise => { - return new Promise((resolve, reject) => { - clientOldPassword.init({ username: email, password: currentPassword }, async () => { - let serverPublicKey; - let salt; - - try { - const clientPublicKey = clientOldPassword.getPublicKey(); - - const res = await srp1({ clientPublicKey }); - - serverPublicKey = res.serverPublicKey; - salt = res.salt; - - clientOldPassword.setSalt(salt); - clientOldPassword.setServerPublicKey(serverPublicKey); - - const clientProof = clientOldPassword.getProof(); - - clientNewPassword.init({ username: email, password: newPassword }, async () => { - clientNewPassword.createVerifier(async (_err, result) => { - try { - const derivedKey = await deriveArgonKey({ - password: newPassword, - salt: result.salt, - mem: 65536, - time: 3, - parallelism: 1, - hashLen: 32 - }); - - if (!derivedKey) throw new Error("Failed to derive key from password"); - - const key = crypto.randomBytes(32); - - const { - ciphertext: encryptedPrivateKey, - iv: encryptedPrivateKeyIV, - tag: encryptedPrivateKeyTag - } = Aes256Gcm.encrypt({ - text: localStorage.getItem("PRIVATE_KEY") as string, - secret: key - }); - - const { - ciphertext: protectedKey, - iv: protectedKeyIV, - tag: protectedKeyTag - } = Aes256Gcm.encrypt({ - text: key.toString("hex"), - secret: Buffer.from(derivedKey.hash) - }); - - await changePassword({ - password: newPassword, - clientProof, - protectedKey, - protectedKeyIV, - protectedKeyTag, - encryptedPrivateKey, - encryptedPrivateKeyIV, - encryptedPrivateKeyTag, - salt: result.salt, - verifier: result.verifier - }); - - saveTokenToLocalStorage({ - encryptedPrivateKey, - iv: encryptedPrivateKeyIV, - tag: encryptedPrivateKeyTag - }); - - resolve(); - } catch (err2) { - console.error(err2); - reject(err2); - } - }); - }); - } catch (err) { - console.error(err); - reject(err); - } - }); - }); -}; - -export default attemptChangePassword; diff --git a/frontend/src/components/utilities/attemptCliLogin.ts b/frontend/src/components/utilities/attemptCliLogin.ts index d897ebeaa..f8339aecf 100644 --- a/frontend/src/components/utilities/attemptCliLogin.ts +++ b/frontend/src/components/utilities/attemptCliLogin.ts @@ -1,18 +1,18 @@ /* eslint-disable prefer-destructuring */ +import axios from "axios"; import jsrp from "jsrp"; -import { decryptPrivateKeyHelper } from "@app/helpers/key"; -import { login1, login2 } from "@app/hooks/api/auth/queries"; +import { login1, login2, loginV3 } from "@app/hooks/api/auth/queries"; +import { createNotification } from "../notifications"; import Telemetry from "./telemetry/Telemetry"; -import { saveTokenToLocalStorage } from "./saveTokenToLocalStorage"; +import { LoginMode } from "./attemptLogin"; import SecurityClient from "./SecurityClient"; // eslint-disable-next-line new-cap const client = new jsrp.client(); export interface IsCliLoginSuccessful { - mfaEnabled: boolean; loginResponse?: { email: string; privateKey: string; @@ -31,14 +31,62 @@ const attemptLogin = async ({ email, password, providerAuthToken, - captchaToken + captchaToken, + loginMode = LoginMode.ServerSide }: { email: string; password: string; providerAuthToken?: string; captchaToken?: string; + loginMode?: LoginMode; }): Promise => { const telemetry = new Telemetry().getInstance(); + + if (loginMode === LoginMode.ServerSide) { + console.log("Attempting login with server side..."); + const data = await loginV3({ + email, + password, + providerAuthToken, + captchaToken + }).catch((err) => { + if (axios.isAxiosError(err) && err.response?.status === 400) { + if (err.response?.data?.error === "LegacyEncryptionScheme") { + createNotification({ + text: "Failed to login without SRP, attempting to authenticate with legacy SRP authentication.", + type: "info" + }); + + return null; + } + } + + throw err; + }); + + if (data === null) { + return attemptLogin({ + email, + password, + providerAuthToken, + captchaToken, + loginMode: LoginMode.LegacySrp + }); + } + + SecurityClient.setProviderAuthToken(""); + SecurityClient.setToken(data.accessToken); + + return { + success: true, + loginResponse: { + email, + privateKey: "", + JTWToken: data.accessToken + } + }; + } + return new Promise((resolve, reject) => { client.init( { @@ -58,79 +106,26 @@ const attemptLogin = async ({ client.setServerPublicKey(serverPublicKey); const clientProof = client.getProof(); // called M1 - const { - mfaEnabled, - encryptionVersion, - protectedKey, - protectedKeyIV, - protectedKeyTag, - token, - publicKey, - encryptedPrivateKey, - iv, - tag - } = await login2({ + const { encryptionVersion, token, encryptedPrivateKey, iv, tag } = await login2({ email, password, clientProof, providerAuthToken, captchaToken }); - if (mfaEnabled) { - // case: MFA is enabled - - // set temporary (MFA) JWT token - SecurityClient.setMfaToken(token); - - resolve({ - mfaEnabled, - success: true - }); - } else if ( - !mfaEnabled && - encryptionVersion && - encryptedPrivateKey && - iv && - tag && - token - ) { - // case: MFA is not enabled - - // unset provider auth token in case it was used + if (encryptionVersion && encryptedPrivateKey && iv && tag && token) { SecurityClient.setProviderAuthToken(""); - // set JWT token SecurityClient.setToken(token); - const privateKey = await decryptPrivateKeyHelper({ - encryptionVersion, - encryptedPrivateKey, - iv, - tag, - password, - salt, - protectedKey, - protectedKeyIV, - protectedKeyTag - }); - - saveTokenToLocalStorage({ - publicKey, - encryptedPrivateKey, - iv, - tag, - privateKey - }); - if (email) { telemetry.identify(email, email); telemetry.capture("User Logged In"); } resolve({ - mfaEnabled: false, loginResponse: { email, - privateKey, + privateKey: "", JTWToken: token }, success: true diff --git a/frontend/src/components/utilities/attemptCliLoginMfa.ts b/frontend/src/components/utilities/attemptCliLoginMfa.ts deleted file mode 100644 index 14a61d817..000000000 --- a/frontend/src/components/utilities/attemptCliLoginMfa.ts +++ /dev/null @@ -1,109 +0,0 @@ -/* eslint-disable prefer-destructuring */ -import jsrp from "jsrp"; - -import { decryptPrivateKeyHelper } from "@app/helpers/key"; -import { login1, verifyMfaToken } from "@app/hooks/api/auth/queries"; - -import { saveTokenToLocalStorage } from "./saveTokenToLocalStorage"; -import SecurityClient from "./SecurityClient"; - -// eslint-disable-next-line new-cap -const client = new jsrp.client(); - -interface IsMfaLoginSuccessful { - success: boolean; - loginResponse: { - privateKey: string; - JTWToken: string; - }; -} - -/** - * Return whether or not MFA-login is successful for user with email [email] - * and MFA token [mfaToken] - * @param {Object} obj - * @param {String} obj.email - email of user - * @param {String} obj.mfaToken - MFA code/token - */ -const attemptLoginMfa = async ({ - email, - password, - providerAuthToken, - mfaToken -}: { - email: string; - password: string; - providerAuthToken?: string; - mfaToken: string; -}): Promise => { - return new Promise((resolve, reject) => { - client.init( - { - username: email, - password - }, - async () => { - try { - const clientPublicKey = client.getPublicKey(); - const { salt } = await login1({ - email, - clientPublicKey, - providerAuthToken - }); - - const { - encryptionVersion, - protectedKey, - protectedKeyIV, - protectedKeyTag, - token, - publicKey, - encryptedPrivateKey, - iv, - tag - } = await verifyMfaToken({ - email, - mfaCode: mfaToken - }); - - // unset temporary (MFA) JWT token and set JWT token - SecurityClient.setMfaToken(""); - SecurityClient.setToken(token); - SecurityClient.setProviderAuthToken(""); - - const privateKey = await decryptPrivateKeyHelper({ - encryptionVersion, - encryptedPrivateKey, - iv, - tag, - password, - salt, - protectedKey, - protectedKeyIV, - protectedKeyTag - }); - - saveTokenToLocalStorage({ - publicKey, - encryptedPrivateKey, - iv, - tag, - privateKey - }); - - resolve({ - success: true, - loginResponse: { - privateKey, - JTWToken: token - } - }); - } catch (err) { - reject(err); - } - } - ); - }); -}; - -export default attemptLoginMfa; diff --git a/frontend/src/components/utilities/attemptLogin.ts b/frontend/src/components/utilities/attemptLogin.ts index c371d9c91..bdaf79042 100644 --- a/frontend/src/components/utilities/attemptLogin.ts +++ b/frontend/src/components/utilities/attemptLogin.ts @@ -1,15 +1,19 @@ /* eslint-disable prefer-destructuring */ +import axios from "axios"; import jsrp from "jsrp"; -import { decryptPrivateKeyHelper } from "@app/helpers/key"; -import { login1, login2 } from "@app/hooks/api/auth/queries"; +import { login1, login2, loginV3 } from "@app/hooks/api/auth/queries"; +import { createNotification } from "../notifications"; import Telemetry from "./telemetry/Telemetry"; -import { saveTokenToLocalStorage } from "./saveTokenToLocalStorage"; import SecurityClient from "./SecurityClient"; +export enum LoginMode { + LegacySrp = "legacy-srp", + ServerSide = "server-side" +} + interface IsLoginSuccessful { - mfaEnabled: boolean; success: boolean; } @@ -23,14 +27,62 @@ const attemptLogin = async ({ email, password, providerAuthToken, - captchaToken + captchaToken, + loginMode = LoginMode.ServerSide }: { email: string; password: string; providerAuthToken?: string; captchaToken?: string; + loginMode?: LoginMode; }): Promise => { const telemetry = new Telemetry().getInstance(); + + if (loginMode === LoginMode.ServerSide) { + console.log("Attempting login with server side..."); + const data = await loginV3({ + email, + password, + providerAuthToken, + captchaToken + }).catch((err) => { + if (axios.isAxiosError(err) && err.response?.status === 400) { + if (err.response?.data?.error === "LegacyEncryptionScheme") { + createNotification({ + text: "Failed to login without SRP, attempting to authenticate with legacy SRP authentication.", + type: "info" + }); + + return null; + } + } + + throw err; + }); + + if (data === null) { + return attemptLogin({ + email, + password, + providerAuthToken, + captchaToken, + loginMode: LoginMode.LegacySrp + }); + } + + SecurityClient.setProviderAuthToken(""); + SecurityClient.setToken(data.accessToken); + + if (email) { + telemetry.identify(email, email); + telemetry.capture("User Logged In"); + } + + return { + success: true + }; + } + // eslint-disable-next-line new-cap const client = new jsrp.client(); await new Promise((resolve) => { @@ -48,18 +100,7 @@ const attemptLogin = async ({ client.setServerPublicKey(serverPublicKey); const clientProof = client.getProof(); // called M1 - const { - mfaEnabled, - encryptionVersion, - protectedKey, - protectedKeyIV, - protectedKeyTag, - token, - publicKey, - encryptedPrivateKey, - iv, - tag - } = await login2({ + const { encryptionVersion, token, encryptedPrivateKey, iv, tag } = await login2({ captchaToken, email, password, @@ -67,56 +108,22 @@ const attemptLogin = async ({ providerAuthToken }); - if (mfaEnabled) { - // case: MFA is enabled - - // set temporary (MFA) JWT token - SecurityClient.setMfaToken(token); - - return { - mfaEnabled, - success: true - }; - } - if (!mfaEnabled && encryptionVersion && encryptedPrivateKey && iv && tag && token) { - // case: MFA is not enabled - + if (encryptionVersion && encryptedPrivateKey && iv && tag && token) { // unset provider auth token in case it was used SecurityClient.setProviderAuthToken(""); // set JWT token SecurityClient.setToken(token); - const privateKey = await decryptPrivateKeyHelper({ - encryptionVersion, - encryptedPrivateKey, - iv, - tag, - password, - salt, - protectedKey, - protectedKeyIV, - protectedKeyTag - }); - - saveTokenToLocalStorage({ - publicKey, - encryptedPrivateKey, - iv, - tag, - privateKey - }); - if (email) { telemetry.identify(email, email); telemetry.capture("User Logged In"); } return { - mfaEnabled: false, success: true }; } - return { success: false, mfaEnabled: false }; + return { success: false }; }; export default attemptLogin; diff --git a/frontend/src/components/utilities/attemptLoginMfa.ts b/frontend/src/components/utilities/attemptLoginMfa.ts deleted file mode 100644 index 10f36ec39..000000000 --- a/frontend/src/components/utilities/attemptLoginMfa.ts +++ /dev/null @@ -1,95 +0,0 @@ -/* eslint-disable prefer-destructuring */ -import jsrp from "jsrp"; - -import { decryptPrivateKeyHelper } from "@app/helpers/key"; -import { login1, verifyMfaToken } from "@app/hooks/api/auth/queries"; - -import { saveTokenToLocalStorage } from "./saveTokenToLocalStorage"; -import SecurityClient from "./SecurityClient"; - -// eslint-disable-next-line new-cap -const client = new jsrp.client(); - -/** - * Return whether or not MFA-login is successful for user with email [email] - * and MFA token [mfaToken] - * @param {Object} obj - * @param {String} obj.email - email of user - * @param {String} obj.mfaToken - MFA code/token - */ -const attemptLoginMfa = async ({ - email, - password, - providerAuthToken, - mfaToken -}: { - email: string; - password: string; - providerAuthToken?: string; - mfaToken: string; -}): Promise => { - return new Promise((resolve, reject) => { - client.init( - { - username: email, - password - }, - async () => { - try { - const clientPublicKey = client.getPublicKey(); - const { salt } = await login1({ - email, - clientPublicKey, - providerAuthToken - }); - - const { - encryptionVersion, - protectedKey, - protectedKeyIV, - protectedKeyTag, - token, - publicKey, - encryptedPrivateKey, - iv, - tag - } = await verifyMfaToken({ - email, - mfaCode: mfaToken - }); - - // unset temporary (MFA) JWT token and set JWT token - SecurityClient.setMfaToken(""); - SecurityClient.setToken(token); - SecurityClient.setProviderAuthToken(""); - - const privateKey = await decryptPrivateKeyHelper({ - encryptionVersion, - encryptedPrivateKey, - iv, - tag, - password, - salt, - protectedKey, - protectedKeyIV, - protectedKeyTag - }); - - saveTokenToLocalStorage({ - publicKey, - encryptedPrivateKey, - iv, - tag, - privateKey - }); - - resolve(true); - } catch (err) { - reject(err); - } - } - ); - }); -}; - -export default attemptLoginMfa; diff --git a/frontend/src/components/utilities/cryptography/issueBackupKey.ts b/frontend/src/components/utilities/cryptography/issueBackupKey.ts deleted file mode 100644 index 52a502e4e..000000000 --- a/frontend/src/components/utilities/cryptography/issueBackupKey.ts +++ /dev/null @@ -1,114 +0,0 @@ -/* eslint-disable new-cap */ -import crypto from "crypto"; - -import jsrp from "jsrp"; - -import { issueBackupPrivateKey, srp1 } from "@app/hooks/api/auth/queries"; - -import generateBackupPDF from "../generateBackupPDF"; -import Aes256Gcm from "./aes-256-gcm"; - -const clientPassword = new jsrp.client(); -const clientKey = new jsrp.client(); - -interface BackupKeyProps { - email: string; - password: string; - personalName: string; - setBackupKeyError: (value: boolean) => void; - setBackupKeyIssued: (value: boolean) => void; -} - -/** - * This function issue a backup key for a user - * @param {obkect} obj - * @param {string} obj.email - email of a user issuing a backup key - * @param {string} obj.password - password of a user issuing a backup key - * @param {string} obj.personalName - name of a user issuing a backup key - * @param {function} obj.setBackupKeyError - state function that turns true if there is an erorr with a backup key - * @param {function} obj.setBackupKeyIssued - state function that turns true if a backup key was issued correctly - * @returns - */ -const issueBackupKey = async ({ - email, - password, - personalName, - setBackupKeyError, - setBackupKeyIssued -}: BackupKeyProps) => { - try { - setBackupKeyError(false); - setBackupKeyIssued(false); - clientPassword.init( - { - username: email, - password - }, - async () => { - const clientPublicKey = clientPassword.getPublicKey(); - - let serverPublicKey; - let salt; - try { - const res = await srp1({ - clientPublicKey - }); - serverPublicKey = res.serverPublicKey; - salt = res.salt; - } catch (err) { - setBackupKeyError(true); - console.log("Wrong current password", err, 1); - } - - clientPassword.setSalt(salt as string); - clientPassword.setServerPublicKey(serverPublicKey as string); - const clientProof = clientPassword.getProof(); // called M1 - - const generatedKey = crypto.randomBytes(16).toString("hex"); - - clientKey.init( - { - username: email, - password: generatedKey - }, - async () => { - clientKey.createVerifier( - async (_err: any, result: { salt: string; verifier: string }) => { - const { ciphertext, iv, tag } = Aes256Gcm.encrypt({ - text: String(localStorage.getItem("PRIVATE_KEY")), - secret: generatedKey - }); - - try { - await issueBackupPrivateKey({ - encryptedPrivateKey: ciphertext, - iv, - tag, - salt: result.salt, - verifier: result.verifier, - clientProof - }); - - generateBackupPDF({ - personalName, - personalEmail: email, - generatedKey - }); - setBackupKeyIssued(true); - } catch { - setBackupKeyError(true); - } - } - ); - } - ); - } - ); - } catch { - setBackupKeyError(true); - console.log("Failed to issue a backup key"); - } - return true; -}; - -export default issueBackupKey; diff --git a/frontend/src/components/utilities/saveTokenToLocalStorage.ts b/frontend/src/components/utilities/saveTokenToLocalStorage.ts deleted file mode 100644 index 1539438f4..000000000 --- a/frontend/src/components/utilities/saveTokenToLocalStorage.ts +++ /dev/null @@ -1,67 +0,0 @@ -interface Props { - protectedKey?: string; - protectedKeyIV?: string; - protectedKeyTag?: string; - publicKey?: string; - encryptedPrivateKey?: string; - iv?: string; - tag?: string; - privateKey?: string; -} - -export const saveTokenToLocalStorage = ({ - protectedKey, - protectedKeyIV, - protectedKeyTag, - publicKey, - encryptedPrivateKey, - iv, - tag, - privateKey -}: Props) => { - try { - if (protectedKey) { - localStorage.removeItem("protectedKey"); - localStorage.setItem("protectedKey", protectedKey); - } - - if (protectedKeyIV) { - localStorage.removeItem("protectedKeyIV"); - localStorage.setItem("protectedKeyIV", protectedKeyIV); - } - - if (protectedKeyTag) { - localStorage.removeItem("protectedKeyTag"); - localStorage.setItem("protectedKeyTag", protectedKeyTag); - } - - if (publicKey) { - localStorage.removeItem("publicKey"); - localStorage.setItem("publicKey", publicKey); - } - - if (encryptedPrivateKey) { - localStorage.removeItem("encryptedPrivateKey"); - localStorage.setItem("encryptedPrivateKey", encryptedPrivateKey); - } - - if (iv) { - localStorage.removeItem("iv"); - localStorage.setItem("iv", iv); - } - - if (tag) { - localStorage.removeItem("tag"); - localStorage.setItem("tag", tag); - } - - if (privateKey) { - localStorage.removeItem("PRIVATE_KEY"); - localStorage.setItem("PRIVATE_KEY", privateKey); - } - } catch (err) { - if (err instanceof Error) { - throw new Error(`Unable to send the tokens in local storage:${err.message}`); - } - } -}; diff --git a/frontend/src/context/OrgPermissionContext/types.ts b/frontend/src/context/OrgPermissionContext/types.ts index 59446eb07..50e147aa0 100644 --- a/frontend/src/context/OrgPermissionContext/types.ts +++ b/frontend/src/context/OrgPermissionContext/types.ts @@ -21,6 +21,15 @@ export enum OrgGatewayPermissionActions { AttachGateways = "attach-gateways" } +export enum OrgPermissionMachineIdentityAuthTemplateActions { + ListTemplates = "list-templates", + CreateTemplates = "create-templates", + EditTemplates = "edit-templates", + DeleteTemplates = "delete-templates", + UnlinkTemplates = "unlink-templates", + AttachTemplates = "attach-templates" +} + export enum OrgPermissionSubjects { Workspace = "workspace", Role = "role", @@ -42,7 +51,8 @@ export enum OrgPermissionSubjects { Kmip = "kmip", Gateway = "gateway", SecretShare = "secret-share", - GithubOrgSync = "github-org-sync" + GithubOrgSync = "github-org-sync", + MachineIdentityAuthTemplate = "machine-identity-auth-template" } export enum OrgPermissionAdminConsoleAction { @@ -113,6 +123,10 @@ export type OrgPermissionSet = | [OrgPermissionAppConnectionActions, OrgPermissionSubjects.AppConnections] | [OrgPermissionIdentityActions, OrgPermissionSubjects.Identity] | [OrgPermissionKmipActions, OrgPermissionSubjects.Kmip] + | [ + OrgPermissionMachineIdentityAuthTemplateActions, + OrgPermissionSubjects.MachineIdentityAuthTemplate + ] | [OrgGatewayPermissionActions, OrgPermissionSubjects.Gateway] | [OrgPermissionSecretShareAction, OrgPermissionSubjects.SecretShare]; // TODO(scott): add back once org UI refactored diff --git a/frontend/src/hooks/api/admin/mutation.ts b/frontend/src/hooks/api/admin/mutation.ts index 916067f00..b196f19e7 100644 --- a/frontend/src/hooks/api/admin/mutation.ts +++ b/frontend/src/hooks/api/admin/mutation.ts @@ -68,6 +68,25 @@ export const useAdminDeleteUser = () => { }); }; +export const useAdminBulkDeleteUsers = () => { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: async (userIds: string[]) => { + await apiRequest.delete("/api/v1/admin/user-management/users", { + data: { userIds } + }); + + return {}; + }, + onSuccess: () => { + queryClient.invalidateQueries({ + queryKey: [adminStandaloneKeys.getUsers] + }); + queryClient.invalidateQueries({ queryKey: adminStandaloneKeys.getOrganizations }); + } + }); +}; + export const useAdminDeleteOrganizationMembership = () => { const queryClient = useQueryClient(); return useMutation({ diff --git a/frontend/src/hooks/api/admin/types.ts b/frontend/src/hooks/api/admin/types.ts index 51ff5a908..fe2cc3eed 100644 --- a/frontend/src/hooks/api/admin/types.ts +++ b/frontend/src/hooks/api/admin/types.ts @@ -35,6 +35,7 @@ export type TServerConfig = { initialized: boolean; allowSignUp: boolean; allowedSignUpDomain?: string | null; + disableAuditLogStorage: boolean; isMigrationModeOn?: boolean; trustSamlEmails: boolean; trustLdapEmails: boolean; diff --git a/frontend/src/hooks/api/appConnections/types/github-connection.ts b/frontend/src/hooks/api/appConnections/types/github-connection.ts index a8b734aa9..27bed2dcb 100644 --- a/frontend/src/hooks/api/appConnections/types/github-connection.ts +++ b/frontend/src/hooks/api/appConnections/types/github-connection.ts @@ -11,6 +11,7 @@ export type TGitHubConnection = TRootAppConnection & { app: AppConnection.GitHub method: GitHubConnectionMethod.OAuth; credentials: { code: string; + instanceType?: "cloud" | "server"; host?: string; }; } @@ -19,6 +20,7 @@ export type TGitHubConnection = TRootAppConnection & { app: AppConnection.GitHub credentials: { code: string; installationId: string; + instanceType?: "cloud" | "server"; host?: string; }; } diff --git a/frontend/src/hooks/api/auth/queries.tsx b/frontend/src/hooks/api/auth/queries.tsx index 796fd3152..cb7e3f64c 100644 --- a/frontend/src/hooks/api/auth/queries.tsx +++ b/frontend/src/hooks/api/auth/queries.tsx @@ -8,26 +8,24 @@ import { organizationKeys } from "../organization/queries"; import { setAuthToken } from "../reactQuery"; import { workspaceKeys } from "../workspace"; import { - ChangePasswordDTO, CompleteAccountDTO, CompleteAccountSignupDTO, GetAuthTokenAPI, GetBackupEncryptedPrivateKeyDTO, - IssueBackupPrivateKeyDTO, Login1DTO, Login1Res, Login2DTO, Login2Res, LoginLDAPDTO, LoginLDAPRes, + LoginV3DTO, + LoginV3Res, MfaMethod, ResetPasswordDTO, ResetPasswordV2DTO, ResetUserPasswordV2DTO, SendMfaTokenDTO, SetupPasswordDTO, - SRP1DTO, - SRPR1Res, TOauthTokenExchangeDTO, UserAgentType, UserEncryptionVersion, @@ -50,21 +48,14 @@ export const login2 = async (loginDetails: Login2DTO) => { return data; }; -export const loginLDAPRedirect = async (loginLDAPDetails: LoginLDAPDTO) => { - const { data } = await apiRequest.post("/api/v1/ldap/login", loginLDAPDetails); // return if account is complete or not + provider auth token +export const loginV3 = async (loginDetails: LoginV3DTO) => { + const { data } = await apiRequest.post("/api/v3/auth/login", loginDetails); return data; }; -export const useLogin1 = () => { - return useMutation({ - mutationFn: async (details: { - email: string; - clientPublicKey: string; - providerAuthToken?: string; - }) => { - return login1(details); - } - }); +export const loginLDAPRedirect = async (loginLDAPDetails: LoginLDAPDTO) => { + const { data } = await apiRequest.post("/api/v1/ldap/login", loginLDAPDetails); // return if account is complete or not + provider auth token + return data; }; export const selectOrganization = async (data: { @@ -143,11 +134,6 @@ export const useOauthTokenExchange = () => { }); }; -export const srp1 = async (details: SRP1DTO) => { - const { data } = await apiRequest.post("/api/v1/password/srp1", details); - return data; -}; - export const completeAccountSignup = async (details: CompleteAccountSignupDTO) => { const { data } = await apiRequest.post("/api/v3/signup/complete-account/signup", details); return data; @@ -158,14 +144,6 @@ export const completeAccountSignupInvite = async (details: CompleteAccountDTO) = return data; }; -export const useCompleteAccountSignup = () => { - return useMutation({ - mutationFn: async (details: CompleteAccountSignupDTO) => { - return completeAccountSignup(details); - } - }); -}; - export const useSendMfaToken = () => { return useMutation({ mutationFn: async ({ email }) => { @@ -263,11 +241,6 @@ export const useVerifyPasswordResetCode = () => { }); }; -export const issueBackupPrivateKey = async (details: IssueBackupPrivateKeyDTO) => { - const { data } = await apiRequest.post("/api/v1/password/backup-private-key", details); - return data; -}; - export const getBackupEncryptedPrivateKey = async ({ verificationToken }: GetBackupEncryptedPrivateKeyDTO) => { @@ -328,20 +301,6 @@ export const useResetUserPasswordV2 = () => { }); }; -export const changePassword = async (details: ChangePasswordDTO) => { - const { data } = await apiRequest.post("/api/v1/password/change-password", details); - return data; -}; - -export const useChangePassword = () => { - // note: use after srp1 - return useMutation({ - mutationFn: async (details: ChangePasswordDTO) => { - return changePassword(details); - } - }); -}; - // Refresh token is set as cookie when logged in // Using that we fetch the auth bearer token needed for auth calls export const fetchAuthToken = async () => { diff --git a/frontend/src/hooks/api/auth/types.ts b/frontend/src/hooks/api/auth/types.ts index cbd20b643..fd9d6fb5c 100644 --- a/frontend/src/hooks/api/auth/types.ts +++ b/frontend/src/hooks/api/auth/types.ts @@ -49,13 +49,19 @@ export type Login2DTO = { password: string; }; +export type LoginV3DTO = { + email: string; + password: string; + providerAuthToken?: string; + captchaToken?: string; +}; + export type Login1Res = { serverPublicKey: string; salt: string; }; export type Login2Res = { - mfaEnabled: boolean; token: string; encryptionVersion?: number; protectedKey?: string; @@ -67,6 +73,11 @@ export type Login2Res = { tag?: string; }; +export type LoginV3Res = { + accessToken: string; + mfaEnabled: boolean; +}; + export type LoginLDAPDTO = { organizationSlug: string; username: string; @@ -77,28 +88,10 @@ export type LoginLDAPRes = { nextUrl: string; }; -export type SRP1DTO = { - clientPublicKey: string; -}; - -export type SRPR1Res = { - serverPublicKey: string; - salt: string; -}; - export type CompleteAccountDTO = { email: string; firstName: string; lastName: string; - protectedKey: string; - protectedKeyIV: string; - protectedKeyTag: string; - publicKey: string; - encryptedPrivateKey: string; - encryptedPrivateKeyIV: string; - encryptedPrivateKeyTag: string; - salt: string; - verifier: string; password: string; tokenMetadata?: string; }; @@ -116,19 +109,6 @@ export type VerifySignupInviteDTO = { organizationId: string; }; -export type ChangePasswordDTO = { - password: string; - clientProof: string; - protectedKey: string; - protectedKeyIV: string; - protectedKeyTag: string; - encryptedPrivateKey: string; - encryptedPrivateKeyIV: string; - encryptedPrivateKeyTag: string; - salt: string; - verifier: string; -}; - export type ResetPasswordDTO = { protectedKey: string; protectedKeyIV: string; @@ -153,27 +133,11 @@ export type ResetUserPasswordV2DTO = { }; export type SetupPasswordDTO = { - protectedKey: string; - protectedKeyIV: string; - protectedKeyTag: string; - encryptedPrivateKey: string; - encryptedPrivateKeyIV: string; - encryptedPrivateKeyTag: string; - salt: string; - verifier: string; + email: string; token: string; password: string; }; -export type IssueBackupPrivateKeyDTO = { - encryptedPrivateKey: string; - iv: string; - tag: string; - salt: string; - verifier: string; - clientProof: string; -}; - export type GetBackupEncryptedPrivateKeyDTO = { verificationToken: string; }; diff --git a/frontend/src/hooks/api/identities/mutations.tsx b/frontend/src/hooks/api/identities/mutations.tsx index 165c94395..c01f1aa85 100644 --- a/frontend/src/hooks/api/identities/mutations.tsx +++ b/frontend/src/hooks/api/identities/mutations.tsx @@ -1385,6 +1385,7 @@ export const useAddIdentityLdapAuth = () => { return useMutation({ mutationFn: async ({ identityId, + templateId, url, bindDN, bindPass, @@ -1400,6 +1401,7 @@ export const useAddIdentityLdapAuth = () => { const { data } = await apiRequest.post<{ identityLdapAuth: IdentityLdapAuth }>( `/api/v1/auth/ldap-auth/identities/${identityId}`, { + templateId, url, bindDN, bindPass, @@ -1432,6 +1434,7 @@ export const useUpdateIdentityLdapAuth = () => { return useMutation({ mutationFn: async ({ identityId, + templateId, url, bindDN, bindPass, @@ -1447,6 +1450,7 @@ export const useUpdateIdentityLdapAuth = () => { const { data } = await apiRequest.patch<{ identityLdapAuth: IdentityLdapAuth }>( `/api/v1/auth/ldap-auth/identities/${identityId}`, { + templateId, url, bindDN, bindPass, diff --git a/frontend/src/hooks/api/identities/types.ts b/frontend/src/hooks/api/identities/types.ts index 1af1cc24c..c0e49e987 100644 --- a/frontend/src/hooks/api/identities/types.ts +++ b/frontend/src/hooks/api/identities/types.ts @@ -567,10 +567,11 @@ export type IdentityTokenAuth = { export type AddIdentityLdapAuthDTO = { organizationId: string; identityId: string; - url: string; - bindDN: string; - bindPass: string; - searchBase: string; + templateId?: string; + url?: string; + bindDN?: string; + bindPass?: string; + searchBase?: string; searchFilter: string; ldapCaCertificate?: string; allowedFields?: { @@ -588,6 +589,7 @@ export type AddIdentityLdapAuthDTO = { export type UpdateIdentityLdapAuthDTO = { identityId: string; organizationId: string; + templateId?: string; url?: string; bindDN?: string; bindPass?: string; @@ -612,10 +614,11 @@ export type DeleteIdentityLdapAuthDTO = { }; export type IdentityLdapAuth = { - url: string; - bindDN: string; - bindPass: string; - searchBase: string; + url?: string; + bindDN?: string; + templateId?: string; + bindPass?: string; + searchBase?: string; searchFilter: string; ldapCaCertificate?: string; allowedFields?: { diff --git a/frontend/src/hooks/api/identityAuthTemplates/index.tsx b/frontend/src/hooks/api/identityAuthTemplates/index.tsx new file mode 100644 index 000000000..177955438 --- /dev/null +++ b/frontend/src/hooks/api/identityAuthTemplates/index.tsx @@ -0,0 +1,3 @@ +export * from "./mutations"; +export * from "./queries"; +export * from "./types"; diff --git a/frontend/src/hooks/api/identityAuthTemplates/mutations.tsx b/frontend/src/hooks/api/identityAuthTemplates/mutations.tsx new file mode 100644 index 000000000..8c05f1e46 --- /dev/null +++ b/frontend/src/hooks/api/identityAuthTemplates/mutations.tsx @@ -0,0 +1,97 @@ +import { useMutation, useQueryClient } from "@tanstack/react-query"; + +import { apiRequest } from "@app/config/request"; + +import { identityAuthTemplatesKeys } from "./queries"; +import { + CreateIdentityAuthTemplateDTO, + DeleteIdentityAuthTemplateDTO, + IdentityAuthTemplate, + MachineAuthTemplateUsage, + UnlinkTemplateUsageDTO, + UpdateIdentityAuthTemplateDTO +} from "./types"; + +export const useCreateIdentityAuthTemplate = () => { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: async (dto: CreateIdentityAuthTemplateDTO) => { + const { data } = await apiRequest.post<{ template: IdentityAuthTemplate }>( + "/api/v1/identity-templates", + dto + ); + return data.template; + }, + onSuccess: (_, { organizationId }) => { + queryClient.invalidateQueries({ + queryKey: identityAuthTemplatesKeys.getTemplates({ organizationId }) + }); + } + }); +}; + +export const useUpdateIdentityAuthTemplate = () => { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: async (dto: UpdateIdentityAuthTemplateDTO) => { + const { data } = await apiRequest.patch<{ template: IdentityAuthTemplate }>( + `/api/v1/identity-templates/${dto.templateId}`, + dto + ); + return data.template; + }, + onSuccess: (_, { organizationId, templateId }) => { + queryClient.invalidateQueries({ + queryKey: identityAuthTemplatesKeys.getTemplates({ organizationId }) + }); + queryClient.invalidateQueries({ + queryKey: identityAuthTemplatesKeys.getTemplate(templateId) + }); + } + }); +}; + +export const useDeleteIdentityAuthTemplate = () => { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: async (dto: DeleteIdentityAuthTemplateDTO) => { + await apiRequest.delete(`/api/v1/identity-templates/${dto.templateId}`, { + params: { organizationId: dto.organizationId } + }); + }, + onSuccess: (_, { organizationId, templateId }) => { + queryClient.invalidateQueries({ + queryKey: identityAuthTemplatesKeys.getTemplates({ organizationId }) + }); + queryClient.removeQueries({ + queryKey: identityAuthTemplatesKeys.getTemplate(templateId) + }); + } + }); +}; + +export const useUnlinkTemplateUsage = () => { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: async (dto: UnlinkTemplateUsageDTO) => { + const { data } = await apiRequest.post( + `/api/v1/identity-templates/${dto.templateId}/delete-usage`, + { identityIds: dto.identityIds }, + { params: { organizationId: dto.organizationId } } + ); + return data; + }, + onSuccess: (_, { templateId, organizationId }) => { + queryClient.invalidateQueries({ + queryKey: identityAuthTemplatesKeys.getTemplateUsages(templateId) + }); + queryClient.invalidateQueries({ + queryKey: identityAuthTemplatesKeys.getTemplates({ organizationId }) + }); + } + }); +}; diff --git a/frontend/src/hooks/api/identityAuthTemplates/queries.tsx b/frontend/src/hooks/api/identityAuthTemplates/queries.tsx new file mode 100644 index 000000000..a9981c0c4 --- /dev/null +++ b/frontend/src/hooks/api/identityAuthTemplates/queries.tsx @@ -0,0 +1,89 @@ +import { useQuery } from "@tanstack/react-query"; + +import { apiRequest } from "@app/config/request"; + +import { + GetIdentityAuthTemplatesDTO, + GetTemplateUsagesDTO, + IdentityAuthTemplate, + MachineAuthTemplateUsage, + MachineIdentityAuthMethod +} from "./types"; + +export const identityAuthTemplatesKeys = { + all: ["identity-auth-templates"] as const, + getTemplates: (dto: GetIdentityAuthTemplatesDTO) => + [...identityAuthTemplatesKeys.all, "list", dto] as const, + getTemplate: (templateId: string) => + [...identityAuthTemplatesKeys.all, "single", templateId] as const, + getAvailableTemplates: (authMethod: MachineIdentityAuthMethod) => + [...identityAuthTemplatesKeys.all, "available", authMethod] as const, + getTemplateUsages: (templateId: string) => + [...identityAuthTemplatesKeys.all, "usages", templateId] as const +}; + +export const useGetIdentityAuthTemplates = (dto: GetIdentityAuthTemplatesDTO) => { + return useQuery({ + queryKey: identityAuthTemplatesKeys.getTemplates(dto), + queryFn: async () => { + const { data } = await apiRequest.get<{ + templates: IdentityAuthTemplate[]; + totalCount: number; + }>("/api/v1/identity-templates/search", { + params: { + organizationId: dto.organizationId, + limit: dto.limit || 50, + offset: dto.offset || 0, + ...(dto.search && { search: dto.search }) + } + }); + return data; + }, + enabled: Boolean(dto.organizationId) + }); +}; + +export const useGetIdentityAuthTemplate = (templateId: string, organizationId: string) => { + return useQuery({ + queryKey: identityAuthTemplatesKeys.getTemplate(templateId), + queryFn: async () => { + const { data } = await apiRequest.get( + `/api/v1/identity-templates/${templateId}`, + { + params: { organizationId } + } + ); + return data; + }, + enabled: Boolean(templateId) && Boolean(organizationId) + }); +}; + +export const useGetAvailableTemplates = (authMethod: MachineIdentityAuthMethod) => { + return useQuery({ + queryKey: identityAuthTemplatesKeys.getAvailableTemplates(authMethod), + queryFn: async () => { + const { data } = await apiRequest.get("/api/v1/identity-templates", { + params: { authMethod } + }); + return data; + }, + enabled: Boolean(authMethod) + }); +}; + +export const useGetTemplateUsages = (dto: GetTemplateUsagesDTO) => { + return useQuery({ + queryKey: identityAuthTemplatesKeys.getTemplateUsages(dto.templateId), + queryFn: async () => { + const { data } = await apiRequest.get( + `/api/v1/identity-templates/${dto.templateId}/usage`, + { + params: { organizationId: dto.organizationId } + } + ); + return data; + }, + enabled: Boolean(dto.templateId) && Boolean(dto.organizationId) + }); +}; diff --git a/frontend/src/hooks/api/identityAuthTemplates/types.ts b/frontend/src/hooks/api/identityAuthTemplates/types.ts new file mode 100644 index 000000000..860f9a9fc --- /dev/null +++ b/frontend/src/hooks/api/identityAuthTemplates/types.ts @@ -0,0 +1,78 @@ +export enum MachineIdentityAuthMethod { + LDAP = "ldap" +} + +export interface LdapTemplateFields { + url: string; + bindDN: string; + bindPass: string; + searchBase: string; + ldapCaCertificate?: string; +} + +export interface IdentityAuthTemplate { + id: string; + name: string; + authMethod: MachineIdentityAuthMethod; + organizationId: string; + templateFields: LdapTemplateFields; + createdAt: string; + updatedAt: string; +} + +export interface CreateIdentityAuthTemplateDTO { + organizationId: string; + name: string; + authMethod: MachineIdentityAuthMethod; + templateFields: LdapTemplateFields; +} + +export interface UpdateIdentityAuthTemplateDTO { + templateId: string; + organizationId: string; + name?: string; + templateFields?: Partial; +} + +export interface DeleteIdentityAuthTemplateDTO { + templateId: string; + organizationId: string; +} + +export interface GetIdentityAuthTemplatesDTO { + organizationId: string; + limit?: number; + offset?: number; + search?: string; +} + +export interface MachineAuthTemplateUsage { + identityId: string; + identityName: string; +} + +export interface GetTemplateUsagesDTO { + templateId: string; + organizationId: string; +} + +export interface UnlinkTemplateUsageDTO { + templateId: string; + identityIds: string[]; + organizationId: string; +} + +export const TEMPLATE_ERROR_MESSAGES = { + UNLINK_SUCCESS: "Successfully unlinked template usages", + UNLINK_FAILED: "Failed to unlink template usages", + SINGLE_UNLINK_SUCCESS: "Successfully unlinked template usage", + SINGLE_UNLINK_FAILED: "Failed to unlink template usage" +} as const; + +export const TEMPLATE_UI_LABELS = { + VIEW_USAGES: "View Usages", + EDIT_TEMPLATE: "Edit Template", + DELETE_TEMPLATE: "Delete Template", + UNLINK: "Unlink", + UNSELECT_ALL: "Unselect All" +} as const; diff --git a/frontend/src/hooks/api/index.tsx b/frontend/src/hooks/api/index.tsx index 4b4967f16..78dcd3b79 100644 --- a/frontend/src/hooks/api/index.tsx +++ b/frontend/src/hooks/api/index.tsx @@ -15,6 +15,7 @@ export * from "./gateways"; export * from "./githubOrgSyncConfig"; export * from "./groups"; export * from "./identities"; +export * from "./identityAuthTemplates"; export * from "./identityProjectAdditionalPrivilege"; export * from "./incidentContacts"; export * from "./integrationAuth"; diff --git a/frontend/src/hooks/api/reminders/queries.tsx b/frontend/src/hooks/api/reminders/queries.tsx index 68c00913d..557bc24ea 100644 --- a/frontend/src/hooks/api/reminders/queries.tsx +++ b/frontend/src/hooks/api/reminders/queries.tsx @@ -12,14 +12,15 @@ export const useCreateReminder = (secretId: string) => { const queryClient = useQueryClient(); return useMutation({ - mutationFn: async ({ message, repeatDays, nextReminderDate, recipients }) => { + mutationFn: async ({ message, repeatDays, nextReminderDate, recipients, fromDate }) => { const { data } = await apiRequest.post<{ reminder: Reminder }>( `/api/v1/reminders/secrets/${secretId}`, { message, repeatDays, nextReminderDate, - recipients + recipients, + fromDate } ); return data.reminder; diff --git a/frontend/src/hooks/api/reminders/types.ts b/frontend/src/hooks/api/reminders/types.ts index 245805aea..cbe96aeaf 100644 --- a/frontend/src/hooks/api/reminders/types.ts +++ b/frontend/src/hooks/api/reminders/types.ts @@ -2,6 +2,7 @@ export type CreateReminderDTO = { message?: string | null; repeatDays?: number | null; nextReminderDate?: Date | null; + fromDate?: Date | null; secretId: string; recipients?: string[]; }; diff --git a/frontend/src/hooks/api/serverDetails/types.ts b/frontend/src/hooks/api/serverDetails/types.ts index 3e22c2684..d43521382 100644 --- a/frontend/src/hooks/api/serverDetails/types.ts +++ b/frontend/src/hooks/api/serverDetails/types.ts @@ -5,4 +5,5 @@ export type ServerStatus = { secretScanningConfigured: boolean; redisConfigured: boolean; samlDefaultOrgSlug: string; + auditLogStorageDisabled: boolean; }; diff --git a/frontend/src/hooks/api/subscriptions/types.ts b/frontend/src/hooks/api/subscriptions/types.ts index 87a02231f..4ded71cfe 100644 --- a/frontend/src/hooks/api/subscriptions/types.ts +++ b/frontend/src/hooks/api/subscriptions/types.ts @@ -53,4 +53,5 @@ export type SubscriptionPlan = { secretScanning: boolean; enterpriseSecretSyncs: boolean; enterpriseAppConnections: boolean; + machineIdentityAuthTemplates: boolean; }; diff --git a/frontend/src/hooks/api/users/queries.tsx b/frontend/src/hooks/api/users/queries.tsx index 75edc0907..77289fee6 100644 --- a/frontend/src/hooks/api/users/queries.tsx +++ b/frontend/src/hooks/api/users/queries.tsx @@ -495,14 +495,6 @@ export const useGetMyOrganizationProjects = (orgId: string) => { }); }; -export const fetchMyPrivateKey = async () => { - const { - data: { privateKey } - } = await apiRequest.get<{ privateKey: string }>("/api/v1/user/private-key"); - - return privateKey; -}; - export const useListUserGroupMemberships = (username: string) => { return useQuery({ queryKey: userKeys.listUserGroupMemberships(username), diff --git a/frontend/src/layouts/OrganizationLayout/OrganizationLayout.tsx b/frontend/src/layouts/OrganizationLayout/OrganizationLayout.tsx index c5a0953de..c33f81699 100644 --- a/frontend/src/layouts/OrganizationLayout/OrganizationLayout.tsx +++ b/frontend/src/layouts/OrganizationLayout/OrganizationLayout.tsx @@ -6,10 +6,11 @@ import { twMerge } from "tailwind-merge"; import { CreateOrgModal } from "@app/components/organization/CreateOrgModal"; import { Banner } from "@app/components/page-frames/Banner"; -import { useServerConfig } from "@app/context"; +import { useServerConfig, useSubscription } from "@app/context"; import { usePopUp } from "@app/hooks"; import { useFetchServerStatus } from "@app/hooks/api"; +import { AuditLogBanner } from "./components/AuditLogBanner"; import { InsecureConnectionBanner } from "./components/InsecureConnectionBanner"; import { Navbar } from "./components/NavBar"; import { OrgSidebar } from "./components/OrgSidebar"; @@ -31,6 +32,7 @@ export const OrganizationLayout = () => { const containerHeight = config.pageFrameContent ? "h-[94vh]" : "h-screen"; const { data: serverDetails, isLoading } = useFetchServerStatus(); + const { subscription } = useSubscription(); return ( <> @@ -41,6 +43,7 @@ export const OrganizationLayout = () => { {!isLoading && !serverDetails?.redisConfigured && } {!isLoading && !serverDetails?.emailConfigured && } + {!isLoading && subscription.auditLogs && } {!window.isSecureContext && }
diff --git a/frontend/src/layouts/OrganizationLayout/components/AuditLogBanner/AuditLogBanner.tsx b/frontend/src/layouts/OrganizationLayout/components/AuditLogBanner/AuditLogBanner.tsx new file mode 100644 index 000000000..9f7494076 --- /dev/null +++ b/frontend/src/layouts/OrganizationLayout/components/AuditLogBanner/AuditLogBanner.tsx @@ -0,0 +1,23 @@ +import { useOrganization } from "@app/context"; +import { useFetchServerStatus, useGetAuditLogStreams } from "@app/hooks/api"; + +import { OrgAlertBanner } from "../OrgAlertBanner"; + +export const AuditLogBanner = () => { + const org = useOrganization(); + const { data: status, isLoading: isLoadingStatus } = useFetchServerStatus(); + const { data: streams, isLoading: isLoadingStreams } = useGetAuditLogStreams(org.currentOrg.id); + + if (isLoadingStreams || isLoadingStatus || !streams) return null; + + if (status?.auditLogStorageDisabled && !streams.length) { + return ( + + ); + } + + return null; +}; diff --git a/frontend/src/layouts/OrganizationLayout/components/AuditLogBanner/index.ts b/frontend/src/layouts/OrganizationLayout/components/AuditLogBanner/index.ts new file mode 100644 index 000000000..c7bf7d475 --- /dev/null +++ b/frontend/src/layouts/OrganizationLayout/components/AuditLogBanner/index.ts @@ -0,0 +1 @@ +export * from "./AuditLogBanner"; diff --git a/frontend/src/lib/crypto/index.ts b/frontend/src/lib/crypto/index.ts index d0d6cd5dd..d722b68ea 100644 --- a/frontend/src/lib/crypto/index.ts +++ b/frontend/src/lib/crypto/index.ts @@ -4,53 +4,6 @@ import jsrp from "jsrp"; import Aes256Gcm from "@app/components/utilities/cryptography/aes-256-gcm"; import { deriveArgonKey, generateKeyPair } from "@app/components/utilities/cryptography/crypto"; -import { issueBackupPrivateKey, srp1 } from "@app/hooks/api/auth/queries"; - -export const generateUserBackupKey = async (email: string, password: string) => { - // eslint-disable-next-line new-cap - const clientKey = new jsrp.client(); - // eslint-disable-next-line new-cap - const clientPassword = new jsrp.client(); - - await new Promise((resolve) => { - clientPassword.init({ username: email, password }, () => resolve(null)); - }); - const clientPublicKey = clientPassword.getPublicKey(); - const srpKeys = await srp1({ clientPublicKey }); - clientPassword.setSalt(srpKeys.salt); - clientPassword.setServerPublicKey(srpKeys.serverPublicKey); - - const clientProof = clientPassword.getProof(); // called M1 - const generatedKey = crypto.randomBytes(16).toString("hex"); - - await new Promise((resolve) => { - clientKey.init({ username: email, password: generatedKey }, () => resolve(null)); - }); - - const { salt, verifier } = await new Promise<{ salt: string; verifier: string }>( - (resolve, reject) => { - clientKey.createVerifier((err, res) => { - if (err) return reject(err); - return resolve(res); - }); - } - ); - const { ciphertext, iv, tag } = Aes256Gcm.encrypt({ - text: String(localStorage.getItem("PRIVATE_KEY")), - secret: generatedKey - }); - - await issueBackupPrivateKey({ - encryptedPrivateKey: ciphertext, - iv, - tag, - salt, - verifier, - clientProof - }); - - return generatedKey; -}; export const generateUserPassKey = async ( email: string, diff --git a/frontend/src/pages/admin/MachineIdentitiesResourcesPage/components/MachineIdentitiesTable.tsx b/frontend/src/pages/admin/MachineIdentitiesResourcesPage/components/MachineIdentitiesTable.tsx index 7d4527032..9c44b72c4 100644 --- a/frontend/src/pages/admin/MachineIdentitiesResourcesPage/components/MachineIdentitiesTable.tsx +++ b/frontend/src/pages/admin/MachineIdentitiesResourcesPage/components/MachineIdentitiesTable.tsx @@ -1,5 +1,11 @@ import { useState } from "react"; -import { faEllipsis, faMagnifyingGlass, faServer } from "@fortawesome/free-solid-svg-icons"; +import { + faEllipsisV, + faMagnifyingGlass, + faServer, + faShieldHalved, + faXmark +} from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { createNotification } from "@app/components/notifications"; @@ -12,6 +18,7 @@ import { DropdownMenuItem, DropdownMenuTrigger, EmptyState, + IconButton, Input, Table, TableContainer, @@ -88,18 +95,33 @@ const IdentityPanelTable = ({ {isInstanceAdmin && (
- -
- -
+ + + + - + {isInstanceAdmin && ( { e.stopPropagation(); handlePopUpOpen("removeServerAdmin", { name, id }); }} + icon={ +
+ + +
+ } > Remove Server Admin
@@ -125,7 +147,7 @@ const IdentityPanelTable = ({ isDisabled={isFetchingNextPage || !hasNextPage} onClick={() => fetchNextPage()} > - {hasNextPage ? "Load More" : "End of list"} + {hasNextPage ? "Load More" : "End of List"} )}
diff --git a/frontend/src/pages/admin/SignUpPage/SignUpPage.tsx b/frontend/src/pages/admin/SignUpPage/SignUpPage.tsx index 2d6aeb9d6..19c1537a2 100644 --- a/frontend/src/pages/admin/SignUpPage/SignUpPage.tsx +++ b/frontend/src/pages/admin/SignUpPage/SignUpPage.tsx @@ -8,7 +8,6 @@ import { z } from "zod"; import { createNotification } from "@app/components/notifications"; // TODO(akhilmhdh): rewrite this into module functions in lib -import { saveTokenToLocalStorage } from "@app/components/utilities/saveTokenToLocalStorage"; import SecurityClient from "@app/components/utilities/SecurityClient"; import { Button, ContentLoader, FormControl, Input } from "@app/components/v2"; import { useServerConfig } from "@app/context"; @@ -63,13 +62,6 @@ export const SignUpPage = () => { }); SecurityClient.setToken(res.token); - saveTokenToLocalStorage({ - publicKey: userPass.publicKey, - encryptedPrivateKey: userPass.encryptedPrivateKey, - iv: userPass.encryptedPrivateKeyIV, - tag: userPass.encryptedPrivateKeyTag, - privateKey - }); await selectOrganization({ organizationId: res.organization.id }); // TODO(akhilmhdh): This is such a confusing pattern and too unreliable diff --git a/frontend/src/pages/admin/UserIdentitiesResourcesPage/components/UserIdentitiesTable.tsx b/frontend/src/pages/admin/UserIdentitiesResourcesPage/components/UserIdentitiesTable.tsx index 500796797..1f2471aa2 100644 --- a/frontend/src/pages/admin/UserIdentitiesResourcesPage/components/UserIdentitiesTable.tsx +++ b/frontend/src/pages/admin/UserIdentitiesResourcesPage/components/UserIdentitiesTable.tsx @@ -1,13 +1,19 @@ -import { useState } from "react"; +import { Dispatch, SetStateAction, useState } from "react"; import { faCheckCircle, - faEllipsis, + faEllipsisV, faFilter, faMagnifyingGlass, + faShieldHalved, + faTrash, faUsers, - faUserShield + faUserShield, + faUserXmark, + faWarning, + faXmark } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { InfiniteData } from "@tanstack/react-query"; import { twMerge } from "tailwind-merge"; import { UpgradePlanModal } from "@app/components/license/UpgradePlanModal"; @@ -15,6 +21,7 @@ import { createNotification } from "@app/components/notifications"; import { Badge, Button, + Checkbox, DeleteActionModal, DropdownMenu, DropdownMenuContent, @@ -31,23 +38,37 @@ import { Td, Th, THead, + Tooltip, Tr } from "@app/components/v2"; -import { useSubscription } from "@app/context"; +import { useSubscription, useUser } from "@app/context"; import { useDebounce, usePopUp } from "@app/hooks"; import { + useAdminBulkDeleteUsers, useAdminDeleteUser, useAdminGetUsers, useAdminGrantServerAdminAccess, useRemoveUserServerAdminAccess } from "@app/hooks/api"; +import { User } from "@app/hooks/api/users/types"; import { UsePopUpState } from "@app/hooks/usePopUp"; const addServerAdminUpgradePlanMessage = "Granting another user Server Admin permissions"; const removeServerAdminUpgradePlanMessage = "Removing Server Admin permissions from user"; const UserPanelTable = ({ - handlePopUpOpen + handlePopUpOpen, + users: usersPages, + isPending, + adminsOnly, + searchUserFilter, + setSearchUserFilter, + setAdminsOnly, + isFetchingNextPage, + fetchNextPage, + hasNextPage, + selectedUsers, + setSelectedUsers }: { handlePopUpOpen: ( popUpName: keyof UsePopUpState< @@ -59,21 +80,38 @@ const UserPanelTable = ({ message?: string; } ) => void; + isPending: boolean; + users: InfiniteData | undefined; + adminsOnly: boolean; + setAdminsOnly: (adminsOnly: boolean) => void; + searchUserFilter: string; + setSearchUserFilter: (filter: string) => void; + selectedUsers: User[]; + setSelectedUsers: Dispatch>; + isFetchingNextPage: boolean; + fetchNextPage: () => void; + hasNextPage: boolean; }) => { - const [searchUserFilter, setSearchUserFilter] = useState(""); - const [adminsOnly, setAdminsOnly] = useState(false); - const [debouncedSearchTerm] = useDebounce(searchUserFilter, 500); const { subscription } = useSubscription(); - const { data, isPending, isFetchingNextPage, hasNextPage, fetchNextPage } = useAdminGetUsers({ - limit: 20, - searchTerm: debouncedSearchTerm, - adminsOnly - }); + const users = usersPages?.pages.flat(); - const isEmpty = !isPending && !data?.pages?.[0].length; + const isEmpty = !isPending && !users?.length; const isTableFiltered = Boolean(adminsOnly); + const selectedUserIds = selectedUsers.map((user) => user.id); + + const isPageSelected = users?.length + ? users.every((user) => selectedUserIds.includes(user.id)) + : false; + + // eslint-disable-next-line no-nested-ternary + const isPageIndeterminate = isPageSelected + ? false + : users?.length + ? users?.some((user) => selectedUserIds.includes(user.id)) + : false; + return ( <>
@@ -121,90 +159,144 @@ const UserPanelTable = ({ + - - + {isPending && } {!isPending && - data?.pages?.map((users) => - users.map(({ username, email, firstName, lastName, id, superAdmin }) => { - const name = firstName || lastName ? `${firstName} ${lastName}` : "-"; + users?.map((user) => { + const { username, email, firstName, lastName, id, superAdmin } = user; + const name = firstName || lastName ? `${firstName} ${lastName}` : null; - return ( - - + + - - + + - - ); - }) - )} + )} + {superAdmin && ( + + + + + } + onClick={(e) => { + e.stopPropagation(); + if (!subscription?.instanceUserManagement) { + handlePopUpOpen("upgradePlan", { + username, + id, + message: removeServerAdminUpgradePlanMessage + }); + return; + } + handlePopUpOpen("removeServerAdmin", { username, id }); + }} + > + Remove Server Admin + + )} + + + + + + ); + })}
+ { + if (isPageSelected) { + setSelectedUsers((prev) => + prev.filter((u) => !users?.find((user) => user.id === u.id)) + ); + } else { + setSelectedUsers((prev) => [ + ...prev, + ...(users?.filter((u) => !prev.find((user) => user.id === u.id)) ?? []) + ]); + } + }} + /> + NameUsername + Username
- {name} + const isSelected = selectedUserIds.includes(id); + return ( +
+ { + e.stopPropagation(); + setSelectedUsers((prev) => + isSelected ? prev.filter((u) => u.id !== id) : [...prev, user] + ); + }} + /> + +
+

+ {name ?? Not Set} +

{superAdmin && ( - + Server Admin )} -
{email} -
- - -
- -
-
- +
+
+

{email}

+
+
+ + + + + + + + { + e.stopPropagation(); + handlePopUpOpen("removeUser", { username, id }); + }} + icon={} + > + Remove User + + {!superAdmin && ( } onClick={(e) => { e.stopPropagation(); - handlePopUpOpen("removeUser", { username, id }); + if (!subscription?.instanceUserManagement) { + handlePopUpOpen("upgradePlan", { + username, + id, + message: addServerAdminUpgradePlanMessage + }); + return; + } + handlePopUpOpen("upgradeToServerAdmin", { username, id }); }} > - Remove User + Make User Server Admin - {!superAdmin && ( - { - e.stopPropagation(); - if (!subscription?.instanceUserManagement) { - handlePopUpOpen("upgradePlan", { - username, - id, - message: addServerAdminUpgradePlanMessage - }); - return; - } - handlePopUpOpen("upgradeToServerAdmin", { username, id }); - }} - > - Make User Server Admin - - )} - {superAdmin && ( - { - e.stopPropagation(); - if (!subscription?.instanceUserManagement) { - handlePopUpOpen("upgradePlan", { - username, - id, - message: removeServerAdminUpgradePlanMessage - }); - return; - } - handlePopUpOpen("removeServerAdmin", { username, id }); - }} - > - Remove Server Admin - - )} - - -
-
{!isPending && isEmpty && } @@ -218,7 +310,7 @@ const UserPanelTable = ({ isDisabled={isFetchingNextPage || !hasNextPage} onClick={() => fetchNextPage()} > - {hasNextPage ? "Load More" : "End of list"} + {hasNextPage ? "Load More" : "End of List"} )}
@@ -231,13 +323,36 @@ export const UserIdentitiesTable = () => { "removeUser", "upgradePlan", "upgradeToServerAdmin", - "removeServerAdmin" + "removeServerAdmin", + "removeUsers" ] as const); + const { + user: { id: userId } + } = useUser(); + const { mutateAsync: deleteUser } = useAdminDeleteUser(); + const { mutateAsync: deleteUsers } = useAdminBulkDeleteUsers(); const { mutateAsync: grantAdminAccess } = useAdminGrantServerAdminAccess(); const { mutateAsync: removeAdminAccess } = useRemoveUserServerAdminAccess(); + const [selectedUsers, setSelectedUsers] = useState([]); + const [searchUserFilter, setSearchUserFilter] = useState(""); + const [adminsOnly, setAdminsOnly] = useState(false); + const [debouncedSearchTerm] = useDebounce(searchUserFilter, 500); + + const { + data: users, + isPending, + isFetchingNextPage, + hasNextPage, + fetchNextPage + } = useAdminGetUsers({ + limit: 20, + searchTerm: debouncedSearchTerm, + adminsOnly + }); + const handleRemoveUser = async () => { const { id } = popUp?.removeUser?.data as { id: string; username: string }; @@ -295,45 +410,158 @@ export const UserIdentitiesTable = () => { handlePopUpClose("removeServerAdmin"); }; + const handleRemoveUsers = async () => { + try { + await deleteUsers(selectedUsers.map((user) => user.id)); + + createNotification({ + text: "Successfully removed users", + type: "success" + }); + + setSelectedUsers([]); + handlePopUpClose("removeUsers"); + } catch { + createNotification({ + text: "Failed to remove users", + type: "error" + }); + } + }; + return ( -
- - handlePopUpToggle("removeUser", isOpen)} - onDeleteApproved={handleRemoveUser} - /> - handlePopUpToggle("upgradeToServerAdmin", isOpen)} - deleteKey="confirm" - onDeleteApproved={handleGrantServerAdminAccess} - buttonText="Grant Access" - /> - handlePopUpToggle("removeServerAdmin", isOpen)} - deleteKey="confirm" - onDeleteApproved={handleRemoveServerAdminAccess} - buttonText="Remove Access" - /> - handlePopUpToggle("upgradePlan", isOpen)} - text={`${popUp?.upgradePlan?.data?.message} is only available on Infisical's Pro plan and above.`} - /> -
+ <> +
0 && "h-16" + )} + > +
+
{selectedUsers.length} Selected
+ + +
+
+
+ + handlePopUpToggle("removeUser", isOpen)} + onDeleteApproved={handleRemoveUser} + /> + handlePopUpToggle("upgradeToServerAdmin", isOpen)} + deleteKey="confirm" + onDeleteApproved={handleGrantServerAdminAccess} + buttonText="Grant Access" + /> + handlePopUpToggle("removeServerAdmin", isOpen)} + deleteKey="confirm" + onDeleteApproved={handleRemoveServerAdminAccess} + buttonText="Remove Access" + /> + handlePopUpToggle("upgradePlan", isOpen)} + text={`${popUp?.upgradePlan?.data?.message} is only available on Infisical's Pro plan and above.`} + /> + handlePopUpToggle("removeUsers", isOpen)} + deleteKey="confirm" + onDeleteApproved={() => handleRemoveUsers()} + buttonText="Remove" + > +
+ The following members will be removed: +
+
+
    + {selectedUsers?.map((user) => { + const email = user.email ?? user.username; + return ( +
  • +
    +

    + {user.firstName || user.lastName ? ( + <> + {`${`${user.firstName} ${user.lastName}`.trim()} `}( + {email}) + + ) : ( + {email} + )}{" "} +

    + {userId === user.id && ( + +
    + + + Removing Yourself + +
    +
    + )} +
    +
  • + ); + })} +
+
+
+
+ ); }; diff --git a/frontend/src/pages/auth/LoginPage/components/PasswordStep/PasswordStep.tsx b/frontend/src/pages/auth/LoginPage/components/PasswordStep/PasswordStep.tsx index 86dff5288..9666a1af4 100644 --- a/frontend/src/pages/auth/LoginPage/components/PasswordStep/PasswordStep.tsx +++ b/frontend/src/pages/auth/LoginPage/components/PasswordStep/PasswordStep.tsx @@ -18,7 +18,7 @@ import { useToggle } from "@app/hooks"; import { useOauthTokenExchange, useSelectOrganization } from "@app/hooks/api"; import { MfaMethod } from "@app/hooks/api/auth/types"; import { fetchOrganizations } from "@app/hooks/api/organization/queries"; -import { fetchMyPrivateKey, fetchUserDuplicateAccounts } from "@app/hooks/api/users/queries"; +import { fetchUserDuplicateAccounts } from "@app/hooks/api/users/queries"; import { EmailDuplicationConfirmation } from "@app/pages/auth/SelectOrgPage/EmailDuplicationConfirmation"; import { navigateUserToOrg, useNavigateToSelectOrganization } from "../../Login.utils"; @@ -70,9 +70,6 @@ export const PasswordStep = ({ // set JWT token SecurityClient.setToken(oauthLogin.token); - const privateKey = await fetchMyPrivateKey(); - localStorage.setItem("PRIVATE_KEY", privateKey); - // case: organization ID is present from the provider auth token -- select the org and use the new jwt token in the CLI, then navigate to the org if (organizationId) { const finishWithOrgWorkflow = async () => { @@ -92,7 +89,7 @@ export const PasswordStep = ({ console.log("organization id was present. new JWT token to be used in CLI:", token); const instance = axios.create(); const payload = { - privateKey, + privateKey: "", // note(daniel): no longer needed by the CLI, because the CLI only uses the private key to create service tokens, and the private key isn't used anymore when creating service tokens. email, JTWToken: token }; diff --git a/frontend/src/pages/auth/PasswordSetupPage/PasswordSetupPage.tsx b/frontend/src/pages/auth/PasswordSetupPage/PasswordSetupPage.tsx index 311d613ef..a134220e8 100644 --- a/frontend/src/pages/auth/PasswordSetupPage/PasswordSetupPage.tsx +++ b/frontend/src/pages/auth/PasswordSetupPage/PasswordSetupPage.tsx @@ -1,22 +1,14 @@ -import crypto from "crypto"; - import { FormEvent, useState } from "react"; import { faCheck, faEye, faEyeSlash, faKey, faX } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { useNavigate, useSearch } from "@tanstack/react-router"; -import jsrp from "jsrp"; import { createNotification } from "@app/components/notifications"; import passwordCheck from "@app/components/utilities/checks/password/PasswordCheck"; -import Aes256Gcm from "@app/components/utilities/cryptography/aes-256-gcm"; -import { deriveArgonKey } from "@app/components/utilities/cryptography/crypto"; import { Button, Card, CardTitle, FormControl, Input } from "@app/components/v2"; import { ROUTE_PATHS } from "@app/const/routes"; import { useSetupPassword } from "@app/hooks/api/auth/queries"; -// eslint-disable-next-line new-cap -const client = new jsrp.client(); - export const PasswordSetupPage = () => { const [password, setPassword] = useState(""); const [confirmPassword, setConfirmPassword] = useState(""); @@ -65,83 +57,31 @@ export const PasswordSetupPage = () => { setPasswordsMatch(true); if (!errorCheck) { - client.init( - { - username: email, + try { + await setupPassword.mutateAsync({ + email, + token, password - }, - async () => { - client.createVerifier(async (_err: any, result: { salt: string; verifier: string }) => { - const derivedKey = await deriveArgonKey({ - password, - salt: result.salt, - mem: 65536, - time: 3, - parallelism: 1, - hashLen: 32 - }); + }); - if (!derivedKey) throw new Error("Failed to derive key from password"); + setIsRedirecting(true); - const key = crypto.randomBytes(32); + createNotification({ + type: "success", + title: "Password successfully set", + text: "Redirecting to login..." + }); - // create encrypted private key by encrypting the private - // key with the symmetric key [key] - const { - ciphertext: encryptedPrivateKey, - iv: encryptedPrivateKeyIV, - tag: encryptedPrivateKeyTag - } = Aes256Gcm.encrypt({ - text: localStorage.getItem("PRIVATE_KEY") as string, - secret: key - }); - - // create the protected key by encrypting the symmetric key - // [key] with the derived key - const { - ciphertext: protectedKey, - iv: protectedKeyIV, - tag: protectedKeyTag - } = Aes256Gcm.encrypt({ - text: key.toString("hex"), - secret: Buffer.from(derivedKey.hash) - }); - - try { - await setupPassword.mutateAsync({ - protectedKey, - protectedKeyIV, - protectedKeyTag, - encryptedPrivateKey, - encryptedPrivateKeyIV, - encryptedPrivateKeyTag, - salt: result.salt, - verifier: result.verifier, - token, - password - }); - - setIsRedirecting(true); - - createNotification({ - type: "success", - title: "Password successfully set", - text: "Redirecting to login..." - }); - - setTimeout(() => { - window.location.href = "/login"; - }, 3000); - } catch (error) { - createNotification({ - type: "error", - text: (error as Error).message ?? "Error setting password" - }); - navigate({ to: "/personal-settings" }); - } - }); - } - ); + setTimeout(() => { + window.location.href = "/login"; + }, 3000); + } catch (error) { + createNotification({ + type: "error", + text: (error as Error).message ?? "Error setting password" + }); + navigate({ to: "/personal-settings" }); + } } }; diff --git a/frontend/src/pages/auth/SelectOrgPage/SelectOrgSection.tsx b/frontend/src/pages/auth/SelectOrgPage/SelectOrgSection.tsx index 3e3a5909f..f331e4bda 100644 --- a/frontend/src/pages/auth/SelectOrgPage/SelectOrgSection.tsx +++ b/frontend/src/pages/auth/SelectOrgPage/SelectOrgSection.tsx @@ -123,11 +123,8 @@ export const SelectOrganizationSection = () => { } if (callbackPort) { - const privateKey = localStorage.getItem("PRIVATE_KEY"); - let error: string | null = null; - if (!privateKey) error = "Private key not found"; if (!user?.email) error = "User email not found"; if (!token) error = "No token found"; @@ -142,7 +139,7 @@ export const SelectOrganizationSection = () => { const payload = { JTWToken: token, email: user?.email, - privateKey + privateKey: "" } as IsCliLoginSuccessful["loginResponse"]; // send request to server endpoint diff --git a/frontend/src/pages/auth/SignUpInvitePage/SignUpInvitePage.tsx b/frontend/src/pages/auth/SignUpInvitePage/SignUpInvitePage.tsx index a4ebe7a11..e05e0f2ad 100644 --- a/frontend/src/pages/auth/SignUpInvitePage/SignUpInvitePage.tsx +++ b/frontend/src/pages/auth/SignUpInvitePage/SignUpInvitePage.tsx @@ -1,23 +1,14 @@ -/* eslint-disable no-nested-ternary */ -/* eslint-disable @typescript-eslint/no-unused-vars */ -import crypto from "crypto"; - import { useState } from "react"; import { Helmet } from "react-helmet"; import { faXmark } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { Link, useNavigate, useSearch } from "@tanstack/react-router"; -import jsrp from "jsrp"; import { Mfa } from "@app/components/auth/Mfa"; import InputField from "@app/components/basic/InputField"; import checkPassword from "@app/components/utilities/checks/password/checkPassword"; -import Aes256Gcm from "@app/components/utilities/cryptography/aes-256-gcm"; -import { deriveArgonKey, generateKeyPair } from "@app/components/utilities/cryptography/crypto"; -import { saveTokenToLocalStorage } from "@app/components/utilities/saveTokenToLocalStorage"; import SecurityClient from "@app/components/utilities/SecurityClient"; import { Button } from "@app/components/v2"; -import { useServerConfig } from "@app/context"; import { useToggle } from "@app/hooks"; import { completeAccountSignupInvite, @@ -28,9 +19,6 @@ import { MfaMethod } from "@app/hooks/api/auth/types"; import { fetchOrganizations } from "@app/hooks/api/organization/queries"; import { isLoggedIn } from "@app/hooks/api/reactQuery"; -// eslint-disable-next-line new-cap -const client = new jsrp.client(); - type Errors = { tooShort?: string; tooLong?: string; @@ -67,7 +55,6 @@ export const SignupInvitePage = () => { const metadata = queryParams.get("metadata") || undefined; const { mutateAsync: selectOrganization } = useSelectOrganization(); - const { config } = useServerConfig(); const loggedIn = isLoggedIn(); @@ -94,123 +81,56 @@ export const SignupInvitePage = () => { } if (!errorCheck) { - // Generate a random pair of a public and a private key - const { publicKey, privateKey } = await generateKeyPair(config.fipsEnabled); + try { + const { token: jwtToken } = await completeAccountSignupInvite({ + email, + password, + firstName, + lastName, + tokenMetadata: metadata + }); - localStorage.setItem("PRIVATE_KEY", privateKey); + // unset temporary signup JWT token and set JWT token + SecurityClient.setSignupToken(""); + SecurityClient.setToken(jwtToken); - client.init( - { - username: email, - password - }, - async () => { - client.createVerifier(async (_err, result) => { - try { - const derivedKey = await deriveArgonKey({ - password, - salt: result.salt, - mem: 65536, - time: 3, - parallelism: 1, - hashLen: 32 - }); + const userOrgs = await fetchOrganizations(); - if (!derivedKey) throw new Error("Failed to derive key from password"); + const orgId = userOrgs[0].id; - const key = crypto.randomBytes(32); + if (!orgId) throw new Error("You are not part of any organization"); - // create encrypted private key by encrypting the private - // key with the symmetric key [key] - const { - ciphertext: encryptedPrivateKey, - iv: encryptedPrivateKeyIV, - tag: encryptedPrivateKeyTag - } = Aes256Gcm.encrypt({ - text: privateKey, - secret: key - }); - - // create the protected key by encrypting the symmetric key - // [key] with the derived key - const { - ciphertext: protectedKey, - iv: protectedKeyIV, - tag: protectedKeyTag - } = Aes256Gcm.encrypt({ - text: key.toString("hex"), - secret: Buffer.from(derivedKey.hash) - }); - - const { token: jwtToken } = await completeAccountSignupInvite({ - email, - password, - firstName, - lastName, - protectedKey, - protectedKeyIV, - protectedKeyTag, - publicKey, - encryptedPrivateKey, - encryptedPrivateKeyIV, - encryptedPrivateKeyTag, - salt: result.salt, - verifier: result.verifier, - tokenMetadata: metadata - }); - - // unset temporary signup JWT token and set JWT token - SecurityClient.setSignupToken(""); - SecurityClient.setToken(jwtToken); - - saveTokenToLocalStorage({ - publicKey, - encryptedPrivateKey, - iv: encryptedPrivateKeyIV, - tag: encryptedPrivateKeyTag, - privateKey - }); - - const userOrgs = await fetchOrganizations(); - - const orgId = userOrgs[0].id; - - if (!orgId) throw new Error("You are not part of any organization"); - - const completeSignupFlow = async () => { - const { - token: mfaToken, - isMfaEnabled, - mfaMethod - } = await selectOrganization({ - organizationId: orgId - }); - - if (isMfaEnabled) { - SecurityClient.setMfaToken(mfaToken); - if (mfaMethod) { - setRequiredMfaMethod(mfaMethod); - } - toggleShowMfa.on(); - setMfaSuccessCallback(() => completeSignupFlow); - return; - } - - localStorage.setItem("orgData.id", orgId); - - navigate({ - to: "/organization/projects" - }); - }; - - await completeSignupFlow(); - } catch (error) { - setIsLoading(false); - console.error(error); - } + const completeSignupFlow = async () => { + const { + token: mfaToken, + isMfaEnabled, + mfaMethod + } = await selectOrganization({ + organizationId: orgId }); - } - ); + + if (isMfaEnabled) { + SecurityClient.setMfaToken(mfaToken); + if (mfaMethod) { + setRequiredMfaMethod(mfaMethod); + } + toggleShowMfa.on(); + setMfaSuccessCallback(() => completeSignupFlow); + return; + } + + localStorage.setItem("orgData.id", orgId); + + navigate({ + to: "/organization/projects" + }); + }; + + await completeSignupFlow(); + } catch (error) { + setIsLoading(false); + console.error(error); + } } else { setIsLoading(false); } diff --git a/frontend/src/pages/auth/SignUpSsoPage/components/UserInfoSSOStep/UserInfoSSOStep.tsx b/frontend/src/pages/auth/SignUpSsoPage/components/UserInfoSSOStep/UserInfoSSOStep.tsx index 85343e79e..1b10ed94c 100644 --- a/frontend/src/pages/auth/SignUpSsoPage/components/UserInfoSSOStep/UserInfoSSOStep.tsx +++ b/frontend/src/pages/auth/SignUpSsoPage/components/UserInfoSSOStep/UserInfoSSOStep.tsx @@ -3,24 +3,16 @@ import crypto from "crypto"; import { useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; import { useNavigate } from "@tanstack/react-router"; -import jsrp from "jsrp"; import { Mfa } from "@app/components/auth/Mfa"; -import Aes256Gcm from "@app/components/utilities/cryptography/aes-256-gcm"; -import { deriveArgonKey, generateKeyPair } from "@app/components/utilities/cryptography/crypto"; -import { saveTokenToLocalStorage } from "@app/components/utilities/saveTokenToLocalStorage"; import SecurityClient from "@app/components/utilities/SecurityClient"; import { Button, Input } from "@app/components/v2"; -import { useServerConfig } from "@app/context"; import { initProjectHelper } from "@app/helpers/project"; import { useToggle } from "@app/hooks"; import { completeAccountSignup, useSelectOrganization } from "@app/hooks/api/auth/queries"; import { MfaMethod } from "@app/hooks/api/auth/types"; import { fetchOrganizations } from "@app/hooks/api/organization/queries"; -// eslint-disable-next-line new-cap -const client = new jsrp.client(); - type Props = { username: string; password: string; @@ -64,7 +56,6 @@ export const UserInfoSSOStep = ({ const { mutateAsync: selectOrganization } = useSelectOrganization(); const [mfaSuccessCallback, setMfaSuccessCallback] = useState<() => void>(() => {}); const navigate = useNavigate(); - const { config } = useServerConfig(); useEffect(() => { const randomPassword = crypto.randomBytes(32).toString("hex"); @@ -93,131 +84,64 @@ export const UserInfoSSOStep = ({ } if (!errorCheck) { - // Generate a random pair of a public and a private key - const { publicKey, privateKey } = await generateKeyPair(config.fipsEnabled); - localStorage.setItem("PRIVATE_KEY", privateKey); + try { + const response = await completeAccountSignup({ + email: username, + password, + firstName: name.split(" ")[0], + lastName: name.split(" ").slice(1).join(" "), + providerAuthToken, + organizationName, + attributionSource, + useDefaultOrg: forceDefaultOrg + }); - client.init( - { - username, - password - }, - async () => { - client.createVerifier(async (_err: any, result: { salt: string; verifier: string }) => { - try { - // TODO: moduralize into KeyService - const derivedKey = await deriveArgonKey({ - password, - salt: result.salt, - mem: 65536, - time: 3, - parallelism: 1, - hashLen: 32 - }); + // unset signup JWT token and set JWT token + SecurityClient.setSignupToken(""); + SecurityClient.setToken(response.token); + SecurityClient.setProviderAuthToken(""); - if (!derivedKey) throw new Error("Failed to derive key from password"); + const userOrgs = await fetchOrganizations(); + const orgId = userOrgs[0]?.id; - const key = crypto.randomBytes(32); + const completeSignupFlow = async () => { + try { + const { isMfaEnabled, token, mfaMethod } = await selectOrganization({ + organizationId: orgId + }); - // create encrypted private key by encrypting the private - // key with the symmetric key [key] - const { - ciphertext: encryptedPrivateKey, - iv: encryptedPrivateKeyIV, - tag: encryptedPrivateKeyTag - } = Aes256Gcm.encrypt({ - text: privateKey, - secret: key - }); - - // create the protected key by encrypting the symmetric key - // [key] with the derived key - const { - ciphertext: protectedKey, - iv: protectedKeyIV, - tag: protectedKeyTag - } = Aes256Gcm.encrypt({ - text: key.toString("hex"), - secret: Buffer.from(derivedKey.hash) - }); - - const response = await completeAccountSignup({ - email: username, - password, - firstName: name.split(" ")[0], - lastName: name.split(" ").slice(1).join(" "), - protectedKey, - protectedKeyIV, - protectedKeyTag, - publicKey, - encryptedPrivateKey, - encryptedPrivateKeyIV, - encryptedPrivateKeyTag, - providerAuthToken, - salt: result.salt, - verifier: result.verifier, - organizationName, - attributionSource, - useDefaultOrg: forceDefaultOrg - }); - - // unset signup JWT token and set JWT token - SecurityClient.setSignupToken(""); - SecurityClient.setToken(response.token); - SecurityClient.setProviderAuthToken(""); - - saveTokenToLocalStorage({ - publicKey, - encryptedPrivateKey, - iv: encryptedPrivateKeyIV, - tag: encryptedPrivateKeyTag, - privateKey - }); - - const userOrgs = await fetchOrganizations(); - const orgId = userOrgs[0]?.id; - - const completeSignupFlow = async () => { - try { - const { isMfaEnabled, token, mfaMethod } = await selectOrganization({ - organizationId: orgId - }); - - if (isMfaEnabled) { - SecurityClient.setMfaToken(token); - if (mfaMethod) { - setRequiredMfaMethod(mfaMethod); - } - toggleShowMfa.on(); - setMfaSuccessCallback(() => completeSignupFlow); - return; - } - - // only create example project if not joining existing org - if (!providerOrganizationName) { - await initProjectHelper({ - projectName: "Example Project" - }); - } - - localStorage.setItem("orgData.id", orgId); - navigate({ - to: "/organization/projects" - }); - } catch (error) { - setIsLoading(false); - console.error(error); - } - }; - - await completeSignupFlow(); - } catch (error) { - setIsLoading(false); - console.error(error); + if (isMfaEnabled) { + SecurityClient.setMfaToken(token); + if (mfaMethod) { + setRequiredMfaMethod(mfaMethod); + } + toggleShowMfa.on(); + setMfaSuccessCallback(() => completeSignupFlow); + return; } - }); - } - ); + + // only create example project if not joining existing org + if (!providerOrganizationName) { + await initProjectHelper({ + projectName: "Example Project" + }); + } + + localStorage.setItem("orgData.id", orgId); + navigate({ + to: "/organization/projects" + }); + } catch (error) { + setIsLoading(false); + console.error(error); + } + }; + + await completeSignupFlow(); + } catch (error) { + setIsLoading(false); + console.error(error); + } } else { setIsLoading(false); } diff --git a/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityAuthTemplateModal.tsx b/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityAuthTemplateModal.tsx new file mode 100644 index 000000000..3bd423982 --- /dev/null +++ b/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityAuthTemplateModal.tsx @@ -0,0 +1,317 @@ +import { useEffect } from "react"; +import { Controller, useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { z } from "zod"; + +import { createNotification } from "@app/components/notifications"; +import { + Button, + FormControl, + Input, + Modal, + ModalContent, + Select, + SelectItem, + TextArea +} from "@app/components/v2"; +import { useOrganization } from "@app/context"; +import { + MachineIdentityAuthMethod, + useCreateIdentityAuthTemplate, + useUpdateIdentityAuthTemplate +} from "@app/hooks/api/identityAuthTemplates"; +import { UsePopUpState } from "@app/hooks/usePopUp"; + +const authMethods = [{ label: "LDAP Auth", value: MachineIdentityAuthMethod.LDAP }]; + +const schema = z.object({ + name: z.string().min(1, "Template name is required"), + method: z.nativeEnum(MachineIdentityAuthMethod), + url: z.string().min(1, "LDAP URL is required"), + bindDN: z.string().min(1, "Bind DN is required"), + bindPass: z.string().min(1, "Bind Pass is required"), + searchBase: z.string().min(1, "Search Base / DN is required"), + ldapCaCertificate: z + .string() + .optional() + .transform((val) => val || undefined) +}); + +export type FormData = z.infer; + +type Props = { + popUp: UsePopUpState<["createTemplate", "editTemplate"]>; + handlePopUpToggle: ( + popUpName: keyof UsePopUpState<["createTemplate", "editTemplate"]>, + state?: boolean + ) => void; +}; + +export const IdentityAuthTemplateModal = ({ popUp, handlePopUpToggle }: Props) => { + const { currentOrg } = useOrganization(); + const orgId = currentOrg?.id || ""; + + const { mutateAsync: createTemplate } = useCreateIdentityAuthTemplate(); + const { mutateAsync: updateTemplate } = useUpdateIdentityAuthTemplate(); + + const isEdit = popUp.editTemplate.isOpen; + const template = popUp.editTemplate?.data?.template; + + const { + control, + handleSubmit, + reset, + watch, + formState: { isSubmitting } + } = useForm({ + resolver: zodResolver(schema), + defaultValues: { + name: "", + method: MachineIdentityAuthMethod.LDAP, + url: "", + bindDN: "", + bindPass: "", + searchBase: "", + ldapCaCertificate: "" + } + }); + + useEffect(() => { + if (isEdit && template) { + reset({ + name: template.name || "", + method: MachineIdentityAuthMethod.LDAP, + url: template.templateFields?.url || "", + bindDN: template.templateFields?.bindDN || "", + bindPass: template.templateFields?.bindPass || "", + searchBase: template.templateFields?.searchBase || "", + ldapCaCertificate: template.templateFields?.ldapCaCertificate || "" + }); + } else { + reset({ + name: "", + method: MachineIdentityAuthMethod.LDAP, + url: "", + bindDN: "", + bindPass: "", + searchBase: "", + ldapCaCertificate: "" + }); + } + }, [isEdit, template, reset]); + + const selectedMethod = watch("method"); + + const onFormSubmit = async (data: FormData) => { + try { + if (isEdit && template) { + await updateTemplate({ + templateId: template.id, + organizationId: orgId, + name: data.name, + templateFields: { + url: data.url, + bindDN: data.bindDN, + bindPass: data.bindPass, + searchBase: data.searchBase, + ldapCaCertificate: data.ldapCaCertificate + } + }); + createNotification({ + text: "Successfully updated auth template", + type: "success" + }); + } else { + await createTemplate({ + organizationId: orgId, + name: data.name, + authMethod: data.method, + templateFields: { + url: data.url, + bindDN: data.bindDN, + bindPass: data.bindPass, + searchBase: data.searchBase, + ldapCaCertificate: data.ldapCaCertificate + } + }); + createNotification({ + text: "Successfully created auth template", + type: "success" + }); + } + + handlePopUpToggle(isEdit ? "editTemplate" : "createTemplate", false); + reset(); + } catch (err) { + console.error(err); + const error = err as any; + const text = + error?.response?.data?.message ?? `Failed to ${isEdit ? "update" : "create"} auth template`; + + createNotification({ + text, + type: "error" + }); + } + }; + + const handleClose = () => { + handlePopUpToggle(isEdit ? "editTemplate" : "createTemplate", false); + reset(); + }; + + return ( + + +
+ ( + + + + )} + /> + + ( + + + + )} + /> + + {/* LDAP Configuration Fields */} + {selectedMethod === "ldap" && ( + <> + ( + + + + )} + /> + + ( + + + + )} + /> + + ( + + + + )} + /> + + ( + + + + )} + /> + + ( + +