mirror of
https://github.com/Infisical/infisical.git
synced 2025-07-02 16:55:02 +00:00
Compare commits
81 Commits
doc/add-do
...
infisical/
Author | SHA1 | Date | |
---|---|---|---|
4f998e3940 | |||
1248840dc8 | |||
64c8125e4b | |||
8d33647739 | |||
d1c142e5b1 | |||
bb1cad0c5b | |||
2a1cfe15b4 | |||
881d70bc64 | |||
902a0b0ed4 | |||
ba92192537 | |||
26ed8df73c | |||
c1decab912 | |||
216c073290 | |||
1070954bdd | |||
cc689d3178 | |||
e6848828f2 | |||
c8b93e4467 | |||
0bca24bb00 | |||
c563ada50f | |||
26d1616e22 | |||
5fd071d1de | |||
a6ac78356b | |||
e4a2137991 | |||
9721d7a15e | |||
93db5c4555 | |||
ad4393fdef | |||
cd06e4e7f3 | |||
711a4179ce | |||
b4a2a477d3 | |||
8e53a1b171 | |||
71af463ad8 | |||
7abd18b11c | |||
1aee50a751 | |||
0f23b7e1d3 | |||
e9b37a1f98 | |||
33193a47ae | |||
43fded2350 | |||
7b6f4d810d | |||
1ad286ca87 | |||
be7c11a3f5 | |||
b97bbe5beb | |||
cf5260b383 | |||
13e0dd8e0f | |||
7f9150e60e | |||
995f0360fb | |||
ecab69a7ab | |||
cca36ab106 | |||
76311a1b5f | |||
55a6740714 | |||
a0490d0fde | |||
78e41a51c0 | |||
8414f04e94 | |||
79e414ea9f | |||
83772c1770 | |||
09928efba3 | |||
48eb4e772f | |||
7467a05fc4 | |||
afba636850 | |||
96cc315762 | |||
e95d7e55c1 | |||
520c068ac4 | |||
9f0d7c6d11 | |||
683e3dd7be | |||
46ca3856b3 | |||
891cb06de0 | |||
02e8f20cbf | |||
d5f4ce4376 | |||
85653a90d5 | |||
879ef2c178 | |||
8777cfe680 | |||
2b630f75aa | |||
91cee20cc8 | |||
4249ec6030 | |||
e7a95e6af2 | |||
a9f04a3c1f | |||
3d380710ee | |||
2177ec6bcc | |||
070eb2aacd | |||
e619cfa313 | |||
c3038e3ca1 | |||
ff0e7feeee |
@ -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
|
||||||
|
6
.github/workflows/run-backend-tests.yml
vendored
6
.github/workflows/run-backend-tests.yml
vendored
@ -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
|
55
backend/package-lock.json
generated
55
backend/package-lock.json
generated
@ -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",
|
||||||
|
@ -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",
|
||||||
|
4
backend/src/@types/fastify.d.ts
vendored
4
backend/src/@types/fastify.d.ts
vendored
@ -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
|
||||||
|
@ -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");
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -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();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
@ -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>;
|
||||||
|
@ -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()
|
||||||
|
@ -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>;
|
||||||
|
@ -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: {
|
||||||
|
@ -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;
|
||||||
|
@ -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) {
|
||||||
|
@ -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(
|
||||||
|
@ -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)
|
||||||
|
@ -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 };
|
||||||
|
@ -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) => {
|
||||||
|
@ -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 = {
|
||||||
|
@ -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 });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -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]
|
||||||
|
@ -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);
|
||||||
|
@ -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;
|
||||||
|
};
|
||||||
|
@ -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)
|
||||||
|
@ -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"
|
||||||
|
@ -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" });
|
||||||
|
@ -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
|
||||||
};
|
};
|
||||||
|
|
||||||
|
38
backend/src/server/plugins/inject-rate-limits.ts
Normal file
38
backend/src/server/plugins/inject-rate-limits.ts
Normal 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
|
||||||
|
};
|
||||||
|
});
|
||||||
|
});
|
@ -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({
|
||||||
|
@ -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({
|
||||||
|
@ -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
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
@ -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: {
|
||||||
|
@ -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" });
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
90
backend/src/server/routes/v1/org-admin-router.ts
Normal file
90
backend/src/server/routes/v1/org-admin-router.ts
Normal 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 };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
@ -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({
|
||||||
|
@ -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",
|
||||||
|
@ -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:
|
||||||
|
@ -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
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@ -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;
|
||||||
|
@ -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"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
@ -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);
|
||||||
|
@ -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 = {
|
||||||
|
5
backend/src/services/org-admin/org-admin-dal.ts
Normal file
5
backend/src/services/org-admin/org-admin-dal.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
export type TOrgAdminDALFactory = ReturnType<typeof orgAdminDALFactory>;
|
||||||
|
|
||||||
|
export const orgAdminDALFactory = () => {
|
||||||
|
return {};
|
||||||
|
};
|
191
backend/src/services/org-admin/org-admin-service.ts
Normal file
191
backend/src/services/org-admin/org-admin-service.ts
Normal 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 };
|
||||||
|
};
|
11
backend/src/services/org-admin/org-admin-types.ts
Normal file
11
backend/src/services/org-admin/org-admin-types.ts
Normal 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">;
|
@ -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`)
|
||||||
|
@ -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,
|
||||||
|
@ -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."
|
||||||
});
|
});
|
||||||
|
@ -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);
|
||||||
|
6
backend/src/services/secret-folder/secret-folder-fns.ts
Normal file
6
backend/src/services/secret-folder/secret-folder-fns.ts
Normal 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));
|
@ -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" });
|
||||||
|
@ -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,
|
||||||
|
@ -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 || "";
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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 {
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
|
@ -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 {
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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"
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
15
company/handbook/meetings.mdx
Normal file
15
company/handbook/meetings.mdx
Normal 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).
|
@ -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"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
@ -0,0 +1,4 @@
|
|||||||
|
---
|
||||||
|
title: "Sign certificate"
|
||||||
|
openapi: "POST /api/v1/pki/ca/{caId}/sign-certificate"
|
||||||
|
---
|
@ -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>
|
||||||
|
|
||||||
|
|
||||||
|
@ -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.
|
||||||

|

|
||||||
|
|
||||||
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.
|
||||||
|
@ -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>
|
||||||
|
|
||||||
|
@ -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)
|
||||||
|
@ -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"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
@ -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>
|
||||||
|
@ -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>
|
||||||
|
@ -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 />
|
|
||||||
);
|
|
||||||
};
|
|
@ -1 +0,0 @@
|
|||||||
export { UpgradeOverlay } from "./UpgradeOverlay";
|
|
@ -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>
|
|
||||||
);
|
|
||||||
};
|
|
@ -1 +0,0 @@
|
|||||||
export { UpgradeProjectAlert } from "./UpgradeProjectAlert";
|
|
@ -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>;
|
||||||
|
@ -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",
|
||||||
|
14
frontend/src/helpers/parseEnvVar.ts
Normal file
14
frontend/src/helpers/parseEnvVar.ts
Normal 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()
|
||||||
|
};
|
||||||
|
};
|
@ -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 } = {
|
||||||
|
@ -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"
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
|
@ -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";
|
||||||
|
@ -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));
|
||||||
|
2
frontend/src/hooks/api/orgAdmin/index.tsx
Normal file
2
frontend/src/hooks/api/orgAdmin/index.tsx
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
export { useOrgAdminAccessProject } from "./mutation";
|
||||||
|
export { useOrgAdminGetProjects } from "./queries";
|
15
frontend/src/hooks/api/orgAdmin/mutation.tsx
Normal file
15
frontend/src/hooks/api/orgAdmin/mutation.tsx
Normal 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;
|
||||||
|
}
|
||||||
|
});
|
30
frontend/src/hooks/api/orgAdmin/queries.tsx
Normal file
30
frontend/src/hooks/api/orgAdmin/queries.tsx
Normal 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;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
9
frontend/src/hooks/api/orgAdmin/types.ts
Normal file
9
frontend/src/hooks/api/orgAdmin/types.ts
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
export type TOrgAdminGetProjectsDTO = {
|
||||||
|
limit?: number;
|
||||||
|
offset?: number;
|
||||||
|
search?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TOrgAdminAccessProjectDTO = {
|
||||||
|
projectId: string;
|
||||||
|
};
|
@ -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;
|
||||||
};
|
};
|
||||||
|
@ -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) => {
|
||||||
|
@ -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 = {
|
||||||
|
@ -317,6 +317,7 @@ export const useDeleteWorkspace = () => {
|
|||||||
},
|
},
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
queryClient.invalidateQueries(workspaceKeys.getAllUserWorkspace);
|
queryClient.invalidateQueries(workspaceKeys.getAllUserWorkspace);
|
||||||
|
queryClient.invalidateQueries(["org-admin-projects"]);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
@ -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 = {
|
||||||
|
@ -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;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -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>
|
||||||
|
21
frontend/src/pages/org/[id]/admin/index.tsx
Normal file
21
frontend/src/pages/org/[id]/admin/index.tsx
Normal 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
@ -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
|
||||||
|
@ -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>
|
||||||
);
|
);
|
||||||
|
@ -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
Reference in New Issue
Block a user