Compare commits

...

81 Commits

Author SHA1 Message Date
4f998e3940 Merge pull request #2251 from akhilmhdh/fix/replication
fix: resolved replication secret not getting deleted
2024-08-07 11:57:14 -04:00
=
1248840dc8 fix: resolved replication secret not getting deleted 2024-08-07 21:23:22 +05:30
64c8125e4b add external secrets operator mention in k8s docs 2024-08-07 11:13:02 -04:00
8d33647739 Merge pull request #2249 from Infisical/maidul-sqhdqwdgvqwjf
patch findProjectUserWorkspaceKey
2024-08-06 22:12:03 +05:30
d1c142e5b1 patch findProjectUserWorkspaceKey 2024-08-06 12:39:06 -04:00
bb1cad0c5b Merge pull request #2223 from Infisical/misc/add-org-level-rate-limit
misc: moved to license-plan-based rate limits
2024-08-06 10:42:57 -04:00
2a1cfe15b4 update text when secrets deleted after integ delete 2024-08-06 10:07:41 -04:00
881d70bc64 Merge pull request #2238 from Infisical/feat/enabled-secrets-deletion-on-integ-removal
feat: added secrets deletion feature on integration removal
2024-08-06 09:54:15 -04:00
902a0b0ed4 Merge pull request #2243 from akhilmhdh/fix/missing-coment-field 2024-08-06 08:18:18 -04:00
ba92192537 misc: removed creation limits completely 2024-08-06 19:41:09 +08:00
26ed8df73c misc: finalized list of license rate limits 2024-08-06 19:14:49 +08:00
c1decab912 misc: addressed comments 2024-08-06 18:58:07 +08:00
=
216c073290 fix: missing comment key in updated project 2024-08-06 16:14:25 +05:30
1070954bdd misc: used destructuring 2024-08-06 02:05:13 +08:00
cc689d3178 feat: added secrets deletion feature on integration removal 2024-08-06 01:52:58 +08:00
e6848828f2 Merge pull request #2184 from Infisical/daniel/keyring-cli-improvements
feat(cli): persistant `file` vault passphrase
2024-08-05 13:13:29 -04:00
c8b93e4467 Update doc to show correct command 2024-08-05 13:11:40 -04:00
0bca24bb00 Merge pull request #2235 from Infisical/handbook-update
add meetings article to handbook
2024-08-05 12:42:07 -04:00
c563ada50f Merge pull request #2237 from akhilmhdh/fix/bot-creation-failing
fix: resolved auto bot create failing on update
2024-08-05 11:15:25 -04:00
=
26d1616e22 fix: resolved auto bot create failing on update 2024-08-05 20:41:19 +05:30
5fd071d1de Merge pull request #2225 from akhilmhdh/feat/org-project-management
Feat/org project management
2024-08-05 10:21:09 -04:00
a6ac78356b rename org admin console subject name 2024-08-05 10:03:33 -04:00
e4a2137991 update permission action name for org admin console 2024-08-05 10:01:15 -04:00
9721d7a15e add meetings article to handbook 2024-08-04 14:04:09 -07:00
93db5c4555 Merge pull request #2234 from Infisical/maidul-mdjhquwqjhd
update broken image in ksm docs
2024-08-04 11:48:16 -04:00
ad4393fdef update broken image in ksm docs 2024-08-04 11:46:58 -04:00
cd06e4e7f3 hot patch 2024-08-03 19:05:34 -04:00
711a4179ce rename admin panel 2024-08-03 07:52:35 -04:00
=
b4a2a477d3 feat: brought back workspace permission and made requested changes 2024-08-03 14:55:30 +05:30
8e53a1b171 Merge pull request #2232 from Infisical/daniel/fix-lint
Fix: Linting
2024-08-02 22:00:28 -04:00
71af463ad8 fix format 2024-08-03 03:49:47 +02:00
7abd18b11c Merge pull request #2219 from LemmyMwaura/parse-secret-on-paste
feat: parse secrets (key,value) on paste
2024-08-03 03:33:17 +02:00
1aee50a751 Fix: Parser improvements and lint fixes 2024-08-03 03:29:45 +02:00
0f23b7e1d3 misc: added check for undefined orgId 2024-08-03 02:10:47 +08:00
e9b37a1f98 Merge pull request #2227 from Vishvsalvi/deleteActionModal-Placeholder
Placeholder value is same as it's label
2024-08-02 14:04:40 -04:00
33193a47ae misc: updated default onprem rate limits 2024-08-03 01:52:04 +08:00
43fded2350 refactor: take into account other delimiters 2024-08-02 20:41:47 +03:00
7b6f4d810d Placeholder value is same as it's label 2024-08-02 20:51:08 +05:30
1ad286ca87 misc: name updates and more comments 2024-08-02 22:58:53 +08:00
be7c11a3f5 Merge remote-tracking branch 'origin/main' into misc/add-org-level-rate-limit 2024-08-02 22:42:23 +08:00
=
b97bbe5beb feat: text change in sidebar 2024-08-02 19:54:43 +05:30
=
cf5260b383 feat: minor bug fix on access operation 2024-08-02 19:54:42 +05:30
=
13e0dd8e0f feat: completed org admin based project access feature 2024-08-02 19:54:42 +05:30
7f9150e60e Merge pull request #2226 from Infisical/maidul-wdqwdwf
Update docker-compose to docker compose in GHA
2024-08-02 19:54:17 +05:30
995f0360fb update docker-compsoe to docker compose 2024-08-02 10:22:21 -04:00
ecab69a7ab Merge pull request #2213 from Infisical/issue-cert-csr
Add Sign Certificate Endpoint for Certificate Issuance
2024-08-02 07:16:17 -07:00
cca36ab106 Merge remote-tracking branch 'origin' into issue-cert-csr 2024-08-02 07:06:58 -07:00
76311a1b5f Update DN parsing fn 2024-08-02 07:00:36 -07:00
55a6740714 misc: moved to plan-based rate limit 2024-08-02 21:37:48 +08:00
a0490d0fde Merge pull request #2220 from Infisical/feat/added-secret-folder-rbac
feat: added secret folder permissions
2024-08-02 19:05:12 +08:00
78e41a51c0 update workspace to project 2024-08-01 17:29:33 -04:00
8414f04e94 Merge pull request #2221 from akhilmhdh/feat/remove-migration-webhooks
feat: resolved invite failing and removed all unused things from frontend for previous upgrade
2024-08-01 11:18:50 -04:00
=
79e414ea9f feat: resolved invite failing and removed all unused things from frontend on previous upgrade 2024-08-01 20:12:23 +05:30
83772c1770 Merge pull request #2218 from GLEF1X/refactor/required-key-secret-input
refactor(secret-key-input): pass `isRequired` prop to secret key input
2024-08-01 10:35:23 -04:00
09928efba3 feat: added secret folder rbac' 2024-08-01 22:24:35 +08:00
48eb4e772f Merge pull request #2217 from akhilmhdh/feat/remove-migration-webhooks
feat: removed all the migration done for webhook and dynamic secret to KMS
2024-08-01 09:26:49 -04:00
7467a05fc4 fix(lint): fix triple equal strict check 2024-08-01 14:42:15 +03:00
afba636850 feat: parse full env secrets (key,value) when pasted from clipboard 2024-08-01 14:22:22 +03:00
96cc315762 refactor(secret-key-input): pass isRequired prop to secret key input 2024-08-01 06:22:49 -04:00
=
e95d7e55c1 feat: removed all the migration done for webhook and dynamic secret towards kms encryption 2024-08-01 13:39:41 +05:30
520c068ac4 Merge pull request #2209 from Infisical/doc/add-documentation-for-kms-with-aws-hsm
doc: added documentation for using AWS HSM
2024-07-31 21:37:23 -04:00
9f0d7c6d11 Correct sign-certificate endpoint ref in docs 2024-07-31 14:04:52 -07:00
683e3dd7be Add sign certificate endpoint 2024-07-31 13:57:47 -07:00
46ca3856b3 change upgrade btn based on admin 2024-07-31 10:59:36 -04:00
891cb06de0 Update keyringwrapper.go 2024-07-31 16:55:53 +02:00
02e8f20cbf remove extra : 2024-07-31 03:14:06 +00:00
d5f4ce4376 Update vault.go 2024-07-30 10:22:15 +02:00
85653a90d5 update phrasing 2024-07-29 22:06:03 -04:00
879ef2c178 Update keyringwrapper.go 2024-07-29 12:37:58 +02:00
8777cfe680 Update keyringwrapper.go 2024-07-29 12:34:35 +02:00
2b630f75aa Update keyringwrapper.go 2024-07-29 12:31:02 +02:00
91cee20cc8 Minor improvemnets 2024-07-29 12:21:38 +02:00
4249ec6030 Update login.go 2024-07-29 12:21:31 +02:00
e7a95e6af2 Update login.go 2024-07-29 12:15:53 +02:00
a9f04a3c1f Update keyringwrapper.go 2024-07-29 12:13:40 +02:00
3d380710ee Update keyringwrapper.go 2024-07-29 12:10:42 +02:00
2177ec6bcc Update vault.go 2024-07-29 12:04:34 +02:00
070eb2aacd Update keyringwrapper.go 2024-07-26 22:47:46 +02:00
e619cfa313 feat(cli): set persistent file vault password 2024-07-26 22:47:37 +02:00
c3038e3ca1 docs: passphrase command 2024-07-26 22:47:07 +02:00
ff0e7feeee feat(cli): CLI Keyring improvements 2024-07-26 19:14:21 +02:00
127 changed files with 3324 additions and 1625 deletions

View File

@ -22,14 +22,14 @@ jobs:
# uncomment this when testing locally using nektos/act # uncomment this when testing locally using nektos/act
- uses: KengoTODA/actions-setup-docker-compose@v1 - uses: KengoTODA/actions-setup-docker-compose@v1
if: ${{ env.ACT }} if: ${{ env.ACT }}
name: Install `docker-compose` for local simulations name: Install `docker compose` for local simulations
with: with:
version: "2.14.2" version: "2.14.2"
- name: 📦Build the latest image - name: 📦Build the latest image
run: docker build --tag infisical-api . run: docker build --tag infisical-api .
working-directory: backend working-directory: backend
- name: Start postgres and redis - name: Start postgres and redis
run: touch .env && docker-compose -f docker-compose.dev.yml up -d db redis run: touch .env && docker compose -f docker-compose.dev.yml up -d db redis
- name: Start the server - name: Start the server
run: | run: |
echo "SECRET_SCANNING_GIT_APP_ID=793712" >> .env echo "SECRET_SCANNING_GIT_APP_ID=793712" >> .env
@ -72,6 +72,6 @@ jobs:
run: oasdiff breaking https://app.infisical.com/api/docs/json http://localhost:4000/api/docs/json --fail-on ERR run: oasdiff breaking https://app.infisical.com/api/docs/json http://localhost:4000/api/docs/json --fail-on ERR
- name: cleanup - name: cleanup
run: | run: |
docker-compose -f "docker-compose.dev.yml" down docker compose -f "docker-compose.dev.yml" down
docker stop infisical-api docker stop infisical-api
docker remove infisical-api docker remove infisical-api

View File

@ -20,7 +20,7 @@ jobs:
uses: actions/checkout@v3 uses: actions/checkout@v3
- uses: KengoTODA/actions-setup-docker-compose@v1 - uses: KengoTODA/actions-setup-docker-compose@v1
if: ${{ env.ACT }} if: ${{ env.ACT }}
name: Install `docker-compose` for local simulations name: Install `docker compose` for local simulations
with: with:
version: "2.14.2" version: "2.14.2"
- name: 🔧 Setup Node 20 - name: 🔧 Setup Node 20
@ -33,7 +33,7 @@ jobs:
run: npm install run: npm install
working-directory: backend working-directory: backend
- name: Start postgres and redis - name: Start postgres and redis
run: touch .env && docker-compose -f docker-compose.dev.yml up -d db redis run: touch .env && docker compose -f docker-compose.dev.yml up -d db redis
- name: Start integration test - name: Start integration test
run: npm run test:e2e run: npm run test:e2e
working-directory: backend working-directory: backend
@ -44,4 +44,4 @@ jobs:
ENCRYPTION_KEY: 4bnfe4e407b8921c104518903515b218 ENCRYPTION_KEY: 4bnfe4e407b8921c104518903515b218
- name: cleanup - name: cleanup
run: | run: |
docker-compose -f "docker-compose.dev.yml" down docker compose -f "docker-compose.dev.yml" down

View File

@ -25,6 +25,7 @@
"@fastify/swagger": "^8.14.0", "@fastify/swagger": "^8.14.0",
"@fastify/swagger-ui": "^2.1.0", "@fastify/swagger-ui": "^2.1.0",
"@node-saml/passport-saml": "^4.0.4", "@node-saml/passport-saml": "^4.0.4",
"@octokit/plugin-retry": "^5.0.5",
"@octokit/rest": "^20.0.2", "@octokit/rest": "^20.0.2",
"@octokit/webhooks-types": "^7.3.1", "@octokit/webhooks-types": "^7.3.1",
"@peculiar/asn1-schema": "^2.3.8", "@peculiar/asn1-schema": "^2.3.8",
@ -7812,19 +7813,45 @@
} }
}, },
"node_modules/@octokit/plugin-retry": { "node_modules/@octokit/plugin-retry": {
"version": "6.0.1", "version": "5.0.5",
"resolved": "https://registry.npmjs.org/@octokit/plugin-retry/-/plugin-retry-6.0.1.tgz", "resolved": "https://registry.npmjs.org/@octokit/plugin-retry/-/plugin-retry-5.0.5.tgz",
"integrity": "sha512-SKs+Tz9oj0g4p28qkZwl/topGcb0k0qPNX/i7vBKmDsjoeqnVfFUquqrE/O9oJY7+oLzdCtkiWSXLpLjvl6uog==", "integrity": "sha512-sB1RWMhSrre02Atv95K6bhESlJ/sPdZkK/wE/w1IdSCe0yM6FxSjksLa6T7aAvxvxlLKzQEC4KIiqpqyov1Tbg==",
"dependencies": { "dependencies": {
"@octokit/request-error": "^5.0.0", "@octokit/request-error": "^4.0.1",
"@octokit/types": "^12.0.0", "@octokit/types": "^10.0.0",
"bottleneck": "^2.15.3" "bottleneck": "^2.15.3"
}, },
"engines": { "engines": {
"node": ">= 18" "node": ">= 18"
}, },
"peerDependencies": { "peerDependencies": {
"@octokit/core": ">=5" "@octokit/core": ">=3"
}
},
"node_modules/@octokit/plugin-retry/node_modules/@octokit/openapi-types": {
"version": "18.1.1",
"resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-18.1.1.tgz",
"integrity": "sha512-VRaeH8nCDtF5aXWnjPuEMIYf1itK/s3JYyJcWFJT8X9pSNnBtriDf7wlEWsGuhPLl4QIH4xM8fqTXDwJ3Mu6sw=="
},
"node_modules/@octokit/plugin-retry/node_modules/@octokit/request-error": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-4.0.2.tgz",
"integrity": "sha512-uqwUEmZw3x4I9DGYq9fODVAAvcLsPQv97NRycP6syEFu5916M189VnNBW2zANNwqg3OiligNcAey7P0SET843w==",
"dependencies": {
"@octokit/types": "^10.0.0",
"deprecation": "^2.0.0",
"once": "^1.4.0"
},
"engines": {
"node": ">= 18"
}
},
"node_modules/@octokit/plugin-retry/node_modules/@octokit/types": {
"version": "10.0.0",
"resolved": "https://registry.npmjs.org/@octokit/types/-/types-10.0.0.tgz",
"integrity": "sha512-Vm8IddVmhCgU1fxC1eyinpwqzXPEYu0NrYzD3YZjlGjyftdLBTeqNblRC0jmJmgxbJIsQlyogVeGnrNaaMVzIg==",
"dependencies": {
"@octokit/openapi-types": "^18.0.0"
} }
}, },
"node_modules/@octokit/plugin-throttling": { "node_modules/@octokit/plugin-throttling": {
@ -17396,6 +17423,22 @@
"node": ">=18" "node": ">=18"
} }
}, },
"node_modules/probot/node_modules/@octokit/plugin-retry": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/@octokit/plugin-retry/-/plugin-retry-6.0.1.tgz",
"integrity": "sha512-SKs+Tz9oj0g4p28qkZwl/topGcb0k0qPNX/i7vBKmDsjoeqnVfFUquqrE/O9oJY7+oLzdCtkiWSXLpLjvl6uog==",
"dependencies": {
"@octokit/request-error": "^5.0.0",
"@octokit/types": "^12.0.0",
"bottleneck": "^2.15.3"
},
"engines": {
"node": ">= 18"
},
"peerDependencies": {
"@octokit/core": ">=5"
}
},
"node_modules/probot/node_modules/commander": { "node_modules/probot/node_modules/commander": {
"version": "11.1.0", "version": "11.1.0",
"resolved": "https://registry.npmjs.org/commander/-/commander-11.1.0.tgz", "resolved": "https://registry.npmjs.org/commander/-/commander-11.1.0.tgz",

View File

@ -121,6 +121,7 @@
"@fastify/swagger": "^8.14.0", "@fastify/swagger": "^8.14.0",
"@fastify/swagger-ui": "^2.1.0", "@fastify/swagger-ui": "^2.1.0",
"@node-saml/passport-saml": "^4.0.4", "@node-saml/passport-saml": "^4.0.4",
"@octokit/plugin-retry": "^5.0.5",
"@octokit/rest": "^20.0.2", "@octokit/rest": "^20.0.2",
"@octokit/webhooks-types": "^7.3.1", "@octokit/webhooks-types": "^7.3.1",
"@peculiar/asn1-schema": "^2.3.8", "@peculiar/asn1-schema": "^2.3.8",

View File

@ -18,6 +18,7 @@ import { TOidcConfigServiceFactory } from "@app/ee/services/oidc/oidc-config-ser
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { TProjectUserAdditionalPrivilegeServiceFactory } from "@app/ee/services/project-user-additional-privilege/project-user-additional-privilege-service"; import { TProjectUserAdditionalPrivilegeServiceFactory } from "@app/ee/services/project-user-additional-privilege/project-user-additional-privilege-service";
import { TRateLimitServiceFactory } from "@app/ee/services/rate-limit/rate-limit-service"; import { TRateLimitServiceFactory } from "@app/ee/services/rate-limit/rate-limit-service";
import { RateLimitConfiguration } from "@app/ee/services/rate-limit/rate-limit-types";
import { TSamlConfigServiceFactory } from "@app/ee/services/saml-config/saml-config-service"; import { TSamlConfigServiceFactory } from "@app/ee/services/saml-config/saml-config-service";
import { TScimServiceFactory } from "@app/ee/services/scim/scim-service"; import { TScimServiceFactory } from "@app/ee/services/scim/scim-service";
import { TSecretApprovalPolicyServiceFactory } from "@app/ee/services/secret-approval-policy/secret-approval-policy-service"; import { TSecretApprovalPolicyServiceFactory } from "@app/ee/services/secret-approval-policy/secret-approval-policy-service";
@ -50,6 +51,7 @@ import { TIntegrationServiceFactory } from "@app/services/integration/integratio
import { TIntegrationAuthServiceFactory } from "@app/services/integration-auth/integration-auth-service"; import { TIntegrationAuthServiceFactory } from "@app/services/integration-auth/integration-auth-service";
import { TOrgRoleServiceFactory } from "@app/services/org/org-role-service"; import { TOrgRoleServiceFactory } from "@app/services/org/org-role-service";
import { TOrgServiceFactory } from "@app/services/org/org-service"; import { TOrgServiceFactory } from "@app/services/org/org-service";
import { TOrgAdminServiceFactory } from "@app/services/org-admin/org-admin-service";
import { TProjectServiceFactory } from "@app/services/project/project-service"; import { TProjectServiceFactory } from "@app/services/project/project-service";
import { TProjectBotServiceFactory } from "@app/services/project-bot/project-bot-service"; import { TProjectBotServiceFactory } from "@app/services/project-bot/project-bot-service";
import { TProjectEnvServiceFactory } from "@app/services/project-env/project-env-service"; import { TProjectEnvServiceFactory } from "@app/services/project-env/project-env-service";
@ -88,6 +90,7 @@ declare module "fastify" {
id: string; id: string;
orgId: string; orgId: string;
}; };
rateLimits: RateLimitConfiguration;
// passport data // passport data
passportUser: { passportUser: {
isUserCompleted: string; isUserCompleted: string;
@ -165,6 +168,7 @@ declare module "fastify" {
rateLimit: TRateLimitServiceFactory; rateLimit: TRateLimitServiceFactory;
userEngagement: TUserEngagementServiceFactory; userEngagement: TUserEngagementServiceFactory;
externalKms: TExternalKmsServiceFactory; externalKms: TExternalKmsServiceFactory;
orgAdmin: TOrgAdminServiceFactory;
}; };
// this is exclusive use for middlewares in which we need to inject data // this is exclusive use for middlewares in which we need to inject data
// everywhere else access using service layer // everywhere else access using service layer

View File

@ -1,178 +1,8 @@
/* eslint-disable @typescript-eslint/ban-ts-comment */ /* eslint-disable @typescript-eslint/ban-ts-comment */
import { Knex } from "knex"; import { Knex } from "knex";
import { infisicalSymmetricDecrypt } from "@app/lib/crypto/encryption"; import { SecretType, TableName } from "../schemas";
import { selectAllTableCols } from "@app/lib/knex/select";
import { SecretKeyEncoding, SecretType, TableName } from "../schemas";
import { createJunctionTable, createOnUpdateTrigger, dropOnUpdateTrigger } from "../utils"; import { createJunctionTable, createOnUpdateTrigger, dropOnUpdateTrigger } from "../utils";
import { getSecretManagerDataKey } from "./utils/kms";
const backfillWebhooks = async (knex: Knex) => {
const hasEncryptedSecretKeyWithKms = await knex.schema.hasColumn(TableName.Webhook, "encryptedSecretKeyWithKms");
const hasEncryptedWebhookUrl = await knex.schema.hasColumn(TableName.Webhook, "encryptedUrl");
const hasUrlCipherText = await knex.schema.hasColumn(TableName.Webhook, "urlCipherText");
const hasUrlIV = await knex.schema.hasColumn(TableName.Webhook, "urlIV");
const hasUrlTag = await knex.schema.hasColumn(TableName.Webhook, "urlTag");
const hasEncryptedSecretKey = await knex.schema.hasColumn(TableName.Webhook, "encryptedSecretKey");
const hasIV = await knex.schema.hasColumn(TableName.Webhook, "iv");
const hasTag = await knex.schema.hasColumn(TableName.Webhook, "tag");
const hasKeyEncoding = await knex.schema.hasColumn(TableName.Webhook, "keyEncoding");
const hasAlgorithm = await knex.schema.hasColumn(TableName.Webhook, "algorithm");
const hasUrl = await knex.schema.hasColumn(TableName.Webhook, "url");
await knex.schema.alterTable(TableName.Webhook, (t) => {
if (!hasEncryptedSecretKeyWithKms) t.binary("encryptedSecretKeyWithKms");
if (!hasEncryptedWebhookUrl) t.binary("encryptedUrl");
if (hasUrl) t.string("url").nullable().alter();
});
const kmsEncryptorGroupByProjectId: Record<string, Awaited<ReturnType<typeof getSecretManagerDataKey>>["encryptor"]> =
{};
if (hasUrlCipherText && hasUrlIV && hasUrlTag && hasEncryptedSecretKey && hasIV && hasTag) {
// eslint-disable-next-line
const webhooksToFill = await knex(TableName.Webhook)
.join(TableName.Environment, `${TableName.Environment}.id`, `${TableName.Webhook}.envId`)
.whereNull("encryptedUrl")
// eslint-disable-next-line
// @ts-ignore knex migration fails
.select(selectAllTableCols(TableName.Webhook))
.select("projectId");
const updatedWebhooks = [];
for (const webhook of webhooksToFill) {
if (!kmsEncryptorGroupByProjectId[webhook.projectId]) {
// eslint-disable-next-line
const { encryptor } = await getSecretManagerDataKey(knex, webhook.projectId);
kmsEncryptorGroupByProjectId[webhook.projectId] = encryptor;
}
const kmsEncryptor = kmsEncryptorGroupByProjectId[webhook.projectId];
// @ts-ignore post migration fails
let webhookUrl = webhook.url;
let webhookSecretKey;
// @ts-ignore post migration fails
if (webhook.urlTag && webhook.urlCipherText && webhook.urlIV) {
webhookUrl = infisicalSymmetricDecrypt({
// @ts-ignore post migration fails
keyEncoding: webhook.keyEncoding as SecretKeyEncoding,
// @ts-ignore post migration fails
ciphertext: webhook.urlCipherText,
// @ts-ignore post migration fails
iv: webhook.urlIV,
// @ts-ignore post migration fails
tag: webhook.urlTag
});
}
// @ts-ignore post migration fails
if (webhook.encryptedSecretKey && webhook.iv && webhook.tag) {
webhookSecretKey = infisicalSymmetricDecrypt({
// @ts-ignore post migration fails
keyEncoding: webhook.keyEncoding as SecretKeyEncoding,
// @ts-ignore post migration fails
ciphertext: webhook.encryptedSecretKey,
// @ts-ignore post migration fails
iv: webhook.iv,
// @ts-ignore post migration fails
tag: webhook.tag
});
}
const { projectId, ...el } = webhook;
updatedWebhooks.push({
...el,
encryptedSecretKeyWithKms: webhookSecretKey
? kmsEncryptor({ plainText: Buffer.from(webhookSecretKey) }).cipherTextBlob
: null,
encryptedUrl: kmsEncryptor({ plainText: Buffer.from(webhookUrl) }).cipherTextBlob
});
}
if (updatedWebhooks.length) {
// eslint-disable-next-line
await knex(TableName.Webhook).insert(updatedWebhooks).onConflict("id").merge();
}
}
await knex.schema.alterTable(TableName.Webhook, (t) => {
t.binary("encryptedUrl").notNullable().alter();
if (hasUrlIV) t.dropColumn("urlIV");
if (hasUrlCipherText) t.dropColumn("urlCipherText");
if (hasUrlTag) t.dropColumn("urlTag");
if (hasIV) t.dropColumn("iv");
if (hasTag) t.dropColumn("tag");
if (hasEncryptedSecretKey) t.dropColumn("encryptedSecretKey");
if (hasKeyEncoding) t.dropColumn("keyEncoding");
if (hasAlgorithm) t.dropColumn("algorithm");
if (hasUrl) t.dropColumn("url");
});
};
const backfillDynamicSecretConfigs = async (knex: Knex) => {
const hasEncryptedConfig = await knex.schema.hasColumn(TableName.DynamicSecret, "encryptedConfig");
const hasInputCipherText = await knex.schema.hasColumn(TableName.DynamicSecret, "inputCiphertext");
const hasInputIV = await knex.schema.hasColumn(TableName.DynamicSecret, "inputIV");
const hasInputTag = await knex.schema.hasColumn(TableName.DynamicSecret, "inputTag");
const hasKeyEncoding = await knex.schema.hasColumn(TableName.DynamicSecret, "keyEncoding");
const hasAlgorithm = await knex.schema.hasColumn(TableName.DynamicSecret, "algorithm");
await knex.schema.alterTable(TableName.DynamicSecret, (t) => {
if (!hasEncryptedConfig) t.binary("encryptedConfig");
});
const kmsEncryptorGroupByProjectId: Record<string, Awaited<ReturnType<typeof getSecretManagerDataKey>>["encryptor"]> =
{};
if (hasInputCipherText && hasInputIV && hasInputTag) {
// eslint-disable-next-line
const dynamicSecretConfigs = await knex(TableName.DynamicSecret)
.join(TableName.SecretFolder, `${TableName.SecretFolder}.id`, `${TableName.DynamicSecret}.folderId`)
.join(TableName.Environment, `${TableName.SecretFolder}.envId`, `${TableName.Environment}.id`)
.whereNull("encryptedConfig")
// @ts-ignore post migration fails
.select(selectAllTableCols(TableName.DynamicSecret))
.select("projectId");
const updatedConfigs = [];
for (const dynamicSecretConfig of dynamicSecretConfigs) {
if (!kmsEncryptorGroupByProjectId[dynamicSecretConfig.projectId]) {
// eslint-disable-next-line
const { encryptor } = await getSecretManagerDataKey(knex, dynamicSecretConfig.projectId);
kmsEncryptorGroupByProjectId[dynamicSecretConfig.projectId] = encryptor;
}
const kmsEncryptor = kmsEncryptorGroupByProjectId[dynamicSecretConfig.projectId];
const inputConfig = infisicalSymmetricDecrypt({
// @ts-ignore post migration fails
keyEncoding: dynamicSecretConfig.keyEncoding as SecretKeyEncoding,
// @ts-ignore post migration fails
ciphertext: dynamicSecretConfig.inputCiphertext as string,
// @ts-ignore post migration fails
iv: dynamicSecretConfig.inputIV as string,
// @ts-ignore post migration fails
tag: dynamicSecretConfig.inputTag as string
});
const { projectId, ...el } = dynamicSecretConfig;
updatedConfigs.push({
...el,
encryptedConfig: kmsEncryptor({ plainText: Buffer.from(inputConfig) }).cipherTextBlob
});
}
if (updatedConfigs.length) {
// eslint-disable-next-line
await knex(TableName.DynamicSecret).insert(updatedConfigs).onConflict("id").merge();
}
}
await knex.schema.alterTable(TableName.DynamicSecret, (t) => {
t.binary("encryptedConfig").notNullable().alter();
if (hasInputTag) t.dropColumn("inputTag");
if (hasInputIV) t.dropColumn("inputIV");
if (hasInputCipherText) t.dropColumn("inputCiphertext");
if (hasKeyEncoding) t.dropColumn("keyEncoding");
if (hasAlgorithm) t.dropColumn("algorithm");
});
};
export async function up(knex: Knex): Promise<void> { export async function up(knex: Knex): Promise<void> {
const doesSecretV2TableExist = await knex.schema.hasTable(TableName.SecretV2); const doesSecretV2TableExist = await knex.schema.hasTable(TableName.SecretV2);
@ -314,14 +144,6 @@ export async function up(knex: Knex): Promise<void> {
t.foreign("rotationId").references("id").inTable(TableName.SecretRotation).onDelete("CASCADE"); t.foreign("rotationId").references("id").inTable(TableName.SecretRotation).onDelete("CASCADE");
}); });
} }
if (await knex.schema.hasTable(TableName.Webhook)) {
await backfillWebhooks(knex);
}
if (await knex.schema.hasTable(TableName.DynamicSecret)) {
await backfillDynamicSecretConfigs(knex);
}
} }
export async function down(knex: Knex): Promise<void> { export async function down(knex: Knex): Promise<void> {
@ -356,49 +178,4 @@ export async function down(knex: Knex): Promise<void> {
if (hasEncryptedAwsIamAssumRole) t.dropColumn("encryptedAwsAssumeIamRoleArn"); if (hasEncryptedAwsIamAssumRole) t.dropColumn("encryptedAwsAssumeIamRoleArn");
}); });
} }
if (await knex.schema.hasTable(TableName.Webhook)) {
const hasEncryptedWebhookSecretKey = await knex.schema.hasColumn(TableName.Webhook, "encryptedSecretKeyWithKms");
const hasEncryptedWebhookUrl = await knex.schema.hasColumn(TableName.Webhook, "encryptedUrl");
const hasUrlCipherText = await knex.schema.hasColumn(TableName.Webhook, "urlCipherText");
const hasUrlIV = await knex.schema.hasColumn(TableName.Webhook, "urlIV");
const hasUrlTag = await knex.schema.hasColumn(TableName.Webhook, "urlTag");
const hasEncryptedSecretKey = await knex.schema.hasColumn(TableName.Webhook, "encryptedSecretKey");
const hasIV = await knex.schema.hasColumn(TableName.Webhook, "iv");
const hasTag = await knex.schema.hasColumn(TableName.Webhook, "tag");
const hasKeyEncoding = await knex.schema.hasColumn(TableName.Webhook, "keyEncoding");
const hasAlgorithm = await knex.schema.hasColumn(TableName.Webhook, "algorithm");
const hasUrl = await knex.schema.hasColumn(TableName.Webhook, "url");
await knex.schema.alterTable(TableName.Webhook, (t) => {
if (hasEncryptedWebhookSecretKey) t.dropColumn("encryptedSecretKeyWithKms");
if (hasEncryptedWebhookUrl) t.dropColumn("encryptedUrl");
if (!hasUrl) t.string("url");
if (!hasEncryptedSecretKey) t.string("encryptedSecretKey");
if (!hasIV) t.string("iv");
if (!hasTag) t.string("tag");
if (!hasAlgorithm) t.string("algorithm");
if (!hasKeyEncoding) t.string("keyEncoding");
if (!hasUrlCipherText) t.string("urlCipherText");
if (!hasUrlIV) t.string("urlIV");
if (!hasUrlTag) t.string("urlTag");
});
}
if (await knex.schema.hasTable(TableName.DynamicSecret)) {
const hasEncryptedConfig = await knex.schema.hasColumn(TableName.DynamicSecret, "encryptedConfig");
const hasInputIV = await knex.schema.hasColumn(TableName.DynamicSecret, "inputIV");
const hasInputCipherText = await knex.schema.hasColumn(TableName.DynamicSecret, "inputCiphertext");
const hasInputTag = await knex.schema.hasColumn(TableName.DynamicSecret, "inputTag");
const hasAlgorithm = await knex.schema.hasColumn(TableName.DynamicSecret, "algorithm");
const hasKeyEncoding = await knex.schema.hasColumn(TableName.DynamicSecret, "keyEncoding");
await knex.schema.alterTable(TableName.DynamicSecret, (t) => {
if (hasEncryptedConfig) t.dropColumn("encryptedConfig");
if (!hasInputIV) t.string("inputIV");
if (!hasInputCipherText) t.text("inputCiphertext");
if (!hasInputTag) t.string("inputTag");
if (!hasAlgorithm) t.string("algorithm");
if (!hasKeyEncoding) t.string("keyEncoding");
});
}
} }

View File

@ -0,0 +1,21 @@
import { Knex } from "knex";
import { TableName } from "../schemas";
export async function up(knex: Knex): Promise<void> {
const hasCreationLimitCol = await knex.schema.hasColumn(TableName.RateLimit, "creationLimit");
await knex.schema.alterTable(TableName.RateLimit, (t) => {
if (hasCreationLimitCol) {
t.dropColumn("creationLimit");
}
});
}
export async function down(knex: Knex): Promise<void> {
const hasCreationLimitCol = await knex.schema.hasColumn(TableName.RateLimit, "creationLimit");
await knex.schema.alterTable(TableName.RateLimit, (t) => {
if (!hasCreationLimitCol) {
t.integer("creationLimit").defaultTo(30).notNullable();
}
});
}

View File

@ -5,8 +5,6 @@
import { z } from "zod"; import { z } from "zod";
import { zodBuffer } from "@app/lib/zod";
import { TImmutableDBKeys } from "./models"; import { TImmutableDBKeys } from "./models";
export const DynamicSecretsSchema = z.object({ export const DynamicSecretsSchema = z.object({
@ -16,12 +14,16 @@ export const DynamicSecretsSchema = z.object({
type: z.string(), type: z.string(),
defaultTTL: z.string(), defaultTTL: z.string(),
maxTTL: z.string().nullable().optional(), maxTTL: z.string().nullable().optional(),
inputIV: z.string(),
inputCiphertext: z.string(),
inputTag: z.string(),
algorithm: z.string().default("aes-256-gcm"),
keyEncoding: z.string().default("utf8"),
folderId: z.string().uuid(), folderId: z.string().uuid(),
status: z.string().nullable().optional(), status: z.string().nullable().optional(),
statusDetails: z.string().nullable().optional(), statusDetails: z.string().nullable().optional(),
createdAt: z.date(), createdAt: z.date(),
updatedAt: z.date(), updatedAt: z.date()
encryptedConfig: zodBuffer
}); });
export type TDynamicSecrets = z.infer<typeof DynamicSecretsSchema>; export type TDynamicSecrets = z.infer<typeof DynamicSecretsSchema>;

View File

@ -15,7 +15,6 @@ export const RateLimitSchema = z.object({
authRateLimit: z.number().default(60), authRateLimit: z.number().default(60),
inviteUserRateLimit: z.number().default(30), inviteUserRateLimit: z.number().default(30),
mfaRateLimit: z.number().default(20), mfaRateLimit: z.number().default(20),
creationLimit: z.number().default(30),
publicEndpointLimit: z.number().default(30), publicEndpointLimit: z.number().default(30),
createdAt: z.date(), createdAt: z.date(),
updatedAt: z.date() updatedAt: z.date()

View File

@ -5,22 +5,27 @@
import { z } from "zod"; import { z } from "zod";
import { zodBuffer } from "@app/lib/zod";
import { TImmutableDBKeys } from "./models"; import { TImmutableDBKeys } from "./models";
export const WebhooksSchema = z.object({ export const WebhooksSchema = z.object({
id: z.string().uuid(), id: z.string().uuid(),
secretPath: z.string().default("/"), secretPath: z.string().default("/"),
url: z.string(),
lastStatus: z.string().nullable().optional(), lastStatus: z.string().nullable().optional(),
lastRunErrorMessage: z.string().nullable().optional(), lastRunErrorMessage: z.string().nullable().optional(),
isDisabled: z.boolean().default(false), isDisabled: z.boolean().default(false),
encryptedSecretKey: z.string().nullable().optional(),
iv: z.string().nullable().optional(),
tag: z.string().nullable().optional(),
algorithm: z.string().nullable().optional(),
keyEncoding: z.string().nullable().optional(),
createdAt: z.date(), createdAt: z.date(),
updatedAt: z.date(), updatedAt: z.date(),
envId: z.string().uuid(), envId: z.string().uuid(),
type: z.string().default("general").nullable().optional(), urlCipherText: z.string().nullable().optional(),
encryptedSecretKeyWithKms: zodBuffer.nullable().optional(), urlIV: z.string().nullable().optional(),
encryptedUrl: zodBuffer urlTag: z.string().nullable().optional(),
type: z.string().default("general").nullable().optional()
}); });
export type TWebhooks = z.infer<typeof WebhooksSchema>; export type TWebhooks = z.infer<typeof WebhooksSchema>;

View File

@ -58,7 +58,6 @@ export const registerRateLimitRouter = async (server: FastifyZodProvider) => {
authRateLimit: z.number(), authRateLimit: z.number(),
inviteUserRateLimit: z.number(), inviteUserRateLimit: z.number(),
mfaRateLimit: z.number(), mfaRateLimit: z.number(),
creationLimit: z.number(),
publicEndpointLimit: z.number() publicEndpointLimit: z.number()
}), }),
response: { response: {

View File

@ -136,6 +136,7 @@ export enum EventType {
IMPORT_CA_CERT = "import-certificate-authority-cert", IMPORT_CA_CERT = "import-certificate-authority-cert",
GET_CA_CRL = "get-certificate-authority-crl", GET_CA_CRL = "get-certificate-authority-crl",
ISSUE_CERT = "issue-cert", ISSUE_CERT = "issue-cert",
SIGN_CERT = "sign-cert",
GET_CERT = "get-cert", GET_CERT = "get-cert",
DELETE_CERT = "delete-cert", DELETE_CERT = "delete-cert",
REVOKE_CERT = "revoke-cert", REVOKE_CERT = "revoke-cert",
@ -146,7 +147,8 @@ export enum EventType {
GET_KMS = "get-kms", GET_KMS = "get-kms",
UPDATE_PROJECT_KMS = "update-project-kms", UPDATE_PROJECT_KMS = "update-project-kms",
GET_PROJECT_KMS_BACKUP = "get-project-kms-backup", GET_PROJECT_KMS_BACKUP = "get-project-kms-backup",
LOAD_PROJECT_KMS_BACKUP = "load-project-kms-backup" LOAD_PROJECT_KMS_BACKUP = "load-project-kms-backup",
ORG_ADMIN_ACCESS_PROJECT = "org-admin-accessed-project"
} }
interface UserActorMetadata { interface UserActorMetadata {
@ -336,6 +338,7 @@ interface DeleteIntegrationEvent {
targetServiceId?: string; targetServiceId?: string;
path?: string; path?: string;
region?: string; region?: string;
shouldDeleteIntegrationSecrets?: boolean;
}; };
} }
@ -1143,6 +1146,15 @@ interface IssueCert {
}; };
} }
interface SignCert {
type: EventType.SIGN_CERT;
metadata: {
caId: string;
dn: string;
serialNumber: string;
};
}
interface GetCert { interface GetCert {
type: EventType.GET_CERT; type: EventType.GET_CERT;
metadata: { metadata: {
@ -1235,6 +1247,16 @@ interface LoadProjectKmsBackupEvent {
metadata: Record<string, string>; // no metadata yet metadata: Record<string, string>; // no metadata yet
} }
interface OrgAdminAccessProjectEvent {
type: EventType.ORG_ADMIN_ACCESS_PROJECT;
metadata: {
userId: string;
username: string;
email: string;
projectId: string;
}; // no metadata yet
}
export type Event = export type Event =
| GetSecretsEvent | GetSecretsEvent
| GetSecretEvent | GetSecretEvent
@ -1333,6 +1355,7 @@ export type Event =
| ImportCaCert | ImportCaCert
| GetCaCrl | GetCaCrl
| IssueCert | IssueCert
| SignCert
| GetCert | GetCert
| DeleteCert | DeleteCert
| RevokeCert | RevokeCert
@ -1343,4 +1366,5 @@ export type Event =
| GetKmsEvent | GetKmsEvent
| UpdateProjectKmsEvent | UpdateProjectKmsEvent
| GetProjectKmsBackupEvent | GetProjectKmsBackupEvent
| LoadProjectKmsBackupEvent; | LoadProjectKmsBackupEvent
| OrgAdminAccessProjectEvent;

View File

@ -12,10 +12,7 @@ export const dynamicSecretLeaseDALFactory = (db: TDbClient) => {
const countLeasesForDynamicSecret = async (dynamicSecretId: string, tx?: Knex) => { const countLeasesForDynamicSecret = async (dynamicSecretId: string, tx?: Knex) => {
try { try {
const doc = await (tx || db.replicaNode())(TableName.DynamicSecretLease) const doc = await (tx || db)(TableName.DynamicSecretLease).count("*").where({ dynamicSecretId }).first();
.count("*")
.where({ dynamicSecretId })
.first();
return parseInt(doc || "0", 10); return parseInt(doc || "0", 10);
} catch (error) { } catch (error) {
throw new DatabaseError({ error, name: "DynamicSecretCountLeases" }); throw new DatabaseError({ error, name: "DynamicSecretCountLeases" });
@ -24,7 +21,7 @@ export const dynamicSecretLeaseDALFactory = (db: TDbClient) => {
const findById = async (id: string, tx?: Knex) => { const findById = async (id: string, tx?: Knex) => {
try { try {
const doc = await (tx || db.replicaNode())(TableName.DynamicSecretLease) const doc = await (tx || db)(TableName.DynamicSecretLease)
.where({ [`${TableName.DynamicSecretLease}.id` as "id"]: id }) .where({ [`${TableName.DynamicSecretLease}.id` as "id"]: id })
.first() .first()
.join( .join(
@ -40,10 +37,14 @@ export const dynamicSecretLeaseDALFactory = (db: TDbClient) => {
db.ref("type").withSchema(TableName.DynamicSecret).as("dynType"), db.ref("type").withSchema(TableName.DynamicSecret).as("dynType"),
db.ref("defaultTTL").withSchema(TableName.DynamicSecret).as("dynDefaultTTL"), db.ref("defaultTTL").withSchema(TableName.DynamicSecret).as("dynDefaultTTL"),
db.ref("maxTTL").withSchema(TableName.DynamicSecret).as("dynMaxTTL"), db.ref("maxTTL").withSchema(TableName.DynamicSecret).as("dynMaxTTL"),
db.ref("inputIV").withSchema(TableName.DynamicSecret).as("dynInputIV"),
db.ref("inputTag").withSchema(TableName.DynamicSecret).as("dynInputTag"),
db.ref("inputCiphertext").withSchema(TableName.DynamicSecret).as("dynInputCiphertext"),
db.ref("algorithm").withSchema(TableName.DynamicSecret).as("dynAlgorithm"),
db.ref("keyEncoding").withSchema(TableName.DynamicSecret).as("dynKeyEncoding"),
db.ref("folderId").withSchema(TableName.DynamicSecret).as("dynFolderId"), db.ref("folderId").withSchema(TableName.DynamicSecret).as("dynFolderId"),
db.ref("status").withSchema(TableName.DynamicSecret).as("dynStatus"), db.ref("status").withSchema(TableName.DynamicSecret).as("dynStatus"),
db.ref("statusDetails").withSchema(TableName.DynamicSecret).as("dynStatusDetails"), db.ref("statusDetails").withSchema(TableName.DynamicSecret).as("dynStatusDetails"),
db.ref("encryptedConfig").withSchema(TableName.DynamicSecret).as("dynEncryptedConfig"),
db.ref("createdAt").withSchema(TableName.DynamicSecret).as("dynCreatedAt"), db.ref("createdAt").withSchema(TableName.DynamicSecret).as("dynCreatedAt"),
db.ref("updatedAt").withSchema(TableName.DynamicSecret).as("dynUpdatedAt") db.ref("updatedAt").withSchema(TableName.DynamicSecret).as("dynUpdatedAt")
); );
@ -58,12 +59,16 @@ export const dynamicSecretLeaseDALFactory = (db: TDbClient) => {
type: doc.dynType, type: doc.dynType,
defaultTTL: doc.dynDefaultTTL, defaultTTL: doc.dynDefaultTTL,
maxTTL: doc.dynMaxTTL, maxTTL: doc.dynMaxTTL,
inputIV: doc.dynInputIV,
inputTag: doc.dynInputTag,
inputCiphertext: doc.dynInputCiphertext,
algorithm: doc.dynAlgorithm,
keyEncoding: doc.dynKeyEncoding,
folderId: doc.dynFolderId, folderId: doc.dynFolderId,
status: doc.dynStatus, status: doc.dynStatus,
statusDetails: doc.dynStatusDetails, statusDetails: doc.dynStatusDetails,
createdAt: doc.dynCreatedAt, createdAt: doc.dynCreatedAt,
updatedAt: doc.dynUpdatedAt, updatedAt: doc.dynUpdatedAt
encryptedConfig: doc.dynEncryptedConfig
} }
}; };
} catch (error) { } catch (error) {

View File

@ -1,9 +1,8 @@
import { SecretKeyEncoding } from "@app/db/schemas";
import { DisableRotationErrors } from "@app/ee/services/secret-rotation/secret-rotation-queue"; import { DisableRotationErrors } from "@app/ee/services/secret-rotation/secret-rotation-queue";
import { infisicalSymmetricDecrypt } from "@app/lib/crypto/encryption";
import { logger } from "@app/lib/logger"; import { logger } from "@app/lib/logger";
import { QueueJobs, QueueName, TQueueServiceFactory } from "@app/queue"; import { QueueJobs, QueueName, TQueueServiceFactory } from "@app/queue";
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
import { KmsDataKey } from "@app/services/kms/kms-types";
import { TSecretFolderDALFactory } from "@app/services/secret-folder/secret-folder-dal";
import { TDynamicSecretDALFactory } from "../dynamic-secret/dynamic-secret-dal"; import { TDynamicSecretDALFactory } from "../dynamic-secret/dynamic-secret-dal";
import { DynamicSecretStatus } from "../dynamic-secret/dynamic-secret-types"; import { DynamicSecretStatus } from "../dynamic-secret/dynamic-secret-types";
@ -15,8 +14,6 @@ type TDynamicSecretLeaseQueueServiceFactoryDep = {
dynamicSecretLeaseDAL: Pick<TDynamicSecretLeaseDALFactory, "findById" | "deleteById" | "find" | "updateById">; dynamicSecretLeaseDAL: Pick<TDynamicSecretLeaseDALFactory, "findById" | "deleteById" | "find" | "updateById">;
dynamicSecretDAL: Pick<TDynamicSecretDALFactory, "findById" | "deleteById" | "updateById">; dynamicSecretDAL: Pick<TDynamicSecretDALFactory, "findById" | "deleteById" | "updateById">;
dynamicSecretProviders: Record<DynamicSecretProviders, TDynamicProviderFns>; dynamicSecretProviders: Record<DynamicSecretProviders, TDynamicProviderFns>;
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
folderDAL: Pick<TSecretFolderDALFactory, "findById">;
}; };
export type TDynamicSecretLeaseQueueServiceFactory = ReturnType<typeof dynamicSecretLeaseQueueServiceFactory>; export type TDynamicSecretLeaseQueueServiceFactory = ReturnType<typeof dynamicSecretLeaseQueueServiceFactory>;
@ -25,9 +22,7 @@ export const dynamicSecretLeaseQueueServiceFactory = ({
queueService, queueService,
dynamicSecretDAL, dynamicSecretDAL,
dynamicSecretProviders, dynamicSecretProviders,
dynamicSecretLeaseDAL, dynamicSecretLeaseDAL
kmsService,
folderDAL
}: TDynamicSecretLeaseQueueServiceFactoryDep) => { }: TDynamicSecretLeaseQueueServiceFactoryDep) => {
const pruneDynamicSecret = async (dynamicSecretCfgId: string) => { const pruneDynamicSecret = async (dynamicSecretCfgId: string) => {
await queueService.queue( await queueService.queue(
@ -82,20 +77,15 @@ export const dynamicSecretLeaseQueueServiceFactory = ({
if (!dynamicSecretLease) throw new DisableRotationErrors({ message: "Dynamic secret lease not found" }); if (!dynamicSecretLease) throw new DisableRotationErrors({ message: "Dynamic secret lease not found" });
const dynamicSecretCfg = dynamicSecretLease.dynamicSecret; const dynamicSecretCfg = dynamicSecretLease.dynamicSecret;
const folder = await folderDAL.findById(dynamicSecretCfg.folderId);
if (!folder) throw new DisableRotationErrors({ message: "Folder not found" });
const { projectId } = folder;
const { decryptor: secretManagerDecryptor } = await kmsService.createCipherPairWithDataKey({
type: KmsDataKey.SecretManager,
projectId
});
const dynamicSecretInputConfig = secretManagerDecryptor({
cipherTextBlob: dynamicSecretCfg.encryptedConfig
}).toString();
const selectedProvider = dynamicSecretProviders[dynamicSecretCfg.type as DynamicSecretProviders]; const selectedProvider = dynamicSecretProviders[dynamicSecretCfg.type as DynamicSecretProviders];
const decryptedStoredInput = JSON.parse(dynamicSecretInputConfig) as object; const decryptedStoredInput = JSON.parse(
infisicalSymmetricDecrypt({
keyEncoding: dynamicSecretCfg.keyEncoding as SecretKeyEncoding,
ciphertext: dynamicSecretCfg.inputCiphertext,
tag: dynamicSecretCfg.inputTag,
iv: dynamicSecretCfg.inputIV
})
) as object;
await selectedProvider.revoke(decryptedStoredInput, dynamicSecretLease.externalEntityId); await selectedProvider.revoke(decryptedStoredInput, dynamicSecretLease.externalEntityId);
await dynamicSecretLeaseDAL.deleteById(dynamicSecretLease.id); await dynamicSecretLeaseDAL.deleteById(dynamicSecretLease.id);
@ -110,22 +100,17 @@ export const dynamicSecretLeaseQueueServiceFactory = ({
if ((dynamicSecretCfg.status as DynamicSecretStatus) !== DynamicSecretStatus.Deleting) if ((dynamicSecretCfg.status as DynamicSecretStatus) !== DynamicSecretStatus.Deleting)
throw new DisableRotationErrors({ message: "Document not deleted" }); throw new DisableRotationErrors({ message: "Document not deleted" });
const folder = await folderDAL.findById(dynamicSecretCfg.folderId);
if (!folder) throw new DisableRotationErrors({ message: "Folder not found" });
const { projectId } = folder;
const { decryptor: secretManagerDecryptor } = await kmsService.createCipherPairWithDataKey({
type: KmsDataKey.SecretManager,
projectId
});
const dynamicSecretLeases = await dynamicSecretLeaseDAL.find({ dynamicSecretId: dynamicSecretCfgId }); const dynamicSecretLeases = await dynamicSecretLeaseDAL.find({ dynamicSecretId: dynamicSecretCfgId });
if (dynamicSecretLeases.length) { if (dynamicSecretLeases.length) {
const selectedProvider = dynamicSecretProviders[dynamicSecretCfg.type as DynamicSecretProviders]; const selectedProvider = dynamicSecretProviders[dynamicSecretCfg.type as DynamicSecretProviders];
const decryptedStoredInput = JSON.parse(
const dynamicSecretInputConfig = secretManagerDecryptor({ infisicalSymmetricDecrypt({
cipherTextBlob: dynamicSecretCfg.encryptedConfig keyEncoding: dynamicSecretCfg.keyEncoding as SecretKeyEncoding,
}).toString(); ciphertext: dynamicSecretCfg.inputCiphertext,
const decryptedStoredInput = JSON.parse(dynamicSecretInputConfig) as object; tag: dynamicSecretCfg.inputTag,
iv: dynamicSecretCfg.inputIV
})
) as object;
await Promise.all(dynamicSecretLeases.map(({ id }) => unsetLeaseRevocation(id))); await Promise.all(dynamicSecretLeases.map(({ id }) => unsetLeaseRevocation(id)));
await Promise.all( await Promise.all(

View File

@ -1,14 +1,14 @@
import { ForbiddenError, subject } from "@casl/ability"; import { ForbiddenError, subject } from "@casl/ability";
import ms from "ms"; import ms from "ms";
import { SecretKeyEncoding } from "@app/db/schemas";
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
import { getConfig } from "@app/lib/config/env"; import { getConfig } from "@app/lib/config/env";
import { infisicalSymmetricDecrypt } from "@app/lib/crypto/encryption";
import { BadRequestError } from "@app/lib/errors"; import { BadRequestError } from "@app/lib/errors";
import { logger } from "@app/lib/logger"; import { logger } from "@app/lib/logger";
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
import { KmsDataKey } from "@app/services/kms/kms-types";
import { TProjectDALFactory } from "@app/services/project/project-dal"; import { TProjectDALFactory } from "@app/services/project/project-dal";
import { TSecretFolderDALFactory } from "@app/services/secret-folder/secret-folder-dal"; import { TSecretFolderDALFactory } from "@app/services/secret-folder/secret-folder-dal";
@ -34,7 +34,6 @@ type TDynamicSecretLeaseServiceFactoryDep = {
folderDAL: Pick<TSecretFolderDALFactory, "findBySecretPath">; folderDAL: Pick<TSecretFolderDALFactory, "findBySecretPath">;
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">; permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
projectDAL: Pick<TProjectDALFactory, "findProjectBySlug">; projectDAL: Pick<TProjectDALFactory, "findProjectBySlug">;
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
}; };
export type TDynamicSecretLeaseServiceFactory = ReturnType<typeof dynamicSecretLeaseServiceFactory>; export type TDynamicSecretLeaseServiceFactory = ReturnType<typeof dynamicSecretLeaseServiceFactory>;
@ -47,8 +46,7 @@ export const dynamicSecretLeaseServiceFactory = ({
permissionService, permissionService,
dynamicSecretQueueService, dynamicSecretQueueService,
projectDAL, projectDAL,
licenseService, licenseService
kmsService
}: TDynamicSecretLeaseServiceFactoryDep) => { }: TDynamicSecretLeaseServiceFactoryDep) => {
const create = async ({ const create = async ({
environmentSlug, environmentSlug,
@ -96,12 +94,14 @@ export const dynamicSecretLeaseServiceFactory = ({
throw new BadRequestError({ message: `Max lease limit reached. Limit: ${appCfg.MAX_LEASE_LIMIT}` }); throw new BadRequestError({ message: `Max lease limit reached. Limit: ${appCfg.MAX_LEASE_LIMIT}` });
const selectedProvider = dynamicSecretProviders[dynamicSecretCfg.type as DynamicSecretProviders]; const selectedProvider = dynamicSecretProviders[dynamicSecretCfg.type as DynamicSecretProviders];
const { decryptor: kmsDecryptor } = await kmsService.createCipherPairWithDataKey({ const decryptedStoredInput = JSON.parse(
type: KmsDataKey.SecretManager, infisicalSymmetricDecrypt({
projectId keyEncoding: dynamicSecretCfg.keyEncoding as SecretKeyEncoding,
}); ciphertext: dynamicSecretCfg.inputCiphertext,
const decryptedStoredInputJson = kmsDecryptor({ cipherTextBlob: dynamicSecretCfg.encryptedConfig }).toString(); tag: dynamicSecretCfg.inputTag,
const decryptedStoredInput = JSON.parse(decryptedStoredInputJson) as object; iv: dynamicSecretCfg.inputIV
})
) as object;
const selectedTTL = ttl ?? dynamicSecretCfg.defaultTTL; const selectedTTL = ttl ?? dynamicSecretCfg.defaultTTL;
const { maxTTL } = dynamicSecretCfg; const { maxTTL } = dynamicSecretCfg;
@ -164,12 +164,14 @@ export const dynamicSecretLeaseServiceFactory = ({
const dynamicSecretCfg = dynamicSecretLease.dynamicSecret; const dynamicSecretCfg = dynamicSecretLease.dynamicSecret;
const selectedProvider = dynamicSecretProviders[dynamicSecretCfg.type as DynamicSecretProviders]; const selectedProvider = dynamicSecretProviders[dynamicSecretCfg.type as DynamicSecretProviders];
const { decryptor: kmsDecryptor } = await kmsService.createCipherPairWithDataKey({ const decryptedStoredInput = JSON.parse(
type: KmsDataKey.SecretManager, infisicalSymmetricDecrypt({
projectId keyEncoding: dynamicSecretCfg.keyEncoding as SecretKeyEncoding,
}); ciphertext: dynamicSecretCfg.inputCiphertext,
const decryptedStoredInputJson = kmsDecryptor({ cipherTextBlob: dynamicSecretCfg.encryptedConfig }).toString(); tag: dynamicSecretCfg.inputTag,
const decryptedStoredInput = JSON.parse(decryptedStoredInputJson) as object; iv: dynamicSecretCfg.inputIV
})
) as object;
const selectedTTL = ttl ?? dynamicSecretCfg.defaultTTL; const selectedTTL = ttl ?? dynamicSecretCfg.defaultTTL;
const { maxTTL } = dynamicSecretCfg; const { maxTTL } = dynamicSecretCfg;
@ -229,12 +231,14 @@ export const dynamicSecretLeaseServiceFactory = ({
const dynamicSecretCfg = dynamicSecretLease.dynamicSecret; const dynamicSecretCfg = dynamicSecretLease.dynamicSecret;
const selectedProvider = dynamicSecretProviders[dynamicSecretCfg.type as DynamicSecretProviders]; const selectedProvider = dynamicSecretProviders[dynamicSecretCfg.type as DynamicSecretProviders];
const { decryptor: kmsDecryptor } = await kmsService.createCipherPairWithDataKey({ const decryptedStoredInput = JSON.parse(
type: KmsDataKey.SecretManager, infisicalSymmetricDecrypt({
projectId keyEncoding: dynamicSecretCfg.keyEncoding as SecretKeyEncoding,
}); ciphertext: dynamicSecretCfg.inputCiphertext,
const decryptedStoredInputJson = kmsDecryptor({ cipherTextBlob: dynamicSecretCfg.encryptedConfig }).toString(); tag: dynamicSecretCfg.inputTag,
const decryptedStoredInput = JSON.parse(decryptedStoredInputJson) as object; iv: dynamicSecretCfg.inputIV
})
) as object;
const revokeResponse = await selectedProvider const revokeResponse = await selectedProvider
.revoke(decryptedStoredInput, dynamicSecretLease.externalEntityId) .revoke(decryptedStoredInput, dynamicSecretLease.externalEntityId)

View File

@ -1,11 +1,11 @@
import { ForbiddenError, subject } from "@casl/ability"; import { ForbiddenError, subject } from "@casl/ability";
import { SecretKeyEncoding } from "@app/db/schemas";
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
import { infisicalSymmetricDecrypt, infisicalSymmetricEncypt } from "@app/lib/crypto/encryption";
import { BadRequestError } from "@app/lib/errors"; import { BadRequestError } from "@app/lib/errors";
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
import { KmsDataKey } from "@app/services/kms/kms-types";
import { TProjectDALFactory } from "@app/services/project/project-dal"; import { TProjectDALFactory } from "@app/services/project/project-dal";
import { TSecretFolderDALFactory } from "@app/services/secret-folder/secret-folder-dal"; import { TSecretFolderDALFactory } from "@app/services/secret-folder/secret-folder-dal";
@ -34,7 +34,6 @@ type TDynamicSecretServiceFactoryDep = {
folderDAL: Pick<TSecretFolderDALFactory, "findBySecretPath">; folderDAL: Pick<TSecretFolderDALFactory, "findBySecretPath">;
projectDAL: Pick<TProjectDALFactory, "findProjectBySlug">; projectDAL: Pick<TProjectDALFactory, "findProjectBySlug">;
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">; permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
}; };
export type TDynamicSecretServiceFactory = ReturnType<typeof dynamicSecretServiceFactory>; export type TDynamicSecretServiceFactory = ReturnType<typeof dynamicSecretServiceFactory>;
@ -47,8 +46,7 @@ export const dynamicSecretServiceFactory = ({
dynamicSecretProviders, dynamicSecretProviders,
permissionService, permissionService,
dynamicSecretQueueService, dynamicSecretQueueService,
projectDAL, projectDAL
kmsService
}: TDynamicSecretServiceFactoryDep) => { }: TDynamicSecretServiceFactoryDep) => {
const create = async ({ const create = async ({
path, path,
@ -98,16 +96,16 @@ export const dynamicSecretServiceFactory = ({
const isConnected = await selectedProvider.validateConnection(provider.inputs); const isConnected = await selectedProvider.validateConnection(provider.inputs);
if (!isConnected) throw new BadRequestError({ message: "Provider connection failed" }); if (!isConnected) throw new BadRequestError({ message: "Provider connection failed" });
const { encryptor: secretManagerEncryptor } = await kmsService.createCipherPairWithDataKey({
type: KmsDataKey.SecretManager,
projectId
});
const encryptedConfig = secretManagerEncryptor({ plainText: Buffer.from(JSON.stringify(inputs)) }).cipherTextBlob; const encryptedInput = infisicalSymmetricEncypt(JSON.stringify(inputs));
const dynamicSecretCfg = await dynamicSecretDAL.create({ const dynamicSecretCfg = await dynamicSecretDAL.create({
type: provider.type, type: provider.type,
version: 1, version: 1,
encryptedConfig, inputIV: encryptedInput.iv,
inputTag: encryptedInput.tag,
inputCiphertext: encryptedInput.ciphertext,
algorithm: encryptedInput.algorithm,
keyEncoding: encryptedInput.encoding,
maxTTL, maxTTL,
defaultTTL, defaultTTL,
folderId: folder.id, folderId: folder.id,
@ -167,28 +165,27 @@ export const dynamicSecretServiceFactory = ({
} }
const selectedProvider = dynamicSecretProviders[dynamicSecretCfg.type as DynamicSecretProviders]; const selectedProvider = dynamicSecretProviders[dynamicSecretCfg.type as DynamicSecretProviders];
const { encryptor: secretManagerEncryptor, decryptor: secretManagerDecryptor } = const decryptedStoredInput = JSON.parse(
await kmsService.createCipherPairWithDataKey({ infisicalSymmetricDecrypt({
type: KmsDataKey.SecretManager, keyEncoding: dynamicSecretCfg.keyEncoding as SecretKeyEncoding,
projectId ciphertext: dynamicSecretCfg.inputCiphertext,
}); tag: dynamicSecretCfg.inputTag,
const dynamicSecretInputConfig = secretManagerDecryptor({ iv: dynamicSecretCfg.inputIV
cipherTextBlob: dynamicSecretCfg.encryptedConfig })
}).toString(); ) as object;
const decryptedStoredInput = JSON.parse(dynamicSecretInputConfig) as object;
const newInput = { ...decryptedStoredInput, ...(inputs || {}) }; const newInput = { ...decryptedStoredInput, ...(inputs || {}) };
const updatedInput = await selectedProvider.validateProviderInputs(newInput); const updatedInput = await selectedProvider.validateProviderInputs(newInput);
const isConnected = await selectedProvider.validateConnection(newInput); const isConnected = await selectedProvider.validateConnection(newInput);
if (!isConnected) throw new BadRequestError({ message: "Provider connection failed" }); if (!isConnected) throw new BadRequestError({ message: "Provider connection failed" });
const encryptedConfig = secretManagerEncryptor({ const encryptedInput = infisicalSymmetricEncypt(JSON.stringify(updatedInput));
plainText: Buffer.from(JSON.stringify(updatedInput))
}).cipherTextBlob;
const updatedDynamicCfg = await dynamicSecretDAL.updateById(dynamicSecretCfg.id, { const updatedDynamicCfg = await dynamicSecretDAL.updateById(dynamicSecretCfg.id, {
encryptedConfig, inputIV: encryptedInput.iv,
inputTag: encryptedInput.tag,
inputCiphertext: encryptedInput.ciphertext,
algorithm: encryptedInput.algorithm,
keyEncoding: encryptedInput.encoding,
maxTTL, maxTTL,
defaultTTL, defaultTTL,
name: newName ?? name, name: newName ?? name,
@ -289,16 +286,14 @@ export const dynamicSecretServiceFactory = ({
const dynamicSecretCfg = await dynamicSecretDAL.findOne({ name, folderId: folder.id }); const dynamicSecretCfg = await dynamicSecretDAL.findOne({ name, folderId: folder.id });
if (!dynamicSecretCfg) throw new BadRequestError({ message: "Dynamic secret not found" }); if (!dynamicSecretCfg) throw new BadRequestError({ message: "Dynamic secret not found" });
const { decryptor: secretManagerDecryptor } = await kmsService.createCipherPairWithDataKey({ const decryptedStoredInput = JSON.parse(
type: KmsDataKey.SecretManager, infisicalSymmetricDecrypt({
projectId keyEncoding: dynamicSecretCfg.keyEncoding as SecretKeyEncoding,
}); ciphertext: dynamicSecretCfg.inputCiphertext,
tag: dynamicSecretCfg.inputTag,
const dynamicSecretInputConfig = secretManagerDecryptor({ iv: dynamicSecretCfg.inputIV
cipherTextBlob: dynamicSecretCfg.encryptedConfig })
}).toString(); ) as object;
const decryptedStoredInput = JSON.parse(dynamicSecretInputConfig) as object;
const selectedProvider = dynamicSecretProviders[dynamicSecretCfg.type as DynamicSecretProviders]; const selectedProvider = dynamicSecretProviders[dynamicSecretCfg.type as DynamicSecretProviders];
const providerInputs = (await selectedProvider.validateProviderInputs(decryptedStoredInput)) as object; const providerInputs = (await selectedProvider.validateProviderInputs(decryptedStoredInput)) as object;
return { ...dynamicSecretCfg, inputs: providerInputs }; return { ...dynamicSecretCfg, inputs: providerInputs };

View File

@ -40,7 +40,12 @@ export const getDefaultOnPremFeatures = (): TFeatureSet => ({
secretRotation: true, secretRotation: true,
caCrl: false, caCrl: false,
instanceUserManagement: false, instanceUserManagement: false,
externalKms: false externalKms: false,
rateLimits: {
readLimit: 60,
writeLimit: 200,
secretsLimit: 40
}
}); });
export const setupLicenceRequestWithStore = (baseURL: string, refreshUrl: string, licenseKey: string) => { export const setupLicenceRequestWithStore = (baseURL: string, refreshUrl: string, licenseKey: string) => {

View File

@ -58,6 +58,11 @@ export type TFeatureSet = {
caCrl: false; caCrl: false;
instanceUserManagement: false; instanceUserManagement: false;
externalKms: false; externalKms: false;
rateLimits: {
readLimit: number;
writeLimit: number;
secretsLimit: number;
};
}; };
export type TOrgPlansTableDTO = { export type TOrgPlansTableDTO = {

View File

@ -9,6 +9,10 @@ export enum OrgPermissionActions {
Delete = "delete" Delete = "delete"
} }
export enum OrgPermissionAdminConsoleAction {
AccessAllProjects = "access-all-projects"
}
export enum OrgPermissionSubjects { export enum OrgPermissionSubjects {
Workspace = "workspace", Workspace = "workspace",
Role = "role", Role = "role",
@ -22,7 +26,8 @@ export enum OrgPermissionSubjects {
Billing = "billing", Billing = "billing",
SecretScanning = "secret-scanning", SecretScanning = "secret-scanning",
Identity = "identity", Identity = "identity",
Kms = "kms" Kms = "kms",
AdminConsole = "organization-admin-console"
} }
export type OrgPermissionSet = export type OrgPermissionSet =
@ -39,7 +44,8 @@ export type OrgPermissionSet =
| [OrgPermissionActions, OrgPermissionSubjects.SecretScanning] | [OrgPermissionActions, OrgPermissionSubjects.SecretScanning]
| [OrgPermissionActions, OrgPermissionSubjects.Billing] | [OrgPermissionActions, OrgPermissionSubjects.Billing]
| [OrgPermissionActions, OrgPermissionSubjects.Identity] | [OrgPermissionActions, OrgPermissionSubjects.Identity]
| [OrgPermissionActions, OrgPermissionSubjects.Kms]; | [OrgPermissionActions, OrgPermissionSubjects.Kms]
| [OrgPermissionAdminConsoleAction, OrgPermissionSubjects.AdminConsole];
const buildAdminPermission = () => { const buildAdminPermission = () => {
const { can, build } = new AbilityBuilder<MongoAbility<OrgPermissionSet>>(createMongoAbility); const { can, build } = new AbilityBuilder<MongoAbility<OrgPermissionSet>>(createMongoAbility);
@ -107,6 +113,8 @@ const buildAdminPermission = () => {
can(OrgPermissionActions.Edit, OrgPermissionSubjects.Kms); can(OrgPermissionActions.Edit, OrgPermissionSubjects.Kms);
can(OrgPermissionActions.Delete, OrgPermissionSubjects.Kms); can(OrgPermissionActions.Delete, OrgPermissionSubjects.Kms);
can(OrgPermissionAdminConsoleAction.AccessAllProjects, OrgPermissionSubjects.AdminConsole);
return build({ conditionsMatcher }); return build({ conditionsMatcher });
}; };

View File

@ -23,6 +23,7 @@ export enum ProjectPermissionSub {
IpAllowList = "ip-allowlist", IpAllowList = "ip-allowlist",
Project = "workspace", Project = "workspace",
Secrets = "secrets", Secrets = "secrets",
SecretFolders = "secret-folders",
SecretRollback = "secret-rollback", SecretRollback = "secret-rollback",
SecretApproval = "secret-approval", SecretApproval = "secret-approval",
SecretRotation = "secret-rotation", SecretRotation = "secret-rotation",
@ -42,6 +43,10 @@ export type ProjectPermissionSet =
ProjectPermissionActions, ProjectPermissionActions,
ProjectPermissionSub.Secrets | (ForcedSubject<ProjectPermissionSub.Secrets> & SubjectFields) ProjectPermissionSub.Secrets | (ForcedSubject<ProjectPermissionSub.Secrets> & SubjectFields)
] ]
| [
ProjectPermissionActions,
ProjectPermissionSub.SecretFolders | (ForcedSubject<ProjectPermissionSub.SecretFolders> & SubjectFields)
]
| [ProjectPermissionActions, ProjectPermissionSub.Role] | [ProjectPermissionActions, ProjectPermissionSub.Role]
| [ProjectPermissionActions, ProjectPermissionSub.Tags] | [ProjectPermissionActions, ProjectPermissionSub.Tags]
| [ProjectPermissionActions, ProjectPermissionSub.Member] | [ProjectPermissionActions, ProjectPermissionSub.Member]

View File

@ -4,17 +4,16 @@ import { logger } from "@app/lib/logger";
import { TLicenseServiceFactory } from "../license/license-service"; import { TLicenseServiceFactory } from "../license/license-service";
import { TRateLimitDALFactory } from "./rate-limit-dal"; import { TRateLimitDALFactory } from "./rate-limit-dal";
import { TRateLimit, TRateLimitUpdateDTO } from "./rate-limit-types"; import { RateLimitConfiguration, TRateLimit, TRateLimitUpdateDTO } from "./rate-limit-types";
let rateLimitMaxConfiguration = { let rateLimitMaxConfiguration: RateLimitConfiguration = {
readLimit: 60, readLimit: 60,
publicEndpointLimit: 30, publicEndpointLimit: 30,
writeLimit: 200, writeLimit: 200,
secretsLimit: 60, secretsLimit: 60,
authRateLimit: 60, authRateLimit: 60,
inviteUserRateLimit: 30, inviteUserRateLimit: 30,
mfaRateLimit: 20, mfaRateLimit: 20
creationLimit: 30
}; };
Object.freeze(rateLimitMaxConfiguration); Object.freeze(rateLimitMaxConfiguration);
@ -67,8 +66,7 @@ export const rateLimitServiceFactory = ({ rateLimitDAL, licenseService }: TRateL
secretsLimit: rateLimit.secretsRateLimit, secretsLimit: rateLimit.secretsRateLimit,
authRateLimit: rateLimit.authRateLimit, authRateLimit: rateLimit.authRateLimit,
inviteUserRateLimit: rateLimit.inviteUserRateLimit, inviteUserRateLimit: rateLimit.inviteUserRateLimit,
mfaRateLimit: rateLimit.mfaRateLimit, mfaRateLimit: rateLimit.mfaRateLimit
creationLimit: rateLimit.creationLimit
}; };
logger.info(`syncRateLimitConfiguration: rate limit configuration: %o`, newRateLimitMaxConfiguration); logger.info(`syncRateLimitConfiguration: rate limit configuration: %o`, newRateLimitMaxConfiguration);

View File

@ -5,7 +5,6 @@ export type TRateLimitUpdateDTO = {
authRateLimit: number; authRateLimit: number;
inviteUserRateLimit: number; inviteUserRateLimit: number;
mfaRateLimit: number; mfaRateLimit: number;
creationLimit: number;
publicEndpointLimit: number; publicEndpointLimit: number;
}; };
@ -14,3 +13,13 @@ export type TRateLimit = {
createdAt: Date; createdAt: Date;
updatedAt: Date; updatedAt: Date;
} & TRateLimitUpdateDTO; } & TRateLimitUpdateDTO;
export type RateLimitConfiguration = {
readLimit: number;
publicEndpointLimit: number;
writeLimit: number;
secretsLimit: number;
authRateLimit: number;
inviteUserRateLimit: number;
mfaRateLimit: number;
};

View File

@ -449,7 +449,7 @@ export const secretReplicationServiceFactory = ({
}); });
} }
if (locallyDeletedSecrets.length) { if (locallyDeletedSecrets.length) {
await secretDAL.delete( await secretV2BridgeDAL.delete(
{ {
$in: { $in: {
id: locallyDeletedSecrets.map(({ id }) => id) id: locallyDeletedSecrets.map(({ id }) => id)

View File

@ -1056,7 +1056,7 @@ export const CERTIFICATE_AUTHORITIES = {
}, },
SIGN_INTERMEDIATE: { SIGN_INTERMEDIATE: {
caId: "The ID of the CA to sign the intermediate certificate with", caId: "The ID of the CA to sign the intermediate certificate with",
csr: "The CSR to sign with the CA", csr: "The pem-encoded CSR to sign with the CA",
notBefore: "The date and time when the intermediate CA becomes valid in YYYY-MM-DDTHH:mm:ss.sssZ format", notBefore: "The date and time when the intermediate CA becomes valid in YYYY-MM-DDTHH:mm:ss.sssZ format",
notAfter: "The date and time when the intermediate CA expires in YYYY-MM-DDTHH:mm:ss.sssZ format", notAfter: "The date and time when the intermediate CA expires in YYYY-MM-DDTHH:mm:ss.sssZ format",
maxPathLength: maxPathLength:
@ -1086,6 +1086,21 @@ export const CERTIFICATE_AUTHORITIES = {
privateKey: "The private key of the issued certificate", privateKey: "The private key of the issued certificate",
serialNumber: "The serial number of the issued certificate" serialNumber: "The serial number of the issued certificate"
}, },
SIGN_CERT: {
caId: "The ID of the CA to issue the certificate from",
csr: "The pem-encoded CSR to sign with the CA to be used for certificate issuance",
friendlyName: "A friendly name for the certificate",
commonName: "The common name (CN) for the certificate",
altNames:
"A comma-delimited list of Subject Alternative Names (SANs) for the certificate; these can be host names or email addresses.",
ttl: "The time to live for the certificate such as 1m, 1h, 1d, 1y, ...",
notBefore: "The date and time when the certificate becomes valid in YYYY-MM-DDTHH:mm:ss.sssZ format",
notAfter: "The date and time when the certificate expires in YYYY-MM-DDTHH:mm:ss.sssZ format",
certificate: "The issued certificate",
issuingCaCertificate: "The certificate of the issuing CA",
certificateChain: "The certificate chain of the issued certificate",
serialNumber: "The serial number of the issued certificate"
},
GET_CRL: { GET_CRL: {
caId: "The ID of the CA to get the certificate revocation list (CRL) for", caId: "The ID of the CA to get the certificate revocation list (CRL) for",
crl: "The certificate revocation list (CRL) of the CA" crl: "The certificate revocation list (CRL) of the CA"

View File

@ -19,23 +19,43 @@ export const withTransaction = <K extends object>(db: Knex, dal: K) => ({
export type TFindFilter<R extends object = object> = Partial<R> & { export type TFindFilter<R extends object = object> = Partial<R> & {
$in?: Partial<{ [k in keyof R]: R[k][] }>; $in?: Partial<{ [k in keyof R]: R[k][] }>;
$search?: Partial<{ [k in keyof R]: R[k] }>;
}; };
export const buildFindFilter = export const buildFindFilter =
<R extends object = object>({ $in, ...filter }: TFindFilter<R>) => <R extends object = object>({ $in, $search, ...filter }: TFindFilter<R>) =>
(bd: Knex.QueryBuilder<R, R>) => { (bd: Knex.QueryBuilder<R, R>) => {
void bd.where(filter); void bd.where(filter);
if ($in) { if ($in) {
Object.entries($in).forEach(([key, val]) => { Object.entries($in).forEach(([key, val]) => {
void bd.whereIn(key as never, val as never); if (val) {
void bd.whereIn(key as never, val as never);
}
});
}
if ($search) {
Object.entries($search).forEach(([key, val]) => {
if (val) {
void bd.whereILike(key as never, val as never);
}
}); });
} }
return bd; return bd;
}; };
export type TFindOpt<R extends object = object> = { export type TFindReturn<TQuery extends Knex.QueryBuilder, TCount extends boolean = false> = Array<
Awaited<TQuery>[0] &
(TCount extends true
? {
count: string;
}
: unknown)
>;
export type TFindOpt<R extends object = object, TCount extends boolean = boolean> = {
limit?: number; limit?: number;
offset?: number; offset?: number;
sort?: Array<[keyof R, "asc" | "desc"] | [keyof R, "asc" | "desc", "first" | "last"]>; sort?: Array<[keyof R, "asc" | "desc"] | [keyof R, "asc" | "desc", "first" | "last"]>;
count?: TCount;
tx?: Knex; tx?: Knex;
}; };
@ -66,18 +86,22 @@ export const ormify = <DbOps extends object, Tname extends keyof Tables>(db: Kne
throw new DatabaseError({ error, name: "Find one" }); throw new DatabaseError({ error, name: "Find one" });
} }
}, },
find: async ( find: async <TCount extends boolean = false>(
filter: TFindFilter<Tables[Tname]["base"]>, filter: TFindFilter<Tables[Tname]["base"]>,
{ offset, limit, sort, tx }: TFindOpt<Tables[Tname]["base"]> = {} { offset, limit, sort, count, tx }: TFindOpt<Tables[Tname]["base"], TCount> = {}
) => { ) => {
try { try {
const query = (tx || db.replicaNode())(tableName).where(buildFindFilter(filter)); const query = (tx || db.replicaNode())(tableName).where(buildFindFilter(filter));
if (count) {
void query.select(db.raw("COUNT(*) OVER() AS count"));
void query.select("*");
}
if (limit) void query.limit(limit); if (limit) void query.limit(limit);
if (offset) void query.offset(offset); if (offset) void query.offset(offset);
if (sort) { if (sort) {
void query.orderBy(sort.map(([column, order, nulls]) => ({ column: column as string, order, nulls }))); void query.orderBy(sort.map(([column, order, nulls]) => ({ column: column as string, order, nulls })));
} }
const res = await query; const res = (await query) as TFindReturn<typeof query, TCount>;
return res; return res;
} catch (error) { } catch (error) {
throw new DatabaseError({ error, name: "Find one" }); throw new DatabaseError({ error, name: "Find one" });

View File

@ -1,7 +1,6 @@
import type { RateLimitOptions, RateLimitPluginOptions } from "@fastify/rate-limit"; import type { RateLimitOptions, RateLimitPluginOptions } from "@fastify/rate-limit";
import { Redis } from "ioredis"; import { Redis } from "ioredis";
import { getRateLimiterConfig } from "@app/ee/services/rate-limit/rate-limit-service";
import { getConfig } from "@app/lib/config/env"; import { getConfig } from "@app/lib/config/env";
export const globalRateLimiterCfg = (): RateLimitPluginOptions => { export const globalRateLimiterCfg = (): RateLimitPluginOptions => {
@ -22,14 +21,16 @@ export const globalRateLimiterCfg = (): RateLimitPluginOptions => {
// GET endpoints // GET endpoints
export const readLimit: RateLimitOptions = { export const readLimit: RateLimitOptions = {
timeWindow: 60 * 1000, timeWindow: 60 * 1000,
max: () => getRateLimiterConfig().readLimit, hook: "preValidation",
max: (req) => req.rateLimits.readLimit,
keyGenerator: (req) => req.realIp keyGenerator: (req) => req.realIp
}; };
// POST, PATCH, PUT, DELETE endpoints // POST, PATCH, PUT, DELETE endpoints
export const writeLimit: RateLimitOptions = { export const writeLimit: RateLimitOptions = {
timeWindow: 60 * 1000, timeWindow: 60 * 1000,
max: () => getRateLimiterConfig().writeLimit, hook: "preValidation",
max: (req) => req.rateLimits.writeLimit,
keyGenerator: (req) => req.realIp keyGenerator: (req) => req.realIp
}; };
@ -37,42 +38,40 @@ export const writeLimit: RateLimitOptions = {
export const secretsLimit: RateLimitOptions = { export const secretsLimit: RateLimitOptions = {
// secrets, folders, secret imports // secrets, folders, secret imports
timeWindow: 60 * 1000, timeWindow: 60 * 1000,
max: () => getRateLimiterConfig().secretsLimit, hook: "preValidation",
max: (req) => req.rateLimits.secretsLimit,
keyGenerator: (req) => req.realIp keyGenerator: (req) => req.realIp
}; };
export const authRateLimit: RateLimitOptions = { export const authRateLimit: RateLimitOptions = {
timeWindow: 60 * 1000, timeWindow: 60 * 1000,
max: () => getRateLimiterConfig().authRateLimit, hook: "preValidation",
max: (req) => req.rateLimits.authRateLimit,
keyGenerator: (req) => req.realIp keyGenerator: (req) => req.realIp
}; };
export const inviteUserRateLimit: RateLimitOptions = { export const inviteUserRateLimit: RateLimitOptions = {
timeWindow: 60 * 1000, timeWindow: 60 * 1000,
max: () => getRateLimiterConfig().inviteUserRateLimit, hook: "preValidation",
max: (req) => req.rateLimits.inviteUserRateLimit,
keyGenerator: (req) => req.realIp keyGenerator: (req) => req.realIp
}; };
export const mfaRateLimit: RateLimitOptions = { export const mfaRateLimit: RateLimitOptions = {
timeWindow: 60 * 1000, timeWindow: 60 * 1000,
max: () => getRateLimiterConfig().mfaRateLimit, hook: "preValidation",
max: (req) => req.rateLimits.mfaRateLimit,
keyGenerator: (req) => { keyGenerator: (req) => {
return req.headers.authorization?.split(" ")[1] || req.realIp; return req.headers.authorization?.split(" ")[1] || req.realIp;
} }
}; };
export const creationLimit: RateLimitOptions = {
// identity, project, org
timeWindow: 60 * 1000,
max: () => getRateLimiterConfig().creationLimit,
keyGenerator: (req) => req.realIp
};
// Public endpoints to avoid brute force attacks // Public endpoints to avoid brute force attacks
export const publicEndpointLimit: RateLimitOptions = { export const publicEndpointLimit: RateLimitOptions = {
// Read Shared Secrets // Read Shared Secrets
timeWindow: 60 * 1000, timeWindow: 60 * 1000,
max: () => getRateLimiterConfig().publicEndpointLimit, hook: "preValidation",
max: (req) => req.rateLimits.publicEndpointLimit,
keyGenerator: (req) => req.realIp keyGenerator: (req) => req.realIp
}; };

View File

@ -0,0 +1,38 @@
import fp from "fastify-plugin";
import { getRateLimiterConfig } from "@app/ee/services/rate-limit/rate-limit-service";
import { getConfig } from "@app/lib/config/env";
export const injectRateLimits = fp(async (server) => {
server.decorateRequest("rateLimits", null);
server.addHook("onRequest", async (req) => {
const appCfg = getConfig();
const instanceRateLimiterConfig = getRateLimiterConfig();
if (!req.auth?.orgId) {
// for public endpoints, we always use the instance-wide default rate limits
req.rateLimits = instanceRateLimiterConfig;
return;
}
const { rateLimits, customRateLimits } = await server.services.license.getPlan(req.auth.orgId);
if (customRateLimits && !appCfg.isCloud) {
// we do this because for self-hosted/dedicated instances, we want custom rate limits to be based on admin configuration
// note that the syncing of custom rate limit happens on the instanceRateLimiterConfig object
req.rateLimits = instanceRateLimiterConfig;
return;
}
// we're using the null coalescing operator in order to handle outdated licenses
req.rateLimits = {
readLimit: rateLimits?.readLimit ?? instanceRateLimiterConfig.readLimit,
writeLimit: rateLimits?.writeLimit ?? instanceRateLimiterConfig.writeLimit,
secretsLimit: rateLimits?.secretsLimit ?? instanceRateLimiterConfig.secretsLimit,
publicEndpointLimit: instanceRateLimiterConfig.publicEndpointLimit,
authRateLimit: instanceRateLimiterConfig.authRateLimit,
inviteUserRateLimit: instanceRateLimiterConfig.inviteUserRateLimit,
mfaRateLimit: instanceRateLimiterConfig.mfaRateLimit
};
});
});

View File

@ -129,6 +129,7 @@ import { orgDALFactory } from "@app/services/org/org-dal";
import { orgRoleDALFactory } from "@app/services/org/org-role-dal"; import { orgRoleDALFactory } from "@app/services/org/org-role-dal";
import { orgRoleServiceFactory } from "@app/services/org/org-role-service"; import { orgRoleServiceFactory } from "@app/services/org/org-role-service";
import { orgServiceFactory } from "@app/services/org/org-service"; import { orgServiceFactory } from "@app/services/org/org-service";
import { orgAdminServiceFactory } from "@app/services/org-admin/org-admin-service";
import { orgMembershipDALFactory } from "@app/services/org-membership/org-membership-dal"; import { orgMembershipDALFactory } from "@app/services/org-membership/org-membership-dal";
import { projectDALFactory } from "@app/services/project/project-dal"; import { projectDALFactory } from "@app/services/project/project-dal";
import { projectQueueFactory } from "@app/services/project/project-queue"; import { projectQueueFactory } from "@app/services/project/project-queue";
@ -183,6 +184,7 @@ import { webhookServiceFactory } from "@app/services/webhook/webhook-service";
import { injectAuditLogInfo } from "../plugins/audit-log"; import { injectAuditLogInfo } from "../plugins/audit-log";
import { injectIdentity } from "../plugins/auth/inject-identity"; import { injectIdentity } from "../plugins/auth/inject-identity";
import { injectPermission } from "../plugins/auth/inject-permission"; import { injectPermission } from "../plugins/auth/inject-permission";
import { injectRateLimits } from "../plugins/inject-rate-limits";
import { registerSecretScannerGhApp } from "../plugins/secret-scanner"; import { registerSecretScannerGhApp } from "../plugins/secret-scanner";
import { registerV1Routes } from "./v1"; import { registerV1Routes } from "./v1";
import { registerV2Routes } from "./v2"; import { registerV2Routes } from "./v2";
@ -498,6 +500,16 @@ export const registerRoutes = async (
keyStore, keyStore,
licenseService licenseService
}); });
const orgAdminService = orgAdminServiceFactory({
projectDAL,
permissionService,
projectUserMembershipRoleDAL,
userDAL,
projectBotDAL,
projectKeyDAL,
projectMembershipDAL
});
const rateLimitService = rateLimitServiceFactory({ const rateLimitService = rateLimitServiceFactory({
rateLimitDAL, rateLimitDAL,
licenseService licenseService
@ -635,7 +647,8 @@ export const registerRoutes = async (
projectUserMembershipRoleDAL, projectUserMembershipRoleDAL,
identityProjectMembershipRoleDAL, identityProjectMembershipRoleDAL,
keyStore, keyStore,
kmsService kmsService,
projectBotDAL
}); });
const projectEnvService = projectEnvServiceFactory({ const projectEnvService = projectEnvServiceFactory({
@ -677,8 +690,7 @@ export const registerRoutes = async (
permissionService, permissionService,
webhookDAL, webhookDAL,
projectEnvDAL, projectEnvDAL,
projectDAL, projectDAL
kmsService
}); });
const secretTagService = secretTagServiceFactory({ secretTagDAL, permissionService }); const secretTagService = secretTagServiceFactory({ secretTagDAL, permissionService });
@ -885,8 +897,15 @@ export const registerRoutes = async (
folderDAL, folderDAL,
integrationDAL, integrationDAL,
integrationAuthDAL, integrationAuthDAL,
secretQueueService secretQueueService,
integrationAuthService,
projectBotService,
secretV2BridgeDAL,
secretImportDAL,
secretDAL,
kmsService
}); });
const serviceTokenService = serviceTokenServiceFactory({ const serviceTokenService = serviceTokenServiceFactory({
projectEnvDAL, projectEnvDAL,
serviceTokenDAL, serviceTokenDAL,
@ -988,9 +1007,7 @@ export const registerRoutes = async (
queueService, queueService,
dynamicSecretLeaseDAL, dynamicSecretLeaseDAL,
dynamicSecretProviders, dynamicSecretProviders,
dynamicSecretDAL, dynamicSecretDAL
kmsService,
folderDAL
}); });
const dynamicSecretService = dynamicSecretServiceFactory({ const dynamicSecretService = dynamicSecretServiceFactory({
projectDAL, projectDAL,
@ -1000,8 +1017,7 @@ export const registerRoutes = async (
dynamicSecretProviders, dynamicSecretProviders,
folderDAL, folderDAL,
permissionService, permissionService,
licenseService, licenseService
kmsService
}); });
const dynamicSecretLeaseService = dynamicSecretLeaseServiceFactory({ const dynamicSecretLeaseService = dynamicSecretLeaseServiceFactory({
projectDAL, projectDAL,
@ -1011,8 +1027,7 @@ export const registerRoutes = async (
dynamicSecretLeaseDAL, dynamicSecretLeaseDAL,
dynamicSecretProviders, dynamicSecretProviders,
folderDAL, folderDAL,
licenseService, licenseService
kmsService
}); });
const dailyResourceCleanUp = dailyResourceCleanUpQueueServiceFactory({ const dailyResourceCleanUp = dailyResourceCleanUpQueueServiceFactory({
auditLogDAL, auditLogDAL,
@ -1117,7 +1132,8 @@ export const registerRoutes = async (
identityProjectAdditionalPrivilege: identityProjectAdditionalPrivilegeService, identityProjectAdditionalPrivilege: identityProjectAdditionalPrivilegeService,
secretSharing: secretSharingService, secretSharing: secretSharingService,
userEngagement: userEngagementService, userEngagement: userEngagementService,
externalKms: externalKmsService externalKms: externalKmsService,
orgAdmin: orgAdminService
}); });
const cronJobs: CronJob[] = []; const cronJobs: CronJob[] = [];
@ -1134,6 +1150,7 @@ export const registerRoutes = async (
await server.register(injectIdentity, { userDAL, serviceTokenDAL }); await server.register(injectIdentity, { userDAL, serviceTokenDAL });
await server.register(injectPermission); await server.register(injectPermission);
await server.register(injectRateLimits);
await server.register(injectAuditLogInfo); await server.register(injectAuditLogInfo);
server.route({ server.route({

View File

@ -129,7 +129,11 @@ export const SanitizedRoleSchema = ProjectRolesSchema.extend({
}); });
export const SanitizedDynamicSecretSchema = DynamicSecretsSchema.omit({ export const SanitizedDynamicSecretSchema = DynamicSecretsSchema.omit({
encryptedConfig: true inputIV: true,
inputTag: true,
inputCiphertext: true,
keyEncoding: true,
algorithm: true
}); });
export const SanitizedAuditLogStreamSchema = z.object({ export const SanitizedAuditLogStreamSchema = z.object({

View File

@ -337,7 +337,7 @@ export const registerCaRouter = async (server: FastifyZodProvider) => {
caId: z.string().trim().describe(CERTIFICATE_AUTHORITIES.SIGN_INTERMEDIATE.caId) caId: z.string().trim().describe(CERTIFICATE_AUTHORITIES.SIGN_INTERMEDIATE.caId)
}), }),
body: z.object({ body: z.object({
csr: z.string().trim().describe(CERTIFICATE_AUTHORITIES.SIGN_INTERMEDIATE.csr), csr: z.string().trim().min(1).describe(CERTIFICATE_AUTHORITIES.SIGN_INTERMEDIATE.csr),
notBefore: validateCaDateField.optional().describe(CERTIFICATE_AUTHORITIES.SIGN_INTERMEDIATE.notBefore), notBefore: validateCaDateField.optional().describe(CERTIFICATE_AUTHORITIES.SIGN_INTERMEDIATE.notBefore),
notAfter: validateCaDateField.describe(CERTIFICATE_AUTHORITIES.SIGN_INTERMEDIATE.notAfter), notAfter: validateCaDateField.describe(CERTIFICATE_AUTHORITIES.SIGN_INTERMEDIATE.notAfter),
maxPathLength: z.number().min(-1).default(-1).describe(CERTIFICATE_AUTHORITIES.SIGN_INTERMEDIATE.maxPathLength) maxPathLength: z.number().min(-1).default(-1).describe(CERTIFICATE_AUTHORITIES.SIGN_INTERMEDIATE.maxPathLength)
@ -453,7 +453,7 @@ export const registerCaRouter = async (server: FastifyZodProvider) => {
}), }),
body: z body: z
.object({ .object({
friendlyName: z.string().optional().describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.friendlyName), friendlyName: z.string().trim().optional().describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.friendlyName),
commonName: z.string().trim().min(1).describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.commonName), commonName: z.string().trim().min(1).describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.commonName),
altNames: validateAltNamesField.describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.altNames), altNames: validateAltNamesField.describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.altNames),
ttl: z ttl: z
@ -516,4 +516,81 @@ export const registerCaRouter = async (server: FastifyZodProvider) => {
}; };
} }
}); });
server.route({
method: "POST",
url: "/:caId/sign-certificate",
config: {
rateLimit: writeLimit
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: {
description: "Sign certificate from CA",
params: z.object({
caId: z.string().trim().describe(CERTIFICATE_AUTHORITIES.SIGN_CERT.caId)
}),
body: z
.object({
csr: z.string().trim().min(1).describe(CERTIFICATE_AUTHORITIES.SIGN_CERT.csr),
friendlyName: z.string().trim().optional().describe(CERTIFICATE_AUTHORITIES.SIGN_CERT.friendlyName),
commonName: z.string().trim().min(1).optional().describe(CERTIFICATE_AUTHORITIES.SIGN_CERT.commonName),
altNames: validateAltNamesField.describe(CERTIFICATE_AUTHORITIES.SIGN_CERT.altNames),
ttl: z
.string()
.refine((val) => ms(val) > 0, "TTL must be a positive number")
.describe(CERTIFICATE_AUTHORITIES.SIGN_CERT.ttl),
notBefore: validateCaDateField.optional().describe(CERTIFICATE_AUTHORITIES.SIGN_CERT.notBefore),
notAfter: validateCaDateField.optional().describe(CERTIFICATE_AUTHORITIES.SIGN_CERT.notAfter)
})
.refine(
(data) => {
const { ttl, notAfter } = data;
return (ttl !== undefined && notAfter === undefined) || (ttl === undefined && notAfter !== undefined);
},
{
message: "Either ttl or notAfter must be present, but not both",
path: ["ttl", "notAfter"]
}
),
response: {
200: z.object({
certificate: z.string().trim().describe(CERTIFICATE_AUTHORITIES.SIGN_CERT.certificate),
issuingCaCertificate: z.string().trim().describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.issuingCaCertificate),
certificateChain: z.string().trim().describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.certificateChain),
serialNumber: z.string().trim().describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.serialNumber)
})
}
},
handler: async (req) => {
const { certificate, certificateChain, issuingCaCertificate, serialNumber, ca } =
await server.services.certificateAuthority.signCertFromCa({
caId: req.params.caId,
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
...req.body
});
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
projectId: ca.projectId,
event: {
type: EventType.SIGN_CERT,
metadata: {
caId: ca.id,
dn: ca.dn,
serialNumber
}
}
});
return {
certificate,
certificateChain,
issuingCaCertificate,
serialNumber
};
}
});
}; };

View File

@ -3,7 +3,7 @@ import { z } from "zod";
import { IdentitiesSchema, IdentityOrgMembershipsSchema, OrgMembershipRole, OrgRolesSchema } from "@app/db/schemas"; import { IdentitiesSchema, IdentityOrgMembershipsSchema, OrgMembershipRole, OrgRolesSchema } from "@app/db/schemas";
import { EventType } from "@app/ee/services/audit-log/audit-log-types"; import { EventType } from "@app/ee/services/audit-log/audit-log-types";
import { IDENTITIES } from "@app/lib/api-docs"; import { IDENTITIES } from "@app/lib/api-docs";
import { creationLimit, readLimit, writeLimit } from "@app/server/config/rateLimiter"; import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { getTelemetryDistinctId } from "@app/server/lib/telemetry"; import { getTelemetryDistinctId } from "@app/server/lib/telemetry";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type"; import { AuthMode } from "@app/services/auth/auth-type";
@ -16,7 +16,7 @@ export const registerIdentityRouter = async (server: FastifyZodProvider) => {
method: "POST", method: "POST",
url: "/", url: "/",
config: { config: {
rateLimit: creationLimit rateLimit: writeLimit
}, },
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: { schema: {

View File

@ -15,6 +15,7 @@ import { registerIdentityUaRouter } from "./identity-universal-auth-router";
import { registerIntegrationAuthRouter } from "./integration-auth-router"; import { registerIntegrationAuthRouter } from "./integration-auth-router";
import { registerIntegrationRouter } from "./integration-router"; import { registerIntegrationRouter } from "./integration-router";
import { registerInviteOrgRouter } from "./invite-org-router"; import { registerInviteOrgRouter } from "./invite-org-router";
import { registerOrgAdminRouter } from "./org-admin-router";
import { registerOrgRouter } from "./organization-router"; import { registerOrgRouter } from "./organization-router";
import { registerPasswordRouter } from "./password-router"; import { registerPasswordRouter } from "./password-router";
import { registerProjectEnvRouter } from "./project-env-router"; import { registerProjectEnvRouter } from "./project-env-router";
@ -50,6 +51,7 @@ export const registerV1Routes = async (server: FastifyZodProvider) => {
await server.register(registerPasswordRouter, { prefix: "/password" }); await server.register(registerPasswordRouter, { prefix: "/password" });
await server.register(registerOrgRouter, { prefix: "/organization" }); await server.register(registerOrgRouter, { prefix: "/organization" });
await server.register(registerAdminRouter, { prefix: "/admin" }); await server.register(registerAdminRouter, { prefix: "/admin" });
await server.register(registerOrgAdminRouter, { prefix: "/organization-admin" });
await server.register(registerUserRouter, { prefix: "/user" }); await server.register(registerUserRouter, { prefix: "/user" });
await server.register(registerInviteOrgRouter, { prefix: "/invite-org" }); await server.register(registerInviteOrgRouter, { prefix: "/invite-org" });
await server.register(registerUserActionRouter, { prefix: "/user-action" }); await server.register(registerUserActionRouter, { prefix: "/user-action" });

View File

@ -170,6 +170,12 @@ export const registerIntegrationRouter = async (server: FastifyZodProvider) => {
params: z.object({ params: z.object({
integrationId: z.string().trim().describe(INTEGRATION.DELETE.integrationId) integrationId: z.string().trim().describe(INTEGRATION.DELETE.integrationId)
}), }),
querystring: z.object({
shouldDeleteIntegrationSecrets: z
.enum(["true", "false"])
.optional()
.transform((val) => val === "true")
}),
response: { response: {
200: z.object({ 200: z.object({
integration: IntegrationsSchema integration: IntegrationsSchema
@ -183,7 +189,8 @@ export const registerIntegrationRouter = async (server: FastifyZodProvider) => {
actorAuthMethod: req.permission.authMethod, actorAuthMethod: req.permission.authMethod,
actor: req.permission.type, actor: req.permission.type,
actorOrgId: req.permission.orgId, actorOrgId: req.permission.orgId,
id: req.params.integrationId id: req.params.integrationId,
shouldDeleteIntegrationSecrets: req.query.shouldDeleteIntegrationSecrets
}); });
await server.services.auditLog.createAuditLog({ await server.services.auditLog.createAuditLog({
@ -205,7 +212,8 @@ export const registerIntegrationRouter = async (server: FastifyZodProvider) => {
targetService: integration.targetService, targetService: integration.targetService,
targetServiceId: integration.targetServiceId, targetServiceId: integration.targetServiceId,
path: integration.path, path: integration.path,
region: integration.region region: integration.region,
shouldDeleteIntegrationSecrets: req.query.shouldDeleteIntegrationSecrets
// eslint-disable-next-line // eslint-disable-next-line
}) as any }) as any
} }

View File

@ -0,0 +1,90 @@
import { z } from "zod";
import { ProjectMembershipsSchema } from "@app/db/schemas";
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
import { readLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type";
import { SanitizedProjectSchema } from "../sanitizedSchemas";
export const registerOrgAdminRouter = async (server: FastifyZodProvider) => {
server.route({
method: "GET",
url: "/projects",
config: {
rateLimit: readLimit
},
schema: {
querystring: z.object({
search: z.string().optional(),
offset: z.coerce.number().default(0),
limit: z.coerce.number().max(100).default(50)
}),
response: {
200: z.object({
projects: SanitizedProjectSchema.array(),
count: z.coerce.number()
})
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const { projects, count } = await server.services.orgAdmin.listOrgProjects({
limit: req.query.limit,
offset: req.query.offset,
search: req.query.search,
actorOrgId: req.permission.orgId,
actorAuthMethod: req.permission.authMethod,
actorId: req.permission.id,
actor: req.permission.type
});
return { projects, count };
}
});
server.route({
method: "POST",
url: "/projects/:projectId/grant-admin-access",
config: {
rateLimit: readLimit
},
schema: {
params: z.object({
projectId: z.string()
}),
response: {
200: z.object({
membership: ProjectMembershipsSchema
})
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
const { membership } = await server.services.orgAdmin.grantProjectAdminAccess({
actorOrgId: req.permission.orgId,
actorAuthMethod: req.permission.authMethod,
actorId: req.permission.id,
actor: req.permission.type,
projectId: req.params.projectId
});
if (req.auth.authMode === AuthMode.JWT) {
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
projectId: req.params.projectId,
event: {
type: EventType.ORG_ADMIN_ACCESS_PROJECT,
metadata: {
projectId: req.params.projectId,
username: req.auth.user.username,
email: req.auth.user.email || "",
userId: req.auth.userId
}
}
});
}
return { membership };
}
});
};

View File

@ -9,7 +9,7 @@ import {
UsersSchema UsersSchema
} from "@app/db/schemas"; } from "@app/db/schemas";
import { ORGANIZATIONS } from "@app/lib/api-docs"; import { ORGANIZATIONS } from "@app/lib/api-docs";
import { creationLimit, readLimit, writeLimit } from "@app/server/config/rateLimiter"; import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { ActorType, AuthMode } from "@app/services/auth/auth-type"; import { ActorType, AuthMode } from "@app/services/auth/auth-type";
@ -307,7 +307,7 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => {
method: "POST", method: "POST",
url: "/", url: "/",
config: { config: {
rateLimit: creationLimit rateLimit: writeLimit
}, },
schema: { schema: {
body: z.object({ body: z.object({

View File

@ -4,7 +4,7 @@ import { z } from "zod";
import { CertificateAuthoritiesSchema, CertificatesSchema, ProjectKeysSchema } from "@app/db/schemas"; import { CertificateAuthoritiesSchema, CertificatesSchema, ProjectKeysSchema } from "@app/db/schemas";
import { EventType } from "@app/ee/services/audit-log/audit-log-types"; import { EventType } from "@app/ee/services/audit-log/audit-log-types";
import { PROJECTS } from "@app/lib/api-docs"; import { PROJECTS } from "@app/lib/api-docs";
import { creationLimit, readLimit, writeLimit } from "@app/server/config/rateLimiter"; import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { getTelemetryDistinctId } from "@app/server/lib/telemetry"; import { getTelemetryDistinctId } from "@app/server/lib/telemetry";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type"; import { AuthMode } from "@app/services/auth/auth-type";
@ -142,7 +142,7 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
method: "POST", method: "POST",
url: "/", url: "/",
config: { config: {
rateLimit: creationLimit rateLimit: writeLimit
}, },
schema: { schema: {
description: "Create a new project", description: "Create a new project",

View File

@ -18,6 +18,40 @@ export const createDistinguishedName = (parts: TDNParts) => {
return dnParts.join(", "); return dnParts.join(", ");
}; };
export const parseDistinguishedName = (dn: string): TDNParts => {
const parts: TDNParts = {};
const dnParts = dn.split(/,\s*/);
for (const part of dnParts) {
const [key, value] = part.split("=");
switch (key.toUpperCase()) {
case "C":
parts.country = value;
break;
case "O":
parts.organization = value;
break;
case "OU":
parts.ou = value;
break;
case "ST":
parts.province = value;
break;
case "CN":
parts.commonName = value;
break;
case "L":
parts.locality = value;
break;
default:
// Ignore unrecognized keys
break;
}
}
return parts;
};
export const keyAlgorithmToAlgCfg = (keyAlgorithm: CertKeyAlgorithm) => { export const keyAlgorithmToAlgCfg = (keyAlgorithm: CertKeyAlgorithm) => {
switch (keyAlgorithm) { switch (keyAlgorithm) {
case CertKeyAlgorithm.RSA_4096: case CertKeyAlgorithm.RSA_4096:

View File

@ -22,7 +22,8 @@ import {
createDistinguishedName, createDistinguishedName,
getCaCertChain, getCaCertChain,
getCaCredentials, getCaCredentials,
keyAlgorithmToAlgCfg keyAlgorithmToAlgCfg,
parseDistinguishedName
} from "./certificate-authority-fns"; } from "./certificate-authority-fns";
import { TCertificateAuthorityQueueFactory } from "./certificate-authority-queue"; import { TCertificateAuthorityQueueFactory } from "./certificate-authority-queue";
import { TCertificateAuthoritySecretDALFactory } from "./certificate-authority-secret-dal"; import { TCertificateAuthoritySecretDALFactory } from "./certificate-authority-secret-dal";
@ -36,6 +37,7 @@ import {
TGetCaDTO, TGetCaDTO,
TImportCertToCaDTO, TImportCertToCaDTO,
TIssueCertFromCaDTO, TIssueCertFromCaDTO,
TSignCertFromCaDTO,
TSignIntermediateDTO, TSignIntermediateDTO,
TUpdateCaDTO TUpdateCaDTO
} from "./certificate-authority-types"; } from "./certificate-authority-types";
@ -651,7 +653,8 @@ export const certificateAuthorityServiceFactory = ({
}; };
/** /**
* Return new leaf certificate issued by CA with id [caId] * Return new leaf certificate issued by CA with id [caId] and private key.
* Note: private key and CSR are generated within Infisical.
*/ */
const issueCertFromCa = async ({ const issueCertFromCa = async ({
caId, caId,
@ -851,6 +854,204 @@ export const certificateAuthorityServiceFactory = ({
}; };
}; };
/**
* Return new leaf certificate issued by CA with id [caId].
* Note: CSR is generated externally and submitted to Infisical.
*/
const signCertFromCa = async ({
caId,
csr,
friendlyName,
commonName,
altNames,
ttl,
notBefore,
notAfter,
actorId,
actorAuthMethod,
actor,
actorOrgId
}: TSignCertFromCaDTO) => {
const ca = await certificateAuthorityDAL.findById(caId);
if (!ca) throw new BadRequestError({ message: "CA not found" });
const { permission } = await permissionService.getProjectPermission(
actor,
actorId,
ca.projectId,
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Create, ProjectPermissionSub.Certificates);
if (ca.status === CaStatus.DISABLED) throw new BadRequestError({ message: "CA is disabled" });
const caCert = await certificateAuthorityCertDAL.findOne({ caId: ca.id });
if (!caCert) throw new BadRequestError({ message: "CA does not have a certificate installed" });
const certificateManagerKmsId = await getProjectKmsCertificateKeyId({
projectId: ca.projectId,
projectDAL,
kmsService
});
const kmsDecryptor = await kmsService.decryptWithKmsKey({
kmsId: certificateManagerKmsId
});
const decryptedCaCert = await kmsDecryptor({
cipherTextBlob: caCert.encryptedCertificate
});
const caCertObj = new x509.X509Certificate(decryptedCaCert);
const notBeforeDate = notBefore ? new Date(notBefore) : new Date();
let notAfterDate = new Date(new Date().setFullYear(new Date().getFullYear() + 1));
if (notAfter) {
notAfterDate = new Date(notAfter);
} else if (ttl) {
notAfterDate = new Date(new Date().getTime() + ms(ttl));
}
const caCertNotBeforeDate = new Date(caCertObj.notBefore);
const caCertNotAfterDate = new Date(caCertObj.notAfter);
// check not before constraint
if (notBeforeDate < caCertNotBeforeDate) {
throw new BadRequestError({ message: "notBefore date is before CA certificate's notBefore date" });
}
if (notBeforeDate > notAfterDate) throw new BadRequestError({ message: "notBefore date is after notAfter date" });
// check not after constraint
if (notAfterDate > caCertNotAfterDate) {
throw new BadRequestError({ message: "notAfter date is after CA certificate's notAfter date" });
}
const alg = keyAlgorithmToAlgCfg(ca.keyAlgorithm as CertKeyAlgorithm);
const csrObj = new x509.Pkcs10CertificateRequest(csr);
const dn = parseDistinguishedName(csrObj.subject);
const cn = commonName || dn.commonName;
if (!cn)
throw new BadRequestError({
message: "A common name (CN) is required in the CSR or as a parameter to this endpoint"
});
const { caPrivateKey } = await getCaCredentials({
caId: ca.id,
certificateAuthorityDAL,
certificateAuthoritySecretDAL,
projectDAL,
kmsService
});
const extensions: x509.Extension[] = [
new x509.KeyUsagesExtension(x509.KeyUsageFlags.digitalSignature | x509.KeyUsageFlags.keyEncipherment, true),
new x509.BasicConstraintsExtension(false),
await x509.AuthorityKeyIdentifierExtension.create(caCertObj, false),
await x509.SubjectKeyIdentifierExtension.create(csrObj.publicKey)
];
if (altNames) {
const altNamesArray: {
type: "email" | "dns";
value: string;
}[] = 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
};
}
// check if the altName is a valid hostname
if (hostnameRegex.test(altName)) {
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}`);
});
const altNamesExtension = new x509.SubjectAlternativeNameExtension(altNamesArray, false);
extensions.push(altNamesExtension);
}
const serialNumber = crypto.randomBytes(32).toString("hex");
const leafCert = await x509.X509CertificateGenerator.create({
serialNumber,
subject: csrObj.subject,
issuer: caCertObj.subject,
notBefore: notBeforeDate,
notAfter: notAfterDate,
signingKey: caPrivateKey,
publicKey: csrObj.publicKey,
signingAlgorithm: alg,
extensions
});
const kmsEncryptor = await kmsService.encryptWithKmsKey({
kmsId: certificateManagerKmsId
});
const { cipherTextBlob: encryptedCertificate } = await kmsEncryptor({
plainText: Buffer.from(new Uint8Array(leafCert.rawData))
});
await certificateDAL.transaction(async (tx) => {
const cert = await certificateDAL.create(
{
caId: ca.id,
status: CertStatus.ACTIVE,
friendlyName: friendlyName || csrObj.subject,
commonName: cn,
altNames,
serialNumber,
notBefore: notBeforeDate,
notAfter: notAfterDate
},
tx
);
await certificateBodyDAL.create(
{
certId: cert.id,
encryptedCertificate
},
tx
);
return cert;
});
const { caCert: issuingCaCertificate, caCertChain } = await getCaCertChain({
caId: ca.id,
certificateAuthorityDAL,
certificateAuthorityCertDAL,
projectDAL,
kmsService
});
return {
certificate: leafCert.toString("pem"),
certificateChain: `${issuingCaCertificate}\n${caCertChain}`.trim(),
issuingCaCertificate,
serialNumber,
ca
};
};
return { return {
createCa, createCa,
getCaById, getCaById,
@ -860,6 +1061,7 @@ export const certificateAuthorityServiceFactory = ({
getCaCert, getCaCert,
signIntermediate, signIntermediate,
importCertToCa, importCertToCa,
issueCertFromCa issueCertFromCa,
signCertFromCa
}; };
}; };

View File

@ -81,6 +81,17 @@ export type TIssueCertFromCaDTO = {
notAfter?: string; notAfter?: string;
} & Omit<TProjectPermission, "projectId">; } & Omit<TProjectPermission, "projectId">;
export type TSignCertFromCaDTO = {
caId: string;
csr: string;
friendlyName?: string;
commonName?: string;
altNames: string;
ttl: string;
notBefore?: string;
notAfter?: string;
} & Omit<TProjectPermission, "projectId">;
export type TDNParts = { export type TDNParts = {
commonName?: string; commonName?: string;
organization?: string; organization?: string;

View File

@ -0,0 +1,357 @@
import { retry } from "@octokit/plugin-retry";
import { Octokit } from "@octokit/rest";
import { TIntegrationAuths, TIntegrations } from "@app/db/schemas";
import { decryptSymmetric128BitHexKeyUTF8 } from "@app/lib/crypto";
import { BadRequestError, NotFoundError } from "@app/lib/errors";
import { logger } from "@app/lib/logger";
import { IntegrationMetadataSchema } from "../integration/integration-schema";
import { TKmsServiceFactory } from "../kms/kms-service";
import { KmsDataKey } from "../kms/kms-types";
import { TProjectBotServiceFactory } from "../project-bot/project-bot-service";
import { TSecretDALFactory } from "../secret/secret-dal";
import { TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal";
import { TSecretImportDALFactory } from "../secret-import/secret-import-dal";
import { fnSecretsV2FromImports } from "../secret-import/secret-import-fns";
import { TSecretV2BridgeDALFactory } from "../secret-v2-bridge/secret-v2-bridge-dal";
import { TIntegrationAuthServiceFactory } from "./integration-auth-service";
import { Integrations } from "./integration-list";
const MAX_SYNC_SECRET_DEPTH = 5;
/**
* Return the secrets in a given [folderId] including secrets from
* nested imported folders recursively.
*/
const getIntegrationSecretsV2 = async (
dto: {
projectId: string;
environment: string;
folderId: string;
depth: number;
decryptor: (value: Buffer | null | undefined) => string;
},
secretV2BridgeDAL: Pick<TSecretV2BridgeDALFactory, "find" | "findByFolderId">,
folderDAL: Pick<TSecretFolderDALFactory, "findByManySecretPath">,
secretImportDAL: Pick<TSecretImportDALFactory, "find" | "findByFolderIds">
) => {
const content: Record<string, boolean> = {};
if (dto.depth > MAX_SYNC_SECRET_DEPTH) {
logger.info(
`getIntegrationSecrets: secret depth exceeded for [projectId=${dto.projectId}] [folderId=${dto.folderId}] [depth=${dto.depth}]`
);
return content;
}
// process secrets in current folder
const secrets = await secretV2BridgeDAL.findByFolderId(dto.folderId);
secrets.forEach((secret) => {
const secretKey = secret.key;
content[secretKey] = true;
});
// check if current folder has any imports from other folders
const secretImports = await secretImportDAL.find({ folderId: dto.folderId, isReplication: false });
// if no imports then return secrets in the current folder
if (!secretImports.length) return content;
const importedSecrets = await fnSecretsV2FromImports({
decryptor: dto.decryptor,
folderDAL,
secretDAL: secretV2BridgeDAL,
secretImportDAL,
allowedImports: secretImports
});
for (let i = importedSecrets.length - 1; i >= 0; i -= 1) {
for (let j = 0; j < importedSecrets[i].secrets.length; j += 1) {
const importedSecret = importedSecrets[i].secrets[j];
if (!content[importedSecret.key]) {
content[importedSecret.key] = true;
}
}
}
return content;
};
/**
* Return the secrets in a given [folderId] including secrets from
* nested imported folders recursively.
*/
const getIntegrationSecretsV1 = async (
dto: {
projectId: string;
environment: string;
folderId: string;
key: string;
depth: number;
},
secretDAL: Pick<TSecretDALFactory, "findByFolderId">,
folderDAL: Pick<TSecretFolderDALFactory, "findByManySecretPath">,
secretImportDAL: Pick<TSecretImportDALFactory, "find" | "findByFolderIds">
) => {
let content: Record<string, boolean> = {};
if (dto.depth > MAX_SYNC_SECRET_DEPTH) {
logger.info(
`getIntegrationSecrets: secret depth exceeded for [projectId=${dto.projectId}] [folderId=${dto.folderId}] [depth=${dto.depth}]`
);
return content;
}
// process secrets in current folder
const secrets = await secretDAL.findByFolderId(dto.folderId);
secrets.forEach((secret) => {
const secretKey = decryptSymmetric128BitHexKeyUTF8({
ciphertext: secret.secretKeyCiphertext,
iv: secret.secretKeyIV,
tag: secret.secretKeyTag,
key: dto.key
});
content[secretKey] = true;
});
// check if current folder has any imports from other folders
const secretImport = await secretImportDAL.find({ folderId: dto.folderId, isReplication: false });
// if no imports then return secrets in the current folder
if (!secretImport) return content;
const importedFolders = await folderDAL.findByManySecretPath(
secretImport.map(({ importEnv, importPath }) => ({
envId: importEnv.id,
secretPath: importPath
}))
);
for await (const folder of importedFolders) {
if (folder) {
// get secrets contained in each imported folder by recursively calling
// this function against the imported folder
const importedSecrets = await getIntegrationSecretsV1(
{
environment: dto.environment,
projectId: dto.projectId,
folderId: folder.id,
key: dto.key,
depth: dto.depth + 1
},
secretDAL,
folderDAL,
secretImportDAL
);
// add the imported secrets to the current folder secrets
content = { ...importedSecrets, ...content };
}
}
return content;
};
export const deleteGithubSecrets = async ({
integration,
secrets,
accessToken
}: {
integration: Omit<TIntegrations, "envId">;
secrets: Record<string, boolean>;
accessToken: string;
}) => {
interface GitHubSecret {
name: string;
created_at: string;
updated_at: string;
visibility?: "all" | "private" | "selected";
selected_repositories_url?: string | undefined;
}
const OctokitWithRetry = Octokit.plugin(retry);
const octokit = new OctokitWithRetry({
auth: accessToken
});
enum GithubScope {
Repo = "github-repo",
Org = "github-org",
Env = "github-env"
}
let encryptedGithubSecrets: GitHubSecret[];
switch (integration.scope) {
case GithubScope.Org: {
encryptedGithubSecrets = (
await octokit.request("GET /orgs/{org}/actions/secrets", {
org: integration.owner as string
})
).data.secrets;
break;
}
case GithubScope.Env: {
encryptedGithubSecrets = (
await octokit.request("GET /repositories/{repository_id}/environments/{environment_name}/secrets", {
repository_id: Number(integration.appId),
environment_name: integration.targetEnvironmentId as string
})
).data.secrets;
break;
}
default: {
encryptedGithubSecrets = (
await octokit.request("GET /repos/{owner}/{repo}/actions/secrets", {
owner: integration.owner as string,
repo: integration.app as string
})
).data.secrets;
break;
}
}
for await (const encryptedSecret of encryptedGithubSecrets) {
if (encryptedSecret.name in secrets) {
switch (integration.scope) {
case GithubScope.Org: {
await octokit.request("DELETE /orgs/{org}/actions/secrets/{secret_name}", {
org: integration.owner as string,
secret_name: encryptedSecret.name
});
break;
}
case GithubScope.Env: {
await octokit.request(
"DELETE /repositories/{repository_id}/environments/{environment_name}/secrets/{secret_name}",
{
repository_id: Number(integration.appId),
environment_name: integration.targetEnvironmentId as string,
secret_name: encryptedSecret.name
}
);
break;
}
default: {
await octokit.request("DELETE /repos/{owner}/{repo}/actions/secrets/{secret_name}", {
owner: integration.owner as string,
repo: integration.app as string,
secret_name: encryptedSecret.name
});
break;
}
}
// small delay to prevent hitting API rate limits
await new Promise((resolve) => {
setTimeout(resolve, 50);
});
}
}
};
export const deleteIntegrationSecrets = async ({
integration,
integrationAuth,
integrationAuthService,
projectBotService,
secretV2BridgeDAL,
folderDAL,
secretDAL,
secretImportDAL,
kmsService
}: {
integration: Omit<TIntegrations, "envId"> & {
projectId: string;
environment: {
id: string;
name: string;
slug: string;
};
secretPath: string;
};
integrationAuth: TIntegrationAuths;
integrationAuthService: Pick<TIntegrationAuthServiceFactory, "getIntegrationAccessToken" | "getIntegrationAuth">;
projectBotService: Pick<TProjectBotServiceFactory, "getBotKey">;
secretV2BridgeDAL: Pick<TSecretV2BridgeDALFactory, "find" | "findByFolderId">;
folderDAL: Pick<TSecretFolderDALFactory, "findByManySecretPath" | "findBySecretPath">;
secretImportDAL: Pick<TSecretImportDALFactory, "find" | "findByFolderIds">;
secretDAL: Pick<TSecretDALFactory, "findByFolderId">;
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
}) => {
const { shouldUseSecretV2Bridge, botKey } = await projectBotService.getBotKey(integration.projectId);
const { decryptor: secretManagerDecryptor } = await kmsService.createCipherPairWithDataKey({
type: KmsDataKey.SecretManager,
projectId: integration.projectId
});
const folder = await folderDAL.findBySecretPath(
integration.projectId,
integration.environment.slug,
integration.secretPath
);
if (!folder) {
throw new NotFoundError({
message: "Folder not found."
});
}
const { accessToken } = await integrationAuthService.getIntegrationAccessToken(
integrationAuth,
shouldUseSecretV2Bridge,
botKey
);
const secrets = shouldUseSecretV2Bridge
? await getIntegrationSecretsV2(
{
environment: integration.environment.id,
projectId: integration.projectId,
folderId: folder.id,
depth: 1,
decryptor: (value) => (value ? secretManagerDecryptor({ cipherTextBlob: value }).toString() : "")
},
secretV2BridgeDAL,
folderDAL,
secretImportDAL
)
: await getIntegrationSecretsV1(
{
environment: integration.environment.id,
projectId: integration.projectId,
folderId: folder.id,
key: botKey as string,
depth: 1
},
secretDAL,
folderDAL,
secretImportDAL
);
const suffixedSecrets: typeof secrets = {};
const metadata = IntegrationMetadataSchema.parse(integration.metadata);
if (metadata) {
Object.keys(secrets).forEach((key) => {
const prefix = metadata?.secretPrefix || "";
const suffix = metadata?.secretSuffix || "";
const newKey = prefix + key + suffix;
suffixedSecrets[newKey] = secrets[key];
});
}
switch (integration.integration) {
case Integrations.GITHUB: {
await deleteGithubSecrets({
integration,
accessToken,
secrets: Object.keys(suffixedSecrets).length !== 0 ? suffixedSecrets : secrets
});
break;
}
default:
throw new BadRequestError({
message: "Invalid integration"
});
}
};

View File

@ -6,8 +6,15 @@ import { BadRequestError } from "@app/lib/errors";
import { TProjectPermission } from "@app/lib/types"; import { TProjectPermission } from "@app/lib/types";
import { TIntegrationAuthDALFactory } from "../integration-auth/integration-auth-dal"; import { TIntegrationAuthDALFactory } from "../integration-auth/integration-auth-dal";
import { TIntegrationAuthServiceFactory } from "../integration-auth/integration-auth-service";
import { deleteIntegrationSecrets } from "../integration-auth/integration-delete-secret";
import { TKmsServiceFactory } from "../kms/kms-service";
import { TProjectBotServiceFactory } from "../project-bot/project-bot-service";
import { TSecretDALFactory } from "../secret/secret-dal";
import { TSecretQueueFactory } from "../secret/secret-queue"; import { TSecretQueueFactory } from "../secret/secret-queue";
import { TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal"; import { TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal";
import { TSecretImportDALFactory } from "../secret-import/secret-import-dal";
import { TSecretV2BridgeDALFactory } from "../secret-v2-bridge/secret-v2-bridge-dal";
import { TIntegrationDALFactory } from "./integration-dal"; import { TIntegrationDALFactory } from "./integration-dal";
import { import {
TCreateIntegrationDTO, TCreateIntegrationDTO,
@ -19,9 +26,15 @@ import {
type TIntegrationServiceFactoryDep = { type TIntegrationServiceFactoryDep = {
integrationDAL: TIntegrationDALFactory; integrationDAL: TIntegrationDALFactory;
integrationAuthDAL: TIntegrationAuthDALFactory; integrationAuthDAL: TIntegrationAuthDALFactory;
folderDAL: Pick<TSecretFolderDALFactory, "findBySecretPath">; integrationAuthService: TIntegrationAuthServiceFactory;
folderDAL: Pick<TSecretFolderDALFactory, "findBySecretPath" | "findByManySecretPath">;
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">; permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
projectBotService: TProjectBotServiceFactory;
secretQueueService: Pick<TSecretQueueFactory, "syncIntegrations">; secretQueueService: Pick<TSecretQueueFactory, "syncIntegrations">;
secretV2BridgeDAL: Pick<TSecretV2BridgeDALFactory, "find" | "findByFolderId">;
secretImportDAL: Pick<TSecretImportDALFactory, "find" | "findByFolderIds">;
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
secretDAL: Pick<TSecretDALFactory, "findByFolderId">;
}; };
export type TIntegrationServiceFactory = ReturnType<typeof integrationServiceFactory>; export type TIntegrationServiceFactory = ReturnType<typeof integrationServiceFactory>;
@ -31,7 +44,13 @@ export const integrationServiceFactory = ({
integrationAuthDAL, integrationAuthDAL,
folderDAL, folderDAL,
permissionService, permissionService,
secretQueueService secretQueueService,
integrationAuthService,
projectBotService,
secretV2BridgeDAL,
secretImportDAL,
kmsService,
secretDAL
}: TIntegrationServiceFactoryDep) => { }: TIntegrationServiceFactoryDep) => {
const createIntegration = async ({ const createIntegration = async ({
app, app,
@ -161,7 +180,14 @@ export const integrationServiceFactory = ({
return updatedIntegration; return updatedIntegration;
}; };
const deleteIntegration = async ({ actorId, id, actor, actorAuthMethod, actorOrgId }: TDeleteIntegrationDTO) => { const deleteIntegration = async ({
actorId,
id,
actor,
actorAuthMethod,
actorOrgId,
shouldDeleteIntegrationSecrets
}: TDeleteIntegrationDTO) => {
const integration = await integrationDAL.findById(id); const integration = await integrationDAL.findById(id);
if (!integration) throw new BadRequestError({ message: "Integration auth not found" }); if (!integration) throw new BadRequestError({ message: "Integration auth not found" });
@ -174,6 +200,22 @@ export const integrationServiceFactory = ({
); );
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Delete, ProjectPermissionSub.Integrations); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Delete, ProjectPermissionSub.Integrations);
const integrationAuth = await integrationAuthDAL.findById(integration.integrationAuthId);
if (shouldDeleteIntegrationSecrets) {
await deleteIntegrationSecrets({
integration,
integrationAuth,
projectBotService,
integrationAuthService,
secretV2BridgeDAL,
folderDAL,
secretImportDAL,
secretDAL,
kmsService
});
}
const deletedIntegration = await integrationDAL.transaction(async (tx) => { const deletedIntegration = await integrationDAL.transaction(async (tx) => {
// delete integration // delete integration
const deletedIntegrationResult = await integrationDAL.deleteById(id, tx); const deletedIntegrationResult = await integrationDAL.deleteById(id, tx);

View File

@ -63,6 +63,7 @@ export type TUpdateIntegrationDTO = {
export type TDeleteIntegrationDTO = { export type TDeleteIntegrationDTO = {
id: string; id: string;
shouldDeleteIntegrationSecrets?: boolean;
} & Omit<TProjectPermission, "projectId">; } & Omit<TProjectPermission, "projectId">;
export type TSyncIntegrationDTO = { export type TSyncIntegrationDTO = {

View File

@ -0,0 +1,5 @@
export type TOrgAdminDALFactory = ReturnType<typeof orgAdminDALFactory>;
export const orgAdminDALFactory = () => {
return {};
};

View File

@ -0,0 +1,191 @@
import { ForbiddenError } from "@casl/ability";
import { ProjectMembershipRole, ProjectVersion, SecretKeyEncoding } from "@app/db/schemas";
import { OrgPermissionAdminConsoleAction, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { infisicalSymmetricDecrypt } from "@app/lib/crypto/encryption";
import { BadRequestError } 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 { TUserDALFactory } from "../user/user-dal";
import { TAccessProjectDTO, TListOrgProjectsDTO } from "./org-admin-types";
type TOrgAdminServiceFactoryDep = {
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">;
projectDAL: Pick<TProjectDALFactory, "find" | "findById" | "findProjectGhostUser">;
projectMembershipDAL: Pick<TProjectMembershipDALFactory, "findOne" | "create" | "transaction" | "delete">;
projectKeyDAL: Pick<TProjectKeyDALFactory, "findLatestProjectKey" | "create">;
projectBotDAL: Pick<TProjectBotDALFactory, "findOne">;
userDAL: Pick<TUserDALFactory, "findUserEncKeyByUserId">;
projectUserMembershipRoleDAL: Pick<TProjectUserMembershipRoleDALFactory, "create" | "delete">;
};
export type TOrgAdminServiceFactory = ReturnType<typeof orgAdminServiceFactory>;
export const orgAdminServiceFactory = ({
permissionService,
projectDAL,
projectMembershipDAL,
projectKeyDAL,
projectBotDAL,
userDAL,
projectUserMembershipRoleDAL
}: TOrgAdminServiceFactoryDep) => {
const listOrgProjects = async ({
actor,
limit,
actorId,
offset,
search,
actorOrgId,
actorAuthMethod
}: TListOrgProjectsDTO) => {
const { permission } = await permissionService.getOrgPermission(
actor,
actorId,
actorOrgId,
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionAdminConsoleAction.AccessAllProjects,
OrgPermissionSubjects.AdminConsole
);
const projects = await projectDAL.find(
{
orgId: actorOrgId,
$search: {
name: search ? `%${search}%` : undefined
}
},
{ offset, limit, sort: [["name", "asc"]], count: true }
);
const count = projects?.[0]?.count ? parseInt(projects?.[0]?.count, 10) : 0;
return { projects, count };
};
const grantProjectAdminAccess = async ({
actor,
actorId,
actorOrgId,
actorAuthMethod,
projectId
}: TAccessProjectDTO) => {
const { permission, membership } = await permissionService.getOrgPermission(
actor,
actorId,
actorOrgId,
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionAdminConsoleAction.AccessAllProjects,
OrgPermissionSubjects.AdminConsole
);
const project = await projectDAL.findById(projectId);
if (!project) throw new BadRequestError({ message: "Project not found" });
if (project.version === ProjectVersion.V1) {
throw new BadRequestError({ message: "Please upgrade your project on your dashboard" });
}
// check already there exist a membership if there return it
const projectMembership = await projectMembershipDAL.findOne({
projectId,
userId: actorId
});
if (projectMembership) {
// reset and make the user admin
await projectMembershipDAL.transaction(async (tx) => {
await projectUserMembershipRoleDAL.delete({ projectMembershipId: projectMembership.id }, tx);
await projectUserMembershipRoleDAL.create(
{
projectMembershipId: projectMembership.id,
role: ProjectMembershipRole.Admin
},
tx
);
});
return { isExistingMember: true, membership: projectMembership };
}
// missing membership thus add admin back as admin to project
const ghostUser = await projectDAL.findProjectGhostUser(projectId);
if (!ghostUser) {
throw new BadRequestError({
message: "Failed to find sudo user"
});
}
const ghostUserLatestKey = await projectKeyDAL.findLatestProjectKey(ghostUser.id, projectId);
if (!ghostUserLatestKey) {
throw new BadRequestError({
message: "Failed to find sudo user latest key"
});
}
const bot = await projectBotDAL.findOne({ projectId });
if (!bot) {
throw new BadRequestError({
message: "Failed to find bot"
});
}
const botPrivateKey = infisicalSymmetricDecrypt({
keyEncoding: bot.keyEncoding as SecretKeyEncoding,
iv: bot.iv,
tag: bot.tag,
ciphertext: bot.encryptedPrivateKey
});
const userEncryptionKey = await userDAL.findUserEncKeyByUserId(actorId);
if (!userEncryptionKey) throw new BadRequestError({ message: "user encryption key not found" });
const [newWsMember] = assignWorkspaceKeysToMembers({
decryptKey: ghostUserLatestKey,
userPrivateKey: botPrivateKey,
members: [
{
orgMembershipId: membership.id,
projectMembershipRole: ProjectMembershipRole.Admin,
userPublicKey: userEncryptionKey.publicKey
}
]
});
const updatedMembership = await projectMembershipDAL.transaction(async (tx) => {
const newProjectMembership = await projectMembershipDAL.create(
{
projectId,
userId: actorId
},
tx
);
await projectUserMembershipRoleDAL.create(
{ projectMembershipId: newProjectMembership.id, role: ProjectMembershipRole.Admin },
tx
);
await projectKeyDAL.create(
{
encryptedKey: newWsMember.workspaceEncryptedKey,
nonce: newWsMember.workspaceEncryptedNonce,
senderId: ghostUser.id,
receiverId: actorId,
projectId
},
tx
);
return newProjectMembership;
});
return { isExistingMember: false, membership: updatedMembership };
};
return { listOrgProjects, grantProjectAdminAccess };
};

View File

@ -0,0 +1,11 @@
import { TOrgPermission } from "@app/lib/types";
export type TListOrgProjectsDTO = {
limit?: number;
offset?: number;
search?: string;
} & Omit<TOrgPermission, "orgId">;
export type TAccessProjectDTO = {
projectId: string;
} & Omit<TOrgPermission, "orgId">;

View File

@ -46,6 +46,7 @@ export const projectBotDALFactory = (db: TDbClient) => {
const doc = await db const doc = await db
.replicaNode()(TableName.ProjectMembership) .replicaNode()(TableName.ProjectMembership)
.where(`${TableName.ProjectMembership}.projectId` as "projectId", projectId) .where(`${TableName.ProjectMembership}.projectId` as "projectId", projectId)
.where(`${TableName.ProjectKeys}.projectId` as "projectId", projectId)
.where(`${TableName.Users}.isGhost` as "isGhost", false) .where(`${TableName.Users}.isGhost` as "isGhost", false)
.join(TableName.Users, `${TableName.ProjectMembership}.userId`, `${TableName.Users}.id`) .join(TableName.Users, `${TableName.ProjectMembership}.userId`, `${TableName.Users}.id`)
.join(TableName.ProjectKeys, `${TableName.ProjectMembership}.userId`, `${TableName.ProjectKeys}.receiverId`) .join(TableName.ProjectKeys, `${TableName.ProjectMembership}.userId`, `${TableName.ProjectKeys}.receiverId`)

View File

@ -66,10 +66,10 @@ export const getBotKeyFnFactory = (
await projectBotDAL.create({ await projectBotDAL.create({
name: "Infisical Bot (Ghost)", name: "Infisical Bot (Ghost)",
projectId, projectId,
isActive: true,
tag, tag,
iv, iv,
encryptedPrivateKey: ciphertext, encryptedPrivateKey: ciphertext,
isActive: true,
publicKey: botKey.publicKey, publicKey: botKey.publicKey,
algorithm, algorithm,
keyEncoding: encoding, keyEncoding: encoding,
@ -80,6 +80,12 @@ export const getBotKeyFnFactory = (
} else { } else {
await projectBotDAL.updateById(bot.id, { await projectBotDAL.updateById(bot.id, {
isActive: true, isActive: true,
tag,
iv,
encryptedPrivateKey: ciphertext,
publicKey: botKey.publicKey,
algorithm,
keyEncoding: encoding,
encryptedProjectKey: encryptedWorkspaceKey.ciphertext, encryptedProjectKey: encryptedWorkspaceKey.ciphertext,
encryptedProjectKeyNonce: encryptedWorkspaceKey.nonce, encryptedProjectKeyNonce: encryptedWorkspaceKey.nonce,
senderId: projectV1Keys.userId senderId: projectV1Keys.userId
@ -89,7 +95,6 @@ export const getBotKeyFnFactory = (
} }
const botPrivateKey = getBotPrivateKey({ bot }); const botPrivateKey = getBotPrivateKey({ bot });
const botKey = decryptAsymmetric({ const botKey = decryptAsymmetric({
ciphertext: bot.encryptedProjectKey, ciphertext: bot.encryptedProjectKey,
privateKey: botPrivateKey, privateKey: botPrivateKey,

View File

@ -256,7 +256,6 @@ export const projectMembershipServiceFactory = ({
} }
const bot = await projectBotDAL.findOne({ projectId }); const bot = await projectBotDAL.findOne({ projectId });
if (!bot) { if (!bot) {
throw new BadRequestError({ throw new BadRequestError({
message: "Failed to find bot" message: "Failed to find bot"
@ -540,7 +539,7 @@ export const projectMembershipServiceFactory = ({
const project = await projectDAL.findById(projectId); const project = await projectDAL.findById(projectId);
if (!project) throw new BadRequestError({ message: "Project not found" }); if (!project) throw new BadRequestError({ message: "Project not found" });
if (project.version !== ProjectVersion.V2) { if (project.version === ProjectVersion.V1) {
throw new BadRequestError({ throw new BadRequestError({
message: "Please ask your project administrator to upgrade the project before leaving." message: "Please ask your project administrator to upgrade the project before leaving."
}); });

View File

@ -22,6 +22,7 @@ import { TIdentityProjectMembershipRoleDALFactory } from "../identity-project/id
import { TKmsServiceFactory } from "../kms/kms-service"; import { TKmsServiceFactory } from "../kms/kms-service";
import { TOrgDALFactory } from "../org/org-dal"; import { TOrgDALFactory } from "../org/org-dal";
import { TOrgServiceFactory } from "../org/org-service"; import { TOrgServiceFactory } from "../org/org-service";
import { TProjectBotDALFactory } from "../project-bot/project-bot-dal";
import { TProjectEnvDALFactory } from "../project-env/project-env-dal"; import { TProjectEnvDALFactory } from "../project-env/project-env-dal";
import { TProjectKeyDALFactory } from "../project-key/project-key-dal"; import { TProjectKeyDALFactory } from "../project-key/project-key-dal";
import { TProjectMembershipDALFactory } from "../project-membership/project-membership-dal"; import { TProjectMembershipDALFactory } from "../project-membership/project-membership-dal";
@ -74,6 +75,7 @@ type TProjectServiceFactoryDep = {
licenseService: Pick<TLicenseServiceFactory, "getPlan">; licenseService: Pick<TLicenseServiceFactory, "getPlan">;
orgDAL: Pick<TOrgDALFactory, "findOne">; orgDAL: Pick<TOrgDALFactory, "findOne">;
keyStore: Pick<TKeyStoreFactory, "deleteItem">; keyStore: Pick<TKeyStoreFactory, "deleteItem">;
projectBotDAL: Pick<TProjectBotDALFactory, "create">;
kmsService: Pick< kmsService: Pick<
TKmsServiceFactory, TKmsServiceFactory,
| "updateProjectSecretManagerKmsKey" | "updateProjectSecretManagerKmsKey"
@ -106,7 +108,8 @@ export const projectServiceFactory = ({
certificateAuthorityDAL, certificateAuthorityDAL,
certificateDAL, certificateDAL,
keyStore, keyStore,
kmsService kmsService,
projectBotDAL
}: TProjectServiceFactoryDep) => { }: TProjectServiceFactoryDep) => {
/* /*
* Create workspace. Make user the admin * Create workspace. Make user the admin
@ -206,7 +209,26 @@ export const projectServiceFactory = ({
tx tx
); );
// const { iv, tag, ciphertext, encoding, algorithm } = infisicalSymmetricEncypt(ghostUser.keys.plainPrivateKey); const { iv, tag, ciphertext, encoding, algorithm } = infisicalSymmetricEncypt(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 // Find the ghost users latest key
const latestKey = await projectKeyDAL.findLatestProjectKey(ghostUser.user.id, project.id, tx); const latestKey = await projectKeyDAL.findLatestProjectKey(ghostUser.user.id, project.id, tx);

View File

@ -0,0 +1,6 @@
import { RawRule } from "@casl/ability";
import { ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
export const shouldCheckFolderPermission = (rules: RawRule[]) =>
rules.some((rule) => (rule.subject as ProjectPermissionSub[]).includes(ProjectPermissionSub.SecretFolders));

View File

@ -11,6 +11,7 @@ import { BadRequestError, NotFoundError } from "@app/lib/errors";
import { TProjectDALFactory } from "../project/project-dal"; import { TProjectDALFactory } from "../project/project-dal";
import { TProjectEnvDALFactory } from "../project-env/project-env-dal"; import { TProjectEnvDALFactory } from "../project-env/project-env-dal";
import { TSecretFolderDALFactory } from "./secret-folder-dal"; import { TSecretFolderDALFactory } from "./secret-folder-dal";
import { shouldCheckFolderPermission } from "./secret-folder-fns";
import { import {
TCreateFolderDTO, TCreateFolderDTO,
TDeleteFolderDTO, TDeleteFolderDTO,
@ -57,10 +58,21 @@ export const secretFolderServiceFactory = ({
actorAuthMethod, actorAuthMethod,
actorOrgId actorOrgId
); );
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Create, // we do this because we've split Secret and SecretFolder resources
subject(ProjectPermissionSub.Secrets, { environment, secretPath }) // previously, if one can create/update/read/delete secrets then they can do the same for folders
); // for backwards compatibility, we handle authorization only when SecretFolders subject is used
if (shouldCheckFolderPermission(permission.rules)) {
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Create,
subject(ProjectPermissionSub.SecretFolders, { environment, secretPath })
);
} else {
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Create,
subject(ProjectPermissionSub.Secrets, { environment, secretPath })
);
}
const env = await projectEnvDAL.findOne({ projectId, slug: environment }); const env = await projectEnvDAL.findOne({ projectId, slug: environment });
if (!env) throw new BadRequestError({ message: "Environment not found", name: "Create folder" }); if (!env) throw new BadRequestError({ message: "Environment not found", name: "Create folder" });
@ -148,10 +160,20 @@ export const secretFolderServiceFactory = ({
); );
folders.forEach(({ environment, path: secretPath }) => { folders.forEach(({ environment, path: secretPath }) => {
ForbiddenError.from(permission).throwUnlessCan( // we do this because we've split Secret and SecretFolder resources
ProjectPermissionActions.Edit, // previously, if one can create/update/read/delete secrets then they can do the same for folders
subject(ProjectPermissionSub.Secrets, { environment, secretPath }) // for backwards compatibility, we handle authorization only when SecretFolders subject is used
); if (shouldCheckFolderPermission(permission.rules)) {
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Edit,
subject(ProjectPermissionSub.SecretFolders, { environment, secretPath })
);
} else {
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Edit,
subject(ProjectPermissionSub.Secrets, { environment, secretPath })
);
}
}); });
const result = await folderDAL.transaction(async (tx) => const result = await folderDAL.transaction(async (tx) =>
@ -243,10 +265,21 @@ export const secretFolderServiceFactory = ({
actorAuthMethod, actorAuthMethod,
actorOrgId actorOrgId
); );
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Edit, // we do this because we've split Secret and SecretFolder resources
subject(ProjectPermissionSub.Secrets, { environment, secretPath }) // previously, if one can create/update/read/delete secrets then they can do the same for folders
); // for backwards compatibility, we handle authorization differently only when SecretFolders subject is used
if (shouldCheckFolderPermission(permission.rules)) {
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Edit,
subject(ProjectPermissionSub.SecretFolders, { environment, secretPath })
);
} else {
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Edit,
subject(ProjectPermissionSub.Secrets, { environment, secretPath })
);
}
const parentFolder = await folderDAL.findBySecretPath(projectId, environment, secretPath); const parentFolder = await folderDAL.findBySecretPath(projectId, environment, secretPath);
if (!parentFolder) throw new BadRequestError({ message: "Secret path not found" }); if (!parentFolder) throw new BadRequestError({ message: "Secret path not found" });
@ -316,10 +349,21 @@ export const secretFolderServiceFactory = ({
actorAuthMethod, actorAuthMethod,
actorOrgId actorOrgId
); );
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Delete, // we do this because we've split Secret and SecretFolder resources
subject(ProjectPermissionSub.Secrets, { environment, secretPath }) // previously, if one can create/update/read/delete secrets then they can do the same for folders
); // for backwards compatibility, we handle authorization differently only when SecretFolders subject is used
if (shouldCheckFolderPermission(permission.rules)) {
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Delete,
subject(ProjectPermissionSub.SecretFolders, { environment, secretPath })
);
} else {
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Delete,
subject(ProjectPermissionSub.Secrets, { environment, secretPath })
);
}
const env = await projectEnvDAL.findOne({ projectId, slug: environment }); const env = await projectEnvDAL.findOne({ projectId, slug: environment });
if (!env) throw new BadRequestError({ message: "Environment not found", name: "Create folder" }); if (!env) throw new BadRequestError({ message: "Environment not found", name: "Create folder" });

View File

@ -542,8 +542,8 @@ export const reshapeBridgeSecret = (
secretPath, secretPath,
workspace: workspaceId, workspace: workspaceId,
environment, environment,
secretValue: secret.value, secretValue: secret.value || "",
secretComment: secret.comment, secretComment: secret.comment || "",
version: secret.version, version: secret.version,
type: secret.type, type: secret.type,
_id: secret.id, _id: secret.id,

View File

@ -490,10 +490,10 @@ export const secretV2BridgeServiceFactory = ({
...secret, ...secret,
value: secret.encryptedValue value: secret.encryptedValue
? secretManagerDecryptor({ cipherTextBlob: secret.encryptedValue }).toString() ? secretManagerDecryptor({ cipherTextBlob: secret.encryptedValue }).toString()
: undefined, : "",
comment: secret.encryptedComment comment: secret.encryptedComment
? secretManagerDecryptor({ cipherTextBlob: secret.encryptedComment }).toString() ? secretManagerDecryptor({ cipherTextBlob: secret.encryptedComment }).toString()
: undefined : ""
}) })
); );
const expandSecretReferences = expandSecretReferencesFactory({ const expandSecretReferences = expandSecretReferencesFactory({
@ -522,7 +522,7 @@ export const secretV2BridgeServiceFactory = ({
await expandSecretReferences(secretsGroupByKey); await expandSecretReferences(secretsGroupByKey);
secretsGroupByPath[secretPathKey].forEach((decryptedSecret) => { secretsGroupByPath[secretPathKey].forEach((decryptedSecret) => {
// eslint-disable-next-line no-param-reassign // eslint-disable-next-line no-param-reassign
decryptedSecret.secretValue = secretsGroupByKey[decryptedSecret.secretKey].value; decryptedSecret.secretValue = secretsGroupByKey[decryptedSecret.secretKey].value || "";
}); });
} }
} }

View File

@ -1133,7 +1133,7 @@ export const secretQueueFactory = ({
}); });
queueService.start(QueueName.SecretWebhook, async (job) => { queueService.start(QueueName.SecretWebhook, async (job) => {
await fnTriggerWebhook({ ...job.data, projectEnvDAL, webhookDAL, projectDAL, kmsService }); await fnTriggerWebhook({ ...job.data, projectEnvDAL, webhookDAL, projectDAL });
}); });
return { return {

View File

@ -3,12 +3,12 @@ import crypto from "node:crypto";
import { AxiosError } from "axios"; import { AxiosError } from "axios";
import picomatch from "picomatch"; import picomatch from "picomatch";
import { SecretKeyEncoding, TWebhooks } from "@app/db/schemas";
import { request } from "@app/lib/config/request"; import { request } from "@app/lib/config/request";
import { infisicalSymmetricDecrypt } from "@app/lib/crypto/encryption";
import { BadRequestError } from "@app/lib/errors"; import { BadRequestError } from "@app/lib/errors";
import { logger } from "@app/lib/logger"; import { logger } from "@app/lib/logger";
import { TKmsServiceFactory } from "../kms/kms-service";
import { KmsDataKey } from "../kms/kms-types";
import { TProjectDALFactory } from "../project/project-dal"; import { TProjectDALFactory } from "../project/project-dal";
import { TProjectEnvDALFactory } from "../project-env/project-env-dal"; import { TProjectEnvDALFactory } from "../project-env/project-env-dal";
import { TWebhookDALFactory } from "./webhook-dal"; import { TWebhookDALFactory } from "./webhook-dal";
@ -16,12 +16,40 @@ import { WebhookType } from "./webhook-types";
const WEBHOOK_TRIGGER_TIMEOUT = 15 * 1000; const WEBHOOK_TRIGGER_TIMEOUT = 15 * 1000;
export const triggerWebhookRequest = async ( export const decryptWebhookDetails = (webhook: TWebhooks) => {
{ webhookSecretKey: secretKey, webhookUrl: url }: { webhookSecretKey?: string; webhookUrl: string }, const { keyEncoding, iv, encryptedSecretKey, tag, urlCipherText, urlIV, urlTag, url } = webhook;
data: Record<string, unknown>
) => { let decryptedSecretKey = "";
let decryptedUrl = url;
if (encryptedSecretKey) {
decryptedSecretKey = infisicalSymmetricDecrypt({
keyEncoding: keyEncoding as SecretKeyEncoding,
ciphertext: encryptedSecretKey,
iv: iv as string,
tag: tag as string
});
}
if (urlCipherText) {
decryptedUrl = infisicalSymmetricDecrypt({
keyEncoding: keyEncoding as SecretKeyEncoding,
ciphertext: urlCipherText,
iv: urlIV as string,
tag: urlTag as string
});
}
return {
secretKey: decryptedSecretKey,
url: decryptedUrl
};
};
export const triggerWebhookRequest = async (webhook: TWebhooks, data: Record<string, unknown>) => {
const headers: Record<string, string> = {}; const headers: Record<string, string> = {};
const payload = { ...data, timestamp: Date.now() }; const payload = { ...data, timestamp: Date.now() };
const { secretKey, url } = decryptWebhookDetails(webhook);
if (secretKey) { if (secretKey) {
const webhookSign = crypto.createHmac("sha256", secretKey).update(JSON.stringify(payload)).digest("hex"); const webhookSign = crypto.createHmac("sha256", secretKey).update(JSON.stringify(payload)).digest("hex");
@ -96,7 +124,6 @@ export type TFnTriggerWebhookDTO = {
webhookDAL: Pick<TWebhookDALFactory, "findAllWebhooks" | "transaction" | "update" | "bulkUpdate">; webhookDAL: Pick<TWebhookDALFactory, "findAllWebhooks" | "transaction" | "update" | "bulkUpdate">;
projectEnvDAL: Pick<TProjectEnvDALFactory, "findOne">; projectEnvDAL: Pick<TProjectEnvDALFactory, "findOne">;
projectDAL: Pick<TProjectDALFactory, "findById">; projectDAL: Pick<TProjectDALFactory, "findById">;
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
}; };
// this is reusable function // this is reusable function
@ -107,8 +134,7 @@ export const fnTriggerWebhook = async ({
projectId, projectId,
webhookDAL, webhookDAL,
projectEnvDAL, projectEnvDAL,
projectDAL, projectDAL
kmsService
}: TFnTriggerWebhookDTO) => { }: TFnTriggerWebhookDTO) => {
const webhooks = await webhookDAL.findAllWebhooks(projectId, environment); const webhooks = await webhookDAL.findAllWebhooks(projectId, environment);
const toBeTriggeredHooks = webhooks.filter( const toBeTriggeredHooks = webhooks.filter(
@ -118,20 +144,10 @@ export const fnTriggerWebhook = async ({
if (!toBeTriggeredHooks.length) return; if (!toBeTriggeredHooks.length) return;
logger.info("Secret webhook job started", { environment, secretPath, projectId }); logger.info("Secret webhook job started", { environment, secretPath, projectId });
const project = await projectDAL.findById(projectId); const project = await projectDAL.findById(projectId);
const { decryptor: kmsDataKeyDecryptor } = await kmsService.createCipherPairWithDataKey({
projectId,
type: KmsDataKey.SecretManager
});
const webhooksTriggered = await Promise.allSettled( const webhooksTriggered = await Promise.allSettled(
toBeTriggeredHooks.map((hook) => { toBeTriggeredHooks.map((hook) =>
const webhookUrl = kmsDataKeyDecryptor({ cipherTextBlob: hook.encryptedUrl }).toString(); triggerWebhookRequest(
const webhookSecretKey = hook.encryptedSecretKeyWithKms hook,
? kmsDataKeyDecryptor({ cipherTextBlob: hook.encryptedSecretKeyWithKms }).toString()
: undefined;
return triggerWebhookRequest(
{ webhookUrl, webhookSecretKey },
getWebhookPayload("secrets.modified", { getWebhookPayload("secrets.modified", {
workspaceName: project.name, workspaceName: project.name,
workspaceId: projectId, workspaceId: projectId,
@ -139,8 +155,8 @@ export const fnTriggerWebhook = async ({
secretPath, secretPath,
type: hook.type type: hook.type
}) })
); )
}) )
); );
// filter hooks by status // filter hooks by status

View File

@ -1,15 +1,15 @@
import { ForbiddenError } from "@casl/ability"; import { ForbiddenError } from "@casl/ability";
import { TWebhooksInsert } from "@app/db/schemas";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
import { infisicalSymmetricEncypt } from "@app/lib/crypto/encryption";
import { BadRequestError } from "@app/lib/errors"; import { BadRequestError } from "@app/lib/errors";
import { TKmsServiceFactory } from "../kms/kms-service";
import { KmsDataKey } from "../kms/kms-types";
import { TProjectDALFactory } from "../project/project-dal"; import { TProjectDALFactory } from "../project/project-dal";
import { TProjectEnvDALFactory } from "../project-env/project-env-dal"; import { TProjectEnvDALFactory } from "../project-env/project-env-dal";
import { TWebhookDALFactory } from "./webhook-dal"; import { TWebhookDALFactory } from "./webhook-dal";
import { getWebhookPayload, triggerWebhookRequest } from "./webhook-fns"; import { decryptWebhookDetails, getWebhookPayload, triggerWebhookRequest } from "./webhook-fns";
import { import {
TCreateWebhookDTO, TCreateWebhookDTO,
TDeleteWebhookDTO, TDeleteWebhookDTO,
@ -23,7 +23,6 @@ type TWebhookServiceFactoryDep = {
projectEnvDAL: TProjectEnvDALFactory; projectEnvDAL: TProjectEnvDALFactory;
projectDAL: Pick<TProjectDALFactory, "findById">; projectDAL: Pick<TProjectDALFactory, "findById">;
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">; permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
}; };
export type TWebhookServiceFactory = ReturnType<typeof webhookServiceFactory>; export type TWebhookServiceFactory = ReturnType<typeof webhookServiceFactory>;
@ -32,8 +31,7 @@ export const webhookServiceFactory = ({
webhookDAL, webhookDAL,
projectEnvDAL, projectEnvDAL,
permissionService, permissionService,
projectDAL, projectDAL
kmsService
}: TWebhookServiceFactoryDep) => { }: TWebhookServiceFactoryDep) => {
const createWebhook = async ({ const createWebhook = async ({
actor, actor,
@ -58,28 +56,33 @@ export const webhookServiceFactory = ({
const env = await projectEnvDAL.findOne({ projectId, slug: environment }); const env = await projectEnvDAL.findOne({ projectId, slug: environment });
if (!env) throw new BadRequestError({ message: "Env not found" }); if (!env) throw new BadRequestError({ message: "Env not found" });
const { encryptor: secretManagerEncryptor } = await kmsService.createCipherPairWithDataKey({ const insertDoc: TWebhooksInsert = {
projectId, url: "", // deprecated - we are moving away from plaintext URLs
type: KmsDataKey.SecretManager
});
const encryptedSecretKeyWithKms = webhookSecretKey
? secretManagerEncryptor({
plainText: Buffer.from(webhookSecretKey)
}).cipherTextBlob
: null;
const encryptedUrl = secretManagerEncryptor({
plainText: Buffer.from(webhookUrl)
}).cipherTextBlob;
const webhook = await webhookDAL.create({
encryptedUrl,
encryptedSecretKeyWithKms,
envId: env.id, envId: env.id,
isDisabled: false, isDisabled: false,
secretPath: secretPath || "/", secretPath: secretPath || "/",
type type
}); };
if (webhookSecretKey) {
const { ciphertext, iv, tag, algorithm, encoding } = infisicalSymmetricEncypt(webhookSecretKey);
insertDoc.encryptedSecretKey = ciphertext;
insertDoc.iv = iv;
insertDoc.tag = tag;
insertDoc.algorithm = algorithm;
insertDoc.keyEncoding = encoding;
}
if (webhookUrl) {
const { ciphertext, iv, tag, algorithm, encoding } = infisicalSymmetricEncypt(webhookUrl);
insertDoc.urlCipherText = ciphertext;
insertDoc.urlIV = iv;
insertDoc.urlTag = tag;
insertDoc.algorithm = algorithm;
insertDoc.keyEncoding = encoding;
}
const webhook = await webhookDAL.create(insertDoc);
return { ...webhook, projectId, environment: env }; return { ...webhook, projectId, environment: env };
}; };
@ -133,18 +136,9 @@ export const webhookServiceFactory = ({
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Webhooks); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Webhooks);
let webhookError: string | undefined; let webhookError: string | undefined;
const { decryptor: kmsDataKeyDecryptor } = await kmsService.createCipherPairWithDataKey({
projectId: project.id,
type: KmsDataKey.SecretManager
});
const webhookUrl = kmsDataKeyDecryptor({ cipherTextBlob: webhook.encryptedUrl }).toString();
const webhookSecretKey = webhook.encryptedSecretKeyWithKms
? kmsDataKeyDecryptor({ cipherTextBlob: webhook.encryptedSecretKeyWithKms }).toString()
: undefined;
try { try {
await triggerWebhookRequest( await triggerWebhookRequest(
{ webhookUrl, webhookSecretKey }, webhook,
getWebhookPayload("test", { getWebhookPayload("test", {
workspaceName: project.name, workspaceName: project.name,
workspaceId: webhook.projectId, workspaceId: webhook.projectId,
@ -183,15 +177,11 @@ export const webhookServiceFactory = ({
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Webhooks); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Webhooks);
const webhooks = await webhookDAL.findAllWebhooks(projectId, environment, secretPath); const webhooks = await webhookDAL.findAllWebhooks(projectId, environment, secretPath);
const { decryptor: kmsDataKeyDecryptor } = await kmsService.createCipherPairWithDataKey({
type: KmsDataKey.SecretManager,
projectId
});
return webhooks.map((w) => { return webhooks.map((w) => {
const decryptedUrl = kmsDataKeyDecryptor({ cipherTextBlob: w.encryptedUrl }).toString(); const { url } = decryptWebhookDetails(w);
return { return {
...w, ...w,
url: decryptedUrl url
}; };
}); });
}; };

View File

@ -4,6 +4,7 @@ Copyright (c) 2023 Infisical Inc.
package cmd package cmd
import ( import (
"encoding/base64"
"fmt" "fmt"
"strings" "strings"
@ -13,53 +14,56 @@ import (
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
var AvailableVaultsAndDescriptions = []string{"auto (automatically select native vault on system)", "file (encrypted file vault)"} type VaultBackendType struct {
var AvailableVaults = []string{"auto", "file"} Name string
Description string
}
var AvailableVaults = []VaultBackendType{
{
Name: "auto",
Description: "automatically select the system keyring",
},
{
Name: "file",
Description: "encrypted file vault",
},
}
var vaultSetCmd = &cobra.Command{ var vaultSetCmd = &cobra.Command{
Example: `infisical vault set pass`, Example: `infisical vault set file --passphrase <your-passphrase>`,
Use: "set [vault-name]", Use: "set [file|auto] [flags]",
Short: "Used to set the vault backend to store your login details securely at rest", Short: "Used to configure the vault backends",
DisableFlagsInUseLine: true, DisableFlagsInUseLine: true,
Args: cobra.MinimumNArgs(1), Args: cobra.MinimumNArgs(1),
Run: func(cmd *cobra.Command, args []string) { Run: func(cmd *cobra.Command, args []string) {
wantedVaultTypeName := args[0]
currentVaultBackend, err := util.GetCurrentVaultBackend() vaultType := args[0]
passphrase, err := cmd.Flags().GetString("passphrase")
if err != nil { if err != nil {
log.Error().Msgf("Unable to set vault to [%s] because of [err=%s]", wantedVaultTypeName, err) util.HandleError(err, "Unable to get passphrase flag")
}
if vaultType == util.VAULT_BACKEND_FILE_MODE && passphrase != "" {
setFileVaultPassphrase(passphrase)
return return
} }
if wantedVaultTypeName == string(currentVaultBackend) { util.PrintWarning("This command has been deprecated. Please use 'infisical vault use [file|auto]' to select which vault to use.\n")
log.Error().Msgf("You are already on vault backend [%s]", currentVaultBackend) selectVaultTypeCmd(cmd, args)
return
}
if wantedVaultTypeName == "auto" || wantedVaultTypeName == "file" {
configFile, err := util.GetConfigFile()
if err != nil {
log.Error().Msgf("Unable to set vault to [%s] because of [err=%s]", wantedVaultTypeName, err)
return
}
configFile.VaultBackendType = wantedVaultTypeName // save selected vault
configFile.LoggedInUserEmail = "" // reset the logged in user to prompt them to re login
err = util.WriteConfigFile(&configFile)
if err != nil {
log.Error().Msgf("Unable to set vault to [%s] because an error occurred when saving the config file [err=%s]", wantedVaultTypeName, err)
return
}
fmt.Printf("\nSuccessfully, switched vault backend from [%s] to [%s]. Please login in again to store your login details in the new vault with [infisical login]\n", currentVaultBackend, wantedVaultTypeName)
Telemetry.CaptureEvent("cli-command:vault set", posthog.NewProperties().Set("currentVault", currentVaultBackend).Set("wantedVault", wantedVaultTypeName).Set("version", util.CLI_VERSION))
} else {
log.Error().Msgf("The requested vault type [%s] is not available on this system. Only the following vault backends are available for you system: %s", wantedVaultTypeName, strings.Join(AvailableVaults, ", "))
}
}, },
} }
var vaultUseCmd = &cobra.Command{
Example: `infisical vault use [file|auto]`,
Use: "use [file|auto]",
Short: "Used to select the the type of vault backend to store sensitive data securely at rest",
DisableFlagsInUseLine: true,
Args: cobra.MinimumNArgs(1),
Run: selectVaultTypeCmd,
}
// runCmd represents the run command // runCmd represents the run command
var vaultCmd = &cobra.Command{ var vaultCmd = &cobra.Command{
Use: "vault", Use: "vault",
@ -71,10 +75,30 @@ var vaultCmd = &cobra.Command{
}, },
} }
func setFileVaultPassphrase(passphrase string) {
configFile, err := util.GetConfigFile()
if err != nil {
log.Error().Msgf("Unable to set passphrase for file vault because of [err=%s]", err)
return
}
// encode with base64
encodedPassphrase := base64.StdEncoding.EncodeToString([]byte(passphrase))
configFile.VaultBackendPassphrase = encodedPassphrase
err = util.WriteConfigFile(&configFile)
if err != nil {
log.Error().Msgf("Unable to set passphrase for file vault because of [err=%s]", err)
return
}
util.PrintSuccessMessage("\nSuccessfully, set passphrase for file vault.\n")
}
func printAvailableVaultBackends() { func printAvailableVaultBackends() {
fmt.Printf("Vaults are used to securely store your login details locally. Available vaults:") fmt.Printf("Vaults are used to securely store your login details locally. Available vaults:")
for _, backend := range AvailableVaultsAndDescriptions { for _, vaultType := range AvailableVaults {
fmt.Printf("\n- %s", backend) fmt.Printf("\n- %s (%s)", vaultType.Name, vaultType.Description)
} }
currentVaultBackend, err := util.GetCurrentVaultBackend() currentVaultBackend, err := util.GetCurrentVaultBackend()
@ -87,7 +111,53 @@ func printAvailableVaultBackends() {
fmt.Printf("\n\nYou are currently using [%s] vault to store your login credentials\n", string(currentVaultBackend)) fmt.Printf("\n\nYou are currently using [%s] vault to store your login credentials\n", string(currentVaultBackend))
} }
func selectVaultTypeCmd(cmd *cobra.Command, args []string) {
wantedVaultTypeName := args[0]
currentVaultBackend, err := util.GetCurrentVaultBackend()
if err != nil {
log.Error().Msgf("Unable to set vault to [%s] because of [err=%s]", wantedVaultTypeName, err)
return
}
if wantedVaultTypeName == string(currentVaultBackend) {
log.Error().Msgf("You are already on vault backend [%s]", currentVaultBackend)
return
}
if wantedVaultTypeName == util.VAULT_BACKEND_AUTO_MODE || wantedVaultTypeName == util.VAULT_BACKEND_FILE_MODE {
configFile, err := util.GetConfigFile()
if err != nil {
log.Error().Msgf("Unable to set vault to [%s] because of [err=%s]", wantedVaultTypeName, err)
return
}
configFile.VaultBackendType = wantedVaultTypeName // save selected vault
configFile.LoggedInUserEmail = "" // reset the logged in user to prompt them to re login
err = util.WriteConfigFile(&configFile)
if err != nil {
log.Error().Msgf("Unable to set vault to [%s] because an error occurred when saving the config file [err=%s]", wantedVaultTypeName, err)
return
}
fmt.Printf("\nSuccessfully, switched vault backend from [%s] to [%s]. Please login in again to store your login details in the new vault with [infisical login]\n", currentVaultBackend, wantedVaultTypeName)
Telemetry.CaptureEvent("cli-command:vault set", posthog.NewProperties().Set("currentVault", currentVaultBackend).Set("wantedVault", wantedVaultTypeName).Set("version", util.CLI_VERSION))
} else {
var availableVaultsNames []string
for _, vault := range AvailableVaults {
availableVaultsNames = append(availableVaultsNames, vault.Name)
}
log.Error().Msgf("The requested vault type [%s] is not available on this system. Only the following vault backends are available for you system: %s", wantedVaultTypeName, strings.Join(availableVaultsNames, ", "))
}
}
func init() { func init() {
vaultSetCmd.Flags().StringP("passphrase", "p", "", "Set the passphrase for the file vault")
vaultCmd.AddCommand(vaultSetCmd) vaultCmd.AddCommand(vaultSetCmd)
vaultCmd.AddCommand(vaultUseCmd)
rootCmd.AddCommand(vaultCmd) rootCmd.AddCommand(vaultCmd)
} }

View File

@ -11,10 +11,11 @@ type UserCredentials struct {
// The file struct for Infisical config file // The file struct for Infisical config file
type ConfigFile struct { type ConfigFile struct {
LoggedInUserEmail string `json:"loggedInUserEmail"` LoggedInUserEmail string `json:"loggedInUserEmail"`
LoggedInUserDomain string `json:"LoggedInUserDomain,omitempty"` LoggedInUserDomain string `json:"LoggedInUserDomain,omitempty"`
LoggedInUsers []LoggedInUser `json:"loggedInUsers,omitempty"` LoggedInUsers []LoggedInUser `json:"loggedInUsers,omitempty"`
VaultBackendType string `json:"vaultBackendType,omitempty"` VaultBackendType string `json:"vaultBackendType,omitempty"`
VaultBackendPassphrase string `json:"vaultBackendPassphrase,omitempty"`
} }
type LoggedInUser struct { type LoggedInUser struct {

View File

@ -1,6 +1,7 @@
package util package util
import ( import (
"encoding/base64"
"encoding/json" "encoding/json"
"errors" "errors"
"fmt" "fmt"
@ -50,10 +51,11 @@ func WriteInitalConfig(userCredentials *models.UserCredentials) error {
} }
configFile := models.ConfigFile{ configFile := models.ConfigFile{
LoggedInUserEmail: userCredentials.Email, LoggedInUserEmail: userCredentials.Email,
LoggedInUserDomain: config.INFISICAL_URL, LoggedInUserDomain: config.INFISICAL_URL,
LoggedInUsers: existingConfigFile.LoggedInUsers, LoggedInUsers: existingConfigFile.LoggedInUsers,
VaultBackendType: existingConfigFile.VaultBackendType, VaultBackendType: existingConfigFile.VaultBackendType,
VaultBackendPassphrase: existingConfigFile.VaultBackendPassphrase,
} }
configFileMarshalled, err := json.Marshal(configFile) configFileMarshalled, err := json.Marshal(configFile)
@ -215,6 +217,14 @@ func GetConfigFile() (models.ConfigFile, error) {
return models.ConfigFile{}, err return models.ConfigFile{}, err
} }
if configFile.VaultBackendPassphrase != "" {
decodedPassphrase, err := base64.StdEncoding.DecodeString(configFile.VaultBackendPassphrase)
if err != nil {
return models.ConfigFile{}, fmt.Errorf("GetConfigFile: Unable to decode base64 passphrase [err=%s]", err)
}
os.Setenv("INFISICAL_VAULT_FILE_PASSPHRASE", string(decodedPassphrase))
}
return configFile, nil return configFile, nil
} }

View File

@ -8,6 +8,10 @@ const (
INFISICAL_WORKSPACE_CONFIG_FILE_NAME = ".infisical.json" INFISICAL_WORKSPACE_CONFIG_FILE_NAME = ".infisical.json"
INFISICAL_TOKEN_NAME = "INFISICAL_TOKEN" INFISICAL_TOKEN_NAME = "INFISICAL_TOKEN"
INFISICAL_UNIVERSAL_AUTH_ACCESS_TOKEN_NAME = "INFISICAL_UNIVERSAL_AUTH_ACCESS_TOKEN" INFISICAL_UNIVERSAL_AUTH_ACCESS_TOKEN_NAME = "INFISICAL_UNIVERSAL_AUTH_ACCESS_TOKEN"
INFISICAL_VAULT_FILE_PASSPHRASE_ENV_NAME = "INFISICAL_VAULT_FILE_PASSPHRASE" // This works because we've forked the keyring package and added support for this env variable. This explains why you won't find any occurrences of it in the CLI codebase.
VAULT_BACKEND_AUTO_MODE = "auto"
VAULT_BACKEND_FILE_MODE = "file"
// Universal Auth // Universal Auth
INFISICAL_UNIVERSAL_AUTH_CLIENT_ID_NAME = "INFISICAL_UNIVERSAL_AUTH_CLIENT_ID" INFISICAL_UNIVERSAL_AUTH_CLIENT_ID_NAME = "INFISICAL_UNIVERSAL_AUTH_CLIENT_ID"

View File

@ -1,6 +1,9 @@
package util package util
import ( import (
"encoding/base64"
"github.com/manifoldco/promptui"
"github.com/zalando/go-keyring" "github.com/zalando/go-keyring"
) )
@ -20,16 +23,51 @@ func SetValueInKeyring(key, value string) error {
PrintErrorAndExit(1, err, "Unable to get current vault. Tip: run [infisical rest] then try again") PrintErrorAndExit(1, err, "Unable to get current vault. Tip: run [infisical rest] then try again")
} }
return keyring.Set(currentVaultBackend, MAIN_KEYRING_SERVICE, key, value) err = keyring.Set(currentVaultBackend, MAIN_KEYRING_SERVICE, key, value)
if err != nil {
configFile, _ := GetConfigFile()
if configFile.VaultBackendPassphrase == "" {
PrintWarning("System keyring could not be used, falling back to `file` vault for sensitive data storage.")
passphrasePrompt := promptui.Prompt{
Label: "Enter the passphrase to use for keyring encryption",
}
passphrase, err := passphrasePrompt.Run()
if err != nil {
return err
}
encodedPassphrase := base64.StdEncoding.EncodeToString([]byte(passphrase))
configFile.VaultBackendPassphrase = encodedPassphrase
err = WriteConfigFile(&configFile)
if err != nil {
return err
}
// We call this function at last to trigger the environment variable to be set
GetConfigFile()
}
err = keyring.Set(VAULT_BACKEND_FILE_MODE, MAIN_KEYRING_SERVICE, key, value)
}
return err
} }
func GetValueInKeyring(key string) (string, error) { func GetValueInKeyring(key string) (string, error) {
currentVaultBackend, err := GetCurrentVaultBackend() currentVaultBackend, err := GetCurrentVaultBackend()
if err != nil { if err != nil {
PrintErrorAndExit(1, err, "Unable to get current vault. Tip: run [infisical rest] then try again") PrintErrorAndExit(1, err, "Unable to get current vault. Tip: run [infisical reset] then try again")
} }
return keyring.Get(currentVaultBackend, MAIN_KEYRING_SERVICE, key) value, err := keyring.Get(currentVaultBackend, MAIN_KEYRING_SERVICE, key)
if err != nil {
value, err = keyring.Get(VAULT_BACKEND_FILE_MODE, MAIN_KEYRING_SERVICE, key)
}
return value, err
} }
func DeleteValueInKeyring(key string) error { func DeleteValueInKeyring(key string) error {
@ -38,5 +76,11 @@ func DeleteValueInKeyring(key string) error {
return err return err
} }
return keyring.Delete(currentVaultBackend, MAIN_KEYRING_SERVICE, key) err = keyring.Delete(currentVaultBackend, MAIN_KEYRING_SERVICE, key)
if err != nil {
err = keyring.Delete(VAULT_BACKEND_FILE_MODE, MAIN_KEYRING_SERVICE, key)
}
return err
} }

View File

@ -11,11 +11,11 @@ func GetCurrentVaultBackend() (string, error) {
} }
if configFile.VaultBackendType == "" { if configFile.VaultBackendType == "" {
return "auto", nil return VAULT_BACKEND_AUTO_MODE, nil
} }
if configFile.VaultBackendType != "auto" && configFile.VaultBackendType != "file" { if configFile.VaultBackendType != VAULT_BACKEND_AUTO_MODE && configFile.VaultBackendType != VAULT_BACKEND_FILE_MODE {
return "auto", nil return VAULT_BACKEND_AUTO_MODE, nil
} }
return configFile.VaultBackendType, nil return configFile.VaultBackendType, nil

View File

@ -0,0 +1,15 @@
---
title: "Meetings"
sidebarTitle: "Meetings"
description: "The guide to meetings at Infisical."
---
## "Let's schedule a meeting about this"
Being a remote-first company, we try to be as async as possible. When an issue arises, it's best to create a public Slack thread and tag all the necessary team members. Otherwise, if you were to "put a meeting on a calendar", the decision making process will inevitable slow down by at least a day (e.g., trying to find the right time for folks in different time zones is not always straightforward).
In other words, we have almost no (recurring) meetings and prefer written communication or quick Slack huddles.
## Weekly All-hands
All-hands is the single recurring meeting that we run every Monday at 8:30am PT. Typically, we would discuss everything important that happened during the previous week and plan out the week ahead. This is also an opportunity to bring up any important topics in front of the whole company (but feel free to post those in Slack too).

View File

@ -59,7 +59,8 @@
"handbook/onboarding", "handbook/onboarding",
"handbook/spending-money", "handbook/spending-money",
"handbook/time-off", "handbook/time-off",
"handbook/hiring" "handbook/hiring",
"handbook/meetings"
] ]
} }
], ],

View File

@ -0,0 +1,4 @@
---
title: "Sign certificate"
openapi: "POST /api/v1/pki/ca/{caId}/sign-certificate"
---

View File

@ -32,6 +32,6 @@ description: "Change the vault type in Infisical"
To safeguard your login details when using the CLI, Infisical places them in a system vault or an encrypted text file, protected by a passphrase that only the user knows. To safeguard your login details when using the CLI, Infisical places them in a system vault or an encrypted text file, protected by a passphrase that only the user knows.
<Tip>To avoid constantly entering your passphrase when using the `file` vault type, set the `INFISICAL_VAULT_FILE_PASSPHRASE` environment variable with your password in your shell</Tip> <Tip>To avoid constantly entering your passphrase when using the `file` vault type, use the `infisical vault set file --passphrase <your-passphrase>` CLI command to specify your password once.</Tip>

View File

@ -16,7 +16,7 @@ Before you begin, you'll first need to choose a method of authentication with AW
<Steps> <Steps>
<Step title="Create the Managing User IAM Role"> <Step title="Create the Managing User IAM Role">
1. Navigate to the [Create IAM Role](https://console.aws.amazon.com/iamv2/home#/roles/create?step=selectEntities) page in your AWS Console. 1. Navigate to the [Create IAM Role](https://console.aws.amazon.com/iamv2/home#/roles/create?step=selectEntities) page in your AWS Console.
![IAM Role Creation](../../images/integrations/aws/integration-aws-iam-assume-role.png) ![IAM Role Creation](/images/integrations/aws/integration-aws-iam-assume-role.png)
2. Select **AWS Account** as the **Trusted Entity Type**. 2. Select **AWS Account** as the **Trusted Entity Type**.
3. Choose **Another AWS Account** and enter **381492033652** (Infisical AWS Account ID). This restricts the role to be assumed only by Infisical. If you are self-hosting, provide the AWS account number where Infisical is hosted. 3. Choose **Another AWS Account** and enter **381492033652** (Infisical AWS Account ID). This restricts the role to be assumed only by Infisical. If you are self-hosting, provide the AWS account number where Infisical is hosted.

View File

@ -74,7 +74,7 @@ In the following steps, we explore how to issue a X.509 certificate under a CA.
</Steps> </Steps>
</Tab> </Tab>
<Tab title="API"> <Tab title="API">
To create a certificate, make an API request to the [Create Certificate](/api-reference/endpoints/certificate-authorities/sign-intermediate) API endpoint, To create a certificate, make an API request to the [Issue Certificate](/api-reference/endpoints/certificates/issue-cert) API endpoint,
specifying the issuing CA. specifying the issuing CA.
### Sample request ### Sample request
@ -84,6 +84,7 @@ In the following steps, we explore how to issue a X.509 certificate under a CA.
--header 'Content-Type: application/json' \ --header 'Content-Type: application/json' \
--data-raw '{ --data-raw '{
"commonName": "My Certificate", "commonName": "My Certificate",
"ttl": "1y",
}' }'
``` ```
@ -103,6 +104,31 @@ In the following steps, we explore how to issue a X.509 certificate under a CA.
Make sure to store the `privateKey` as it is only returned once here at the time of certificate issuance. The `certificate` and `certificateChain` will remain accessible and can be retrieved at any time. Make sure to store the `privateKey` as it is only returned once here at the time of certificate issuance. The `certificate` and `certificateChain` will remain accessible and can be retrieved at any time.
</Note> </Note>
If you have an external private key, you can also create a certificate by making an API request containing a pem-encoded CSR (Certificate Signing Request) to the [Sign Certificate](/api-reference/endpoints/certificates/sign-cert) API endpoint, specifying the issuing CA.
### Sample request
```bash Request
curl --location --request POST 'https://app.infisical.com/api/v1/pki/ca/<ca-id>/sign-certificate' \
--header 'Content-Type: application/json' \
--data-raw '{
"csr": "...",
"ttl": "1y",
}'
```
### Sample response
```bash Response
{
certificate: "...",
certificateChain: "...",
issuingCaCertificate: "...",
privateKey: "...",
serialNumber: "..."
}
```
</Tab> </Tab>
</Tabs> </Tabs>

View File

@ -1,5 +1,5 @@
--- ---
title: "Kubernetes" title: "Kubernetes Operator"
description: "How to use Infisical to inject secrets into Kubernetes clusters." description: "How to use Infisical to inject secrets into Kubernetes clusters."
--- ---
@ -9,6 +9,10 @@ The Infisical Secrets Operator is a Kubernetes controller that retrieves secrets
It uses an `InfisicalSecret` resource to specify authentication and storage methods. It uses an `InfisicalSecret` resource to specify authentication and storage methods.
The operator continuously updates secrets and can also reload dependent deployments automatically. The operator continuously updates secrets and can also reload dependent deployments automatically.
<Note>
If you are already using the External Secrets operator, you can you can view the integration documentation for it [here](https://external-secrets.io/latest/provider/infisical/).
</Note>
## Install Operator ## Install Operator
The operator can be install via [Helm](https://helm.sh) or [kubectl](https://github.com/kubernetes/kubectl) The operator can be install via [Helm](https://helm.sh) or [kubectl](https://github.com/kubernetes/kubectl)

View File

@ -155,7 +155,7 @@
] ]
}, },
{ {
"group": "Key Management", "group": "Key Management (KMS)",
"pages": [ "pages": [
"documentation/platform/kms/overview", "documentation/platform/kms/overview",
"documentation/platform/kms/aws-kms", "documentation/platform/kms/aws-kms",
@ -672,6 +672,7 @@
"api-reference/endpoints/certificate-authorities/sign-intermediate", "api-reference/endpoints/certificate-authorities/sign-intermediate",
"api-reference/endpoints/certificate-authorities/import-cert", "api-reference/endpoints/certificate-authorities/import-cert",
"api-reference/endpoints/certificate-authorities/issue-cert", "api-reference/endpoints/certificate-authorities/issue-cert",
"api-reference/endpoints/certificate-authorities/sign-cert",
"api-reference/endpoints/certificate-authorities/crl" "api-reference/endpoints/certificate-authorities/crl"
] ]
}, },

View File

@ -1,4 +1,4 @@
import { useEffect, useState } from "react"; import { ReactNode, useEffect, useState } from "react";
import { useToggle } from "@app/hooks"; import { useToggle } from "@app/hooks";
@ -16,6 +16,7 @@ type Props = {
subTitle?: string; subTitle?: string;
onDeleteApproved: () => Promise<void>; onDeleteApproved: () => Promise<void>;
buttonText?: string; buttonText?: string;
children?: ReactNode;
}; };
export const DeleteActionModal = ({ export const DeleteActionModal = ({
@ -26,7 +27,8 @@ export const DeleteActionModal = ({
onDeleteApproved, onDeleteApproved,
title, title,
subTitle = "This action is irreversible.", subTitle = "This action is irreversible.",
buttonText = "Delete" buttonText = "Delete",
children
}: Props): JSX.Element => { }: Props): JSX.Element => {
const [inputData, setInputData] = useState(""); const [inputData, setInputData] = useState("");
const [isLoading, setIsLoading] = useToggle(); const [isLoading, setIsLoading] = useToggle();
@ -94,9 +96,10 @@ export const DeleteActionModal = ({
<Input <Input
value={inputData} value={inputData}
onChange={(e) => setInputData(e.target.value)} onChange={(e) => setInputData(e.target.value)}
placeholder="Type confirm..." placeholder={`Type ${deleteKey} here`}
/> />
</FormControl> </FormControl>
{children}
</form> </form>
</ModalContent> </ModalContent>
</Modal> </Modal>

View File

@ -50,7 +50,7 @@ export const Pagination = ({
> >
<div className="mr-6 flex items-center space-x-2"> <div className="mr-6 flex items-center space-x-2">
<div className="text-xs"> <div className="text-xs">
{(page - 1) * perPage} - {(page - 1) * perPage + perPage} of {count} {(page - 1) * perPage} - {Math.min((page - 1) * perPage + perPage, count)} of {count}
</div> </div>
<DropdownMenu> <DropdownMenu>
<DropdownMenuTrigger asChild> <DropdownMenuTrigger asChild>

View File

@ -1,47 +0,0 @@
import { useRouter } from "next/router";
import { Spinner } from "@app/components/v2";
import { useWorkspace } from "@app/context";
import { useToggle } from "@app/hooks";
import { useGetUpgradeProjectStatus } from "@app/hooks/api/workspace/queries";
import { ProjectVersion } from "@app/hooks/api/workspace/types";
export const UpgradeOverlay = () => {
const router = useRouter();
const { currentWorkspace } = useWorkspace();
const [isUpgrading, setIsUpgrading] = useToggle(false);
const isProjectRoute = router.pathname.includes("/project");
const { isLoading: isUpgradeStatusLoading } = useGetUpgradeProjectStatus({
projectId: currentWorkspace?.id ?? "",
enabled: isProjectRoute && currentWorkspace && currentWorkspace.version === ProjectVersion.V1,
refetchInterval: 5_000,
onSuccess: (data) => {
if (!data) return;
if (data.status !== "IN_PROGRESS") {
setIsUpgrading.off();
} else if (data?.status === "IN_PROGRESS") {
setIsUpgrading.on();
}
}
});
// make sure only to display this on /project routes
if (!currentWorkspace || !isProjectRoute) {
return null;
}
return !isUpgradeStatusLoading && isUpgrading ? (
<div className="absolute top-0 left-0 z-50 flex h-screen w-screen items-center justify-center bg-bunker-500 bg-opacity-80">
<Spinner size="lg" className="text-primary" />
<div className="ml-4 flex flex-col space-y-1">
<div className="text-3xl font-medium text-white">Please wait</div>
<span className="inline-block text-white">Upgrading your project...</span>
</div>
</div>
) : (
<div />
);
};

View File

@ -1 +0,0 @@
export { UpgradeOverlay } from "./UpgradeOverlay";

View File

@ -1,168 +0,0 @@
import { useCallback, useState } from "react";
import Link from "next/link";
import { useRouter } from "next/router";
import { faWarning } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { twMerge } from "tailwind-merge";
import { createNotification } from "@app/components/notifications";
import { useProjectPermission } from "@app/context";
import { useGetUpgradeProjectStatus, useUpgradeProject } from "@app/hooks/api";
import { Workspace } from "@app/hooks/api/types";
import { workspaceKeys } from "@app/hooks/api/workspace/queries";
import { ProjectVersion } from "@app/hooks/api/workspace/types";
import { queryClient } from "@app/reactQuery";
import { Button } from "../Button";
import { Tooltip } from "../Tooltip";
export type UpgradeProjectAlertProps = {
project: Workspace;
transparent?: boolean;
};
export const UpgradeProjectAlert = ({
project,
transparent
}: UpgradeProjectAlertProps): JSX.Element | null => {
const router = useRouter();
const { hasProjectRole } = useProjectPermission();
const upgradeProject = useUpgradeProject();
const [currentStatus, setCurrentStatus] = useState<string | null>(null);
const [isUpgrading, setIsUpgrading] = useState(false);
const isProjectAdmin = hasProjectRole("admin");
const {
data: projectStatus,
isLoading: statusIsLoading,
refetch: manualProjectStatusRefetch
} = useGetUpgradeProjectStatus({
projectId: project.id,
enabled: isProjectAdmin && project.version === ProjectVersion.V1,
refetchInterval: 5_000,
onSuccess: (data) => {
if (!isProjectAdmin) {
return;
}
if (data && data?.status !== null) {
if (data.status === "IN_PROGRESS") {
setCurrentStatus("Your upgrade is being processed.");
} else if (data.status === "FAILED") {
setCurrentStatus("Upgrade failed, please try again.");
}
}
if (currentStatus !== null && data?.status === null) {
queryClient.invalidateQueries(workspaceKeys.getAllUserWorkspace);
router.reload();
}
}
});
const onUpgradeProject = useCallback(async () => {
if (upgradeProject.isLoading) {
return;
}
setIsUpgrading(true);
const PRIVATE_KEY = localStorage.getItem("PRIVATE_KEY");
if (!PRIVATE_KEY) {
createNotification({
type: "error",
text: "Private key not found"
});
return;
}
await upgradeProject.mutateAsync({
projectId: project.id,
privateKey: PRIVATE_KEY
});
manualProjectStatusRefetch();
setTimeout(() => setIsUpgrading(false), 5_000);
}, []);
const isLoading =
isUpgrading ||
((upgradeProject.isLoading ||
currentStatus !== null ||
(currentStatus === null && statusIsLoading)) &&
projectStatus?.status !== "FAILED");
if (project.version !== ProjectVersion.V1) return null;
if (transparent) {
return (
<Button
colorSchema="primary"
variant="solid"
size="md"
isLoading={isLoading}
isDisabled={isLoading || !isProjectAdmin}
onClick={onUpgradeProject}
>
Upgrade
</Button>
);
}
return (
<div
className={twMerge(
"mt-4 flex w-full flex-row items-center rounded-md border border-primary-600/70 bg-primary/[.07] p-4 text-base text-white",
!isProjectAdmin && "opacity-80"
)}
>
<FontAwesomeIcon icon={faWarning} className="pr-6 text-6xl text-white/80" />
<div className="flex w-full flex-col text-sm">
<span className="mb-2 text-lg font-semibold">Upgrade your project</span>
{isProjectAdmin ? (
<>
<p>
Upgrade your project version to continue receiving the latest improvements and
patches.
</p>
<Link href="https://infisical.com/docs/documentation/platform/project-upgrade">
<a target="_blank" className="text-primary-400">
Learn more
</a>
</Link>
</>
) : (
<>
<p>
<span className="font-bold">Please ask a project admin to upgrade the project.</span>
<br />
Upgrading the project version is required to continue receiving the latest
improvements and patches.
</p>
<Link href="https://infisical.com/docs/documentation/platform/project-upgrade">
<a target="_blank" className="text-primary-400">
Learn more
</a>
</Link>
</>
)}
{currentStatus && <p className="mt-2 opacity-80">Status: {currentStatus}</p>}
</div>
<div className="my-2">
<Tooltip
className={twMerge(isProjectAdmin && "hidden")}
content="You need to be an admin to upgrade the project."
>
<Button
isLoading={isLoading}
isDisabled={isLoading || !isProjectAdmin}
onClick={onUpgradeProject}
>
Upgrade
</Button>
</Tooltip>
</div>
</div>
);
};

View File

@ -1 +0,0 @@
export { UpgradeProjectAlert } from "./UpgradeProjectAlert";

View File

@ -20,7 +20,12 @@ export enum OrgPermissionSubjects {
Billing = "billing", Billing = "billing",
SecretScanning = "secret-scanning", SecretScanning = "secret-scanning",
Identity = "identity", Identity = "identity",
Kms = "kms" Kms = "kms",
AdminConsole = "organization-admin-console"
}
export enum OrgPermissionAdminConsoleAction {
AccessAllProjects = "access-all-projects"
} }
export type OrgPermissionSet = export type OrgPermissionSet =
@ -37,6 +42,7 @@ export type OrgPermissionSet =
| [OrgPermissionActions, OrgPermissionSubjects.SecretScanning] | [OrgPermissionActions, OrgPermissionSubjects.SecretScanning]
| [OrgPermissionActions, OrgPermissionSubjects.Billing] | [OrgPermissionActions, OrgPermissionSubjects.Billing]
| [OrgPermissionActions, OrgPermissionSubjects.Identity] | [OrgPermissionActions, OrgPermissionSubjects.Identity]
| [OrgPermissionActions, OrgPermissionSubjects.Kms]; | [OrgPermissionActions, OrgPermissionSubjects.Kms]
| [OrgPermissionAdminConsoleAction, OrgPermissionSubjects.AdminConsole];
export type TOrgPermission = MongoAbility<OrgPermissionSet>; export type TOrgPermission = MongoAbility<OrgPermissionSet>;

View File

@ -21,6 +21,7 @@ export enum ProjectPermissionSub {
IpAllowList = "ip-allowlist", IpAllowList = "ip-allowlist",
Workspace = "workspace", Workspace = "workspace",
Secrets = "secrets", Secrets = "secrets",
SecretFolders = "secret-folders",
SecretRollback = "secret-rollback", SecretRollback = "secret-rollback",
SecretApproval = "secret-approval", SecretApproval = "secret-approval",
SecretRotation = "secret-rotation", SecretRotation = "secret-rotation",

View File

@ -0,0 +1,14 @@
/** Extracts the key and value from a passed in env string based on the provided delimiters. */
export const getKeyValue = (pastedContent: string, delimiters: string[]) => {
const foundDelimiter = delimiters.find((delimiter) => pastedContent.includes(delimiter));
if (!foundDelimiter) {
return { key: pastedContent.trim(), value: "" };
}
const [key, value] = pastedContent.split(foundDelimiter);
return {
key: key.trim(),
value: (value ?? "").trim()
};
};

View File

@ -56,7 +56,8 @@ export const eventToNameMap: { [K in EventType]: string } = {
[EventType.GET_CERT]: "Get certificate", [EventType.GET_CERT]: "Get certificate",
[EventType.DELETE_CERT]: "Delete certificate", [EventType.DELETE_CERT]: "Delete certificate",
[EventType.REVOKE_CERT]: "Revoke certificate", [EventType.REVOKE_CERT]: "Revoke certificate",
[EventType.GET_CERT_BODY]: "Get certificate body" [EventType.GET_CERT_BODY]: "Get certificate body",
[EventType.ORG_ADMIN_ACCESS_PROJECT]: "Org admin accessed project"
}; };
export const userAgentTTypeoNameMap: { [K in UserAgentType]: string } = { export const userAgentTTypeoNameMap: { [K in UserAgentType]: string } = {

View File

@ -70,5 +70,6 @@ export enum EventType {
GET_CERT = "get-cert", GET_CERT = "get-cert",
DELETE_CERT = "delete-cert", DELETE_CERT = "delete-cert",
REVOKE_CERT = "revoke-cert", REVOKE_CERT = "revoke-cert",
GET_CERT_BODY = "get-cert-body" GET_CERT_BODY = "get-cert-body",
ORG_ADMIN_ACCESS_PROJECT = "org-admin-accessed-project"
} }

View File

@ -579,6 +579,16 @@ interface GetCertBody {
}; };
} }
interface OrgAdminAccessProjectEvent {
type: EventType.ORG_ADMIN_ACCESS_PROJECT;
metadata: {
userId: string;
username: string;
email: string;
projectId: string;
}; // no metadata yet
}
export type Event = export type Event =
| GetSecretsEvent | GetSecretsEvent
| GetSecretEvent | GetSecretEvent
@ -635,7 +645,8 @@ export type Event =
| GetCert | GetCert
| DeleteCert | DeleteCert
| RevokeCert | RevokeCert
| GetCertBody; | GetCertBody
| OrgAdminAccessProjectEvent;
export type AuditLog = { export type AuditLog = {
id: string; id: string;

View File

@ -19,6 +19,7 @@ export * from "./keys";
export * from "./kms"; export * from "./kms";
export * from "./ldapConfig"; export * from "./ldapConfig";
export * from "./oidcConfig"; export * from "./oidcConfig";
export * from "./orgAdmin";
export * from "./organization"; export * from "./organization";
export * from "./projectUserAdditionalPrivilege"; export * from "./projectUserAdditionalPrivilege";
export * from "./rateLimit"; export * from "./rateLimit";

View File

@ -110,8 +110,15 @@ export const useCreateIntegration = () => {
export const useDeleteIntegration = () => { export const useDeleteIntegration = () => {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
return useMutation<{}, {}, { id: string; workspaceId: string }>({ return useMutation<
mutationFn: ({ id }) => apiRequest.delete(`/api/v1/integration/${id}`), {},
{},
{ id: string; workspaceId: string; shouldDeleteIntegrationSecrets: boolean }
>({
mutationFn: ({ id, shouldDeleteIntegrationSecrets }) =>
apiRequest.delete(
`/api/v1/integration/${id}?shouldDeleteIntegrationSecrets=${shouldDeleteIntegrationSecrets}`
),
onSuccess: (_, { workspaceId }) => { onSuccess: (_, { workspaceId }) => {
queryClient.invalidateQueries(workspaceKeys.getWorkspaceIntegrations(workspaceId)); queryClient.invalidateQueries(workspaceKeys.getWorkspaceIntegrations(workspaceId));
queryClient.invalidateQueries(workspaceKeys.getWorkspaceAuthorization(workspaceId)); queryClient.invalidateQueries(workspaceKeys.getWorkspaceAuthorization(workspaceId));

View File

@ -0,0 +1,2 @@
export { useOrgAdminAccessProject } from "./mutation";
export { useOrgAdminGetProjects } from "./queries";

View File

@ -0,0 +1,15 @@
import { useMutation } from "@tanstack/react-query";
import { apiRequest } from "@app/config/request";
import { TOrgAdminAccessProjectDTO } from "./types";
export const useOrgAdminAccessProject = () =>
useMutation({
mutationFn: async ({ projectId }: TOrgAdminAccessProjectDTO) => {
const { data } = await apiRequest.post(
`/api/v1/organization-admin/projects/${projectId}/grant-admin-access`
);
return data;
}
});

View File

@ -0,0 +1,30 @@
import { useQuery } from "@tanstack/react-query";
import { apiRequest } from "@app/config/request";
import { Workspace } from "../types";
import { TOrgAdminGetProjectsDTO } from "./types";
export const orgAdminQueryKeys = {
getProjects: (filter: TOrgAdminGetProjectsDTO) => ["org-admin-projects", filter] as const
};
export const useOrgAdminGetProjects = ({ search, offset, limit = 50 }: TOrgAdminGetProjectsDTO) => {
return useQuery({
queryKey: orgAdminQueryKeys.getProjects({ search, offset, limit }),
queryFn: async () => {
const { data } = await apiRequest.get<{ projects: Workspace[]; count: number }>(
"/api/v1/organization-admin/projects",
{
params: {
limit,
offset,
search
}
}
);
return data;
}
});
};

View File

@ -0,0 +1,9 @@
export type TOrgAdminGetProjectsDTO = {
limit?: number;
offset?: number;
search?: string;
};
export type TOrgAdminAccessProjectDTO = {
projectId: string;
};

View File

@ -5,6 +5,5 @@ export type TRateLimit = {
authRateLimit: number; authRateLimit: number;
inviteUserRateLimit: number; inviteUserRateLimit: number;
mfaRateLimit: number; mfaRateLimit: number;
creationLimit: number;
publicEndpointLimit: number; publicEndpointLimit: number;
}; };

View File

@ -194,7 +194,6 @@ const fetchSecretApprovalRequestDetails = async ({
export const useGetSecretApprovalRequestDetails = ({ export const useGetSecretApprovalRequestDetails = ({
id, id,
decryptKey,
options = {} options = {}
}: TGetSecretApprovalRequestDetails & { }: TGetSecretApprovalRequestDetails & {
options?: Omit< options?: Omit<
@ -210,7 +209,7 @@ export const useGetSecretApprovalRequestDetails = ({
useQuery({ useQuery({
queryKey: secretApprovalRequestKeys.detail({ id }), queryKey: secretApprovalRequestKeys.detail({ id }),
queryFn: () => fetchSecretApprovalRequestDetails({ id }), queryFn: () => fetchSecretApprovalRequestDetails({ id }),
enabled: Boolean(id && decryptKey) && (options?.enabled ?? true) enabled: Boolean(id) && (options?.enabled ?? true)
}); });
const fetchSecretApprovalRequestCount = async ({ workspaceId }: TGetSecretApprovalRequestCount) => { const fetchSecretApprovalRequestCount = async ({ workspaceId }: TGetSecretApprovalRequestCount) => {

View File

@ -1,4 +1,3 @@
import { UserWsKeyPair } from "../keys/types";
import { TSecretApprovalPolicy } from "../secretApproval/types"; import { TSecretApprovalPolicy } from "../secretApproval/types";
import { SecretV3Raw } from "../secrets/types"; import { SecretV3Raw } from "../secrets/types";
import { WsTag } from "../tags/types"; import { WsTag } from "../tags/types";
@ -110,7 +109,6 @@ export type TGetSecretApprovalRequestCount = {
export type TGetSecretApprovalRequestDetails = { export type TGetSecretApprovalRequestDetails = {
id: string; id: string;
decryptKey: UserWsKeyPair;
}; };
export type TUpdateSecretApprovalReviewStatusDTO = { export type TUpdateSecretApprovalReviewStatusDTO = {

View File

@ -317,6 +317,7 @@ export const useDeleteWorkspace = () => {
}, },
onSuccess: () => { onSuccess: () => {
queryClient.invalidateQueries(workspaceKeys.getAllUserWorkspace); queryClient.invalidateQueries(workspaceKeys.getAllUserWorkspace);
queryClient.invalidateQueries(["org-admin-projects"]);
} }
}); });
}; };

View File

@ -21,6 +21,7 @@ export type Workspace = {
pitVersionLimit: number; pitVersionLimit: number;
auditLogsRetentionDays: number; auditLogsRetentionDays: number;
slug: string; slug: string;
createdAt: string;
}; };
export type WorkspaceEnv = { export type WorkspaceEnv = {

View File

@ -13,7 +13,7 @@ interface UsePopUpProps {
export type UsePopUpState<T extends Readonly<string[]> | UsePopUpProps[]> = { export type UsePopUpState<T extends Readonly<string[]> | UsePopUpProps[]> = {
[P in T extends UsePopUpProps[] ? T[number]["name"] : T[number]]: { [P in T extends UsePopUpProps[] ? T[number]["name"] : T[number]]: {
isOpen: boolean; isOpen: boolean;
data?: unknown; data?: any;
}; };
}; };

View File

@ -286,12 +286,12 @@ export const AppLayout = ({ children }: LayoutProps) => {
// eslint-disable-next-line no-promise-executor-return -- We do this because the function returns too fast, which sometimes causes an error when the user is redirected. // eslint-disable-next-line no-promise-executor-return -- We do this because the function returns too fast, which sometimes causes an error when the user is redirected.
await new Promise((resolve) => setTimeout(resolve, 2_000)); await new Promise((resolve) => setTimeout(resolve, 2_000));
createNotification({ text: "Workspace created", type: "success" }); createNotification({ text: "Project created", type: "success" });
handlePopUpClose("addNewWs"); handlePopUpClose("addNewWs");
router.push(`/project/${newProjectId}/secrets/overview`); router.push(`/project/${newProjectId}/secrets/overview`);
} catch (err) { } catch (err) {
console.error(err); console.error(err);
createNotification({ text: "Failed to create workspace", type: "error" }); createNotification({ text: "Failed to create project", type: "error" });
} }
}; };
@ -476,10 +476,15 @@ export const AppLayout = ({ children }: LayoutProps) => {
{user?.superAdmin && ( {user?.superAdmin && (
<Link href="/admin" legacyBehavior> <Link href="/admin" legacyBehavior>
<DropdownMenuItem className="mt-1 border-t border-mineshaft-600"> <DropdownMenuItem className="mt-1 border-t border-mineshaft-600">
Admin Panel Server Admin Panel
</DropdownMenuItem> </DropdownMenuItem>
</Link> </Link>
)} )}
<Link href={`/org/${currentOrg?.id}/admin`} legacyBehavior>
<DropdownMenuItem className="mt-1 border-t border-mineshaft-600">
Organization Admin Console
</DropdownMenuItem>
</Link>
<div className="mt-1 h-1 border-t border-mineshaft-600" /> <div className="mt-1 h-1 border-t border-mineshaft-600" />
<button type="button" onClick={logOutUser} className="w-full"> <button type="button" onClick={logOutUser} className="w-full">
<DropdownMenuItem>Log Out</DropdownMenuItem> <DropdownMenuItem>Log Out</DropdownMenuItem>

View File

@ -0,0 +1,21 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
import { useTranslation } from "react-i18next";
import Head from "next/head";
import { OrgAdminPage } from "@app/views/OrgAdminPage";
export default function SettingsOrg() {
const { t } = useTranslation();
return (
<>
<Head>
<title>{t("common.head-title", { title: t("settings.org.title") })}</title>
<link rel="icon" href="/infisical.ico" />
</Head>
<OrgAdminPage />
</>
);
}
SettingsOrg.requireAuth = true;

File diff suppressed because it is too large Load Diff

View File

@ -106,9 +106,13 @@ export const IntegrationsPage = withProjectPermission(
handleProviderIntegration(provider); handleProviderIntegration(provider);
}; };
const handleIntegrationDelete = async (integrationId: string, cb: () => void) => { const handleIntegrationDelete = async (
integrationId: string,
shouldDeleteIntegrationSecrets: boolean,
cb: () => void
) => {
try { try {
await deleteIntegration({ id: integrationId, workspaceId }); await deleteIntegration({ id: integrationId, workspaceId, shouldDeleteIntegrationSecrets });
if (cb) cb(); if (cb) cb();
createNotification({ createNotification({
type: "success", type: "success",
@ -152,7 +156,7 @@ export const IntegrationsPage = withProjectPermission(
isLoading={isIntegrationLoading} isLoading={isIntegrationLoading}
integrations={integrations} integrations={integrations}
environments={environments} environments={environments}
onIntegrationDelete={({ id }, cb) => handleIntegrationDelete(id, cb)} onIntegrationDelete={handleIntegrationDelete}
workspaceId={workspaceId} workspaceId={workspaceId}
/> />
<CloudIntegrationSection <CloudIntegrationSection

View File

@ -7,6 +7,7 @@ import { integrationSlugNameMapping } from "public/data/frequentConstants";
import { ProjectPermissionCan } from "@app/components/permissions"; import { ProjectPermissionCan } from "@app/components/permissions";
import { import {
Button, Button,
Checkbox,
DeleteActionModal, DeleteActionModal,
EmptyState, EmptyState,
FormLabel, FormLabel,
@ -16,7 +17,7 @@ import {
Tooltip Tooltip
} from "@app/components/v2"; } from "@app/components/v2";
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context"; import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context";
import { usePopUp } from "@app/hooks"; import { usePopUp, useToggle } from "@app/hooks";
import { useSyncIntegration } from "@app/hooks/api/integrations/queries"; import { useSyncIntegration } from "@app/hooks/api/integrations/queries";
import { IntegrationMappingBehavior } from "@app/hooks/api/integrations/types"; import { IntegrationMappingBehavior } from "@app/hooks/api/integrations/types";
import { TIntegration } from "@app/hooks/api/types"; import { TIntegration } from "@app/hooks/api/types";
@ -25,7 +26,11 @@ type Props = {
environments: Array<{ name: string; slug: string; id: string }>; environments: Array<{ name: string; slug: string; id: string }>;
integrations?: TIntegration[]; integrations?: TIntegration[];
isLoading?: boolean; isLoading?: boolean;
onIntegrationDelete: (integration: TIntegration, cb: () => void) => void; onIntegrationDelete: (
integrationId: string,
shouldDeleteIntegrationSecrets: boolean,
cb: () => void
) => Promise<void>;
workspaceId: string; workspaceId: string;
}; };
@ -37,10 +42,12 @@ export const IntegrationsSection = ({
workspaceId workspaceId
}: Props) => { }: Props) => {
const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([ const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([
"deleteConfirmation" "deleteConfirmation",
"deleteSecretsConfirmation"
] as const); ] as const);
const { mutate: syncIntegration } = useSyncIntegration(); const { mutate: syncIntegration } = useSyncIntegration();
const [shouldDeleteSecrets, setShouldDeleteSecrets] = useToggle(false);
return ( return (
<div className="mb-8"> <div className="mb-8">
@ -249,7 +256,10 @@ export const IntegrationsSection = ({
<div className="flex items-end opacity-80 duration-200 hover:opacity-100"> <div className="flex items-end opacity-80 duration-200 hover:opacity-100">
<Tooltip content="Remove Integration"> <Tooltip content="Remove Integration">
<IconButton <IconButton
onClick={() => handlePopUpOpen("deleteConfirmation", integration)} onClick={() => {
setShouldDeleteSecrets.off();
handlePopUpOpen("deleteConfirmation", integration);
}}
ariaLabel="delete" ariaLabel="delete"
isDisabled={!isAllowed} isDisabled={!isAllowed}
colorSchema="danger" colorSchema="danger"
@ -281,11 +291,49 @@ export const IntegrationsSection = ({
(popUp?.deleteConfirmation?.data as TIntegration)?.integration || (popUp?.deleteConfirmation?.data as TIntegration)?.integration ||
"" ""
} }
onDeleteApproved={async () => onDeleteApproved={async () => {
onIntegrationDelete(popUp?.deleteConfirmation.data as TIntegration, () => if (shouldDeleteSecrets) {
handlePopUpClose("deleteConfirmation") handlePopUpOpen("deleteSecretsConfirmation");
) return;
} }
await onIntegrationDelete(
(popUp?.deleteConfirmation.data as TIntegration).id,
false,
() => handlePopUpClose("deleteConfirmation")
);
}}
>
{(popUp?.deleteConfirmation?.data as TIntegration)?.integration === "github" && (
<div className="mt-4">
<Checkbox
id="delete-integration-secrets"
checkIndicatorBg="text-white"
onCheckedChange={() => setShouldDeleteSecrets.toggle()}
>
Delete previously synced secrets from the destination
</Checkbox>
</div>
)}
</DeleteActionModal>
<DeleteActionModal
isOpen={popUp.deleteSecretsConfirmation.isOpen}
title={`Are you sure you also want to delete secrets on ${
(popUp?.deleteConfirmation.data as TIntegration)?.integration
}?`}
subTitle="By confirming, you acknowledge that all secrets managed by this integration will be removed from the destination. This action is irreversible."
onChange={(isOpen) => handlePopUpToggle("deleteSecretsConfirmation", isOpen)}
deleteKey="confirm"
onDeleteApproved={async () => {
await onIntegrationDelete(
(popUp?.deleteConfirmation.data as TIntegration).id,
true,
() => {
handlePopUpClose("deleteSecretsConfirmation");
handlePopUpClose("deleteConfirmation");
}
);
}}
/> />
</div> </div>
); );

View File

@ -1,133 +0,0 @@
import { useEffect, useMemo } from "react";
import { Control, Controller, UseFormSetValue, useWatch } from "react-hook-form";
import { faMoneyBill } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { motion } from "framer-motion";
import { twMerge } from "tailwind-merge";
import { Checkbox, Select, SelectItem } from "@app/components/v2";
import { useToggle } from "@app/hooks";
import { TFormSchema } from "../../../../RolePage/components/OrgRoleModifySection.utils";
type Props = {
isNonEditable?: boolean;
setValue: UseFormSetValue<TFormSchema>;
control: Control<TFormSchema>;
};
enum Permission {
NoAccess = "no-access",
ReadOnly = "read-only",
FullAccess = "full-acess",
Custom = "custom"
}
const PERMISSIONS = [
{ action: "read", label: "View projects" },
{ action: "create", label: "Create new projects" }
] as const;
export const WorkspacePermission = ({ isNonEditable, setValue, control }: Props) => {
const rule = useWatch({
control,
name: "permissions.workspace"
});
const [isCustom, setIsCustom] = useToggle();
const selectedPermissionCategory = useMemo(() => {
const actions = Object.keys(rule || {}) as Array<keyof typeof rule>;
const totalActions = PERMISSIONS.length;
const score = actions.map((key) => (rule?.[key] ? 1 : 0)).reduce((a, b) => a + b, 0 as number);
if (isCustom) return Permission.Custom;
if (score === 0) return Permission.NoAccess;
if (score === totalActions) return Permission.FullAccess;
if (score === 1 && rule?.read) return Permission.ReadOnly;
return Permission.Custom;
}, [rule, isCustom]);
useEffect(() => {
if (selectedPermissionCategory === Permission.Custom) setIsCustom.on();
else setIsCustom.off();
}, [selectedPermissionCategory]);
const handlePermissionChange = (val: Permission) => {
if (val === Permission.Custom) setIsCustom.on();
else setIsCustom.off();
switch (val) {
case Permission.NoAccess:
setValue("permissions.workspace", { read: false, create: false }, { shouldDirty: true });
break;
case Permission.FullAccess:
setValue("permissions.workspace", { read: true, create: true }, { shouldDirty: true });
break;
case Permission.ReadOnly:
setValue("permissions.workspace", { read: true, create: false }, { shouldDirty: true });
break;
default:
setValue("permissions.workspace", { read: false, create: false }, { shouldDirty: true });
break;
}
};
return (
<div
className={twMerge(
"rounded-md bg-mineshaft-800 px-10 py-6",
selectedPermissionCategory !== Permission.NoAccess && "border-l-2 border-primary-600"
)}
>
<div className="flex items-center space-x-4">
<div>
<FontAwesomeIcon icon={faMoneyBill} className="text-4xl" />
</div>
<div className="flex flex-grow flex-col">
<div className="mb-1 text-lg font-medium">Project</div>
<div className="text-xs font-light">
View and create new projects in this organization
</div>
</div>
<div>
<Select
defaultValue={Permission.NoAccess}
isDisabled={isNonEditable}
value={selectedPermissionCategory}
onValueChange={handlePermissionChange}
>
<SelectItem value={Permission.NoAccess}>No Access</SelectItem>
<SelectItem value={Permission.ReadOnly}>Read Only</SelectItem>
<SelectItem value={Permission.FullAccess}>Full Access</SelectItem>
<SelectItem value={Permission.Custom}>Custom</SelectItem>
</Select>
</div>
</div>
<motion.div
initial={false}
animate={{ height: isCustom ? "2.5rem" : 0, paddingTop: isCustom ? "1rem" : 0 }}
className="grid auto-cols-min grid-flow-col gap-8 overflow-hidden"
>
{isCustom &&
PERMISSIONS.map(({ action, label }) => (
<Controller
name={`permissions.workspace.${action}`}
key={`permissions.workspace.${action}`}
control={control}
render={({ field }) => (
<Checkbox
isChecked={field.value}
onCheckedChange={field.onChange}
id={`permissions.workspace.${action}`}
isDisabled={isNonEditable}
>
{label}
</Checkbox>
)}
/>
))}
</motion.div>
</div>
);
};

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