mirror of
https://github.com/Infisical/infisical.git
synced 2025-07-22 13:29:55 +00:00
Compare commits
160 Commits
doc/add-do
...
infisical/
Author | SHA1 | Date | |
---|---|---|---|
4570de09ae | |||
4feff5b4ca | |||
6081e2927e | |||
0b42f29916 | |||
b60d0992f4 | |||
a8a68f600c | |||
742f5f6621 | |||
f3cd7efe0e | |||
2b16c19b70 | |||
943b540383 | |||
e180021aa6 | |||
8e08c443ad | |||
dae26daeeb | |||
170f8d9add | |||
8d41ef198a | |||
69d60a227a | |||
c8eefcfbf9 | |||
53cec754cc | |||
5db3e177eb | |||
3fcc3ccff4 | |||
df07d7b6d7 | |||
28a655bef1 | |||
5f2cd04f46 | |||
897ce1f267 | |||
6afc17b84b | |||
9017a5e838 | |||
cb8e4d884e | |||
16807c3dd6 | |||
61791e385c | |||
bbd7bfb0f5 | |||
4de8c48b2c | |||
a4bbe2c612 | |||
541a2e7d05 | |||
ea4e51d826 | |||
3bc920c593 | |||
df38c761ad | |||
32a84471f2 | |||
ea14df2cbd | |||
6bd6cac366 | |||
45294253aa | |||
635fbdc80b | |||
d20c48b7cf | |||
1fc18fe23b | |||
99403e122b | |||
5176e70437 | |||
82b2b0af97 | |||
e313c866a2 | |||
2d81606049 | |||
718f4ef129 | |||
a42f3b3763 | |||
f7d882a6fc | |||
385afdfcf8 | |||
281d703cc3 | |||
6f56ed5474 | |||
809e4eeba1 | |||
254446c895 | |||
bb52e2beb4 | |||
2739b08e59 | |||
ba5e877a3b | |||
d2752216f6 | |||
d91fb0db02 | |||
4892eea009 | |||
09c6fcb73b | |||
79181a1e3d | |||
bb934ef7b1 | |||
cd9316537d | |||
942e5f2f65 | |||
353d231a4e | |||
68e05b7198 | |||
4f998e3940 | |||
1248840dc8 | |||
64c8125e4b | |||
c109fbab3e | |||
15fb01089b | |||
6f4be3e25a | |||
8d33647739 | |||
d1c142e5b1 | |||
bb1cad0c5b | |||
2a1cfe15b4 | |||
881d70bc64 | |||
14c1b4f07b | |||
3028bdd424 | |||
902a0b0ed4 | |||
ba92192537 | |||
26ed8df73c | |||
c1decab912 | |||
216c073290 | |||
8626bce632 | |||
c5a2b0321f | |||
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 | |||
dbe771dba0 | |||
273fd6c98f | |||
d5f4ce4376 | |||
18aac6508b | |||
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
|
||||
- uses: KengoTODA/actions-setup-docker-compose@v1
|
||||
if: ${{ env.ACT }}
|
||||
name: Install `docker-compose` for local simulations
|
||||
name: Install `docker compose` for local simulations
|
||||
with:
|
||||
version: "2.14.2"
|
||||
- name: 📦Build the latest image
|
||||
run: docker build --tag infisical-api .
|
||||
working-directory: backend
|
||||
- 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
|
||||
run: |
|
||||
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
|
||||
- name: cleanup
|
||||
run: |
|
||||
docker-compose -f "docker-compose.dev.yml" down
|
||||
docker compose -f "docker-compose.dev.yml" down
|
||||
docker stop 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: KengoTODA/actions-setup-docker-compose@v1
|
||||
if: ${{ env.ACT }}
|
||||
name: Install `docker-compose` for local simulations
|
||||
name: Install `docker compose` for local simulations
|
||||
with:
|
||||
version: "2.14.2"
|
||||
- name: 🔧 Setup Node 20
|
||||
@ -33,7 +33,7 @@ jobs:
|
||||
run: npm install
|
||||
working-directory: backend
|
||||
- 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
|
||||
run: npm run test:e2e
|
||||
working-directory: backend
|
||||
@ -44,4 +44,4 @@ jobs:
|
||||
ENCRYPTION_KEY: 4bnfe4e407b8921c104518903515b218
|
||||
- name: cleanup
|
||||
run: |
|
||||
docker-compose -f "docker-compose.dev.yml" down
|
||||
docker compose -f "docker-compose.dev.yml" down
|
2
.github/workflows/run-cli-tests.yml
vendored
2
.github/workflows/run-cli-tests.yml
vendored
@ -50,6 +50,6 @@ jobs:
|
||||
CLI_TESTS_ENV_SLUG: ${{ secrets.CLI_TESTS_ENV_SLUG }}
|
||||
CLI_TESTS_USER_EMAIL: ${{ secrets.CLI_TESTS_USER_EMAIL }}
|
||||
CLI_TESTS_USER_PASSWORD: ${{ secrets.CLI_TESTS_USER_PASSWORD }}
|
||||
INFISICAL_VAULT_FILE_PASSPHRASE: ${{ secrets.CLI_TESTS_INFISICAL_VAULT_FILE_PASSPHRASE }}
|
||||
# INFISICAL_VAULT_FILE_PASSPHRASE: ${{ secrets.CLI_TESTS_INFISICAL_VAULT_FILE_PASSPHRASE }}
|
||||
|
||||
run: go test -v -count=1 ./test
|
||||
|
13
Makefile
13
Makefile
@ -15,3 +15,16 @@ up-prod:
|
||||
|
||||
down:
|
||||
docker compose -f docker-compose.dev.yml down
|
||||
|
||||
reviewable-ui:
|
||||
cd frontend && \
|
||||
npm run lint:fix && \
|
||||
npm run type:check
|
||||
|
||||
reviewable-api:
|
||||
cd backend && \
|
||||
npm run lint:fix && \
|
||||
npm run type:check
|
||||
|
||||
reviewable: reviewable-ui reviewable-api
|
||||
|
||||
|
55
backend/package-lock.json
generated
55
backend/package-lock.json
generated
@ -25,6 +25,7 @@
|
||||
"@fastify/swagger": "^8.14.0",
|
||||
"@fastify/swagger-ui": "^2.1.0",
|
||||
"@node-saml/passport-saml": "^4.0.4",
|
||||
"@octokit/plugin-retry": "^5.0.5",
|
||||
"@octokit/rest": "^20.0.2",
|
||||
"@octokit/webhooks-types": "^7.3.1",
|
||||
"@peculiar/asn1-schema": "^2.3.8",
|
||||
@ -7812,19 +7813,45 @@
|
||||
}
|
||||
},
|
||||
"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==",
|
||||
"version": "5.0.5",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/plugin-retry/-/plugin-retry-5.0.5.tgz",
|
||||
"integrity": "sha512-sB1RWMhSrre02Atv95K6bhESlJ/sPdZkK/wE/w1IdSCe0yM6FxSjksLa6T7aAvxvxlLKzQEC4KIiqpqyov1Tbg==",
|
||||
"dependencies": {
|
||||
"@octokit/request-error": "^5.0.0",
|
||||
"@octokit/types": "^12.0.0",
|
||||
"@octokit/request-error": "^4.0.1",
|
||||
"@octokit/types": "^10.0.0",
|
||||
"bottleneck": "^2.15.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 18"
|
||||
},
|
||||
"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": {
|
||||
@ -17396,6 +17423,22 @@
|
||||
"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": {
|
||||
"version": "11.1.0",
|
||||
"resolved": "https://registry.npmjs.org/commander/-/commander-11.1.0.tgz",
|
||||
|
@ -121,6 +121,7 @@
|
||||
"@fastify/swagger": "^8.14.0",
|
||||
"@fastify/swagger-ui": "^2.1.0",
|
||||
"@node-saml/passport-saml": "^4.0.4",
|
||||
"@octokit/plugin-retry": "^5.0.5",
|
||||
"@octokit/rest": "^20.0.2",
|
||||
"@octokit/webhooks-types": "^7.3.1",
|
||||
"@peculiar/asn1-schema": "^2.3.8",
|
||||
|
@ -7,14 +7,33 @@ const prompt = promptSync({
|
||||
sigint: true
|
||||
});
|
||||
|
||||
type ComponentType = 1 | 2 | 3;
|
||||
|
||||
console.log(`
|
||||
Component List
|
||||
--------------
|
||||
0. Exit
|
||||
1. Service component
|
||||
2. DAL component
|
||||
3. Router component
|
||||
`);
|
||||
const componentType = parseInt(prompt("Select a component: "), 10);
|
||||
|
||||
function getComponentType(): ComponentType {
|
||||
while (true) {
|
||||
const input = prompt("Select a component (0-3): ");
|
||||
const componentType = parseInt(input, 10);
|
||||
|
||||
if (componentType === 0) {
|
||||
console.log("Exiting the program. Goodbye!");
|
||||
process.exit(0);
|
||||
} else if (componentType === 1 || componentType === 2 || componentType === 3) {
|
||||
return componentType;
|
||||
} else {
|
||||
console.log("Invalid input. Please enter 0, 1, 2, or 3.");
|
||||
}
|
||||
}
|
||||
}
|
||||
const componentType = getComponentType();
|
||||
|
||||
if (componentType === 1) {
|
||||
const componentName = prompt("Enter service name: ");
|
||||
|
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 { 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 { RateLimitConfiguration } from "@app/ee/services/rate-limit/rate-limit-types";
|
||||
import { TSamlConfigServiceFactory } from "@app/ee/services/saml-config/saml-config-service";
|
||||
import { TScimServiceFactory } from "@app/ee/services/scim/scim-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 { TOrgRoleServiceFactory } from "@app/services/org/org-role-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 { TProjectBotServiceFactory } from "@app/services/project-bot/project-bot-service";
|
||||
import { TProjectEnvServiceFactory } from "@app/services/project-env/project-env-service";
|
||||
@ -88,6 +90,7 @@ declare module "fastify" {
|
||||
id: string;
|
||||
orgId: string;
|
||||
};
|
||||
rateLimits: RateLimitConfiguration;
|
||||
// passport data
|
||||
passportUser: {
|
||||
isUserCompleted: string;
|
||||
@ -165,6 +168,7 @@ declare module "fastify" {
|
||||
rateLimit: TRateLimitServiceFactory;
|
||||
userEngagement: TUserEngagementServiceFactory;
|
||||
externalKms: TExternalKmsServiceFactory;
|
||||
orgAdmin: TOrgAdminServiceFactory;
|
||||
};
|
||||
// this is exclusive use for middlewares in which we need to inject data
|
||||
// everywhere else access using service layer
|
||||
|
@ -1,178 +1,8 @@
|
||||
/* eslint-disable @typescript-eslint/ban-ts-comment */
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { infisicalSymmetricDecrypt } from "@app/lib/crypto/encryption";
|
||||
import { selectAllTableCols } from "@app/lib/knex/select";
|
||||
|
||||
import { SecretKeyEncoding, SecretType, TableName } from "../schemas";
|
||||
import { SecretType, TableName } from "../schemas";
|
||||
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> {
|
||||
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");
|
||||
});
|
||||
}
|
||||
|
||||
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> {
|
||||
@ -356,49 +178,4 @@ export async function down(knex: Knex): Promise<void> {
|
||||
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();
|
||||
}
|
||||
});
|
||||
}
|
21
backend/src/db/migrations/20240806185442_drop-tag-name.ts
Normal file
21
backend/src/db/migrations/20240806185442_drop-tag-name.ts
Normal file
@ -0,0 +1,21 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TableName } from "../schemas";
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
const hasNameField = await knex.schema.hasColumn(TableName.SecretTag, "name");
|
||||
if (hasNameField) {
|
||||
await knex.schema.alterTable(TableName.SecretTag, (t) => {
|
||||
t.dropColumn("name");
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
const hasNameField = await knex.schema.hasColumn(TableName.SecretTag, "name");
|
||||
if (!hasNameField) {
|
||||
await knex.schema.alterTable(TableName.SecretTag, (t) => {
|
||||
t.string("name");
|
||||
});
|
||||
}
|
||||
}
|
@ -5,8 +5,6 @@
|
||||
|
||||
import { z } from "zod";
|
||||
|
||||
import { zodBuffer } from "@app/lib/zod";
|
||||
|
||||
import { TImmutableDBKeys } from "./models";
|
||||
|
||||
export const DynamicSecretsSchema = z.object({
|
||||
@ -16,12 +14,16 @@ export const DynamicSecretsSchema = z.object({
|
||||
type: z.string(),
|
||||
defaultTTL: z.string(),
|
||||
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(),
|
||||
status: z.string().nullable().optional(),
|
||||
statusDetails: z.string().nullable().optional(),
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date(),
|
||||
encryptedConfig: zodBuffer
|
||||
updatedAt: z.date()
|
||||
});
|
||||
|
||||
export type TDynamicSecrets = z.infer<typeof DynamicSecretsSchema>;
|
||||
|
@ -15,7 +15,6 @@ export const RateLimitSchema = z.object({
|
||||
authRateLimit: z.number().default(60),
|
||||
inviteUserRateLimit: z.number().default(30),
|
||||
mfaRateLimit: z.number().default(20),
|
||||
creationLimit: z.number().default(30),
|
||||
publicEndpointLimit: z.number().default(30),
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date()
|
||||
|
@ -9,7 +9,6 @@ import { TImmutableDBKeys } from "./models";
|
||||
|
||||
export const SecretTagsSchema = z.object({
|
||||
id: z.string().uuid(),
|
||||
name: z.string(),
|
||||
slug: z.string(),
|
||||
color: z.string().nullable().optional(),
|
||||
createdAt: z.date(),
|
||||
|
@ -5,22 +5,27 @@
|
||||
|
||||
import { z } from "zod";
|
||||
|
||||
import { zodBuffer } from "@app/lib/zod";
|
||||
|
||||
import { TImmutableDBKeys } from "./models";
|
||||
|
||||
export const WebhooksSchema = z.object({
|
||||
id: z.string().uuid(),
|
||||
secretPath: z.string().default("/"),
|
||||
url: z.string(),
|
||||
lastStatus: z.string().nullable().optional(),
|
||||
lastRunErrorMessage: z.string().nullable().optional(),
|
||||
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(),
|
||||
updatedAt: z.date(),
|
||||
envId: z.string().uuid(),
|
||||
type: z.string().default("general").nullable().optional(),
|
||||
encryptedSecretKeyWithKms: zodBuffer.nullable().optional(),
|
||||
encryptedUrl: zodBuffer
|
||||
urlCipherText: z.string().nullable().optional(),
|
||||
urlIV: z.string().nullable().optional(),
|
||||
urlTag: z.string().nullable().optional(),
|
||||
type: z.string().default("general").nullable().optional()
|
||||
});
|
||||
|
||||
export type TWebhooks = z.infer<typeof WebhooksSchema>;
|
||||
|
@ -58,7 +58,6 @@ export const registerRateLimitRouter = async (server: FastifyZodProvider) => {
|
||||
authRateLimit: z.number(),
|
||||
inviteUserRateLimit: z.number(),
|
||||
mfaRateLimit: z.number(),
|
||||
creationLimit: z.number(),
|
||||
publicEndpointLimit: z.number()
|
||||
}),
|
||||
response: {
|
||||
|
@ -75,15 +75,16 @@ export const auditLogDALFactory = (db: TDbClient) => {
|
||||
.del()
|
||||
.returning("id");
|
||||
numberOfRetryOnFailure = 0; // reset
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
await new Promise((resolve) => {
|
||||
setTimeout(resolve, 100); // time to breathe for db
|
||||
});
|
||||
} catch (error) {
|
||||
numberOfRetryOnFailure += 1;
|
||||
logger.error(error, "Failed to delete audit log on pruning");
|
||||
} finally {
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
await new Promise((resolve) => {
|
||||
setTimeout(resolve, 10); // time to breathe for db
|
||||
});
|
||||
}
|
||||
} while (deletedAuditLogIds.length > 0 && numberOfRetryOnFailure < MAX_RETRY_ON_FAILURE);
|
||||
} while (deletedAuditLogIds.length > 0 || numberOfRetryOnFailure < MAX_RETRY_ON_FAILURE);
|
||||
};
|
||||
|
||||
return { ...auditLogOrm, pruneAuditLog, find };
|
||||
|
@ -136,6 +136,7 @@ export enum EventType {
|
||||
IMPORT_CA_CERT = "import-certificate-authority-cert",
|
||||
GET_CA_CRL = "get-certificate-authority-crl",
|
||||
ISSUE_CERT = "issue-cert",
|
||||
SIGN_CERT = "sign-cert",
|
||||
GET_CERT = "get-cert",
|
||||
DELETE_CERT = "delete-cert",
|
||||
REVOKE_CERT = "revoke-cert",
|
||||
@ -146,7 +147,8 @@ export enum EventType {
|
||||
GET_KMS = "get-kms",
|
||||
UPDATE_PROJECT_KMS = "update-project-kms",
|
||||
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 {
|
||||
@ -336,6 +338,7 @@ interface DeleteIntegrationEvent {
|
||||
targetServiceId?: string;
|
||||
path?: 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 {
|
||||
type: EventType.GET_CERT;
|
||||
metadata: {
|
||||
@ -1235,6 +1247,16 @@ interface LoadProjectKmsBackupEvent {
|
||||
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 =
|
||||
| GetSecretsEvent
|
||||
| GetSecretEvent
|
||||
@ -1333,6 +1355,7 @@ export type Event =
|
||||
| ImportCaCert
|
||||
| GetCaCrl
|
||||
| IssueCert
|
||||
| SignCert
|
||||
| GetCert
|
||||
| DeleteCert
|
||||
| RevokeCert
|
||||
@ -1343,4 +1366,5 @@ export type Event =
|
||||
| GetKmsEvent
|
||||
| UpdateProjectKmsEvent
|
||||
| GetProjectKmsBackupEvent
|
||||
| LoadProjectKmsBackupEvent;
|
||||
| LoadProjectKmsBackupEvent
|
||||
| OrgAdminAccessProjectEvent;
|
||||
|
@ -12,10 +12,7 @@ export const dynamicSecretLeaseDALFactory = (db: TDbClient) => {
|
||||
|
||||
const countLeasesForDynamicSecret = async (dynamicSecretId: string, tx?: Knex) => {
|
||||
try {
|
||||
const doc = await (tx || db.replicaNode())(TableName.DynamicSecretLease)
|
||||
.count("*")
|
||||
.where({ dynamicSecretId })
|
||||
.first();
|
||||
const doc = await (tx || db)(TableName.DynamicSecretLease).count("*").where({ dynamicSecretId }).first();
|
||||
return parseInt(doc || "0", 10);
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: "DynamicSecretCountLeases" });
|
||||
@ -24,7 +21,7 @@ export const dynamicSecretLeaseDALFactory = (db: TDbClient) => {
|
||||
|
||||
const findById = async (id: string, tx?: Knex) => {
|
||||
try {
|
||||
const doc = await (tx || db.replicaNode())(TableName.DynamicSecretLease)
|
||||
const doc = await (tx || db)(TableName.DynamicSecretLease)
|
||||
.where({ [`${TableName.DynamicSecretLease}.id` as "id"]: id })
|
||||
.first()
|
||||
.join(
|
||||
@ -40,10 +37,14 @@ export const dynamicSecretLeaseDALFactory = (db: TDbClient) => {
|
||||
db.ref("type").withSchema(TableName.DynamicSecret).as("dynType"),
|
||||
db.ref("defaultTTL").withSchema(TableName.DynamicSecret).as("dynDefaultTTL"),
|
||||
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("status").withSchema(TableName.DynamicSecret).as("dynStatus"),
|
||||
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("updatedAt").withSchema(TableName.DynamicSecret).as("dynUpdatedAt")
|
||||
);
|
||||
@ -58,12 +59,16 @@ export const dynamicSecretLeaseDALFactory = (db: TDbClient) => {
|
||||
type: doc.dynType,
|
||||
defaultTTL: doc.dynDefaultTTL,
|
||||
maxTTL: doc.dynMaxTTL,
|
||||
inputIV: doc.dynInputIV,
|
||||
inputTag: doc.dynInputTag,
|
||||
inputCiphertext: doc.dynInputCiphertext,
|
||||
algorithm: doc.dynAlgorithm,
|
||||
keyEncoding: doc.dynKeyEncoding,
|
||||
folderId: doc.dynFolderId,
|
||||
status: doc.dynStatus,
|
||||
statusDetails: doc.dynStatusDetails,
|
||||
createdAt: doc.dynCreatedAt,
|
||||
updatedAt: doc.dynUpdatedAt,
|
||||
encryptedConfig: doc.dynEncryptedConfig
|
||||
updatedAt: doc.dynUpdatedAt
|
||||
}
|
||||
};
|
||||
} catch (error) {
|
||||
|
@ -1,9 +1,8 @@
|
||||
import { SecretKeyEncoding } from "@app/db/schemas";
|
||||
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 { 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 { DynamicSecretStatus } from "../dynamic-secret/dynamic-secret-types";
|
||||
@ -15,8 +14,6 @@ type TDynamicSecretLeaseQueueServiceFactoryDep = {
|
||||
dynamicSecretLeaseDAL: Pick<TDynamicSecretLeaseDALFactory, "findById" | "deleteById" | "find" | "updateById">;
|
||||
dynamicSecretDAL: Pick<TDynamicSecretDALFactory, "findById" | "deleteById" | "updateById">;
|
||||
dynamicSecretProviders: Record<DynamicSecretProviders, TDynamicProviderFns>;
|
||||
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
|
||||
folderDAL: Pick<TSecretFolderDALFactory, "findById">;
|
||||
};
|
||||
|
||||
export type TDynamicSecretLeaseQueueServiceFactory = ReturnType<typeof dynamicSecretLeaseQueueServiceFactory>;
|
||||
@ -25,9 +22,7 @@ export const dynamicSecretLeaseQueueServiceFactory = ({
|
||||
queueService,
|
||||
dynamicSecretDAL,
|
||||
dynamicSecretProviders,
|
||||
dynamicSecretLeaseDAL,
|
||||
kmsService,
|
||||
folderDAL
|
||||
dynamicSecretLeaseDAL
|
||||
}: TDynamicSecretLeaseQueueServiceFactoryDep) => {
|
||||
const pruneDynamicSecret = async (dynamicSecretCfgId: string) => {
|
||||
await queueService.queue(
|
||||
@ -82,20 +77,15 @@ export const dynamicSecretLeaseQueueServiceFactory = ({
|
||||
if (!dynamicSecretLease) throw new DisableRotationErrors({ message: "Dynamic secret lease not found" });
|
||||
|
||||
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 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 dynamicSecretLeaseDAL.deleteById(dynamicSecretLease.id);
|
||||
@ -110,22 +100,17 @@ export const dynamicSecretLeaseQueueServiceFactory = ({
|
||||
if ((dynamicSecretCfg.status as DynamicSecretStatus) !== DynamicSecretStatus.Deleting)
|
||||
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 });
|
||||
if (dynamicSecretLeases.length) {
|
||||
const selectedProvider = dynamicSecretProviders[dynamicSecretCfg.type as DynamicSecretProviders];
|
||||
|
||||
const dynamicSecretInputConfig = secretManagerDecryptor({
|
||||
cipherTextBlob: dynamicSecretCfg.encryptedConfig
|
||||
}).toString();
|
||||
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 Promise.all(dynamicSecretLeases.map(({ id }) => unsetLeaseRevocation(id)));
|
||||
await Promise.all(
|
||||
|
@ -1,14 +1,14 @@
|
||||
import { ForbiddenError, subject } from "@casl/ability";
|
||||
import ms from "ms";
|
||||
|
||||
import { SecretKeyEncoding } from "@app/db/schemas";
|
||||
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
|
||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
|
||||
import { getConfig } from "@app/lib/config/env";
|
||||
import { infisicalSymmetricDecrypt } from "@app/lib/crypto/encryption";
|
||||
import { BadRequestError } from "@app/lib/errors";
|
||||
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 { TSecretFolderDALFactory } from "@app/services/secret-folder/secret-folder-dal";
|
||||
|
||||
@ -34,7 +34,6 @@ type TDynamicSecretLeaseServiceFactoryDep = {
|
||||
folderDAL: Pick<TSecretFolderDALFactory, "findBySecretPath">;
|
||||
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
|
||||
projectDAL: Pick<TProjectDALFactory, "findProjectBySlug">;
|
||||
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
|
||||
};
|
||||
|
||||
export type TDynamicSecretLeaseServiceFactory = ReturnType<typeof dynamicSecretLeaseServiceFactory>;
|
||||
@ -47,8 +46,7 @@ export const dynamicSecretLeaseServiceFactory = ({
|
||||
permissionService,
|
||||
dynamicSecretQueueService,
|
||||
projectDAL,
|
||||
licenseService,
|
||||
kmsService
|
||||
licenseService
|
||||
}: TDynamicSecretLeaseServiceFactoryDep) => {
|
||||
const create = async ({
|
||||
environmentSlug,
|
||||
@ -96,12 +94,14 @@ export const dynamicSecretLeaseServiceFactory = ({
|
||||
throw new BadRequestError({ message: `Max lease limit reached. Limit: ${appCfg.MAX_LEASE_LIMIT}` });
|
||||
|
||||
const selectedProvider = dynamicSecretProviders[dynamicSecretCfg.type as DynamicSecretProviders];
|
||||
const { decryptor: kmsDecryptor } = await kmsService.createCipherPairWithDataKey({
|
||||
type: KmsDataKey.SecretManager,
|
||||
projectId
|
||||
});
|
||||
const decryptedStoredInputJson = kmsDecryptor({ cipherTextBlob: dynamicSecretCfg.encryptedConfig }).toString();
|
||||
const decryptedStoredInput = JSON.parse(decryptedStoredInputJson) as object;
|
||||
const decryptedStoredInput = JSON.parse(
|
||||
infisicalSymmetricDecrypt({
|
||||
keyEncoding: dynamicSecretCfg.keyEncoding as SecretKeyEncoding,
|
||||
ciphertext: dynamicSecretCfg.inputCiphertext,
|
||||
tag: dynamicSecretCfg.inputTag,
|
||||
iv: dynamicSecretCfg.inputIV
|
||||
})
|
||||
) as object;
|
||||
|
||||
const selectedTTL = ttl ?? dynamicSecretCfg.defaultTTL;
|
||||
const { maxTTL } = dynamicSecretCfg;
|
||||
@ -164,12 +164,14 @@ export const dynamicSecretLeaseServiceFactory = ({
|
||||
|
||||
const dynamicSecretCfg = dynamicSecretLease.dynamicSecret;
|
||||
const selectedProvider = dynamicSecretProviders[dynamicSecretCfg.type as DynamicSecretProviders];
|
||||
const { decryptor: kmsDecryptor } = await kmsService.createCipherPairWithDataKey({
|
||||
type: KmsDataKey.SecretManager,
|
||||
projectId
|
||||
});
|
||||
const decryptedStoredInputJson = kmsDecryptor({ cipherTextBlob: dynamicSecretCfg.encryptedConfig }).toString();
|
||||
const decryptedStoredInput = JSON.parse(decryptedStoredInputJson) as object;
|
||||
const decryptedStoredInput = JSON.parse(
|
||||
infisicalSymmetricDecrypt({
|
||||
keyEncoding: dynamicSecretCfg.keyEncoding as SecretKeyEncoding,
|
||||
ciphertext: dynamicSecretCfg.inputCiphertext,
|
||||
tag: dynamicSecretCfg.inputTag,
|
||||
iv: dynamicSecretCfg.inputIV
|
||||
})
|
||||
) as object;
|
||||
|
||||
const selectedTTL = ttl ?? dynamicSecretCfg.defaultTTL;
|
||||
const { maxTTL } = dynamicSecretCfg;
|
||||
@ -229,12 +231,14 @@ export const dynamicSecretLeaseServiceFactory = ({
|
||||
|
||||
const dynamicSecretCfg = dynamicSecretLease.dynamicSecret;
|
||||
const selectedProvider = dynamicSecretProviders[dynamicSecretCfg.type as DynamicSecretProviders];
|
||||
const { decryptor: kmsDecryptor } = await kmsService.createCipherPairWithDataKey({
|
||||
type: KmsDataKey.SecretManager,
|
||||
projectId
|
||||
});
|
||||
const decryptedStoredInputJson = kmsDecryptor({ cipherTextBlob: dynamicSecretCfg.encryptedConfig }).toString();
|
||||
const decryptedStoredInput = JSON.parse(decryptedStoredInputJson) as object;
|
||||
const decryptedStoredInput = JSON.parse(
|
||||
infisicalSymmetricDecrypt({
|
||||
keyEncoding: dynamicSecretCfg.keyEncoding as SecretKeyEncoding,
|
||||
ciphertext: dynamicSecretCfg.inputCiphertext,
|
||||
tag: dynamicSecretCfg.inputTag,
|
||||
iv: dynamicSecretCfg.inputIV
|
||||
})
|
||||
) as object;
|
||||
|
||||
const revokeResponse = await selectedProvider
|
||||
.revoke(decryptedStoredInput, dynamicSecretLease.externalEntityId)
|
||||
|
@ -1,11 +1,11 @@
|
||||
import { ForbiddenError, subject } from "@casl/ability";
|
||||
|
||||
import { SecretKeyEncoding } from "@app/db/schemas";
|
||||
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
|
||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
|
||||
import { infisicalSymmetricDecrypt, infisicalSymmetricEncypt } from "@app/lib/crypto/encryption";
|
||||
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 { TSecretFolderDALFactory } from "@app/services/secret-folder/secret-folder-dal";
|
||||
|
||||
@ -34,7 +34,6 @@ type TDynamicSecretServiceFactoryDep = {
|
||||
folderDAL: Pick<TSecretFolderDALFactory, "findBySecretPath">;
|
||||
projectDAL: Pick<TProjectDALFactory, "findProjectBySlug">;
|
||||
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
|
||||
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
|
||||
};
|
||||
|
||||
export type TDynamicSecretServiceFactory = ReturnType<typeof dynamicSecretServiceFactory>;
|
||||
@ -47,8 +46,7 @@ export const dynamicSecretServiceFactory = ({
|
||||
dynamicSecretProviders,
|
||||
permissionService,
|
||||
dynamicSecretQueueService,
|
||||
projectDAL,
|
||||
kmsService
|
||||
projectDAL
|
||||
}: TDynamicSecretServiceFactoryDep) => {
|
||||
const create = async ({
|
||||
path,
|
||||
@ -98,16 +96,16 @@ export const dynamicSecretServiceFactory = ({
|
||||
|
||||
const isConnected = await selectedProvider.validateConnection(provider.inputs);
|
||||
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({
|
||||
type: provider.type,
|
||||
version: 1,
|
||||
encryptedConfig,
|
||||
inputIV: encryptedInput.iv,
|
||||
inputTag: encryptedInput.tag,
|
||||
inputCiphertext: encryptedInput.ciphertext,
|
||||
algorithm: encryptedInput.algorithm,
|
||||
keyEncoding: encryptedInput.encoding,
|
||||
maxTTL,
|
||||
defaultTTL,
|
||||
folderId: folder.id,
|
||||
@ -167,28 +165,27 @@ export const dynamicSecretServiceFactory = ({
|
||||
}
|
||||
|
||||
const selectedProvider = dynamicSecretProviders[dynamicSecretCfg.type as DynamicSecretProviders];
|
||||
const { encryptor: secretManagerEncryptor, decryptor: secretManagerDecryptor } =
|
||||
await kmsService.createCipherPairWithDataKey({
|
||||
type: KmsDataKey.SecretManager,
|
||||
projectId
|
||||
});
|
||||
const dynamicSecretInputConfig = secretManagerDecryptor({
|
||||
cipherTextBlob: dynamicSecretCfg.encryptedConfig
|
||||
}).toString();
|
||||
|
||||
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;
|
||||
const newInput = { ...decryptedStoredInput, ...(inputs || {}) };
|
||||
const updatedInput = await selectedProvider.validateProviderInputs(newInput);
|
||||
|
||||
const isConnected = await selectedProvider.validateConnection(newInput);
|
||||
if (!isConnected) throw new BadRequestError({ message: "Provider connection failed" });
|
||||
|
||||
const encryptedConfig = secretManagerEncryptor({
|
||||
plainText: Buffer.from(JSON.stringify(updatedInput))
|
||||
}).cipherTextBlob;
|
||||
|
||||
const encryptedInput = infisicalSymmetricEncypt(JSON.stringify(updatedInput));
|
||||
const updatedDynamicCfg = await dynamicSecretDAL.updateById(dynamicSecretCfg.id, {
|
||||
encryptedConfig,
|
||||
inputIV: encryptedInput.iv,
|
||||
inputTag: encryptedInput.tag,
|
||||
inputCiphertext: encryptedInput.ciphertext,
|
||||
algorithm: encryptedInput.algorithm,
|
||||
keyEncoding: encryptedInput.encoding,
|
||||
maxTTL,
|
||||
defaultTTL,
|
||||
name: newName ?? name,
|
||||
@ -289,16 +286,14 @@ export const dynamicSecretServiceFactory = ({
|
||||
|
||||
const dynamicSecretCfg = await dynamicSecretDAL.findOne({ name, folderId: folder.id });
|
||||
if (!dynamicSecretCfg) throw new BadRequestError({ message: "Dynamic secret not found" });
|
||||
const { decryptor: secretManagerDecryptor } = await kmsService.createCipherPairWithDataKey({
|
||||
type: KmsDataKey.SecretManager,
|
||||
projectId
|
||||
});
|
||||
|
||||
const dynamicSecretInputConfig = secretManagerDecryptor({
|
||||
cipherTextBlob: dynamicSecretCfg.encryptedConfig
|
||||
}).toString();
|
||||
|
||||
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;
|
||||
const selectedProvider = dynamicSecretProviders[dynamicSecretCfg.type as DynamicSecretProviders];
|
||||
const providerInputs = (await selectedProvider.validateProviderInputs(decryptedStoredInput)) as object;
|
||||
return { ...dynamicSecretCfg, inputs: providerInputs };
|
||||
|
@ -40,7 +40,12 @@ export const getDefaultOnPremFeatures = (): TFeatureSet => ({
|
||||
secretRotation: true,
|
||||
caCrl: false,
|
||||
instanceUserManagement: false,
|
||||
externalKms: false
|
||||
externalKms: false,
|
||||
rateLimits: {
|
||||
readLimit: 60,
|
||||
writeLimit: 200,
|
||||
secretsLimit: 40
|
||||
}
|
||||
});
|
||||
|
||||
export const setupLicenceRequestWithStore = (baseURL: string, refreshUrl: string, licenseKey: string) => {
|
||||
|
@ -58,6 +58,11 @@ export type TFeatureSet = {
|
||||
caCrl: false;
|
||||
instanceUserManagement: false;
|
||||
externalKms: false;
|
||||
rateLimits: {
|
||||
readLimit: number;
|
||||
writeLimit: number;
|
||||
secretsLimit: number;
|
||||
};
|
||||
};
|
||||
|
||||
export type TOrgPlansTableDTO = {
|
||||
|
@ -9,6 +9,10 @@ export enum OrgPermissionActions {
|
||||
Delete = "delete"
|
||||
}
|
||||
|
||||
export enum OrgPermissionAdminConsoleAction {
|
||||
AccessAllProjects = "access-all-projects"
|
||||
}
|
||||
|
||||
export enum OrgPermissionSubjects {
|
||||
Workspace = "workspace",
|
||||
Role = "role",
|
||||
@ -22,7 +26,8 @@ export enum OrgPermissionSubjects {
|
||||
Billing = "billing",
|
||||
SecretScanning = "secret-scanning",
|
||||
Identity = "identity",
|
||||
Kms = "kms"
|
||||
Kms = "kms",
|
||||
AdminConsole = "organization-admin-console"
|
||||
}
|
||||
|
||||
export type OrgPermissionSet =
|
||||
@ -39,7 +44,8 @@ export type OrgPermissionSet =
|
||||
| [OrgPermissionActions, OrgPermissionSubjects.SecretScanning]
|
||||
| [OrgPermissionActions, OrgPermissionSubjects.Billing]
|
||||
| [OrgPermissionActions, OrgPermissionSubjects.Identity]
|
||||
| [OrgPermissionActions, OrgPermissionSubjects.Kms];
|
||||
| [OrgPermissionActions, OrgPermissionSubjects.Kms]
|
||||
| [OrgPermissionAdminConsoleAction, OrgPermissionSubjects.AdminConsole];
|
||||
|
||||
const buildAdminPermission = () => {
|
||||
const { can, build } = new AbilityBuilder<MongoAbility<OrgPermissionSet>>(createMongoAbility);
|
||||
@ -107,6 +113,8 @@ const buildAdminPermission = () => {
|
||||
can(OrgPermissionActions.Edit, OrgPermissionSubjects.Kms);
|
||||
can(OrgPermissionActions.Delete, OrgPermissionSubjects.Kms);
|
||||
|
||||
can(OrgPermissionAdminConsoleAction.AccessAllProjects, OrgPermissionSubjects.AdminConsole);
|
||||
|
||||
return build({ conditionsMatcher });
|
||||
};
|
||||
|
||||
|
@ -23,6 +23,7 @@ export enum ProjectPermissionSub {
|
||||
IpAllowList = "ip-allowlist",
|
||||
Project = "workspace",
|
||||
Secrets = "secrets",
|
||||
SecretFolders = "secret-folders",
|
||||
SecretRollback = "secret-rollback",
|
||||
SecretApproval = "secret-approval",
|
||||
SecretRotation = "secret-rotation",
|
||||
@ -42,6 +43,10 @@ export type ProjectPermissionSet =
|
||||
ProjectPermissionActions,
|
||||
ProjectPermissionSub.Secrets | (ForcedSubject<ProjectPermissionSub.Secrets> & SubjectFields)
|
||||
]
|
||||
| [
|
||||
ProjectPermissionActions,
|
||||
ProjectPermissionSub.SecretFolders | (ForcedSubject<ProjectPermissionSub.SecretFolders> & SubjectFields)
|
||||
]
|
||||
| [ProjectPermissionActions, ProjectPermissionSub.Role]
|
||||
| [ProjectPermissionActions, ProjectPermissionSub.Tags]
|
||||
| [ProjectPermissionActions, ProjectPermissionSub.Member]
|
||||
|
@ -4,17 +4,16 @@ import { logger } from "@app/lib/logger";
|
||||
|
||||
import { TLicenseServiceFactory } from "../license/license-service";
|
||||
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,
|
||||
publicEndpointLimit: 30,
|
||||
writeLimit: 200,
|
||||
secretsLimit: 60,
|
||||
authRateLimit: 60,
|
||||
inviteUserRateLimit: 30,
|
||||
mfaRateLimit: 20,
|
||||
creationLimit: 30
|
||||
mfaRateLimit: 20
|
||||
};
|
||||
|
||||
Object.freeze(rateLimitMaxConfiguration);
|
||||
@ -67,8 +66,7 @@ export const rateLimitServiceFactory = ({ rateLimitDAL, licenseService }: TRateL
|
||||
secretsLimit: rateLimit.secretsRateLimit,
|
||||
authRateLimit: rateLimit.authRateLimit,
|
||||
inviteUserRateLimit: rateLimit.inviteUserRateLimit,
|
||||
mfaRateLimit: rateLimit.mfaRateLimit,
|
||||
creationLimit: rateLimit.creationLimit
|
||||
mfaRateLimit: rateLimit.mfaRateLimit
|
||||
};
|
||||
|
||||
logger.info(`syncRateLimitConfiguration: rate limit configuration: %o`, newRateLimitMaxConfiguration);
|
||||
|
@ -5,7 +5,6 @@ export type TRateLimitUpdateDTO = {
|
||||
authRateLimit: number;
|
||||
inviteUserRateLimit: number;
|
||||
mfaRateLimit: number;
|
||||
creationLimit: number;
|
||||
publicEndpointLimit: number;
|
||||
};
|
||||
|
||||
@ -14,3 +13,13 @@ export type TRateLimit = {
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
} & TRateLimitUpdateDTO;
|
||||
|
||||
export type RateLimitConfiguration = {
|
||||
readLimit: number;
|
||||
publicEndpointLimit: number;
|
||||
writeLimit: number;
|
||||
secretsLimit: number;
|
||||
authRateLimit: number;
|
||||
inviteUserRateLimit: number;
|
||||
mfaRateLimit: number;
|
||||
};
|
||||
|
@ -81,15 +81,13 @@ export const secretApprovalRequestSecretDALFactory = (db: TDbClient) => {
|
||||
.select({
|
||||
secVerTagId: "secVerTag.id",
|
||||
secVerTagColor: "secVerTag.color",
|
||||
secVerTagSlug: "secVerTag.slug",
|
||||
secVerTagName: "secVerTag.name"
|
||||
secVerTagSlug: "secVerTag.slug"
|
||||
})
|
||||
.select(
|
||||
db.ref("id").withSchema(TableName.SecretTag).as("tagId"),
|
||||
db.ref("id").withSchema(TableName.SecretApprovalRequestSecretTag).as("tagJnId"),
|
||||
db.ref("color").withSchema(TableName.SecretTag).as("tagColor"),
|
||||
db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug"),
|
||||
db.ref("name").withSchema(TableName.SecretTag).as("tagName")
|
||||
db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug")
|
||||
)
|
||||
.select(
|
||||
db.ref("secretBlindIndex").withSchema(TableName.Secret).as("orgSecBlindIndex"),
|
||||
@ -124,9 +122,9 @@ export const secretApprovalRequestSecretDALFactory = (db: TDbClient) => {
|
||||
{
|
||||
key: "tagJnId",
|
||||
label: "tags" as const,
|
||||
mapper: ({ tagId: id, tagName: name, tagSlug: slug, tagColor: color }) => ({
|
||||
mapper: ({ tagId: id, tagSlug: slug, tagColor: color }) => ({
|
||||
id,
|
||||
name,
|
||||
name: slug,
|
||||
slug,
|
||||
color
|
||||
})
|
||||
@ -200,11 +198,11 @@ export const secretApprovalRequestSecretDALFactory = (db: TDbClient) => {
|
||||
{
|
||||
key: "secVerTagId",
|
||||
label: "tags" as const,
|
||||
mapper: ({ secVerTagId: id, secVerTagName: name, secVerTagSlug: slug, secVerTagColor: color }) => ({
|
||||
mapper: ({ secVerTagId: id, secVerTagSlug: slug, secVerTagColor: color }) => ({
|
||||
// eslint-disable-next-line
|
||||
id,
|
||||
// eslint-disable-next-line
|
||||
name,
|
||||
name: slug,
|
||||
// eslint-disable-next-line
|
||||
slug,
|
||||
// eslint-disable-next-line
|
||||
@ -262,15 +260,13 @@ export const secretApprovalRequestSecretDALFactory = (db: TDbClient) => {
|
||||
.select({
|
||||
secVerTagId: "secVerTag.id",
|
||||
secVerTagColor: "secVerTag.color",
|
||||
secVerTagSlug: "secVerTag.slug",
|
||||
secVerTagName: "secVerTag.name"
|
||||
secVerTagSlug: "secVerTag.slug"
|
||||
})
|
||||
.select(
|
||||
db.ref("id").withSchema(TableName.SecretTag).as("tagId"),
|
||||
db.ref("id").withSchema(TableName.SecretApprovalRequestSecretTagV2).as("tagJnId"),
|
||||
db.ref("color").withSchema(TableName.SecretTag).as("tagColor"),
|
||||
db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug"),
|
||||
db.ref("name").withSchema(TableName.SecretTag).as("tagName")
|
||||
db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug")
|
||||
)
|
||||
.select(
|
||||
db.ref("version").withSchema(TableName.SecretV2).as("orgSecVersion"),
|
||||
@ -292,9 +288,9 @@ export const secretApprovalRequestSecretDALFactory = (db: TDbClient) => {
|
||||
{
|
||||
key: "tagJnId",
|
||||
label: "tags" as const,
|
||||
mapper: ({ tagId: id, tagName: name, tagSlug: slug, tagColor: color }) => ({
|
||||
mapper: ({ tagId: id, tagSlug: slug, tagColor: color }) => ({
|
||||
id,
|
||||
name,
|
||||
name: slug,
|
||||
slug,
|
||||
color
|
||||
})
|
||||
@ -330,11 +326,11 @@ export const secretApprovalRequestSecretDALFactory = (db: TDbClient) => {
|
||||
{
|
||||
key: "secVerTagId",
|
||||
label: "tags" as const,
|
||||
mapper: ({ secVerTagId: id, secVerTagName: name, secVerTagSlug: slug, secVerTagColor: color }) => ({
|
||||
mapper: ({ secVerTagId: id, secVerTagSlug: slug, secVerTagColor: color }) => ({
|
||||
// eslint-disable-next-line
|
||||
id,
|
||||
// eslint-disable-next-line
|
||||
name,
|
||||
name: slug,
|
||||
// eslint-disable-next-line
|
||||
slug,
|
||||
// eslint-disable-next-line
|
||||
|
@ -224,12 +224,10 @@ export const secretApprovalRequestServiceFactory = ({
|
||||
secretKey: el.key,
|
||||
id: el.id,
|
||||
version: el.version,
|
||||
secretValue: el.encryptedValue
|
||||
? secretManagerDecryptor({ cipherTextBlob: el.encryptedValue }).toString()
|
||||
: undefined,
|
||||
secretValue: el.encryptedValue ? secretManagerDecryptor({ cipherTextBlob: el.encryptedValue }).toString() : "",
|
||||
secretComment: el.encryptedComment
|
||||
? secretManagerDecryptor({ cipherTextBlob: el.encryptedComment }).toString()
|
||||
: undefined,
|
||||
: "",
|
||||
secret: el.secret
|
||||
? {
|
||||
secretKey: el.secret.key,
|
||||
@ -237,10 +235,10 @@ export const secretApprovalRequestServiceFactory = ({
|
||||
version: el.secret.version,
|
||||
secretValue: el.secret.encryptedValue
|
||||
? secretManagerDecryptor({ cipherTextBlob: el.secret.encryptedValue }).toString()
|
||||
: undefined,
|
||||
: "",
|
||||
secretComment: el.secret.encryptedComment
|
||||
? secretManagerDecryptor({ cipherTextBlob: el.secret.encryptedComment }).toString()
|
||||
: undefined
|
||||
: ""
|
||||
}
|
||||
: undefined,
|
||||
secretVersion: el.secretVersion
|
||||
@ -250,10 +248,10 @@ export const secretApprovalRequestServiceFactory = ({
|
||||
version: el.secretVersion.version,
|
||||
secretValue: el.secretVersion.encryptedValue
|
||||
? secretManagerDecryptor({ cipherTextBlob: el.secretVersion.encryptedValue }).toString()
|
||||
: undefined,
|
||||
: "",
|
||||
secretComment: el.secretVersion.encryptedComment
|
||||
? secretManagerDecryptor({ cipherTextBlob: el.secretVersion.encryptedComment }).toString()
|
||||
: undefined
|
||||
: ""
|
||||
}
|
||||
: undefined
|
||||
}));
|
||||
|
@ -257,7 +257,7 @@ export const secretReplicationServiceFactory = ({
|
||||
secretDAL: secretV2BridgeDAL,
|
||||
folderDAL,
|
||||
secretImportDAL,
|
||||
decryptor: (value) => (value ? secretManagerDecryptor({ cipherTextBlob: value }).toString() : undefined)
|
||||
decryptor: (value) => (value ? secretManagerDecryptor({ cipherTextBlob: value }).toString() : "")
|
||||
});
|
||||
// secrets that gets replicated across imports
|
||||
const sourceDecryptedLocalSecrets = sourceLocalSecrets.map((el) => ({
|
||||
@ -449,7 +449,7 @@ export const secretReplicationServiceFactory = ({
|
||||
});
|
||||
}
|
||||
if (locallyDeletedSecrets.length) {
|
||||
await secretDAL.delete(
|
||||
await secretV2BridgeDAL.delete(
|
||||
{
|
||||
$in: {
|
||||
id: locallyDeletedSecrets.map(({ id }) => id)
|
||||
|
@ -164,10 +164,10 @@ export const secretSnapshotServiceFactory = ({
|
||||
secretKey: el.key,
|
||||
secretValue: el.encryptedValue
|
||||
? secretManagerDecryptor({ cipherTextBlob: el.encryptedValue }).toString()
|
||||
: undefined,
|
||||
: "",
|
||||
secretComment: el.encryptedComment
|
||||
? secretManagerDecryptor({ cipherTextBlob: el.encryptedComment }).toString()
|
||||
: undefined
|
||||
: ""
|
||||
}))
|
||||
};
|
||||
} else {
|
||||
|
@ -100,8 +100,7 @@ export const snapshotDALFactory = (db: TDbClient) => {
|
||||
db.ref("id").withSchema(TableName.SecretTag).as("tagId"),
|
||||
db.ref("id").withSchema(TableName.SecretVersionTag).as("tagVersionId"),
|
||||
db.ref("color").withSchema(TableName.SecretTag).as("tagColor"),
|
||||
db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug"),
|
||||
db.ref("name").withSchema(TableName.SecretTag).as("tagName")
|
||||
db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug")
|
||||
);
|
||||
return sqlNestRelationships({
|
||||
data,
|
||||
@ -132,9 +131,9 @@ export const snapshotDALFactory = (db: TDbClient) => {
|
||||
{
|
||||
key: "tagVersionId",
|
||||
label: "tags" as const,
|
||||
mapper: ({ tagId: id, tagName: name, tagSlug: slug, tagColor: color, tagVersionId: vId }) => ({
|
||||
mapper: ({ tagId: id, tagSlug: slug, tagColor: color, tagVersionId: vId }) => ({
|
||||
id,
|
||||
name,
|
||||
name: slug,
|
||||
slug,
|
||||
color,
|
||||
vId
|
||||
@ -195,8 +194,7 @@ export const snapshotDALFactory = (db: TDbClient) => {
|
||||
db.ref("id").withSchema(TableName.SecretTag).as("tagId"),
|
||||
db.ref("id").withSchema(TableName.SecretVersionV2Tag).as("tagVersionId"),
|
||||
db.ref("color").withSchema(TableName.SecretTag).as("tagColor"),
|
||||
db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug"),
|
||||
db.ref("name").withSchema(TableName.SecretTag).as("tagName")
|
||||
db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug")
|
||||
);
|
||||
return sqlNestRelationships({
|
||||
data,
|
||||
@ -227,9 +225,9 @@ export const snapshotDALFactory = (db: TDbClient) => {
|
||||
{
|
||||
key: "tagVersionId",
|
||||
label: "tags" as const,
|
||||
mapper: ({ tagId: id, tagName: name, tagSlug: slug, tagColor: color, tagVersionId: vId }) => ({
|
||||
mapper: ({ tagId: id, tagSlug: slug, tagColor: color, tagVersionId: vId }) => ({
|
||||
id,
|
||||
name,
|
||||
name: slug,
|
||||
slug,
|
||||
color,
|
||||
vId
|
||||
@ -353,8 +351,7 @@ export const snapshotDALFactory = (db: TDbClient) => {
|
||||
db.ref("id").withSchema(TableName.SecretTag).as("tagId"),
|
||||
db.ref("id").withSchema(TableName.SecretVersionTag).as("tagVersionId"),
|
||||
db.ref("color").withSchema(TableName.SecretTag).as("tagColor"),
|
||||
db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug"),
|
||||
db.ref("name").withSchema(TableName.SecretTag).as("tagName")
|
||||
db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug")
|
||||
);
|
||||
|
||||
const formated = sqlNestRelationships({
|
||||
@ -377,9 +374,9 @@ export const snapshotDALFactory = (db: TDbClient) => {
|
||||
{
|
||||
key: "tagVersionId",
|
||||
label: "tags" as const,
|
||||
mapper: ({ tagId: id, tagName: name, tagSlug: slug, tagColor: color, tagVersionId: vId }) => ({
|
||||
mapper: ({ tagId: id, tagSlug: slug, tagColor: color, tagVersionId: vId }) => ({
|
||||
id,
|
||||
name,
|
||||
name: slug,
|
||||
slug,
|
||||
color,
|
||||
vId
|
||||
@ -508,8 +505,7 @@ export const snapshotDALFactory = (db: TDbClient) => {
|
||||
db.ref("id").withSchema(TableName.SecretTag).as("tagId"),
|
||||
db.ref("id").withSchema(TableName.SecretVersionV2Tag).as("tagVersionId"),
|
||||
db.ref("color").withSchema(TableName.SecretTag).as("tagColor"),
|
||||
db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug"),
|
||||
db.ref("name").withSchema(TableName.SecretTag).as("tagName")
|
||||
db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug")
|
||||
);
|
||||
|
||||
const formated = sqlNestRelationships({
|
||||
@ -532,9 +528,9 @@ export const snapshotDALFactory = (db: TDbClient) => {
|
||||
{
|
||||
key: "tagVersionId",
|
||||
label: "tags" as const,
|
||||
mapper: ({ tagId: id, tagName: name, tagSlug: slug, tagColor: color, tagVersionId: vId }) => ({
|
||||
mapper: ({ tagId: id, tagSlug: slug, tagColor: color, tagVersionId: vId }) => ({
|
||||
id,
|
||||
name,
|
||||
name: slug,
|
||||
slug,
|
||||
color,
|
||||
vId
|
||||
|
@ -596,7 +596,8 @@ export const RAW_SECRETS = {
|
||||
"The slug of the project to list secrets from. This parameter is only applicable by machine identities.",
|
||||
environment: "The slug of the environment to list secrets from.",
|
||||
secretPath: "The secret path to list secrets from.",
|
||||
includeImports: "Weather to include imported secrets or not."
|
||||
includeImports: "Weather to include imported secrets or not.",
|
||||
tagSlugs: "The comma separated tag slugs to filter secrets"
|
||||
},
|
||||
CREATE: {
|
||||
secretName: "The name of the secret to create.",
|
||||
@ -1056,7 +1057,7 @@ export const CERTIFICATE_AUTHORITIES = {
|
||||
},
|
||||
SIGN_INTERMEDIATE: {
|
||||
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",
|
||||
notAfter: "The date and time when the intermediate CA expires in YYYY-MM-DDTHH:mm:ss.sssZ format",
|
||||
maxPathLength:
|
||||
@ -1086,6 +1087,21 @@ export const CERTIFICATE_AUTHORITIES = {
|
||||
privateKey: "The private key 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: {
|
||||
caId: "The ID of the CA to get the certificate revocation list (CRL) for",
|
||||
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> & {
|
||||
$in?: Partial<{ [k in keyof R]: R[k][] }>;
|
||||
$search?: Partial<{ [k in keyof R]: R[k] }>;
|
||||
};
|
||||
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>) => {
|
||||
void bd.where(filter);
|
||||
if ($in) {
|
||||
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;
|
||||
};
|
||||
|
||||
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;
|
||||
offset?: number;
|
||||
sort?: Array<[keyof R, "asc" | "desc"] | [keyof R, "asc" | "desc", "first" | "last"]>;
|
||||
count?: TCount;
|
||||
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" });
|
||||
}
|
||||
},
|
||||
find: async (
|
||||
find: async <TCount extends boolean = false>(
|
||||
filter: TFindFilter<Tables[Tname]["base"]>,
|
||||
{ offset, limit, sort, tx }: TFindOpt<Tables[Tname]["base"]> = {}
|
||||
{ offset, limit, sort, count, tx }: TFindOpt<Tables[Tname]["base"], TCount> = {}
|
||||
) => {
|
||||
try {
|
||||
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 (offset) void query.offset(offset);
|
||||
if (sort) {
|
||||
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;
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: "Find one" });
|
||||
@ -104,6 +128,16 @@ export const ormify = <DbOps extends object, Tname extends keyof Tables>(db: Kne
|
||||
throw new DatabaseError({ error, name: "Create" });
|
||||
}
|
||||
},
|
||||
// This spilit the insert into multiple chunk
|
||||
batchInsert: async (data: readonly Tables[Tname]["insert"][], tx?: Knex) => {
|
||||
try {
|
||||
if (!data.length) return [];
|
||||
const res = await (tx || db).batchInsert(tableName, data as never).returning("*");
|
||||
return res as Tables[Tname]["base"][];
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: "batchInsert" });
|
||||
}
|
||||
},
|
||||
upsert: async (data: readonly Tables[Tname]["insert"][], onConflictField: keyof Tables[Tname]["base"], tx?: Knex) => {
|
||||
try {
|
||||
if (!data.length) return [];
|
||||
|
@ -1,7 +1,6 @@
|
||||
import type { RateLimitOptions, RateLimitPluginOptions } from "@fastify/rate-limit";
|
||||
import { Redis } from "ioredis";
|
||||
|
||||
import { getRateLimiterConfig } from "@app/ee/services/rate-limit/rate-limit-service";
|
||||
import { getConfig } from "@app/lib/config/env";
|
||||
|
||||
export const globalRateLimiterCfg = (): RateLimitPluginOptions => {
|
||||
@ -22,14 +21,16 @@ export const globalRateLimiterCfg = (): RateLimitPluginOptions => {
|
||||
// GET endpoints
|
||||
export const readLimit: RateLimitOptions = {
|
||||
timeWindow: 60 * 1000,
|
||||
max: () => getRateLimiterConfig().readLimit,
|
||||
hook: "preValidation",
|
||||
max: (req) => req.rateLimits.readLimit,
|
||||
keyGenerator: (req) => req.realIp
|
||||
};
|
||||
|
||||
// POST, PATCH, PUT, DELETE endpoints
|
||||
export const writeLimit: RateLimitOptions = {
|
||||
timeWindow: 60 * 1000,
|
||||
max: () => getRateLimiterConfig().writeLimit,
|
||||
hook: "preValidation",
|
||||
max: (req) => req.rateLimits.writeLimit,
|
||||
keyGenerator: (req) => req.realIp
|
||||
};
|
||||
|
||||
@ -37,42 +38,40 @@ export const writeLimit: RateLimitOptions = {
|
||||
export const secretsLimit: RateLimitOptions = {
|
||||
// secrets, folders, secret imports
|
||||
timeWindow: 60 * 1000,
|
||||
max: () => getRateLimiterConfig().secretsLimit,
|
||||
hook: "preValidation",
|
||||
max: (req) => req.rateLimits.secretsLimit,
|
||||
keyGenerator: (req) => req.realIp
|
||||
};
|
||||
|
||||
export const authRateLimit: RateLimitOptions = {
|
||||
timeWindow: 60 * 1000,
|
||||
max: () => getRateLimiterConfig().authRateLimit,
|
||||
hook: "preValidation",
|
||||
max: (req) => req.rateLimits.authRateLimit,
|
||||
keyGenerator: (req) => req.realIp
|
||||
};
|
||||
|
||||
export const inviteUserRateLimit: RateLimitOptions = {
|
||||
timeWindow: 60 * 1000,
|
||||
max: () => getRateLimiterConfig().inviteUserRateLimit,
|
||||
hook: "preValidation",
|
||||
max: (req) => req.rateLimits.inviteUserRateLimit,
|
||||
keyGenerator: (req) => req.realIp
|
||||
};
|
||||
|
||||
export const mfaRateLimit: RateLimitOptions = {
|
||||
timeWindow: 60 * 1000,
|
||||
max: () => getRateLimiterConfig().mfaRateLimit,
|
||||
hook: "preValidation",
|
||||
max: (req) => req.rateLimits.mfaRateLimit,
|
||||
keyGenerator: (req) => {
|
||||
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
|
||||
export const publicEndpointLimit: RateLimitOptions = {
|
||||
// Read Shared Secrets
|
||||
timeWindow: 60 * 1000,
|
||||
max: () => getRateLimiterConfig().publicEndpointLimit,
|
||||
hook: "preValidation",
|
||||
max: (req) => req.rateLimits.publicEndpointLimit,
|
||||
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 { orgRoleServiceFactory } from "@app/services/org/org-role-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 { projectDALFactory } from "@app/services/project/project-dal";
|
||||
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 { injectIdentity } from "../plugins/auth/inject-identity";
|
||||
import { injectPermission } from "../plugins/auth/inject-permission";
|
||||
import { injectRateLimits } from "../plugins/inject-rate-limits";
|
||||
import { registerSecretScannerGhApp } from "../plugins/secret-scanner";
|
||||
import { registerV1Routes } from "./v1";
|
||||
import { registerV2Routes } from "./v2";
|
||||
@ -498,6 +500,16 @@ export const registerRoutes = async (
|
||||
keyStore,
|
||||
licenseService
|
||||
});
|
||||
const orgAdminService = orgAdminServiceFactory({
|
||||
projectDAL,
|
||||
permissionService,
|
||||
projectUserMembershipRoleDAL,
|
||||
userDAL,
|
||||
projectBotDAL,
|
||||
projectKeyDAL,
|
||||
projectMembershipDAL
|
||||
});
|
||||
|
||||
const rateLimitService = rateLimitServiceFactory({
|
||||
rateLimitDAL,
|
||||
licenseService
|
||||
@ -635,7 +647,8 @@ export const registerRoutes = async (
|
||||
projectUserMembershipRoleDAL,
|
||||
identityProjectMembershipRoleDAL,
|
||||
keyStore,
|
||||
kmsService
|
||||
kmsService,
|
||||
projectBotDAL
|
||||
});
|
||||
|
||||
const projectEnvService = projectEnvServiceFactory({
|
||||
@ -677,8 +690,7 @@ export const registerRoutes = async (
|
||||
permissionService,
|
||||
webhookDAL,
|
||||
projectEnvDAL,
|
||||
projectDAL,
|
||||
kmsService
|
||||
projectDAL
|
||||
});
|
||||
|
||||
const secretTagService = secretTagServiceFactory({ secretTagDAL, permissionService });
|
||||
@ -885,8 +897,15 @@ export const registerRoutes = async (
|
||||
folderDAL,
|
||||
integrationDAL,
|
||||
integrationAuthDAL,
|
||||
secretQueueService
|
||||
secretQueueService,
|
||||
integrationAuthService,
|
||||
projectBotService,
|
||||
secretV2BridgeDAL,
|
||||
secretImportDAL,
|
||||
secretDAL,
|
||||
kmsService
|
||||
});
|
||||
|
||||
const serviceTokenService = serviceTokenServiceFactory({
|
||||
projectEnvDAL,
|
||||
serviceTokenDAL,
|
||||
@ -988,9 +1007,7 @@ export const registerRoutes = async (
|
||||
queueService,
|
||||
dynamicSecretLeaseDAL,
|
||||
dynamicSecretProviders,
|
||||
dynamicSecretDAL,
|
||||
kmsService,
|
||||
folderDAL
|
||||
dynamicSecretDAL
|
||||
});
|
||||
const dynamicSecretService = dynamicSecretServiceFactory({
|
||||
projectDAL,
|
||||
@ -1000,8 +1017,7 @@ export const registerRoutes = async (
|
||||
dynamicSecretProviders,
|
||||
folderDAL,
|
||||
permissionService,
|
||||
licenseService,
|
||||
kmsService
|
||||
licenseService
|
||||
});
|
||||
const dynamicSecretLeaseService = dynamicSecretLeaseServiceFactory({
|
||||
projectDAL,
|
||||
@ -1011,8 +1027,7 @@ export const registerRoutes = async (
|
||||
dynamicSecretLeaseDAL,
|
||||
dynamicSecretProviders,
|
||||
folderDAL,
|
||||
licenseService,
|
||||
kmsService
|
||||
licenseService
|
||||
});
|
||||
const dailyResourceCleanUp = dailyResourceCleanUpQueueServiceFactory({
|
||||
auditLogDAL,
|
||||
@ -1022,7 +1037,8 @@ export const registerRoutes = async (
|
||||
snapshotDAL,
|
||||
identityAccessTokenDAL,
|
||||
secretSharingDAL,
|
||||
secretVersionV2DAL: secretVersionV2BridgeDAL
|
||||
secretVersionV2DAL: secretVersionV2BridgeDAL,
|
||||
identityUniversalAuthClientSecretDAL: identityUaClientSecretDAL
|
||||
});
|
||||
|
||||
const oidcService = oidcConfigServiceFactory({
|
||||
@ -1117,7 +1133,8 @@ export const registerRoutes = async (
|
||||
identityProjectAdditionalPrivilege: identityProjectAdditionalPrivilegeService,
|
||||
secretSharing: secretSharingService,
|
||||
userEngagement: userEngagementService,
|
||||
externalKms: externalKmsService
|
||||
externalKms: externalKmsService,
|
||||
orgAdmin: orgAdminService
|
||||
});
|
||||
|
||||
const cronJobs: CronJob[] = [];
|
||||
@ -1134,6 +1151,7 @@ export const registerRoutes = async (
|
||||
|
||||
await server.register(injectIdentity, { userDAL, serviceTokenDAL });
|
||||
await server.register(injectPermission);
|
||||
await server.register(injectRateLimits);
|
||||
await server.register(injectAuditLogInfo);
|
||||
|
||||
server.route({
|
||||
|
@ -63,8 +63,8 @@ export const secretRawSchema = z.object({
|
||||
version: z.number(),
|
||||
type: z.string(),
|
||||
secretKey: z.string(),
|
||||
secretValue: z.string().optional(),
|
||||
secretComment: z.string().optional(),
|
||||
secretValue: z.string(),
|
||||
secretComment: z.string(),
|
||||
secretReminderNote: z.string().nullable().optional(),
|
||||
secretReminderRepeatDays: z.number().nullable().optional(),
|
||||
skipMultilineEncoding: z.boolean().default(false).nullable().optional(),
|
||||
@ -129,7 +129,11 @@ export const SanitizedRoleSchema = ProjectRolesSchema.extend({
|
||||
});
|
||||
|
||||
export const SanitizedDynamicSecretSchema = DynamicSecretsSchema.omit({
|
||||
encryptedConfig: true
|
||||
inputIV: true,
|
||||
inputTag: true,
|
||||
inputCiphertext: true,
|
||||
keyEncoding: true,
|
||||
algorithm: true
|
||||
});
|
||||
|
||||
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)
|
||||
}),
|
||||
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),
|
||||
notAfter: validateCaDateField.describe(CERTIFICATE_AUTHORITIES.SIGN_INTERMEDIATE.notAfter),
|
||||
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
|
||||
.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),
|
||||
altNames: validateAltNamesField.describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.altNames),
|
||||
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 { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
||||
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 { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||
import { AuthMode } from "@app/services/auth/auth-type";
|
||||
@ -16,7 +16,7 @@ export const registerIdentityRouter = async (server: FastifyZodProvider) => {
|
||||
method: "POST",
|
||||
url: "/",
|
||||
config: {
|
||||
rateLimit: creationLimit
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
schema: {
|
||||
|
@ -15,6 +15,7 @@ import { registerIdentityUaRouter } from "./identity-universal-auth-router";
|
||||
import { registerIntegrationAuthRouter } from "./integration-auth-router";
|
||||
import { registerIntegrationRouter } from "./integration-router";
|
||||
import { registerInviteOrgRouter } from "./invite-org-router";
|
||||
import { registerOrgAdminRouter } from "./org-admin-router";
|
||||
import { registerOrgRouter } from "./organization-router";
|
||||
import { registerPasswordRouter } from "./password-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(registerOrgRouter, { prefix: "/organization" });
|
||||
await server.register(registerAdminRouter, { prefix: "/admin" });
|
||||
await server.register(registerOrgAdminRouter, { prefix: "/organization-admin" });
|
||||
await server.register(registerUserRouter, { prefix: "/user" });
|
||||
await server.register(registerInviteOrgRouter, { prefix: "/invite-org" });
|
||||
await server.register(registerUserActionRouter, { prefix: "/user-action" });
|
||||
|
@ -170,6 +170,12 @@ export const registerIntegrationRouter = async (server: FastifyZodProvider) => {
|
||||
params: z.object({
|
||||
integrationId: z.string().trim().describe(INTEGRATION.DELETE.integrationId)
|
||||
}),
|
||||
querystring: z.object({
|
||||
shouldDeleteIntegrationSecrets: z
|
||||
.enum(["true", "false"])
|
||||
.optional()
|
||||
.transform((val) => val === "true")
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
integration: IntegrationsSchema
|
||||
@ -183,7 +189,8 @@ export const registerIntegrationRouter = async (server: FastifyZodProvider) => {
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actor: req.permission.type,
|
||||
actorOrgId: req.permission.orgId,
|
||||
id: req.params.integrationId
|
||||
id: req.params.integrationId,
|
||||
shouldDeleteIntegrationSecrets: req.query.shouldDeleteIntegrationSecrets
|
||||
});
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
@ -205,7 +212,8 @@ export const registerIntegrationRouter = async (server: FastifyZodProvider) => {
|
||||
targetService: integration.targetService,
|
||||
targetServiceId: integration.targetServiceId,
|
||||
path: integration.path,
|
||||
region: integration.region
|
||||
region: integration.region,
|
||||
shouldDeleteIntegrationSecrets: req.query.shouldDeleteIntegrationSecrets
|
||||
// eslint-disable-next-line
|
||||
}) 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 };
|
||||
}
|
||||
});
|
||||
};
|
@ -1,3 +1,4 @@
|
||||
import slugify from "@sindresorhus/slugify";
|
||||
import { z } from "zod";
|
||||
|
||||
import { SecretTagsSchema } from "@app/db/schemas";
|
||||
@ -49,7 +50,8 @@ export const registerSecretTagRouter = async (server: FastifyZodProvider) => {
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
workspaceTag: SecretTagsSchema
|
||||
// akhilmhdh: for terraform backward compatiability
|
||||
workspaceTag: SecretTagsSchema.extend({ name: z.string() })
|
||||
})
|
||||
}
|
||||
},
|
||||
@ -79,7 +81,8 @@ export const registerSecretTagRouter = async (server: FastifyZodProvider) => {
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
workspaceTag: SecretTagsSchema
|
||||
// akhilmhdh: for terraform backward compatiability
|
||||
workspaceTag: SecretTagsSchema.extend({ name: z.string() })
|
||||
})
|
||||
}
|
||||
},
|
||||
@ -108,8 +111,14 @@ export const registerSecretTagRouter = async (server: FastifyZodProvider) => {
|
||||
projectId: z.string().trim().describe(SECRET_TAGS.CREATE.projectId)
|
||||
}),
|
||||
body: z.object({
|
||||
name: z.string().trim().describe(SECRET_TAGS.CREATE.name),
|
||||
slug: z.string().trim().describe(SECRET_TAGS.CREATE.slug),
|
||||
slug: z
|
||||
.string()
|
||||
.toLowerCase()
|
||||
.trim()
|
||||
.describe(SECRET_TAGS.CREATE.slug)
|
||||
.refine((v) => slugify(v) === v, {
|
||||
message: "Invalid slug. Slug can only contain alphanumeric characters and hyphens."
|
||||
}),
|
||||
color: z.string().trim().describe(SECRET_TAGS.CREATE.color)
|
||||
}),
|
||||
response: {
|
||||
@ -144,8 +153,14 @@ export const registerSecretTagRouter = async (server: FastifyZodProvider) => {
|
||||
tagId: z.string().trim().describe(SECRET_TAGS.UPDATE.tagId)
|
||||
}),
|
||||
body: z.object({
|
||||
name: z.string().trim().describe(SECRET_TAGS.UPDATE.name),
|
||||
slug: z.string().trim().describe(SECRET_TAGS.UPDATE.slug),
|
||||
slug: z
|
||||
.string()
|
||||
.toLowerCase()
|
||||
.trim()
|
||||
.describe(SECRET_TAGS.UPDATE.slug)
|
||||
.refine((v) => slugify(v) === v, {
|
||||
message: "Invalid slug. Slug can only contain alphanumeric characters and hyphens."
|
||||
}),
|
||||
color: z.string().trim().describe(SECRET_TAGS.UPDATE.color)
|
||||
}),
|
||||
response: {
|
||||
|
@ -9,7 +9,7 @@ import {
|
||||
UsersSchema
|
||||
} from "@app/db/schemas";
|
||||
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 { ActorType, AuthMode } from "@app/services/auth/auth-type";
|
||||
|
||||
@ -307,7 +307,7 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => {
|
||||
method: "POST",
|
||||
url: "/",
|
||||
config: {
|
||||
rateLimit: creationLimit
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
schema: {
|
||||
body: z.object({
|
||||
|
@ -4,7 +4,7 @@ import { z } from "zod";
|
||||
import { CertificateAuthoritiesSchema, CertificatesSchema, ProjectKeysSchema } from "@app/db/schemas";
|
||||
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
||||
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 { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||
import { AuthMode } from "@app/services/auth/auth-type";
|
||||
@ -142,7 +142,7 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
|
||||
method: "POST",
|
||||
url: "/",
|
||||
config: {
|
||||
rateLimit: creationLimit
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
schema: {
|
||||
description: "Create a new project",
|
||||
|
@ -59,9 +59,10 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
|
||||
tags: SecretTagsSchema.pick({
|
||||
id: true,
|
||||
slug: true,
|
||||
name: true,
|
||||
color: true
|
||||
}).array()
|
||||
})
|
||||
.extend({ name: z.string() })
|
||||
.array()
|
||||
})
|
||||
)
|
||||
})
|
||||
@ -116,16 +117,15 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
secret: SecretsSchema.omit({ secretBlindIndex: true }).merge(
|
||||
z.object({
|
||||
tags: SecretTagsSchema.pick({
|
||||
id: true,
|
||||
slug: true,
|
||||
name: true,
|
||||
color: true
|
||||
}).array()
|
||||
secret: SecretsSchema.omit({ secretBlindIndex: true }).extend({
|
||||
tags: SecretTagsSchema.pick({
|
||||
id: true,
|
||||
slug: true,
|
||||
color: true
|
||||
})
|
||||
)
|
||||
.extend({ name: z.string() })
|
||||
.array()
|
||||
})
|
||||
})
|
||||
}
|
||||
},
|
||||
@ -180,7 +180,13 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
|
||||
.enum(["true", "false"])
|
||||
.default("false")
|
||||
.transform((value) => value === "true")
|
||||
.describe(RAW_SECRETS.LIST.includeImports)
|
||||
.describe(RAW_SECRETS.LIST.includeImports),
|
||||
tagSlugs: z
|
||||
.string()
|
||||
.describe(RAW_SECRETS.LIST.tagSlugs)
|
||||
.optional()
|
||||
// split by comma and trim the strings
|
||||
.transform((el) => (el ? el.split(",").map((i) => i.trim()) : []))
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
@ -190,9 +196,9 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
|
||||
tags: SecretTagsSchema.pick({
|
||||
id: true,
|
||||
slug: true,
|
||||
name: true,
|
||||
color: true
|
||||
})
|
||||
.extend({ name: z.string() })
|
||||
.array()
|
||||
.optional()
|
||||
})
|
||||
@ -251,7 +257,8 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
|
||||
projectId: workspaceId,
|
||||
path: secretPath,
|
||||
includeImports: req.query.include_imports,
|
||||
recursive: req.query.recursive
|
||||
recursive: req.query.recursive,
|
||||
tagSlugs: req.query.tagSlugs
|
||||
});
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
@ -325,9 +332,9 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
|
||||
tags: SecretTagsSchema.pick({
|
||||
id: true,
|
||||
slug: true,
|
||||
name: true,
|
||||
color: true
|
||||
})
|
||||
.extend({ name: z.string() })
|
||||
.array()
|
||||
.optional()
|
||||
})
|
||||
@ -731,9 +738,10 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
|
||||
tags: SecretTagsSchema.pick({
|
||||
id: true,
|
||||
slug: true,
|
||||
name: true,
|
||||
color: true
|
||||
}).array()
|
||||
})
|
||||
.extend({ name: z.string() })
|
||||
.array()
|
||||
})
|
||||
.array(),
|
||||
imports: z
|
||||
|
@ -18,6 +18,40 @@ export const createDistinguishedName = (parts: TDNParts) => {
|
||||
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) => {
|
||||
switch (keyAlgorithm) {
|
||||
case CertKeyAlgorithm.RSA_4096:
|
||||
|
@ -22,7 +22,8 @@ import {
|
||||
createDistinguishedName,
|
||||
getCaCertChain,
|
||||
getCaCredentials,
|
||||
keyAlgorithmToAlgCfg
|
||||
keyAlgorithmToAlgCfg,
|
||||
parseDistinguishedName
|
||||
} from "./certificate-authority-fns";
|
||||
import { TCertificateAuthorityQueueFactory } from "./certificate-authority-queue";
|
||||
import { TCertificateAuthoritySecretDALFactory } from "./certificate-authority-secret-dal";
|
||||
@ -36,6 +37,7 @@ import {
|
||||
TGetCaDTO,
|
||||
TImportCertToCaDTO,
|
||||
TIssueCertFromCaDTO,
|
||||
TSignCertFromCaDTO,
|
||||
TSignIntermediateDTO,
|
||||
TUpdateCaDTO
|
||||
} 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 ({
|
||||
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 {
|
||||
createCa,
|
||||
getCaById,
|
||||
@ -860,6 +1061,7 @@ export const certificateAuthorityServiceFactory = ({
|
||||
getCaCert,
|
||||
signIntermediate,
|
||||
importCertToCa,
|
||||
issueCertFromCa
|
||||
issueCertFromCa,
|
||||
signCertFromCa
|
||||
};
|
||||
};
|
||||
|
@ -81,6 +81,17 @@ export type TIssueCertFromCaDTO = {
|
||||
notAfter?: string;
|
||||
} & 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 = {
|
||||
commonName?: string;
|
||||
organization?: string;
|
||||
|
@ -4,6 +4,7 @@ import { TDbClient } from "@app/db";
|
||||
import { TableName } from "@app/db/schemas";
|
||||
import { DatabaseError } from "@app/lib/errors";
|
||||
import { ormify } from "@app/lib/knex";
|
||||
import { logger } from "@app/lib/logger";
|
||||
|
||||
export type TIdentityUaClientSecretDALFactory = ReturnType<typeof identityUaClientSecretDALFactory>;
|
||||
|
||||
@ -23,5 +24,55 @@ export const identityUaClientSecretDALFactory = (db: TDbClient) => {
|
||||
}
|
||||
};
|
||||
|
||||
return { ...uaClientSecretOrm, incrementUsage };
|
||||
const removeExpiredClientSecrets = async (tx?: Knex) => {
|
||||
const BATCH_SIZE = 10000;
|
||||
const MAX_RETRY_ON_FAILURE = 3;
|
||||
|
||||
let deletedClientSecret: { id: string }[] = [];
|
||||
let numberOfRetryOnFailure = 0;
|
||||
|
||||
do {
|
||||
try {
|
||||
const findExpiredClientSecretQuery = (tx || db)(TableName.IdentityUaClientSecret)
|
||||
.where({
|
||||
isClientSecretRevoked: true
|
||||
})
|
||||
.orWhere((qb) => {
|
||||
void qb
|
||||
.where("clientSecretNumUses", ">", 0)
|
||||
.andWhere(
|
||||
"clientSecretNumUses",
|
||||
">=",
|
||||
db.ref("clientSecretNumUsesLimit").withSchema(TableName.IdentityUaClientSecret)
|
||||
);
|
||||
})
|
||||
.orWhere((qb) => {
|
||||
void qb
|
||||
.where("clientSecretTTL", ">", 0)
|
||||
.andWhereRaw(
|
||||
`"${TableName.IdentityUaClientSecret}"."createdAt" + make_interval(secs => "${TableName.IdentityUaClientSecret}"."clientSecretTTL") < NOW()`
|
||||
);
|
||||
})
|
||||
.select("id")
|
||||
.limit(BATCH_SIZE);
|
||||
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
deletedClientSecret = await (tx || db)(TableName.IdentityUaClientSecret)
|
||||
.whereIn("id", findExpiredClientSecretQuery)
|
||||
.del()
|
||||
.returning("id");
|
||||
numberOfRetryOnFailure = 0; // reset
|
||||
} catch (error) {
|
||||
numberOfRetryOnFailure += 1;
|
||||
logger.error(error, "Failed to delete client secret on pruning");
|
||||
} finally {
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
await new Promise((resolve) => {
|
||||
setTimeout(resolve, 10); // time to breathe for db
|
||||
});
|
||||
}
|
||||
} while (deletedClientSecret.length > 0 || numberOfRetryOnFailure < MAX_RETRY_ON_FAILURE);
|
||||
};
|
||||
|
||||
return { ...uaClientSecretOrm, incrementUsage, removeExpiredClientSecrets };
|
||||
};
|
||||
|
@ -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"
|
||||
});
|
||||
}
|
||||
};
|
@ -538,19 +538,20 @@ const syncSecretsAWSParameterStore = async ({
|
||||
integration,
|
||||
secrets,
|
||||
accessId,
|
||||
accessToken
|
||||
accessToken,
|
||||
projectId
|
||||
}: {
|
||||
integration: TIntegrations;
|
||||
integration: TIntegrations & { secretPath: string; environment: { slug: string } };
|
||||
secrets: Record<string, { value: string; comment?: string }>;
|
||||
accessId: string | null;
|
||||
accessToken: string;
|
||||
projectId?: string;
|
||||
}) => {
|
||||
let response: { isSynced: boolean; syncMessage: string } | null = null;
|
||||
|
||||
if (!accessId) {
|
||||
throw new Error("AWS access ID is required");
|
||||
}
|
||||
|
||||
const config = new AWS.Config({
|
||||
region: integration.region as string,
|
||||
credentials: {
|
||||
@ -567,7 +568,9 @@ const syncSecretsAWSParameterStore = async ({
|
||||
|
||||
const metadata = z.record(z.any()).parse(integration.metadata || {});
|
||||
const awsParameterStoreSecretsObj: Record<string, AWS.SSM.Parameter> = {};
|
||||
|
||||
logger.info(
|
||||
`getIntegrationSecrets: integration sync triggered for ssm with [projectId=${projectId}] [environment=${integration.environment.slug}] [secretPath=${integration.secretPath}] [shouldDisableDelete=${metadata.shouldDisableDelete}]`
|
||||
);
|
||||
// now fetch all aws parameter store secrets
|
||||
let hasNext = true;
|
||||
let nextToken: string | undefined;
|
||||
@ -594,6 +597,18 @@ const syncSecretsAWSParameterStore = async ({
|
||||
nextToken = parameters.NextToken;
|
||||
}
|
||||
|
||||
logger.info(
|
||||
`getIntegrationSecrets: all fetched keys from AWS SSM [projectId=${projectId}] [environment=${
|
||||
integration.environment.slug
|
||||
}] [secretPath=${integration.secretPath}] [awsParameterStoreSecretsObj=${Object.keys(
|
||||
awsParameterStoreSecretsObj
|
||||
).join(",")}]`
|
||||
);
|
||||
logger.info(
|
||||
`getIntegrationSecrets: all secrets from Infisical to send to AWS SSM [projectId=${projectId}] [environment=${
|
||||
integration.environment.slug
|
||||
}] [secretPath=${integration.secretPath}] [secrets=${Object.keys(secrets).join(",")}]`
|
||||
);
|
||||
// Identify secrets to create
|
||||
// don't use Promise.all() and promise map here
|
||||
// it will cause rate limit
|
||||
@ -603,24 +618,56 @@ const syncSecretsAWSParameterStore = async ({
|
||||
// case: secret does not exist in AWS parameter store
|
||||
// -> create secret
|
||||
if (secrets[key].value) {
|
||||
logger.info(
|
||||
`getIntegrationSecrets: create secret in AWS SSM for [projectId=${projectId}] [environment=${integration.environment.slug}] [secretPath=${integration.secretPath}] [key=${key}]`
|
||||
);
|
||||
await ssm
|
||||
.putParameter({
|
||||
Name: `${integration.path}${key}`,
|
||||
Type: "SecureString",
|
||||
Value: secrets[key].value,
|
||||
...(metadata.kmsKeyId && { KeyId: metadata.kmsKeyId }),
|
||||
// Overwrite: true,
|
||||
Tags: metadata.secretAWSTag
|
||||
? metadata.secretAWSTag.map((tag: { key: string; value: string }) => ({
|
||||
Key: tag.key,
|
||||
Value: tag.value
|
||||
}))
|
||||
: []
|
||||
Overwrite: true
|
||||
})
|
||||
.promise();
|
||||
if (metadata.secretAWSTag?.length) {
|
||||
try {
|
||||
await ssm
|
||||
.addTagsToResource({
|
||||
ResourceType: "Parameter",
|
||||
ResourceId: `${integration.path}${key}`,
|
||||
Tags: metadata.secretAWSTag
|
||||
? metadata.secretAWSTag.map((tag: { key: string; value: string }) => ({
|
||||
Key: tag.key,
|
||||
Value: tag.value
|
||||
}))
|
||||
: []
|
||||
})
|
||||
.promise();
|
||||
} catch (err) {
|
||||
logger.error(
|
||||
err,
|
||||
`getIntegrationSecrets: create secret in AWS SSM for failed [projectId=${projectId}] [environment=${integration.environment.slug}] [secretPath=${integration.secretPath}] [key=${key}]`
|
||||
);
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
if ((err as any).code === "AccessDeniedException") {
|
||||
logger.error(
|
||||
`AWS Parameter Store Error [integration=${integration.id}]: double check AWS account permissions (refer to the Infisical docs)`
|
||||
);
|
||||
}
|
||||
|
||||
response = {
|
||||
isSynced: false,
|
||||
syncMessage: (err as AWSError)?.message || "Error syncing with AWS Parameter Store"
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
// case: secret exists in AWS parameter store
|
||||
} else {
|
||||
logger.info(
|
||||
`getIntegrationSecrets: update secret in AWS SSM for [projectId=${projectId}] [environment=${integration.environment.slug}] [secretPath=${integration.secretPath}] [key=${key}]`
|
||||
);
|
||||
// -> update secret
|
||||
if (awsParameterStoreSecretsObj[key].Value !== secrets[key].value) {
|
||||
await ssm
|
||||
@ -648,6 +695,10 @@ const syncSecretsAWSParameterStore = async ({
|
||||
})
|
||||
.promise();
|
||||
} catch (err) {
|
||||
logger.error(
|
||||
err,
|
||||
`getIntegrationSecrets: update secret in AWS SSM for failed [projectId=${projectId}] [environment=${integration.environment.slug}] [secretPath=${integration.secretPath}] [key=${key}]`
|
||||
);
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
if ((err as any).code === "AccessDeniedException") {
|
||||
logger.error(
|
||||
@ -670,9 +721,18 @@ const syncSecretsAWSParameterStore = async ({
|
||||
}
|
||||
|
||||
if (!metadata.shouldDisableDelete) {
|
||||
logger.info(
|
||||
`getIntegrationSecrets: inside of shouldDisableDelete AWS SSM [projectId=${projectId}] [environment=${integration.environment.slug}] [secretPath=${integration.secretPath}] [step=1]`
|
||||
);
|
||||
for (const key in awsParameterStoreSecretsObj) {
|
||||
if (Object.hasOwn(awsParameterStoreSecretsObj, key)) {
|
||||
logger.info(
|
||||
`getIntegrationSecrets: inside of shouldDisableDelete AWS SSM [projectId=${projectId}] [environment=${integration.environment.slug}] [secretPath=${integration.secretPath}] [key=${key}] [step=2]`
|
||||
);
|
||||
if (!(key in secrets)) {
|
||||
logger.info(
|
||||
`getIntegrationSecrets: inside of shouldDisableDelete AWS SSM [projectId=${projectId}] [environment=${integration.environment.slug}] [secretPath=${integration.secretPath}] [key=${key}] [step=3]`
|
||||
);
|
||||
// case:
|
||||
// -> delete secret
|
||||
await ssm
|
||||
@ -680,6 +740,9 @@ const syncSecretsAWSParameterStore = async ({
|
||||
Name: awsParameterStoreSecretsObj[key].Name as string
|
||||
})
|
||||
.promise();
|
||||
logger.info(
|
||||
`getIntegrationSecrets: inside of shouldDisableDelete AWS SSM [projectId=${projectId}] [environment=${integration.environment.slug}] [secretPath=${integration.secretPath}] [key=${key}] [step=4]`
|
||||
);
|
||||
}
|
||||
await new Promise((resolve) => {
|
||||
setTimeout(resolve, 50);
|
||||
@ -3656,7 +3719,8 @@ export const syncIntegrationSecrets = async ({
|
||||
integration,
|
||||
secrets,
|
||||
accessId,
|
||||
accessToken
|
||||
accessToken,
|
||||
projectId
|
||||
});
|
||||
break;
|
||||
case Integrations.AWS_SECRET_MANAGER:
|
||||
|
@ -6,8 +6,15 @@ import { BadRequestError } from "@app/lib/errors";
|
||||
import { TProjectPermission } from "@app/lib/types";
|
||||
|
||||
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 { 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 {
|
||||
TCreateIntegrationDTO,
|
||||
@ -19,9 +26,15 @@ import {
|
||||
type TIntegrationServiceFactoryDep = {
|
||||
integrationDAL: TIntegrationDALFactory;
|
||||
integrationAuthDAL: TIntegrationAuthDALFactory;
|
||||
folderDAL: Pick<TSecretFolderDALFactory, "findBySecretPath">;
|
||||
integrationAuthService: TIntegrationAuthServiceFactory;
|
||||
folderDAL: Pick<TSecretFolderDALFactory, "findBySecretPath" | "findByManySecretPath">;
|
||||
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
|
||||
projectBotService: TProjectBotServiceFactory;
|
||||
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>;
|
||||
@ -31,7 +44,13 @@ export const integrationServiceFactory = ({
|
||||
integrationAuthDAL,
|
||||
folderDAL,
|
||||
permissionService,
|
||||
secretQueueService
|
||||
secretQueueService,
|
||||
integrationAuthService,
|
||||
projectBotService,
|
||||
secretV2BridgeDAL,
|
||||
secretImportDAL,
|
||||
kmsService,
|
||||
secretDAL
|
||||
}: TIntegrationServiceFactoryDep) => {
|
||||
const createIntegration = async ({
|
||||
app,
|
||||
@ -161,7 +180,14 @@ export const integrationServiceFactory = ({
|
||||
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);
|
||||
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);
|
||||
|
||||
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) => {
|
||||
// delete integration
|
||||
const deletedIntegrationResult = await integrationDAL.deleteById(id, tx);
|
||||
|
@ -63,6 +63,7 @@ export type TUpdateIntegrationDTO = {
|
||||
|
||||
export type TDeleteIntegrationDTO = {
|
||||
id: string;
|
||||
shouldDeleteIntegrationSecrets?: boolean;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
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
|
||||
.replicaNode()(TableName.ProjectMembership)
|
||||
.where(`${TableName.ProjectMembership}.projectId` as "projectId", projectId)
|
||||
.where(`${TableName.ProjectKeys}.projectId` as "projectId", projectId)
|
||||
.where(`${TableName.Users}.isGhost` as "isGhost", false)
|
||||
.join(TableName.Users, `${TableName.ProjectMembership}.userId`, `${TableName.Users}.id`)
|
||||
.join(TableName.ProjectKeys, `${TableName.ProjectMembership}.userId`, `${TableName.ProjectKeys}.receiverId`)
|
||||
|
@ -66,10 +66,10 @@ export const getBotKeyFnFactory = (
|
||||
await projectBotDAL.create({
|
||||
name: "Infisical Bot (Ghost)",
|
||||
projectId,
|
||||
isActive: true,
|
||||
tag,
|
||||
iv,
|
||||
encryptedPrivateKey: ciphertext,
|
||||
isActive: true,
|
||||
publicKey: botKey.publicKey,
|
||||
algorithm,
|
||||
keyEncoding: encoding,
|
||||
@ -80,6 +80,12 @@ export const getBotKeyFnFactory = (
|
||||
} else {
|
||||
await projectBotDAL.updateById(bot.id, {
|
||||
isActive: true,
|
||||
tag,
|
||||
iv,
|
||||
encryptedPrivateKey: ciphertext,
|
||||
publicKey: botKey.publicKey,
|
||||
algorithm,
|
||||
keyEncoding: encoding,
|
||||
encryptedProjectKey: encryptedWorkspaceKey.ciphertext,
|
||||
encryptedProjectKeyNonce: encryptedWorkspaceKey.nonce,
|
||||
senderId: projectV1Keys.userId
|
||||
@ -89,7 +95,6 @@ export const getBotKeyFnFactory = (
|
||||
}
|
||||
|
||||
const botPrivateKey = getBotPrivateKey({ bot });
|
||||
|
||||
const botKey = decryptAsymmetric({
|
||||
ciphertext: bot.encryptedProjectKey,
|
||||
privateKey: botPrivateKey,
|
||||
|
@ -256,7 +256,6 @@ export const projectMembershipServiceFactory = ({
|
||||
}
|
||||
|
||||
const bot = await projectBotDAL.findOne({ projectId });
|
||||
|
||||
if (!bot) {
|
||||
throw new BadRequestError({
|
||||
message: "Failed to find bot"
|
||||
@ -540,7 +539,7 @@ export const projectMembershipServiceFactory = ({
|
||||
const project = await projectDAL.findById(projectId);
|
||||
if (!project) throw new BadRequestError({ message: "Project not found" });
|
||||
|
||||
if (project.version !== ProjectVersion.V2) {
|
||||
if (project.version === ProjectVersion.V1) {
|
||||
throw new BadRequestError({
|
||||
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 { TOrgDALFactory } from "../org/org-dal";
|
||||
import { TOrgServiceFactory } from "../org/org-service";
|
||||
import { TProjectBotDALFactory } from "../project-bot/project-bot-dal";
|
||||
import { TProjectEnvDALFactory } from "../project-env/project-env-dal";
|
||||
import { TProjectKeyDALFactory } from "../project-key/project-key-dal";
|
||||
import { TProjectMembershipDALFactory } from "../project-membership/project-membership-dal";
|
||||
@ -74,6 +75,7 @@ type TProjectServiceFactoryDep = {
|
||||
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
|
||||
orgDAL: Pick<TOrgDALFactory, "findOne">;
|
||||
keyStore: Pick<TKeyStoreFactory, "deleteItem">;
|
||||
projectBotDAL: Pick<TProjectBotDALFactory, "create">;
|
||||
kmsService: Pick<
|
||||
TKmsServiceFactory,
|
||||
| "updateProjectSecretManagerKmsKey"
|
||||
@ -106,7 +108,8 @@ export const projectServiceFactory = ({
|
||||
certificateAuthorityDAL,
|
||||
certificateDAL,
|
||||
keyStore,
|
||||
kmsService
|
||||
kmsService,
|
||||
projectBotDAL
|
||||
}: TProjectServiceFactoryDep) => {
|
||||
/*
|
||||
* Create workspace. Make user the admin
|
||||
@ -206,7 +209,26 @@ export const projectServiceFactory = ({
|
||||
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
|
||||
const latestKey = await projectKeyDAL.findLatestProjectKey(ghostUser.user.id, project.id, tx);
|
||||
|
@ -4,6 +4,7 @@ import { logger } from "@app/lib/logger";
|
||||
import { QueueJobs, QueueName, TQueueServiceFactory } from "@app/queue";
|
||||
|
||||
import { TIdentityAccessTokenDALFactory } from "../identity-access-token/identity-access-token-dal";
|
||||
import { TIdentityUaClientSecretDALFactory } from "../identity-ua/identity-ua-client-secret-dal";
|
||||
import { TSecretVersionDALFactory } from "../secret/secret-version-dal";
|
||||
import { TSecretFolderVersionDALFactory } from "../secret-folder/secret-folder-version-dal";
|
||||
import { TSecretSharingDALFactory } from "../secret-sharing/secret-sharing-dal";
|
||||
@ -12,6 +13,7 @@ import { TSecretVersionV2DALFactory } from "../secret-v2-bridge/secret-version-d
|
||||
type TDailyResourceCleanUpQueueServiceFactoryDep = {
|
||||
auditLogDAL: Pick<TAuditLogDALFactory, "pruneAuditLog">;
|
||||
identityAccessTokenDAL: Pick<TIdentityAccessTokenDALFactory, "removeExpiredTokens">;
|
||||
identityUniversalAuthClientSecretDAL: Pick<TIdentityUaClientSecretDALFactory, "removeExpiredClientSecrets">;
|
||||
secretVersionDAL: Pick<TSecretVersionDALFactory, "pruneExcessVersions">;
|
||||
secretVersionV2DAL: Pick<TSecretVersionV2DALFactory, "pruneExcessVersions">;
|
||||
secretFolderVersionDAL: Pick<TSecretFolderVersionDALFactory, "pruneExcessVersions">;
|
||||
@ -30,12 +32,14 @@ export const dailyResourceCleanUpQueueServiceFactory = ({
|
||||
secretFolderVersionDAL,
|
||||
identityAccessTokenDAL,
|
||||
secretSharingDAL,
|
||||
secretVersionV2DAL
|
||||
secretVersionV2DAL,
|
||||
identityUniversalAuthClientSecretDAL
|
||||
}: TDailyResourceCleanUpQueueServiceFactoryDep) => {
|
||||
queueService.start(QueueName.DailyResourceCleanUp, async () => {
|
||||
logger.info(`${QueueName.DailyResourceCleanUp}: queue task started`);
|
||||
await auditLogDAL.pruneAuditLog();
|
||||
await identityAccessTokenDAL.removeExpiredTokens();
|
||||
await identityUniversalAuthClientSecretDAL.removeExpiredClientSecrets();
|
||||
await secretSharingDAL.pruneExpiredSharedSecrets();
|
||||
await snapshotDAL.pruneExcessSnapshots();
|
||||
await secretVersionDAL.pruneExcessVersions();
|
||||
|
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 { TProjectEnvDALFactory } from "../project-env/project-env-dal";
|
||||
import { TSecretFolderDALFactory } from "./secret-folder-dal";
|
||||
import { shouldCheckFolderPermission } from "./secret-folder-fns";
|
||||
import {
|
||||
TCreateFolderDTO,
|
||||
TDeleteFolderDTO,
|
||||
@ -57,10 +58,21 @@ export const secretFolderServiceFactory = ({
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Create,
|
||||
subject(ProjectPermissionSub.Secrets, { environment, secretPath })
|
||||
);
|
||||
|
||||
// we do this because we've split Secret and SecretFolder resources
|
||||
// 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 });
|
||||
if (!env) throw new BadRequestError({ message: "Environment not found", name: "Create folder" });
|
||||
@ -148,10 +160,20 @@ export const secretFolderServiceFactory = ({
|
||||
);
|
||||
|
||||
folders.forEach(({ environment, path: secretPath }) => {
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Edit,
|
||||
subject(ProjectPermissionSub.Secrets, { environment, secretPath })
|
||||
);
|
||||
// we do this because we've split Secret and SecretFolder resources
|
||||
// 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.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) =>
|
||||
@ -243,10 +265,21 @@ export const secretFolderServiceFactory = ({
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Edit,
|
||||
subject(ProjectPermissionSub.Secrets, { environment, secretPath })
|
||||
);
|
||||
|
||||
// we do this because we've split Secret and SecretFolder resources
|
||||
// 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);
|
||||
if (!parentFolder) throw new BadRequestError({ message: "Secret path not found" });
|
||||
@ -316,10 +349,21 @@ export const secretFolderServiceFactory = ({
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Delete,
|
||||
subject(ProjectPermissionSub.Secrets, { environment, secretPath })
|
||||
);
|
||||
|
||||
// we do this because we've split Secret and SecretFolder resources
|
||||
// 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 });
|
||||
if (!env) throw new BadRequestError({ message: "Environment not found", name: "Create folder" });
|
||||
|
@ -36,8 +36,8 @@ type TSecretImportSecretsV2 = {
|
||||
secretKey: string;
|
||||
// akhilmhdh: yes i know you can put ?.
|
||||
// But for somereason ts consider ? and undefined explicit as different just ts things
|
||||
secretValue: string | undefined;
|
||||
secretComment: string | undefined;
|
||||
secretValue: string;
|
||||
secretComment: string;
|
||||
})[];
|
||||
};
|
||||
|
||||
@ -157,7 +157,7 @@ export const fnSecretsV2FromImports = async ({
|
||||
secretImportDAL: Pick<TSecretImportDALFactory, "findByFolderIds">;
|
||||
depth?: number;
|
||||
cyclicDetector?: Set<string>;
|
||||
decryptor: (value?: Buffer | null) => string | undefined;
|
||||
decryptor: (value?: Buffer | null) => string;
|
||||
expandSecretReferences?: (
|
||||
secrets: Record<string, { value?: string; comment?: string; skipMultilineEncoding?: boolean | null }>
|
||||
) => Promise<Record<string, { value?: string; comment?: string; skipMultilineEncoding?: boolean | null }>>;
|
||||
@ -231,6 +231,7 @@ export const fnSecretsV2FromImports = async ({
|
||||
_id: item.id // The old Python SDK depends on the _id field being returned. We return this to keep the older Python SDK versions backwards compatible with the new Postgres backend.
|
||||
}))
|
||||
.concat(folderDeeperImportSecrets);
|
||||
|
||||
return {
|
||||
secretPath: importPath,
|
||||
environment: importEnv.slug,
|
||||
@ -254,7 +255,7 @@ export const fnSecretsV2FromImports = async ({
|
||||
};
|
||||
return acc;
|
||||
},
|
||||
{} as Record<string, { value?: string; comment?: string; skipMultilineEncoding?: boolean | null }>
|
||||
{} as Record<string, { value: string; comment?: string; skipMultilineEncoding?: boolean | null }>
|
||||
);
|
||||
// eslint-disable-next-line
|
||||
await expandSecretReferences(secretsGroupByKey);
|
||||
|
@ -507,7 +507,7 @@ export const secretImportServiceFactory = ({
|
||||
folderDAL,
|
||||
secretDAL: secretV2BridgeDAL,
|
||||
secretImportDAL,
|
||||
decryptor: (value) => (value ? secretManagerDecryptor({ cipherTextBlob: value }).toString() : undefined)
|
||||
decryptor: (value) => (value ? secretManagerDecryptor({ cipherTextBlob: value }).toString() : "")
|
||||
});
|
||||
return importedSecrets;
|
||||
}
|
||||
|
@ -51,7 +51,7 @@ export const secretTagDALFactory = (db: TDbClient) => {
|
||||
...secretTagOrm,
|
||||
saveTagsToSecret: secretJnTagOrm.insertMany,
|
||||
deleteTagsToSecret: secretJnTagOrm.delete,
|
||||
saveTagsToSecretV2: secretV2JnTagOrm.insertMany,
|
||||
saveTagsToSecretV2: secretV2JnTagOrm.batchInsert,
|
||||
deleteTagsToSecretV2: secretV2JnTagOrm.delete,
|
||||
findSecretTagsByProjectId,
|
||||
deleteTagsManySecret,
|
||||
|
@ -22,16 +22,7 @@ type TSecretTagServiceFactoryDep = {
|
||||
export type TSecretTagServiceFactory = ReturnType<typeof secretTagServiceFactory>;
|
||||
|
||||
export const secretTagServiceFactory = ({ secretTagDAL, permissionService }: TSecretTagServiceFactoryDep) => {
|
||||
const createTag = async ({
|
||||
name,
|
||||
slug,
|
||||
actor,
|
||||
color,
|
||||
actorId,
|
||||
actorOrgId,
|
||||
actorAuthMethod,
|
||||
projectId
|
||||
}: TCreateTagDTO) => {
|
||||
const createTag = async ({ slug, actor, color, actorId, actorOrgId, actorAuthMethod, projectId }: TCreateTagDTO) => {
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
actorId,
|
||||
@ -46,7 +37,6 @@ export const secretTagServiceFactory = ({ secretTagDAL, permissionService }: TSe
|
||||
|
||||
const newTag = await secretTagDAL.create({
|
||||
projectId,
|
||||
name,
|
||||
slug,
|
||||
color,
|
||||
createdBy: actorId,
|
||||
@ -55,7 +45,7 @@ export const secretTagServiceFactory = ({ secretTagDAL, permissionService }: TSe
|
||||
return newTag;
|
||||
};
|
||||
|
||||
const updateTag = async ({ actorId, actor, actorOrgId, actorAuthMethod, id, name, color, slug }: TUpdateTagDTO) => {
|
||||
const updateTag = async ({ actorId, actor, actorOrgId, actorAuthMethod, id, color, slug }: TUpdateTagDTO) => {
|
||||
const tag = await secretTagDAL.findById(id);
|
||||
if (!tag) throw new BadRequestError({ message: "Tag doesn't exist" });
|
||||
|
||||
@ -73,7 +63,7 @@ export const secretTagServiceFactory = ({ secretTagDAL, permissionService }: TSe
|
||||
);
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Tags);
|
||||
|
||||
const updatedTag = await secretTagDAL.updateById(tag.id, { name, color, slug });
|
||||
const updatedTag = await secretTagDAL.updateById(tag.id, { color, slug });
|
||||
return updatedTag;
|
||||
};
|
||||
|
||||
@ -107,7 +97,7 @@ export const secretTagServiceFactory = ({ secretTagDAL, permissionService }: TSe
|
||||
);
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Tags);
|
||||
|
||||
return tag;
|
||||
return { ...tag, name: tag.slug };
|
||||
};
|
||||
|
||||
const getTagBySlug = async ({ actorId, actor, actorOrgId, actorAuthMethod, slug, projectId }: TGetTagBySlugDTO) => {
|
||||
@ -123,7 +113,7 @@ export const secretTagServiceFactory = ({ secretTagDAL, permissionService }: TSe
|
||||
);
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Tags);
|
||||
|
||||
return tag;
|
||||
return { ...tag, name: tag.slug };
|
||||
};
|
||||
|
||||
const getProjectTags = async ({ actor, actorId, actorOrgId, actorAuthMethod, projectId }: TListProjectTagsDTO) => {
|
||||
|
@ -1,14 +1,12 @@
|
||||
import { TProjectPermission } from "@app/lib/types";
|
||||
|
||||
export type TCreateTagDTO = {
|
||||
name: string;
|
||||
color: string;
|
||||
slug: string;
|
||||
} & TProjectPermission;
|
||||
|
||||
export type TUpdateTagDTO = {
|
||||
id: string;
|
||||
name?: string;
|
||||
slug?: string;
|
||||
color?: string;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
@ -136,7 +136,6 @@ export const secretV2BridgeDALFactory = (db: TDbClient) => {
|
||||
.select(db.ref("id").withSchema(TableName.SecretTag).as("tagId"))
|
||||
.select(db.ref("color").withSchema(TableName.SecretTag).as("tagColor"))
|
||||
.select(db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug"))
|
||||
.select(db.ref("name").withSchema(TableName.SecretTag).as("tagName"))
|
||||
.orderBy("id", "asc");
|
||||
|
||||
const data = sqlNestRelationships({
|
||||
@ -147,11 +146,11 @@ export const secretV2BridgeDALFactory = (db: TDbClient) => {
|
||||
{
|
||||
key: "tagId",
|
||||
label: "tags" as const,
|
||||
mapper: ({ tagId: id, tagColor: color, tagSlug: slug, tagName: name }) => ({
|
||||
mapper: ({ tagId: id, tagColor: color, tagSlug: slug }) => ({
|
||||
id,
|
||||
color,
|
||||
slug,
|
||||
name
|
||||
name: slug
|
||||
})
|
||||
}
|
||||
]
|
||||
@ -169,14 +168,13 @@ export const secretV2BridgeDALFactory = (db: TDbClient) => {
|
||||
.where({ [`${TableName.SecretV2}Id` as const]: secretId })
|
||||
.select(db.ref("id").withSchema(TableName.SecretTag).as("tagId"))
|
||||
.select(db.ref("color").withSchema(TableName.SecretTag).as("tagColor"))
|
||||
.select(db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug"))
|
||||
.select(db.ref("name").withSchema(TableName.SecretTag).as("tagName"));
|
||||
.select(db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug"));
|
||||
|
||||
return tags.map((el) => ({
|
||||
id: el.tagId,
|
||||
color: el.tagColor,
|
||||
slug: el.tagSlug,
|
||||
name: el.tagName
|
||||
name: el.tagSlug
|
||||
}));
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: "get secret tags" });
|
||||
@ -210,7 +208,6 @@ export const secretV2BridgeDALFactory = (db: TDbClient) => {
|
||||
.select(db.ref("id").withSchema(TableName.SecretTag).as("tagId"))
|
||||
.select(db.ref("color").withSchema(TableName.SecretTag).as("tagColor"))
|
||||
.select(db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug"))
|
||||
.select(db.ref("name").withSchema(TableName.SecretTag).as("tagName"))
|
||||
.orderBy("id", "asc");
|
||||
|
||||
const data = sqlNestRelationships({
|
||||
@ -221,11 +218,11 @@ export const secretV2BridgeDALFactory = (db: TDbClient) => {
|
||||
{
|
||||
key: "tagId",
|
||||
label: "tags" as const,
|
||||
mapper: ({ tagId: id, tagColor: color, tagSlug: slug, tagName: name }) => ({
|
||||
mapper: ({ tagId: id, tagColor: color, tagSlug: slug }) => ({
|
||||
id,
|
||||
color,
|
||||
slug,
|
||||
name
|
||||
name: slug
|
||||
})
|
||||
}
|
||||
]
|
||||
@ -290,7 +287,7 @@ export const secretV2BridgeDALFactory = (db: TDbClient) => {
|
||||
}))
|
||||
);
|
||||
if (!newSecretReferences.length) return;
|
||||
const secretReferences = await (tx || db)(TableName.SecretReferenceV2).insert(newSecretReferences);
|
||||
const secretReferences = await (tx || db).batchInsert(TableName.SecretReferenceV2, newSecretReferences);
|
||||
return secretReferences;
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: "UpsertSecretReference" });
|
||||
@ -350,8 +347,7 @@ export const secretV2BridgeDALFactory = (db: TDbClient) => {
|
||||
.select(selectAllTableCols(TableName.SecretV2))
|
||||
.select(db.ref("id").withSchema(TableName.SecretTag).as("tagId"))
|
||||
.select(db.ref("color").withSchema(TableName.SecretTag).as("tagColor"))
|
||||
.select(db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug"))
|
||||
.select(db.ref("name").withSchema(TableName.SecretTag).as("tagName"));
|
||||
.select(db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug"));
|
||||
const docs = sqlNestRelationships({
|
||||
data: rawDocs,
|
||||
key: "id",
|
||||
@ -360,11 +356,11 @@ export const secretV2BridgeDALFactory = (db: TDbClient) => {
|
||||
{
|
||||
key: "tagId",
|
||||
label: "tags" as const,
|
||||
mapper: ({ tagId: id, tagColor: color, tagSlug: slug, tagName: name }) => ({
|
||||
mapper: ({ tagId: id, tagColor: color, tagSlug: slug }) => ({
|
||||
id,
|
||||
color,
|
||||
slug,
|
||||
name
|
||||
name: slug
|
||||
})
|
||||
}
|
||||
]
|
||||
|
@ -528,8 +528,8 @@ export const reshapeBridgeSecret = (
|
||||
environment: string,
|
||||
secretPath: string,
|
||||
secret: Omit<TSecretsV2, "encryptedValue" | "encryptedComment"> & {
|
||||
value?: string;
|
||||
comment?: string;
|
||||
value: string;
|
||||
comment: string;
|
||||
tags?: {
|
||||
id: string;
|
||||
slug: string;
|
||||
@ -542,8 +542,8 @@ export const reshapeBridgeSecret = (
|
||||
secretPath,
|
||||
workspace: workspaceId,
|
||||
environment,
|
||||
secretValue: secret.value,
|
||||
secretComment: secret.comment,
|
||||
secretValue: secret.value || "",
|
||||
secretComment: secret.comment || "",
|
||||
version: secret.version,
|
||||
type: secret.type,
|
||||
_id: secret.id,
|
||||
|
@ -196,7 +196,7 @@ export const secretV2BridgeServiceFactory = ({
|
||||
return reshapeBridgeSecret(projectId, environment, secretPath, {
|
||||
...secret[0],
|
||||
value: inputSecret.secretValue,
|
||||
comment: inputSecret.secretComment
|
||||
comment: inputSecret.secretComment || ""
|
||||
});
|
||||
};
|
||||
|
||||
@ -339,8 +339,8 @@ export const secretV2BridgeServiceFactory = ({
|
||||
});
|
||||
return reshapeBridgeSecret(projectId, environment, secretPath, {
|
||||
...updatedSecret[0],
|
||||
value: inputSecret.secretValue,
|
||||
comment: inputSecret.secretComment
|
||||
value: inputSecret.secretValue || "",
|
||||
comment: inputSecret.secretComment || ""
|
||||
});
|
||||
};
|
||||
|
||||
@ -378,6 +378,18 @@ export const secretV2BridgeServiceFactory = ({
|
||||
throw new BadRequestError({ message: "Must be user to delete personal secret" });
|
||||
}
|
||||
|
||||
const secretToDelete = await secretDAL.findOne({
|
||||
key: inputSecret.secretName,
|
||||
folderId,
|
||||
...(inputSecret.type === SecretType.Shared
|
||||
? {}
|
||||
: {
|
||||
type: SecretType.Personal,
|
||||
userId: actorId
|
||||
})
|
||||
});
|
||||
if (!secretToDelete) throw new NotFoundError({ message: "Secret not found" });
|
||||
|
||||
const deletedSecret = await secretDAL.transaction(async (tx) =>
|
||||
fnSecretBulkDelete({
|
||||
projectId,
|
||||
@ -412,10 +424,10 @@ export const secretV2BridgeServiceFactory = ({
|
||||
...deletedSecret[0],
|
||||
value: deletedSecret[0].encryptedValue
|
||||
? secretManagerDecryptor({ cipherTextBlob: deletedSecret[0].encryptedValue }).toString()
|
||||
: undefined,
|
||||
: "",
|
||||
comment: deletedSecret[0].encryptedComment
|
||||
? secretManagerDecryptor({ cipherTextBlob: deletedSecret[0].encryptedComment }).toString()
|
||||
: undefined
|
||||
: ""
|
||||
});
|
||||
};
|
||||
|
||||
@ -429,6 +441,7 @@ export const secretV2BridgeServiceFactory = ({
|
||||
actorAuthMethod,
|
||||
includeImports,
|
||||
recursive,
|
||||
tagSlugs = [],
|
||||
expandSecretReferences: shouldExpandSecretReferences
|
||||
}: TGetSecretsDTO) => {
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
@ -490,12 +503,15 @@ export const secretV2BridgeServiceFactory = ({
|
||||
...secret,
|
||||
value: secret.encryptedValue
|
||||
? secretManagerDecryptor({ cipherTextBlob: secret.encryptedValue }).toString()
|
||||
: undefined,
|
||||
: "",
|
||||
comment: secret.encryptedComment
|
||||
? secretManagerDecryptor({ cipherTextBlob: secret.encryptedComment }).toString()
|
||||
: undefined
|
||||
: ""
|
||||
})
|
||||
);
|
||||
const filteredSecrets = tagSlugs.length
|
||||
? decryptedSecrets.filter((secret) => Boolean(secret.tags?.find((el) => tagSlugs.includes(el.slug))))
|
||||
: decryptedSecrets;
|
||||
const expandSecretReferences = expandSecretReferencesFactory({
|
||||
projectId,
|
||||
folderDAL,
|
||||
@ -504,7 +520,7 @@ export const secretV2BridgeServiceFactory = ({
|
||||
});
|
||||
|
||||
if (shouldExpandSecretReferences) {
|
||||
const secretsGroupByPath = groupBy(decryptedSecrets, (i) => i.secretPath);
|
||||
const secretsGroupByPath = groupBy(filteredSecrets, (i) => i.secretPath);
|
||||
for (const secretPathKey in secretsGroupByPath) {
|
||||
if (Object.hasOwn(secretsGroupByPath, secretPathKey)) {
|
||||
const secretsGroupByKey = secretsGroupByPath[secretPathKey].reduce(
|
||||
@ -522,7 +538,7 @@ export const secretV2BridgeServiceFactory = ({
|
||||
await expandSecretReferences(secretsGroupByKey);
|
||||
secretsGroupByPath[secretPathKey].forEach((decryptedSecret) => {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
decryptedSecret.secretValue = secretsGroupByKey[decryptedSecret.secretKey].value;
|
||||
decryptedSecret.secretValue = secretsGroupByKey[decryptedSecret.secretKey].value || "";
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -530,7 +546,7 @@ export const secretV2BridgeServiceFactory = ({
|
||||
|
||||
if (!includeImports) {
|
||||
return {
|
||||
secrets: decryptedSecrets
|
||||
secrets: filteredSecrets
|
||||
};
|
||||
}
|
||||
|
||||
@ -554,11 +570,11 @@ export const secretV2BridgeServiceFactory = ({
|
||||
folderDAL,
|
||||
secretImportDAL,
|
||||
expandSecretReferences,
|
||||
decryptor: (value) => (value ? secretManagerDecryptor({ cipherTextBlob: value }).toString() : undefined)
|
||||
decryptor: (value) => (value ? secretManagerDecryptor({ cipherTextBlob: value }).toString() : "")
|
||||
});
|
||||
|
||||
return {
|
||||
secrets: decryptedSecrets,
|
||||
secrets: filteredSecrets,
|
||||
imports: importedSecrets
|
||||
};
|
||||
};
|
||||
@ -654,7 +670,7 @@ export const secretV2BridgeServiceFactory = ({
|
||||
secretDAL,
|
||||
folderDAL,
|
||||
secretImportDAL,
|
||||
decryptor: (value) => (value ? secretManagerDecryptor({ cipherTextBlob: value }).toString() : undefined),
|
||||
decryptor: (value) => (value ? secretManagerDecryptor({ cipherTextBlob: value }).toString() : ""),
|
||||
expandSecretReferences: shouldExpandSecretReferences ? expandSecretReferences : undefined
|
||||
});
|
||||
|
||||
@ -662,12 +678,11 @@ export const secretV2BridgeServiceFactory = ({
|
||||
for (let j = 0; j < importedSecrets[i].secrets.length; j += 1) {
|
||||
const importedSecret = importedSecrets[i].secrets[j];
|
||||
if (secretName === importedSecret.key) {
|
||||
return reshapeBridgeSecret(
|
||||
projectId,
|
||||
importedSecrets[i].environment,
|
||||
importedSecrets[i].secretPath,
|
||||
importedSecret
|
||||
);
|
||||
return reshapeBridgeSecret(projectId, importedSecrets[i].environment, importedSecrets[i].secretPath, {
|
||||
...importedSecret,
|
||||
value: importedSecret.secretValue || "",
|
||||
comment: importedSecret.secretComment || ""
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -676,7 +691,7 @@ export const secretV2BridgeServiceFactory = ({
|
||||
|
||||
let secretValue = secret.encryptedValue
|
||||
? secretManagerDecryptor({ cipherTextBlob: secret.encryptedValue }).toString()
|
||||
: undefined;
|
||||
: "";
|
||||
if (shouldExpandSecretReferences && secretValue) {
|
||||
const secretReferenceExpandedRecord = {
|
||||
[secret.key]: { value: secretValue }
|
||||
@ -691,7 +706,7 @@ export const secretV2BridgeServiceFactory = ({
|
||||
value: secretValue,
|
||||
comment: secret.encryptedComment
|
||||
? secretManagerDecryptor({ cipherTextBlob: secret.encryptedComment }).toString()
|
||||
: undefined
|
||||
: ""
|
||||
});
|
||||
};
|
||||
|
||||
@ -781,10 +796,8 @@ export const secretV2BridgeServiceFactory = ({
|
||||
return newSecrets.map((el) =>
|
||||
reshapeBridgeSecret(projectId, environment, secretPath, {
|
||||
...el,
|
||||
value: el.encryptedValue ? secretManagerDecryptor({ cipherTextBlob: el.encryptedValue }).toString() : undefined,
|
||||
comment: el.encryptedComment
|
||||
? secretManagerDecryptor({ cipherTextBlob: el.encryptedComment }).toString()
|
||||
: undefined
|
||||
value: el.encryptedValue ? secretManagerDecryptor({ cipherTextBlob: el.encryptedValue }).toString() : "",
|
||||
comment: el.encryptedComment ? secretManagerDecryptor({ cipherTextBlob: el.encryptedComment }).toString() : ""
|
||||
})
|
||||
);
|
||||
};
|
||||
@ -902,10 +915,8 @@ export const secretV2BridgeServiceFactory = ({
|
||||
return secrets.map((el) =>
|
||||
reshapeBridgeSecret(projectId, environment, secretPath, {
|
||||
...el,
|
||||
value: el.encryptedValue ? secretManagerDecryptor({ cipherTextBlob: el.encryptedValue }).toString() : undefined,
|
||||
comment: el.encryptedComment
|
||||
? secretManagerDecryptor({ cipherTextBlob: el.encryptedComment }).toString()
|
||||
: undefined
|
||||
value: el.encryptedValue ? secretManagerDecryptor({ cipherTextBlob: el.encryptedValue }).toString() : "",
|
||||
comment: el.encryptedComment ? secretManagerDecryptor({ cipherTextBlob: el.encryptedComment }).toString() : ""
|
||||
})
|
||||
);
|
||||
};
|
||||
@ -981,10 +992,8 @@ export const secretV2BridgeServiceFactory = ({
|
||||
return secretsDeleted.map((el) =>
|
||||
reshapeBridgeSecret(projectId, environment, secretPath, {
|
||||
...el,
|
||||
value: el.encryptedValue ? secretManagerDecryptor({ cipherTextBlob: el.encryptedValue }).toString() : undefined,
|
||||
comment: el.encryptedComment
|
||||
? secretManagerDecryptor({ cipherTextBlob: el.encryptedComment }).toString()
|
||||
: undefined
|
||||
value: el.encryptedValue ? secretManagerDecryptor({ cipherTextBlob: el.encryptedValue }).toString() : "",
|
||||
comment: el.encryptedComment ? secretManagerDecryptor({ cipherTextBlob: el.encryptedComment }).toString() : ""
|
||||
})
|
||||
);
|
||||
};
|
||||
@ -1020,10 +1029,8 @@ export const secretV2BridgeServiceFactory = ({
|
||||
return secretVersions.map((el) =>
|
||||
reshapeBridgeSecret(folder.projectId, folder.environment.envSlug, "/", {
|
||||
...el,
|
||||
value: el.encryptedValue ? secretManagerDecryptor({ cipherTextBlob: el.encryptedValue }).toString() : undefined,
|
||||
comment: el.encryptedComment
|
||||
? secretManagerDecryptor({ cipherTextBlob: el.encryptedComment }).toString()
|
||||
: undefined
|
||||
value: el.encryptedValue ? secretManagerDecryptor({ cipherTextBlob: el.encryptedValue }).toString() : "",
|
||||
comment: el.encryptedComment ? secretManagerDecryptor({ cipherTextBlob: el.encryptedComment }).toString() : ""
|
||||
})
|
||||
);
|
||||
};
|
||||
|
@ -20,6 +20,7 @@ export type TGetSecretsDTO = {
|
||||
environment: string;
|
||||
includeImports?: boolean;
|
||||
recursive?: boolean;
|
||||
tagSlugs?: string[];
|
||||
} & TProjectPermission;
|
||||
|
||||
export type TGetASecretDTO = {
|
||||
|
@ -123,7 +123,6 @@ export const secretDALFactory = (db: TDbClient) => {
|
||||
.select(db.ref("id").withSchema(TableName.SecretTag).as("tagId"))
|
||||
.select(db.ref("color").withSchema(TableName.SecretTag).as("tagColor"))
|
||||
.select(db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug"))
|
||||
.select(db.ref("name").withSchema(TableName.SecretTag).as("tagName"))
|
||||
.orderBy("id", "asc");
|
||||
const data = sqlNestRelationships({
|
||||
data: secs,
|
||||
@ -133,11 +132,11 @@ export const secretDALFactory = (db: TDbClient) => {
|
||||
{
|
||||
key: "tagId",
|
||||
label: "tags" as const,
|
||||
mapper: ({ tagId: id, tagColor: color, tagSlug: slug, tagName: name }) => ({
|
||||
mapper: ({ tagId: id, tagColor: color, tagSlug: slug }) => ({
|
||||
id,
|
||||
color,
|
||||
slug,
|
||||
name
|
||||
name: slug
|
||||
})
|
||||
}
|
||||
]
|
||||
@ -155,14 +154,13 @@ export const secretDALFactory = (db: TDbClient) => {
|
||||
.where({ [`${TableName.Secret}Id` as const]: secretId })
|
||||
.select(db.ref("id").withSchema(TableName.SecretTag).as("tagId"))
|
||||
.select(db.ref("color").withSchema(TableName.SecretTag).as("tagColor"))
|
||||
.select(db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug"))
|
||||
.select(db.ref("name").withSchema(TableName.SecretTag).as("tagName"));
|
||||
.select(db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug"));
|
||||
|
||||
return tags.map((el) => ({
|
||||
id: el.tagId,
|
||||
color: el.tagColor,
|
||||
slug: el.tagSlug,
|
||||
name: el.tagName
|
||||
name: el.tagSlug
|
||||
}));
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: "get secret tags" });
|
||||
@ -188,7 +186,6 @@ export const secretDALFactory = (db: TDbClient) => {
|
||||
.select(db.ref("id").withSchema(TableName.SecretTag).as("tagId"))
|
||||
.select(db.ref("color").withSchema(TableName.SecretTag).as("tagColor"))
|
||||
.select(db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug"))
|
||||
.select(db.ref("name").withSchema(TableName.SecretTag).as("tagName"))
|
||||
.orderBy("id", "asc");
|
||||
const data = sqlNestRelationships({
|
||||
data: secs,
|
||||
@ -198,11 +195,11 @@ export const secretDALFactory = (db: TDbClient) => {
|
||||
{
|
||||
key: "tagId",
|
||||
label: "tags" as const,
|
||||
mapper: ({ tagId: id, tagColor: color, tagSlug: slug, tagName: name }) => ({
|
||||
mapper: ({ tagId: id, tagColor: color, tagSlug: slug }) => ({
|
||||
id,
|
||||
color,
|
||||
slug,
|
||||
name
|
||||
name: slug
|
||||
})
|
||||
}
|
||||
]
|
||||
@ -318,8 +315,7 @@ export const secretDALFactory = (db: TDbClient) => {
|
||||
.select(selectAllTableCols(TableName.Secret))
|
||||
.select(db.ref("id").withSchema(TableName.SecretTag).as("tagId"))
|
||||
.select(db.ref("color").withSchema(TableName.SecretTag).as("tagColor"))
|
||||
.select(db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug"))
|
||||
.select(db.ref("name").withSchema(TableName.SecretTag).as("tagName"));
|
||||
.select(db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug"));
|
||||
const docs = sqlNestRelationships({
|
||||
data: rawDocs,
|
||||
key: "id",
|
||||
@ -328,11 +324,11 @@ export const secretDALFactory = (db: TDbClient) => {
|
||||
{
|
||||
key: "tagId",
|
||||
label: "tags" as const,
|
||||
mapper: ({ tagId: id, tagColor: color, tagSlug: slug, tagName: name }) => ({
|
||||
mapper: ({ tagId: id, tagColor: color, tagSlug: slug }) => ({
|
||||
id,
|
||||
color,
|
||||
slug,
|
||||
name
|
||||
name: slug
|
||||
})
|
||||
}
|
||||
]
|
||||
|
@ -370,7 +370,6 @@ export const decryptSecretRaw = (
|
||||
id: string;
|
||||
slug: string;
|
||||
color?: string | null;
|
||||
name: string;
|
||||
}[];
|
||||
},
|
||||
key: string
|
||||
@ -412,7 +411,7 @@ export const decryptSecretRaw = (
|
||||
_id: secret.id,
|
||||
id: secret.id,
|
||||
user: secret.userId,
|
||||
tags: secret.tags,
|
||||
tags: secret.tags?.map((el) => ({ ...el, name: el.slug })),
|
||||
skipMultilineEncoding: secret.skipMultilineEncoding,
|
||||
secretReminderRepeatDays: secret.secretReminderRepeatDays,
|
||||
secretReminderNote: secret.secretReminderNote,
|
||||
|
@ -73,12 +73,12 @@ type TSecretQueueFactoryDep = {
|
||||
secretVersionTagDAL: TSecretVersionTagDALFactory;
|
||||
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
|
||||
secretV2BridgeDAL: TSecretV2BridgeDALFactory;
|
||||
secretVersionV2BridgeDAL: Pick<TSecretVersionV2DALFactory, "insertMany" | "findLatestVersionMany">;
|
||||
secretVersionTagV2BridgeDAL: Pick<TSecretVersionV2TagDALFactory, "insertMany">;
|
||||
secretVersionV2BridgeDAL: Pick<TSecretVersionV2DALFactory, "batchInsert" | "insertMany" | "findLatestVersionMany">;
|
||||
secretVersionTagV2BridgeDAL: Pick<TSecretVersionV2TagDALFactory, "insertMany" | "batchInsert">;
|
||||
secretRotationDAL: Pick<TSecretRotationDALFactory, "secretOutputV2InsertMany" | "find">;
|
||||
secretApprovalRequestDAL: Pick<TSecretApprovalRequestDALFactory, "deleteByProjectId">;
|
||||
snapshotDAL: Pick<TSnapshotDALFactory, "findNSecretV1SnapshotByFolderId" | "deleteSnapshotsAboveLimit">;
|
||||
snapshotSecretV2BridgeDAL: Pick<TSnapshotSecretV2DALFactory, "insertMany">;
|
||||
snapshotSecretV2BridgeDAL: Pick<TSnapshotSecretV2DALFactory, "insertMany" | "batchInsert">;
|
||||
};
|
||||
|
||||
export type TGetSecrets = {
|
||||
@ -728,7 +728,10 @@ export const secretQueueFactory = ({
|
||||
isSynced: response?.isSynced ?? true
|
||||
});
|
||||
} catch (err) {
|
||||
logger.info("Secret integration sync error: %o", err);
|
||||
logger.error(
|
||||
err,
|
||||
`Secret integration sync error [projectId=${job.data.projectId}] [environment=${job.data.environment}] [secretPath=${job.data.secretPath}]`
|
||||
);
|
||||
|
||||
const message =
|
||||
(err instanceof AxiosError ? JSON.stringify(err?.response?.data) : (err as Error)?.message) ||
|
||||
@ -828,7 +831,7 @@ export const secretQueueFactory = ({
|
||||
secretId: string;
|
||||
references: { environment: string; secretPath: string; secretKey: string }[];
|
||||
}[] = [];
|
||||
await secretV2BridgeDAL.insertMany(
|
||||
await secretV2BridgeDAL.batchInsert(
|
||||
projectV1Secrets.map((el) => {
|
||||
const key = decryptSymmetric128BitHexKeyUTF8({
|
||||
ciphertext: el.secretKeyCiphertext,
|
||||
@ -1004,14 +1007,14 @@ export const secretQueueFactory = ({
|
||||
|
||||
const projectV3SecretVersions = Object.values(projectV3SecretVersionsGroupById);
|
||||
if (projectV3SecretVersions.length) {
|
||||
await secretVersionV2BridgeDAL.insertMany(projectV3SecretVersions, tx);
|
||||
await secretVersionV2BridgeDAL.batchInsert(projectV3SecretVersions, tx);
|
||||
}
|
||||
if (projectV3SecretVersionTags.length) {
|
||||
await secretVersionTagV2BridgeDAL.insertMany(projectV3SecretVersionTags, tx);
|
||||
await secretVersionTagV2BridgeDAL.batchInsert(projectV3SecretVersionTags, tx);
|
||||
}
|
||||
|
||||
if (projectV3SnapshotSecrets.length) {
|
||||
await snapshotSecretV2BridgeDAL.insertMany(projectV3SnapshotSecrets, tx);
|
||||
await snapshotSecretV2BridgeDAL.batchInsert(projectV3SnapshotSecrets, tx);
|
||||
}
|
||||
await snapshotDAL.deleteSnapshotsAboveLimit(folderId, SNAPSHOT_BATCH_SIZE, tx);
|
||||
}
|
||||
@ -1133,7 +1136,7 @@ export const secretQueueFactory = ({
|
||||
});
|
||||
|
||||
queueService.start(QueueName.SecretWebhook, async (job) => {
|
||||
await fnTriggerWebhook({ ...job.data, projectEnvDAL, webhookDAL, projectDAL, kmsService });
|
||||
await fnTriggerWebhook({ ...job.data, projectEnvDAL, webhookDAL, projectDAL });
|
||||
});
|
||||
|
||||
return {
|
||||
|
@ -964,7 +964,8 @@ export const secretServiceFactory = ({
|
||||
environment,
|
||||
includeImports,
|
||||
expandSecretReferences,
|
||||
recursive
|
||||
recursive,
|
||||
tagSlugs = []
|
||||
}: TGetSecretsRawDTO) => {
|
||||
const { botKey, shouldUseSecretV2Bridge } = await projectBotService.getBotKey(projectId);
|
||||
if (shouldUseSecretV2Bridge) {
|
||||
@ -978,7 +979,8 @@ export const secretServiceFactory = ({
|
||||
path,
|
||||
recursive,
|
||||
actorAuthMethod,
|
||||
includeImports
|
||||
includeImports,
|
||||
tagSlugs
|
||||
});
|
||||
return { secrets, imports };
|
||||
}
|
||||
@ -998,6 +1000,9 @@ export const secretServiceFactory = ({
|
||||
});
|
||||
|
||||
const decryptedSecrets = secrets.map((el) => decryptSecretRaw(el, botKey));
|
||||
const filteredSecrets = tagSlugs.length
|
||||
? decryptedSecrets.filter((secret) => Boolean(secret.tags?.find((el) => tagSlugs.includes(el.slug))))
|
||||
: decryptedSecrets;
|
||||
const processedImports = (imports || [])?.map(({ secrets: importedSecrets, ...el }) => {
|
||||
const decryptedImportSecrets = importedSecrets.map((sec) =>
|
||||
decryptSecretRaw(
|
||||
@ -1106,14 +1111,14 @@ export const secretServiceFactory = ({
|
||||
};
|
||||
|
||||
// expand secrets
|
||||
await batchSecretsExpand(decryptedSecrets);
|
||||
await batchSecretsExpand(filteredSecrets);
|
||||
|
||||
// expand imports by batch
|
||||
await Promise.all(processedImports.map((processedImport) => batchSecretsExpand(processedImport.secrets)));
|
||||
}
|
||||
|
||||
return {
|
||||
secrets: decryptedSecrets,
|
||||
secrets: filteredSecrets,
|
||||
imports: processedImports
|
||||
};
|
||||
};
|
||||
@ -1149,6 +1154,7 @@ export const secretServiceFactory = ({
|
||||
type,
|
||||
secretName
|
||||
});
|
||||
|
||||
return secret;
|
||||
}
|
||||
|
||||
@ -2081,7 +2087,7 @@ export const secretServiceFactory = ({
|
||||
|
||||
return {
|
||||
...updatedSecret[0],
|
||||
tags: [...existingSecretTags, ...tags].map((t) => ({ id: t.id, slug: t.slug, name: t.name, color: t.color }))
|
||||
tags: [...existingSecretTags, ...tags].map((t) => ({ id: t.id, slug: t.slug, name: t.slug, color: t.color }))
|
||||
};
|
||||
};
|
||||
|
||||
|
@ -149,6 +149,7 @@ export type TGetSecretsRawDTO = {
|
||||
environment: string;
|
||||
includeImports?: boolean;
|
||||
recursive?: boolean;
|
||||
tagSlugs?: string[];
|
||||
} & TProjectPermission;
|
||||
|
||||
export type TGetASecretRawDTO = {
|
||||
|
@ -3,12 +3,12 @@ import crypto from "node:crypto";
|
||||
import { AxiosError } from "axios";
|
||||
import picomatch from "picomatch";
|
||||
|
||||
import { SecretKeyEncoding, TWebhooks } from "@app/db/schemas";
|
||||
import { request } from "@app/lib/config/request";
|
||||
import { infisicalSymmetricDecrypt } from "@app/lib/crypto/encryption";
|
||||
import { BadRequestError } from "@app/lib/errors";
|
||||
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 { TProjectEnvDALFactory } from "../project-env/project-env-dal";
|
||||
import { TWebhookDALFactory } from "./webhook-dal";
|
||||
@ -16,12 +16,40 @@ import { WebhookType } from "./webhook-types";
|
||||
|
||||
const WEBHOOK_TRIGGER_TIMEOUT = 15 * 1000;
|
||||
|
||||
export const triggerWebhookRequest = async (
|
||||
{ webhookSecretKey: secretKey, webhookUrl: url }: { webhookSecretKey?: string; webhookUrl: string },
|
||||
data: Record<string, unknown>
|
||||
) => {
|
||||
export const decryptWebhookDetails = (webhook: TWebhooks) => {
|
||||
const { keyEncoding, iv, encryptedSecretKey, tag, urlCipherText, urlIV, urlTag, url } = webhook;
|
||||
|
||||
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 payload = { ...data, timestamp: Date.now() };
|
||||
const { secretKey, url } = decryptWebhookDetails(webhook);
|
||||
|
||||
if (secretKey) {
|
||||
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">;
|
||||
projectEnvDAL: Pick<TProjectEnvDALFactory, "findOne">;
|
||||
projectDAL: Pick<TProjectDALFactory, "findById">;
|
||||
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
|
||||
};
|
||||
|
||||
// this is reusable function
|
||||
@ -107,8 +134,7 @@ export const fnTriggerWebhook = async ({
|
||||
projectId,
|
||||
webhookDAL,
|
||||
projectEnvDAL,
|
||||
projectDAL,
|
||||
kmsService
|
||||
projectDAL
|
||||
}: TFnTriggerWebhookDTO) => {
|
||||
const webhooks = await webhookDAL.findAllWebhooks(projectId, environment);
|
||||
const toBeTriggeredHooks = webhooks.filter(
|
||||
@ -118,20 +144,10 @@ export const fnTriggerWebhook = async ({
|
||||
if (!toBeTriggeredHooks.length) return;
|
||||
logger.info("Secret webhook job started", { environment, secretPath, projectId });
|
||||
const project = await projectDAL.findById(projectId);
|
||||
const { decryptor: kmsDataKeyDecryptor } = await kmsService.createCipherPairWithDataKey({
|
||||
projectId,
|
||||
type: KmsDataKey.SecretManager
|
||||
});
|
||||
|
||||
const webhooksTriggered = await Promise.allSettled(
|
||||
toBeTriggeredHooks.map((hook) => {
|
||||
const webhookUrl = kmsDataKeyDecryptor({ cipherTextBlob: hook.encryptedUrl }).toString();
|
||||
const webhookSecretKey = hook.encryptedSecretKeyWithKms
|
||||
? kmsDataKeyDecryptor({ cipherTextBlob: hook.encryptedSecretKeyWithKms }).toString()
|
||||
: undefined;
|
||||
|
||||
return triggerWebhookRequest(
|
||||
{ webhookUrl, webhookSecretKey },
|
||||
toBeTriggeredHooks.map((hook) =>
|
||||
triggerWebhookRequest(
|
||||
hook,
|
||||
getWebhookPayload("secrets.modified", {
|
||||
workspaceName: project.name,
|
||||
workspaceId: projectId,
|
||||
@ -139,8 +155,8 @@ export const fnTriggerWebhook = async ({
|
||||
secretPath,
|
||||
type: hook.type
|
||||
})
|
||||
);
|
||||
})
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
// filter hooks by status
|
||||
|
@ -1,15 +1,15 @@
|
||||
import { ForbiddenError } from "@casl/ability";
|
||||
|
||||
import { TWebhooksInsert } from "@app/db/schemas";
|
||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
|
||||
import { infisicalSymmetricEncypt } from "@app/lib/crypto/encryption";
|
||||
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 { TProjectEnvDALFactory } from "../project-env/project-env-dal";
|
||||
import { TWebhookDALFactory } from "./webhook-dal";
|
||||
import { getWebhookPayload, triggerWebhookRequest } from "./webhook-fns";
|
||||
import { decryptWebhookDetails, getWebhookPayload, triggerWebhookRequest } from "./webhook-fns";
|
||||
import {
|
||||
TCreateWebhookDTO,
|
||||
TDeleteWebhookDTO,
|
||||
@ -23,7 +23,6 @@ type TWebhookServiceFactoryDep = {
|
||||
projectEnvDAL: TProjectEnvDALFactory;
|
||||
projectDAL: Pick<TProjectDALFactory, "findById">;
|
||||
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
|
||||
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
|
||||
};
|
||||
|
||||
export type TWebhookServiceFactory = ReturnType<typeof webhookServiceFactory>;
|
||||
@ -32,8 +31,7 @@ export const webhookServiceFactory = ({
|
||||
webhookDAL,
|
||||
projectEnvDAL,
|
||||
permissionService,
|
||||
projectDAL,
|
||||
kmsService
|
||||
projectDAL
|
||||
}: TWebhookServiceFactoryDep) => {
|
||||
const createWebhook = async ({
|
||||
actor,
|
||||
@ -58,28 +56,33 @@ export const webhookServiceFactory = ({
|
||||
const env = await projectEnvDAL.findOne({ projectId, slug: environment });
|
||||
if (!env) throw new BadRequestError({ message: "Env not found" });
|
||||
|
||||
const { encryptor: secretManagerEncryptor } = await kmsService.createCipherPairWithDataKey({
|
||||
projectId,
|
||||
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,
|
||||
const insertDoc: TWebhooksInsert = {
|
||||
url: "", // deprecated - we are moving away from plaintext URLs
|
||||
envId: env.id,
|
||||
isDisabled: false,
|
||||
secretPath: secretPath || "/",
|
||||
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 };
|
||||
};
|
||||
|
||||
@ -133,18 +136,9 @@ export const webhookServiceFactory = ({
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Webhooks);
|
||||
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 {
|
||||
await triggerWebhookRequest(
|
||||
{ webhookUrl, webhookSecretKey },
|
||||
webhook,
|
||||
getWebhookPayload("test", {
|
||||
workspaceName: project.name,
|
||||
workspaceId: webhook.projectId,
|
||||
@ -183,15 +177,11 @@ export const webhookServiceFactory = ({
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Webhooks);
|
||||
|
||||
const webhooks = await webhookDAL.findAllWebhooks(projectId, environment, secretPath);
|
||||
const { decryptor: kmsDataKeyDecryptor } = await kmsService.createCipherPairWithDataKey({
|
||||
type: KmsDataKey.SecretManager,
|
||||
projectId
|
||||
});
|
||||
return webhooks.map((w) => {
|
||||
const decryptedUrl = kmsDataKeyDecryptor({ cipherTextBlob: w.encryptedUrl }).toString();
|
||||
const { url } = decryptWebhookDetails(w);
|
||||
return {
|
||||
...w,
|
||||
url: decryptedUrl
|
||||
url
|
||||
};
|
||||
});
|
||||
};
|
||||
|
@ -404,6 +404,10 @@ func CallGetRawSecretsV3(httpClient *resty.Client, request GetRawSecretsV3Reques
|
||||
SetQueryParam("environment", request.Environment).
|
||||
SetQueryParam("secretPath", request.SecretPath)
|
||||
|
||||
if request.TagSlugs != "" {
|
||||
req.SetQueryParam("tagSlugs", request.TagSlugs)
|
||||
}
|
||||
|
||||
if request.IncludeImport {
|
||||
req.SetQueryParam("include_imports", "true")
|
||||
}
|
||||
|
@ -574,6 +574,7 @@ type GetRawSecretsV3Request struct {
|
||||
SecretPath string `json:"secretPath"`
|
||||
IncludeImport bool `json:"include_imports"`
|
||||
Recursive bool `json:"recursive"`
|
||||
TagSlugs string `json:"tagSlugs,omitempty"`
|
||||
}
|
||||
|
||||
type GetRawSecretsV3Response struct {
|
||||
|
@ -312,7 +312,7 @@ func ParseAgentConfig(configFile []byte) (*Config, error) {
|
||||
|
||||
func secretTemplateFunction(accessToken string, existingEtag string, currentEtag *string) func(string, string, string) ([]models.SingleEnvironmentVariable, error) {
|
||||
return func(projectID, envSlug, secretPath string) ([]models.SingleEnvironmentVariable, error) {
|
||||
res, err := util.GetPlainTextSecretsV3(accessToken, projectID, envSlug, secretPath, false, false)
|
||||
res, err := util.GetPlainTextSecretsV3(accessToken, projectID, envSlug, secretPath, false, false, "")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -14,6 +14,7 @@ import (
|
||||
"github.com/Infisical/infisical-merge/packages/util"
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/spf13/cobra"
|
||||
"gopkg.in/yaml.v2"
|
||||
)
|
||||
|
||||
const (
|
||||
@ -188,7 +189,7 @@ func formatEnvs(envs []models.SingleEnvironmentVariable, format string) (string,
|
||||
case FormatCSV:
|
||||
return formatAsCSV(envs), nil
|
||||
case FormatYaml:
|
||||
return formatAsYaml(envs), nil
|
||||
return formatAsYaml(envs)
|
||||
default:
|
||||
return "", fmt.Errorf("invalid format type: %s. Available format types are [%s]", format, []string{FormatDotenv, FormatJson, FormatCSV, FormatYaml, FormatDotEnvExport})
|
||||
}
|
||||
@ -224,12 +225,18 @@ func formatAsDotEnvExport(envs []models.SingleEnvironmentVariable) string {
|
||||
return dotenv
|
||||
}
|
||||
|
||||
func formatAsYaml(envs []models.SingleEnvironmentVariable) string {
|
||||
var dotenv string
|
||||
func formatAsYaml(envs []models.SingleEnvironmentVariable) (string, error) {
|
||||
m := make(map[string]string)
|
||||
for _, env := range envs {
|
||||
dotenv += fmt.Sprintf("%s: %s\n", env.Key, env.Value)
|
||||
m[env.Key] = env.Value
|
||||
}
|
||||
return dotenv
|
||||
|
||||
yamlBytes, err := yaml.Marshal(m)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to format environment variables as YAML: %w", err)
|
||||
}
|
||||
|
||||
return string(yamlBytes), nil
|
||||
}
|
||||
|
||||
// Format environment variables as a JSON file
|
||||
|
79
cli/packages/cmd/export_test.go
Normal file
79
cli/packages/cmd/export_test.go
Normal file
@ -0,0 +1,79 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/Infisical/infisical-merge/packages/models"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"gopkg.in/yaml.v2"
|
||||
)
|
||||
|
||||
func TestFormatAsYaml(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input []models.SingleEnvironmentVariable
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
name: "Empty input",
|
||||
input: []models.SingleEnvironmentVariable{},
|
||||
expected: "{}\n",
|
||||
},
|
||||
{
|
||||
name: "Single environment variable",
|
||||
input: []models.SingleEnvironmentVariable{
|
||||
{Key: "KEY1", Value: "VALUE1"},
|
||||
},
|
||||
expected: "KEY1: VALUE1\n",
|
||||
},
|
||||
{
|
||||
name: "Multiple environment variables",
|
||||
input: []models.SingleEnvironmentVariable{
|
||||
{Key: "KEY1", Value: "VALUE1"},
|
||||
{Key: "KEY2", Value: "VALUE2"},
|
||||
{Key: "KEY3", Value: "VALUE3"},
|
||||
},
|
||||
expected: "KEY1: VALUE1\nKEY2: VALUE2\nKEY3: VALUE3\n",
|
||||
},
|
||||
{
|
||||
name: "Overwriting duplicate keys",
|
||||
input: []models.SingleEnvironmentVariable{
|
||||
{Key: "KEY1", Value: "VALUE1"},
|
||||
{Key: "KEY1", Value: "VALUE2"},
|
||||
},
|
||||
expected: "KEY1: VALUE2\n",
|
||||
},
|
||||
{
|
||||
name: "Special characters in values",
|
||||
input: []models.SingleEnvironmentVariable{
|
||||
{Key: "KEY1", Value: "Value with spaces"},
|
||||
{Key: "KEY2", Value: "Value:with:colons"},
|
||||
{Key: "KEY3", Value: "Value\nwith\nnewlines"},
|
||||
},
|
||||
expected: "KEY1: Value with spaces\nKEY2: Value:with:colons\nKEY3: |-\n Value\n with\n newlines\n",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result, err := formatAsYaml(tt.input)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Compare the result with the expected output
|
||||
assert.Equal(t, tt.expected, result)
|
||||
|
||||
// Additionally, parse the result back into a map to ensure it's valid YAML
|
||||
var resultMap map[string]string
|
||||
err = yaml.Unmarshal([]byte(result), &resultMap)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Create an expected map from the input
|
||||
expectedMap := make(map[string]string)
|
||||
for _, env := range tt.input {
|
||||
expectedMap[env.Key] = env.Value
|
||||
}
|
||||
|
||||
assert.Equal(t, expectedMap, resultMap)
|
||||
})
|
||||
}
|
||||
}
|
@ -155,22 +155,24 @@ var secretsSetCmd = &cobra.Command{
|
||||
DisableFlagsInUseLine: true,
|
||||
Args: cobra.MinimumNArgs(1),
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
util.RequireLocalWorkspaceFile()
|
||||
|
||||
environmentName, _ := cmd.Flags().GetString("env")
|
||||
if !cmd.Flags().Changed("env") {
|
||||
environmentFromWorkspace := util.GetEnvFromWorkspaceFile()
|
||||
if environmentFromWorkspace != "" {
|
||||
environmentName = environmentFromWorkspace
|
||||
}
|
||||
}
|
||||
|
||||
token, err := util.GetInfisicalToken(cmd)
|
||||
if err != nil {
|
||||
util.HandleError(err, "Unable to parse flag")
|
||||
}
|
||||
|
||||
projectId, err := cmd.Flags().GetString("projectId")
|
||||
if (token == nil) {
|
||||
util.RequireLocalWorkspaceFile()
|
||||
}
|
||||
|
||||
environmentName, _ := cmd.Flags().GetString("env")
|
||||
if !cmd.Flags().Changed("env") {
|
||||
environmentFromWorkspace := util.GetEnvFromWorkspaceFile()
|
||||
if environmentFromWorkspace != "" {
|
||||
environmentName = environmentFromWorkspace
|
||||
}
|
||||
}
|
||||
|
||||
projectId, err := cmd.Flags().GetString("projectId")
|
||||
if err != nil {
|
||||
util.HandleError(err, "Unable to parse flag")
|
||||
}
|
||||
@ -374,6 +376,11 @@ func getSecretsByNames(cmd *cobra.Command, args []string) {
|
||||
util.HandleError(err, "Unable to parse flag")
|
||||
}
|
||||
|
||||
secretOverriding, err := cmd.Flags().GetBool("secret-overriding")
|
||||
if err != nil {
|
||||
util.HandleError(err, "Unable to parse flag")
|
||||
}
|
||||
|
||||
request := models.GetAllSecretsParameters{
|
||||
Environment: environmentName,
|
||||
WorkspaceId: projectId,
|
||||
@ -394,6 +401,12 @@ func getSecretsByNames(cmd *cobra.Command, args []string) {
|
||||
util.HandleError(err, "To fetch all secrets")
|
||||
}
|
||||
|
||||
if secretOverriding {
|
||||
secrets = util.OverrideSecrets(secrets, util.SECRET_TYPE_PERSONAL)
|
||||
} else {
|
||||
secrets = util.OverrideSecrets(secrets, util.SECRET_TYPE_SHARED)
|
||||
}
|
||||
|
||||
if shouldExpand {
|
||||
authParams := models.ExpandSecretsAuthentication{}
|
||||
if token != nil && token.Type == util.SERVICE_TOKEN_IDENTIFIER {
|
||||
@ -413,11 +426,13 @@ func getSecretsByNames(cmd *cobra.Command, args []string) {
|
||||
if value, ok := secretsMap[secretKeyFromArg]; ok {
|
||||
requestedSecrets = append(requestedSecrets, value)
|
||||
} else {
|
||||
requestedSecrets = append(requestedSecrets, models.SingleEnvironmentVariable{
|
||||
Key: secretKeyFromArg,
|
||||
Type: "*not found*",
|
||||
Value: "*not found*",
|
||||
})
|
||||
if !(plainOutput || showOnlyValue) {
|
||||
requestedSecrets = append(requestedSecrets, models.SingleEnvironmentVariable{
|
||||
Key: secretKeyFromArg,
|
||||
Type: "*not found*",
|
||||
Value: "*not found*",
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -688,6 +703,7 @@ func init() {
|
||||
secretsGetCmd.Flags().Bool("include-imports", true, "Imported linked secrets ")
|
||||
secretsGetCmd.Flags().Bool("expand", true, "Parse shell parameter expansions in your secrets, and process your referenced secrets")
|
||||
secretsGetCmd.Flags().Bool("recursive", false, "Fetch secrets from all sub-folders")
|
||||
secretsGetCmd.Flags().Bool("secret-overriding", true, "Prioritizes personal secrets, if any, with the same name over shared secrets")
|
||||
secretsCmd.AddCommand(secretsGetCmd)
|
||||
secretsCmd.Flags().Bool("secret-overriding", true, "Prioritizes personal secrets, if any, with the same name over shared secrets")
|
||||
secretsCmd.AddCommand(secretsSetCmd)
|
||||
|
@ -4,6 +4,7 @@ Copyright (c) 2023 Infisical Inc.
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
@ -13,13 +14,26 @@ import (
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var AvailableVaultsAndDescriptions = []string{"auto (automatically select native vault on system)", "file (encrypted file vault)"}
|
||||
var AvailableVaults = []string{"auto", "file"}
|
||||
type VaultBackendType struct {
|
||||
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{
|
||||
Example: `infisical vault set pass`,
|
||||
Use: "set [vault-name]",
|
||||
Short: "Used to set the vault backend to store your login details securely at rest",
|
||||
Example: `infisical vault set file`,
|
||||
Use: "set [file|auto]",
|
||||
Short: "Used to configure the vault backends",
|
||||
DisableFlagsInUseLine: true,
|
||||
Args: cobra.MinimumNArgs(1),
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
@ -35,15 +49,16 @@ var vaultSetCmd = &cobra.Command{
|
||||
return
|
||||
}
|
||||
|
||||
if wantedVaultTypeName == "auto" || wantedVaultTypeName == "file" {
|
||||
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
|
||||
configFile.VaultBackendType = wantedVaultTypeName
|
||||
configFile.LoggedInUserEmail = ""
|
||||
configFile.VaultBackendPassphrase = base64.StdEncoding.EncodeToString([]byte(util.GenerateRandomString(10)))
|
||||
|
||||
err = util.WriteConfigFile(&configFile)
|
||||
if err != nil {
|
||||
@ -55,7 +70,11 @@ var vaultSetCmd = &cobra.Command{
|
||||
|
||||
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 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, ", "))
|
||||
}
|
||||
},
|
||||
}
|
||||
@ -73,8 +92,8 @@ var vaultCmd = &cobra.Command{
|
||||
|
||||
func printAvailableVaultBackends() {
|
||||
fmt.Printf("Vaults are used to securely store your login details locally. Available vaults:")
|
||||
for _, backend := range AvailableVaultsAndDescriptions {
|
||||
fmt.Printf("\n- %s", backend)
|
||||
for _, vaultType := range AvailableVaults {
|
||||
fmt.Printf("\n- %s (%s)", vaultType.Name, vaultType.Description)
|
||||
}
|
||||
|
||||
currentVaultBackend, err := util.GetCurrentVaultBackend()
|
||||
@ -89,5 +108,6 @@ func printAvailableVaultBackends() {
|
||||
|
||||
func init() {
|
||||
vaultCmd.AddCommand(vaultSetCmd)
|
||||
|
||||
rootCmd.AddCommand(vaultCmd)
|
||||
}
|
||||
|
@ -11,10 +11,11 @@ type UserCredentials struct {
|
||||
|
||||
// The file struct for Infisical config file
|
||||
type ConfigFile struct {
|
||||
LoggedInUserEmail string `json:"loggedInUserEmail"`
|
||||
LoggedInUserDomain string `json:"LoggedInUserDomain,omitempty"`
|
||||
LoggedInUsers []LoggedInUser `json:"loggedInUsers,omitempty"`
|
||||
VaultBackendType string `json:"vaultBackendType,omitempty"`
|
||||
LoggedInUserEmail string `json:"loggedInUserEmail"`
|
||||
LoggedInUserDomain string `json:"LoggedInUserDomain,omitempty"`
|
||||
LoggedInUsers []LoggedInUser `json:"loggedInUsers,omitempty"`
|
||||
VaultBackendType string `json:"vaultBackendType,omitempty"`
|
||||
VaultBackendPassphrase string `json:"vaultBackendPassphrase,omitempty"`
|
||||
}
|
||||
|
||||
type LoggedInUser struct {
|
||||
|
@ -1,6 +1,7 @@
|
||||
package util
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
@ -50,10 +51,11 @@ func WriteInitalConfig(userCredentials *models.UserCredentials) error {
|
||||
}
|
||||
|
||||
configFile := models.ConfigFile{
|
||||
LoggedInUserEmail: userCredentials.Email,
|
||||
LoggedInUserDomain: config.INFISICAL_URL,
|
||||
LoggedInUsers: existingConfigFile.LoggedInUsers,
|
||||
VaultBackendType: existingConfigFile.VaultBackendType,
|
||||
LoggedInUserEmail: userCredentials.Email,
|
||||
LoggedInUserDomain: config.INFISICAL_URL,
|
||||
LoggedInUsers: existingConfigFile.LoggedInUsers,
|
||||
VaultBackendType: existingConfigFile.VaultBackendType,
|
||||
VaultBackendPassphrase: existingConfigFile.VaultBackendPassphrase,
|
||||
}
|
||||
|
||||
configFileMarshalled, err := json.Marshal(configFile)
|
||||
@ -215,6 +217,14 @@ func GetConfigFile() (models.ConfigFile, error) {
|
||||
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
|
||||
}
|
||||
|
||||
|
@ -8,6 +8,10 @@ const (
|
||||
INFISICAL_WORKSPACE_CONFIG_FILE_NAME = ".infisical.json"
|
||||
INFISICAL_TOKEN_NAME = "INFISICAL_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
|
||||
INFISICAL_UNIVERSAL_AUTH_CLIENT_ID_NAME = "INFISICAL_UNIVERSAL_AUTH_CLIENT_ID"
|
||||
@ -34,7 +38,8 @@ const (
|
||||
SERVICE_TOKEN_IDENTIFIER = "service-token"
|
||||
UNIVERSAL_AUTH_TOKEN_IDENTIFIER = "universal-auth-token"
|
||||
|
||||
INFISICAL_BACKUP_SECRET = "infisical-backup-secrets"
|
||||
INFISICAL_BACKUP_SECRET = "infisical-backup-secrets" // akhilmhdh: @depreciated remove in version v0.30
|
||||
INFISICAL_BACKUP_SECRET_ENCRYPTION_KEY = "infisical-backup-secret-encryption-key"
|
||||
)
|
||||
|
||||
var (
|
||||
|
@ -71,7 +71,7 @@ func GetCurrentLoggedInUserDetails() (LoggedInUserDetails, error) {
|
||||
if strings.Contains(err.Error(), "credentials not found in system keyring") {
|
||||
return LoggedInUserDetails{}, errors.New("we couldn't find your logged in details, try running [infisical login] then try again")
|
||||
} else {
|
||||
return LoggedInUserDetails{}, fmt.Errorf("failed to fetch creditnals from keyring because [err=%s]", err)
|
||||
return LoggedInUserDetails{}, fmt.Errorf("failed to fetch credentials from keyring because [err=%s]", err)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -5,6 +5,7 @@ import (
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path"
|
||||
@ -25,6 +26,8 @@ type DecodedSymmetricEncryptionDetails = struct {
|
||||
Key []byte
|
||||
}
|
||||
|
||||
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
|
||||
|
||||
func GetBase64DecodedSymmetricEncryptionDetails(key string, cipher string, IV string, tag string) (DecodedSymmetricEncryptionDetails, error) {
|
||||
cipherx, err := base64.StdEncoding.DecodeString(cipher)
|
||||
if err != nil {
|
||||
@ -287,3 +290,11 @@ func GetCmdFlagOrEnv(cmd *cobra.Command, flag, envName string) (string, error) {
|
||||
}
|
||||
return value, nil
|
||||
}
|
||||
|
||||
func GenerateRandomString(length int) string {
|
||||
b := make([]byte, length)
|
||||
for i := range b {
|
||||
b[i] = charset[rand.Intn(len(charset))]
|
||||
}
|
||||
return string(b)
|
||||
}
|
||||
|
@ -1,6 +1,10 @@
|
||||
package util
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/zalando/go-keyring"
|
||||
)
|
||||
|
||||
@ -20,16 +24,39 @@ func SetValueInKeyring(key, value string) error {
|
||||
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 {
|
||||
log.Debug().Msg(fmt.Sprintf("Error while setting default keyring: %v", err))
|
||||
configFile, _ := GetConfigFile()
|
||||
|
||||
if configFile.VaultBackendPassphrase == "" {
|
||||
encodedPassphrase := base64.StdEncoding.EncodeToString([]byte(GenerateRandomString(10))) // generate random passphrase
|
||||
configFile.VaultBackendPassphrase = encodedPassphrase
|
||||
configFile.VaultBackendType = VAULT_BACKEND_FILE_MODE
|
||||
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)
|
||||
log.Debug().Msg(fmt.Sprintf("Error while setting file keyring: %v", err))
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func GetValueInKeyring(key string) (string, error) {
|
||||
currentVaultBackend, err := GetCurrentVaultBackend()
|
||||
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)
|
||||
|
||||
}
|
||||
|
||||
func DeleteValueInKeyring(key string) error {
|
||||
|
@ -1,14 +1,15 @@
|
||||
package util
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path"
|
||||
"regexp"
|
||||
"slices"
|
||||
"strings"
|
||||
"unicode"
|
||||
|
||||
@ -20,7 +21,7 @@ import (
|
||||
"github.com/zalando/go-keyring"
|
||||
)
|
||||
|
||||
func GetPlainTextSecretsViaServiceToken(fullServiceToken string, environment string, secretPath string, includeImports bool, recursive bool) ([]models.SingleEnvironmentVariable, error) {
|
||||
func GetPlainTextSecretsViaServiceToken(fullServiceToken string, environment string, secretPath string, includeImports bool, recursive bool, tagSlugs string) ([]models.SingleEnvironmentVariable, error) {
|
||||
serviceTokenParts := strings.SplitN(fullServiceToken, ".", 4)
|
||||
if len(serviceTokenParts) < 4 {
|
||||
return nil, fmt.Errorf("invalid service token entered. Please double check your service token and try again")
|
||||
@ -53,6 +54,7 @@ func GetPlainTextSecretsViaServiceToken(fullServiceToken string, environment str
|
||||
SecretPath: secretPath,
|
||||
IncludeImport: includeImports,
|
||||
Recursive: recursive,
|
||||
TagSlugs: tagSlugs,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
@ -76,7 +78,7 @@ func GetPlainTextSecretsViaServiceToken(fullServiceToken string, environment str
|
||||
|
||||
}
|
||||
|
||||
func GetPlainTextSecretsV3(accessToken string, workspaceId string, environmentName string, secretsPath string, includeImports bool, recursive bool) (models.PlaintextSecretResult, error) {
|
||||
func GetPlainTextSecretsV3(accessToken string, workspaceId string, environmentName string, secretsPath string, includeImports bool, recursive bool, tagSlugs string) (models.PlaintextSecretResult, error) {
|
||||
httpClient := resty.New()
|
||||
httpClient.SetAuthToken(accessToken).
|
||||
SetHeader("Accept", "application/json")
|
||||
@ -86,7 +88,7 @@ func GetPlainTextSecretsV3(accessToken string, workspaceId string, environmentNa
|
||||
Environment: environmentName,
|
||||
IncludeImport: includeImports,
|
||||
Recursive: recursive,
|
||||
// TagSlugs: tagSlugs,
|
||||
TagSlugs: tagSlugs,
|
||||
}
|
||||
|
||||
if secretsPath != "" {
|
||||
@ -281,29 +283,36 @@ func GetAllEnvironmentVariables(params models.GetAllSecretsParameters, projectCo
|
||||
}
|
||||
|
||||
res, err := GetPlainTextSecretsV3(loggedInUserDetails.UserCredentials.JTWToken, infisicalDotJson.WorkspaceId,
|
||||
params.Environment, params.SecretsPath, params.IncludeImport, params.Recursive)
|
||||
params.Environment, params.SecretsPath, params.IncludeImport, params.Recursive, params.TagSlugs)
|
||||
log.Debug().Msgf("GetAllEnvironmentVariables: Trying to fetch secrets JTW token [err=%s]", err)
|
||||
|
||||
if err == nil {
|
||||
WriteBackupSecrets(infisicalDotJson.WorkspaceId, params.Environment, params.SecretsPath, res.Secrets)
|
||||
backupEncryptionKey, err := GetBackupEncryptionKey()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
WriteBackupSecrets(infisicalDotJson.WorkspaceId, params.Environment, params.SecretsPath, backupEncryptionKey, res.Secrets)
|
||||
}
|
||||
|
||||
secretsToReturn = res.Secrets
|
||||
errorToReturn = err
|
||||
// only attempt to serve cached secrets if no internet connection and if at least one secret cached
|
||||
if !isConnected {
|
||||
backedSecrets, err := ReadBackupSecrets(infisicalDotJson.WorkspaceId, params.Environment, params.SecretsPath)
|
||||
if len(backedSecrets) > 0 {
|
||||
PrintWarning("Unable to fetch latest secret(s) due to connection error, serving secrets from last successful fetch. For more info, run with --debug")
|
||||
secretsToReturn = backedSecrets
|
||||
errorToReturn = err
|
||||
backupEncryptionKey, _ := GetBackupEncryptionKey()
|
||||
if backupEncryptionKey != nil {
|
||||
backedUpSecrets, err := ReadBackupSecrets(infisicalDotJson.WorkspaceId, params.Environment, params.SecretsPath, backupEncryptionKey)
|
||||
if len(backedUpSecrets) > 0 {
|
||||
PrintWarning("Unable to fetch the latest secret(s) due to connection error, serving secrets from last successful fetch. For more info, run with --debug")
|
||||
secretsToReturn = backedUpSecrets
|
||||
errorToReturn = err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} else {
|
||||
if params.InfisicalToken != "" {
|
||||
log.Debug().Msg("Trying to fetch secrets using service token")
|
||||
secretsToReturn, errorToReturn = GetPlainTextSecretsViaServiceToken(params.InfisicalToken, params.Environment, params.SecretsPath, params.IncludeImport, params.Recursive)
|
||||
secretsToReturn, errorToReturn = GetPlainTextSecretsViaServiceToken(params.InfisicalToken, params.Environment, params.SecretsPath, params.IncludeImport, params.Recursive, params.TagSlugs)
|
||||
} else if params.UniversalAuthAccessToken != "" {
|
||||
|
||||
if params.WorkspaceId == "" {
|
||||
@ -311,7 +320,7 @@ func GetAllEnvironmentVariables(params models.GetAllSecretsParameters, projectCo
|
||||
}
|
||||
|
||||
log.Debug().Msg("Trying to fetch secrets using universal auth")
|
||||
res, err := GetPlainTextSecretsV3(params.UniversalAuthAccessToken, params.WorkspaceId, params.Environment, params.SecretsPath, params.IncludeImport, params.Recursive)
|
||||
res, err := GetPlainTextSecretsV3(params.UniversalAuthAccessToken, params.WorkspaceId, params.Environment, params.SecretsPath, params.IncludeImport, params.Recursive, params.TagSlugs)
|
||||
|
||||
errorToReturn = err
|
||||
secretsToReturn = res.Secrets
|
||||
@ -476,71 +485,99 @@ func OverrideSecrets(secrets []models.SingleEnvironmentVariable, secretType stri
|
||||
return secretsToReturn
|
||||
}
|
||||
|
||||
func WriteBackupSecrets(workspace string, environment string, secretsPath string, secrets []models.SingleEnvironmentVariable) error {
|
||||
var backedUpSecrets []models.BackupSecretKeyRing
|
||||
secretValueInKeyRing, err := GetValueInKeyring(INFISICAL_BACKUP_SECRET)
|
||||
func GetBackupEncryptionKey() ([]byte, error) {
|
||||
encryptionKey, err := GetValueInKeyring(INFISICAL_BACKUP_SECRET_ENCRYPTION_KEY)
|
||||
if err != nil {
|
||||
if err == keyring.ErrUnsupportedPlatform {
|
||||
return errors.New("your OS does not support keyring. Consider using a service token https://infisical.com/docs/documentation/platform/token")
|
||||
} else if err != keyring.ErrNotFound {
|
||||
return fmt.Errorf("something went wrong, failed to retrieve value from system keyring [error=%v]", err)
|
||||
return nil, errors.New("your OS does not support keyring. Consider using a service token https://infisical.com/docs/documentation/platform/token")
|
||||
} else if err == keyring.ErrNotFound {
|
||||
// generate a new key
|
||||
randomizedKey := make([]byte, 16)
|
||||
rand.Read(randomizedKey)
|
||||
encryptionKey = hex.EncodeToString(randomizedKey)
|
||||
if err := SetValueInKeyring(INFISICAL_BACKUP_SECRET_ENCRYPTION_KEY, encryptionKey); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return []byte(encryptionKey), nil
|
||||
} else {
|
||||
return nil, fmt.Errorf("something went wrong, failed to retrieve value from system keyring [error=%v]", err)
|
||||
}
|
||||
}
|
||||
_ = json.Unmarshal([]byte(secretValueInKeyRing), &backedUpSecrets)
|
||||
return []byte(encryptionKey), nil
|
||||
}
|
||||
|
||||
backedUpSecrets = slices.DeleteFunc(backedUpSecrets, func(e models.BackupSecretKeyRing) bool {
|
||||
return e.SecretPath == secretsPath && e.ProjectID == workspace && e.Environment == environment
|
||||
})
|
||||
newBackupSecret := models.BackupSecretKeyRing{
|
||||
ProjectID: workspace,
|
||||
Environment: environment,
|
||||
SecretPath: secretsPath,
|
||||
Secrets: secrets,
|
||||
}
|
||||
backedUpSecrets = append(backedUpSecrets, newBackupSecret)
|
||||
func WriteBackupSecrets(workspace string, environment string, secretsPath string, encryptionKey []byte, secrets []models.SingleEnvironmentVariable) error {
|
||||
formattedPath := strings.ReplaceAll(secretsPath, "/", "-")
|
||||
fileName := fmt.Sprintf("project_secrets_%s_%s_%s.json", workspace, environment, formattedPath)
|
||||
secrets_backup_folder_name := "secrets-backup"
|
||||
|
||||
listOfSecretsMarshalled, err := json.Marshal(backedUpSecrets)
|
||||
_, fullConfigFileDirPath, err := GetFullConfigFilePath()
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("WriteBackupSecrets: unable to get full config folder path [err=%s]", err)
|
||||
}
|
||||
|
||||
err = SetValueInKeyring(INFISICAL_BACKUP_SECRET, string(listOfSecretsMarshalled))
|
||||
// create secrets backup directory
|
||||
fullPathToSecretsBackupFolder := fmt.Sprintf("%s/%s", fullConfigFileDirPath, secrets_backup_folder_name)
|
||||
if _, err := os.Stat(fullPathToSecretsBackupFolder); errors.Is(err, os.ErrNotExist) {
|
||||
err := os.Mkdir(fullPathToSecretsBackupFolder, os.ModePerm)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
marshaledSecrets, _ := json.Marshal(secrets)
|
||||
result, err := crypto.EncryptSymmetric(marshaledSecrets, encryptionKey)
|
||||
if err != nil {
|
||||
return fmt.Errorf("StoreUserCredsInKeyRing: unable to store user credentials because [err=%s]", err)
|
||||
return fmt.Errorf("WriteBackupSecrets: Unable to encrypt local secret backup to file [err=%s]", err)
|
||||
}
|
||||
listOfSecretsMarshalled, _ := json.Marshal(result)
|
||||
err = os.WriteFile(fmt.Sprintf("%s/%s", fullPathToSecretsBackupFolder, fileName), listOfSecretsMarshalled, 0600)
|
||||
if err != nil {
|
||||
return fmt.Errorf("WriteBackupSecrets: Unable to write backup secrets to file [err=%s]", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func ReadBackupSecrets(workspace string, environment string, secretsPath string) ([]models.SingleEnvironmentVariable, error) {
|
||||
secretValueInKeyRing, err := GetValueInKeyring(INFISICAL_BACKUP_SECRET)
|
||||
func ReadBackupSecrets(workspace string, environment string, secretsPath string, encryptionKey []byte) ([]models.SingleEnvironmentVariable, error) {
|
||||
formattedPath := strings.ReplaceAll(secretsPath, "/", "-")
|
||||
fileName := fmt.Sprintf("project_secrets_%s_%s_%s.json", workspace, environment, formattedPath)
|
||||
secrets_backup_folder_name := "secrets-backup"
|
||||
|
||||
_, fullConfigFileDirPath, err := GetFullConfigFilePath()
|
||||
if err != nil {
|
||||
if err == keyring.ErrUnsupportedPlatform {
|
||||
return nil, errors.New("your OS does not support keyring. Consider using a service token https://infisical.com/docs/documentation/platform/token")
|
||||
} else if err == keyring.ErrNotFound {
|
||||
return nil, errors.New("credentials not found in system keyring")
|
||||
} else {
|
||||
return nil, fmt.Errorf("something went wrong, failed to retrieve value from system keyring [error=%v]", err)
|
||||
}
|
||||
return nil, fmt.Errorf("ReadBackupSecrets: unable to write config file because an error occurred when getting config file path [err=%s]", err)
|
||||
}
|
||||
|
||||
var backedUpSecrets []models.BackupSecretKeyRing
|
||||
err = json.Unmarshal([]byte(secretValueInKeyRing), &backedUpSecrets)
|
||||
fullPathToSecretsBackupFolder := fmt.Sprintf("%s/%s", fullConfigFileDirPath, secrets_backup_folder_name)
|
||||
if _, err := os.Stat(fullPathToSecretsBackupFolder); errors.Is(err, os.ErrNotExist) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
encryptedBackupSecretsFilePath := fmt.Sprintf("%s/%s", fullPathToSecretsBackupFolder, fileName)
|
||||
|
||||
encryptedBackupSecretsAsBytes, err := os.ReadFile(encryptedBackupSecretsFilePath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("getUserCredsFromKeyRing: Something went wrong when unmarshalling user creds [err=%s]", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, backupSecret := range backedUpSecrets {
|
||||
if backupSecret.Environment == environment && backupSecret.ProjectID == workspace && backupSecret.SecretPath == secretsPath {
|
||||
return backupSecret.Secrets, nil
|
||||
}
|
||||
var encryptedBackUpSecrets models.SymmetricEncryptionResult
|
||||
err = json.Unmarshal(encryptedBackupSecretsAsBytes, &encryptedBackUpSecrets)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("ReadBackupSecrets: unable to parse encrypted backup secrets. The secrets backup may be malformed [err=%s]", err)
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
result, err := crypto.DecryptSymmetric(encryptionKey, encryptedBackUpSecrets.CipherText, encryptedBackUpSecrets.AuthTag, encryptedBackUpSecrets.Nonce)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("ReadBackupSecrets: unable to decrypt encrypted backup secrets [err=%s]", err)
|
||||
}
|
||||
var plainTextSecrets []models.SingleEnvironmentVariable
|
||||
_ = json.Unmarshal(result, &plainTextSecrets)
|
||||
|
||||
return plainTextSecrets, nil
|
||||
|
||||
}
|
||||
|
||||
func DeleteBackupSecrets() error {
|
||||
// keeping this logic for now. Need to remove it later as more users migrate keyring would be used and this folder will be removed completely by then
|
||||
secrets_backup_folder_name := "secrets-backup"
|
||||
|
||||
_, fullConfigFileDirPath, err := GetFullConfigFilePath()
|
||||
@ -549,8 +586,8 @@ func DeleteBackupSecrets() error {
|
||||
}
|
||||
|
||||
fullPathToSecretsBackupFolder := fmt.Sprintf("%s/%s", fullConfigFileDirPath, secrets_backup_folder_name)
|
||||
|
||||
DeleteValueInKeyring(INFISICAL_BACKUP_SECRET)
|
||||
DeleteValueInKeyring(INFISICAL_BACKUP_SECRET_ENCRYPTION_KEY)
|
||||
|
||||
return os.RemoveAll(fullPathToSecretsBackupFolder)
|
||||
}
|
||||
|
@ -11,11 +11,11 @@ func GetCurrentVaultBackend() (string, error) {
|
||||
}
|
||||
|
||||
if configFile.VaultBackendType == "" {
|
||||
return "auto", nil
|
||||
return VAULT_BACKEND_AUTO_MODE, nil
|
||||
}
|
||||
|
||||
if configFile.VaultBackendType != "auto" && configFile.VaultBackendType != "file" {
|
||||
return "auto", nil
|
||||
if configFile.VaultBackendType != VAULT_BACKEND_AUTO_MODE && configFile.VaultBackendType != VAULT_BACKEND_FILE_MODE {
|
||||
return VAULT_BACKEND_AUTO_MODE, nil
|
||||
}
|
||||
|
||||
return configFile.VaultBackendType, nil
|
||||
|
@ -1,4 +1,4 @@
|
||||
Warning: Unable to fetch latest secret(s) due to connection error, serving secrets from last successful fetch. For more info, run with --debug
|
||||
Warning: Unable to fetch the latest secret(s) due to connection error, serving secrets from last successful fetch. For more info, run with --debug
|
||||
┌───────────────┬──────────────┬─────────────┐
|
||||
│ SECRET NAME │ SECRET VALUE │ SECRET TYPE │
|
||||
├───────────────┼──────────────┼─────────────┤
|
||||
|
@ -7,7 +7,6 @@ import (
|
||||
"github.com/bradleyjkemp/cupaloy/v2"
|
||||
)
|
||||
|
||||
|
||||
func TestServiceToken_SecretsGetWithImportsAndRecursiveCmd(t *testing.T) {
|
||||
output, err := ExecuteCliCommand(FORMATTED_CLI_NAME, "secrets", "--token", creds.ServiceToken, "--projectId", creds.ProjectID, "--env", creds.EnvSlug, "--recursive", "--silent")
|
||||
|
||||
@ -94,7 +93,7 @@ func TestUserAuth_SecretsGetAll(t *testing.T) {
|
||||
}
|
||||
|
||||
// explicitly called here because it should happen directly after successful secretsGetAll
|
||||
testUserAuth_SecretsGetAllWithoutConnection(t)
|
||||
// testUserAuth_SecretsGetAllWithoutConnection(t)
|
||||
}
|
||||
|
||||
func testUserAuth_SecretsGetAllWithoutConnection(t *testing.T) {
|
||||
@ -107,7 +106,7 @@ func testUserAuth_SecretsGetAllWithoutConnection(t *testing.T) {
|
||||
// set it to a URL that will always be unreachable
|
||||
newConfigFile.LoggedInUserDomain = "http://localhost:4999"
|
||||
util.WriteConfigFile(&newConfigFile)
|
||||
|
||||
|
||||
// restore config file
|
||||
defer util.WriteConfigFile(&originalConfigFile)
|
||||
|
||||
@ -121,4 +120,4 @@ func testUserAuth_SecretsGetAllWithoutConnection(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatalf("snapshot failed: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
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).
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user