Compare commits

..

173 Commits

Author SHA1 Message Date
6112bc9356 Add certificate template field + warning to pki issuer docs 2024-09-07 19:23:11 -07:00
6c3156273c Add docs for infisical pki issuer 2024-09-07 16:28:28 -07:00
f09e18a706 Merge pull request #2383 from Infisical/fix/resolve-cert-invalid-issue
fix: resolve cert invalid issue due to invalid root EKU
2024-09-07 01:09:24 +08:00
5d9a43a3fd fix: resolve cert invalid issue 2024-09-07 00:42:55 +08:00
8d66272ab2 Merge pull request #2366 from ThallesP/patch-1
docs: add mention of SITE_URL as being required
2024-09-05 16:06:49 -04:00
0e44e630cb Merge pull request #2377 from Infisical/daniel/refactor-circleci-integration
fix(integrations/circle-ci): Refactored Circle CI integration
2024-09-05 16:04:04 -04:00
49c4929c9c Update azure-key-vault.mdx 2024-09-05 15:13:42 -04:00
da561e37c5 Fix: Backwards compatibility and UI fixes 2024-09-05 21:43:10 +04:00
ebc584d36f Merge pull request #2379 from Infisical/fix/client-secret-patch
Update identity-ua-client-secret-dal.ts
2024-09-05 11:02:35 -04:00
656d979d7d Update identity-ua-client-secret-dal.ts 2024-09-05 20:29:18 +05:30
a29fb613b9 Requested changes 2024-09-05 18:48:20 +04:00
5382f3de2d Merge pull request #2378 from Infisical/vmatsiiako-patch-elasticsearch-1
Elasticsearch is one word
2024-09-05 09:11:18 -04:00
b2b858f7e8 Elasticsearch is one word 2024-09-05 09:07:23 -04:00
8f3d328b9a Update integration-sync-secret.ts 2024-09-05 13:38:31 +04:00
b7d683ee1b fix(integrations/circle-ci): Refactored Circle CI integration
The integration seemingly never worked in the first place due to inpropper project slugs. This PR resolves it.
2024-09-05 13:30:20 +04:00
9bd6ec19c4 revert "docs: add mention of SITE_URL as being required" 2024-09-04 18:04:25 -03:00
03fd0a1eb9 chore: add site url as required in kubernetes helm deployment 2024-09-04 18:03:18 -03:00
97023e7714 chore: add SITE_URL as required in docker installation 2024-09-04 17:58:42 -03:00
1d23ed0680 chore: add site url as required in envars docs 2024-09-04 17:56:38 -03:00
302e068c74 Merge pull request #2376 from Infisical/daniel/info-notif-for-secret-changes
fix(ui): show info notification when secret change is pending review
2024-09-04 20:09:58 +04:00
95b92caff3 Merge pull request #2375 from Infisical/daniel/fix-access-policy-creation
fix(access-requests): policy creation and edits
2024-09-04 20:00:04 +04:00
5d894b6d43 fix(ui): info notification when secret change is pending review 2024-09-04 19:57:32 +04:00
dab3e2efad fix(access-requests): policy creation and edits 2024-09-04 19:46:44 +04:00
04cbbccd25 Merge pull request #2374 from Infisical/revert-2362-bugfix/incorrect-alignment-of-logo-on-login-page
Revert "FIX : padding-and-alignment-login-page"
2024-09-04 19:16:08 +05:30
7f48e9d62e Revert "FIX : padding-and-alignment-login-page" 2024-09-04 19:12:58 +05:30
8a0018eff2 Merge pull request #2373 from Infisical/daniel/elastisearch-dynamic-secrets
feat(dynamic-secrets): elastic search support
2024-09-04 15:23:23 +04:00
e6a920caa3 Merge pull request #2362 from mukulpadwal/bugfix/incorrect-alignment-of-logo-on-login-page
FIX : padding-and-alignment-login-page
2024-09-04 16:15:36 +05:30
11411ca4eb Requested changes 2024-09-04 13:47:35 +04:00
b7c79fa45b Requested changes 2024-09-04 13:47:35 +04:00
18951b99de Further doc fixes 2024-09-04 13:47:17 +04:00
bd05c440c3 Update elastic-search.ts 2024-09-04 13:47:17 +04:00
9ca5013a59 Update mint.json 2024-09-04 13:47:17 +04:00
b65b8bc362 docs(dynamic-secrets): Elastic Search documentation 2024-09-04 13:47:17 +04:00
f494c182ff Update aws-elasticache.mdx 2024-09-04 13:47:17 +04:00
2fae822e1f Fix docs for AWS ElastiCache 2024-09-04 13:47:17 +04:00
5df140cbd5 feat(dynamic-secrets): ElasticSearch support 2024-09-04 13:47:17 +04:00
d93cbb023d Update redis.ts 2024-09-04 13:47:17 +04:00
9056d1be0c feat(dynamic-secrets): ElasticSearch support 2024-09-04 13:47:17 +04:00
5f503949eb Installed elasticsearch SDK 2024-09-04 13:47:16 +04:00
9cf917de07 Merge pull request #2360 from Infisical/daniel/redirect-node-docs
feat(integrations): Add visibility support to Github Integration
2024-09-04 10:32:13 +04:00
ce7bb82f02 Merge pull request #2313 from akhilmhdh/feat/test-import
Feat/test import
2024-09-03 09:33:26 -04:00
7cd092c0cf Merge pull request #2368 from akhilmhdh/fix/audit-log-loop
Audit log queue looping
2024-09-03 08:32:04 -04:00
=
cbfb9af0b9 feat: moved log points inside each function respectively 2024-09-03 17:59:32 +05:30
=
ef236106b4 feat: added log points for resoruce clean up tasks 2024-09-03 17:37:14 +05:30
=
773a338397 fix: resolved looping in audit log resource queue 2024-09-03 17:33:38 +05:30
=
afb5820113 feat: added 1-N sink import pattern testing and fixed padding issue 2024-09-03 15:02:49 +05:30
5acc0fc243 Update build-staging-and-deploy-aws.yml 2024-09-02 23:56:24 -04:00
c56469ecdb Run integration tests build building gamma 2024-09-02 23:55:05 -04:00
c59a53180c Update integrations-github-scope-org.png 2024-09-03 04:40:59 +04:00
f56d265e62 Revert "Docs: Redirect to new SDK"
This reverts commit 56dce67378b3601aec9f45eee0c52e50c1a7e36a.
2024-09-03 04:40:59 +04:00
cc0ff98d4f chore: cleaned up integrations page 2024-09-03 04:40:59 +04:00
4a14c3efd2 feat(integrations): visibility support for github integration 2024-09-03 04:40:59 +04:00
b2d2297914 Fix: Document formatting & changed tooltipText prop to ReactNode type 2024-09-03 04:40:59 +04:00
836bb6d835 feat(integrations): visibility support for github integration 2024-09-03 04:40:19 +04:00
177eb2afee docs(github-integration): Updated documentation for github integration 2024-09-03 04:40:19 +04:00
594df18611 Docs: Redirect to new SDK 2024-09-03 04:40:19 +04:00
3bcb8bf6fc Merge pull request #2364 from akhilmhdh/fix/scim-rfc
Resolved scim failing due to missing rfc cases
2024-09-02 18:59:20 -04:00
23c362f9cd docs: add mention of SITE_URL as being required 2024-09-02 12:54:00 -03:00
a74c37c18b Merge pull request #2350 from akhilmhdh/dynamic-secret/atlas
MongoDB atlas dynamic secret
2024-09-02 10:39:34 -04:00
=
3ece81d663 docs: improved test as commented 2024-09-02 14:43:11 +05:30
=
f6d87ebf32 feat: changed text to advanced as review comment 2024-09-02 14:36:32 +05:30
=
23483ab7e1 feat: removed non rfc related groups in user scim resource 2024-09-02 13:55:56 +05:30
=
fe31d44d22 feat: made scim user default permission as no access in org 2024-09-02 13:50:55 +05:30
=
58bab4d163 feat: resolved some more missing corner case in scim 2024-09-02 13:50:55 +05:30
=
8f48a64fd6 feat: finished fixing scim group 2024-09-02 13:50:55 +05:30
=
929dc059c3 feat: updated scim user endpoint 2024-09-02 13:50:55 +05:30
45e471b16a FIX : padding-and-alignment-login-page 2024-08-31 16:25:54 +05:30
7c540b6be8 Merge pull request #2244 from LemmyMwaura/password-protect-secret-share
feat: password protect secret share
2024-08-30 13:43:24 -04:00
=
7dbe8dd3c9 feat: patched lock file 2024-08-30 10:56:28 +05:30
=
0dec602729 feat: changed all licence type to license 2024-08-30 10:52:46 +05:30
=
66ded779fc feat: added secret version test with secret import 2024-08-30 10:52:46 +05:30
=
01d24291f2 feat: resolved type error 2024-08-30 10:52:46 +05:30
=
55b36b033e feat: changed expand secret factory to iterative solution 2024-08-30 10:52:46 +05:30
=
8f461bf50c feat: added test for checking secret reference expansion 2024-08-30 10:52:46 +05:30
=
1847491cb3 feat: implemented new secret reference strategy 2024-08-30 10:52:46 +05:30
=
541c7b63cd feat: added test for checkings secrets from import via replication and non replicaiton 2024-08-30 10:52:45 +05:30
=
7e5e177680 feat: vitest mocking by alias for license fns 2024-08-30 10:52:45 +05:30
=
40f552e4f1 feat: fixed typo in license function file name 2024-08-30 10:52:45 +05:30
=
ecb54ee3b3 feat: resolved migration down failing for secret approval policy change 2024-08-30 10:52:45 +05:30
b975996158 Merge pull request #2330 from Infisical/doc/made-ecs-with-agent-doc-use-aws-native
doc: updated ecs-with-agent documentation to use AWS native auth
2024-08-29 14:25:45 -04:00
122f789cdf Merge pull request #2329 from Infisical/feat/raw-agent-template
feat: added raw template for agent
2024-08-29 14:24:50 -04:00
c9911aa841 Merge pull request #2335 from Infisical/vmatsiiako-patch-handbook-1
Update onboarding.mdx
2024-08-29 14:22:30 -04:00
32cd0d8af8 Merge pull request #2352 from Infisical/revert-2341-daniel/cli-run-watch-mode
Revert "feat(cli): `run` watch mode"
2024-08-29 12:46:39 -04:00
585f0d9f1b Revert "feat(cli): run watch mode" 2024-08-29 12:44:24 -04:00
d0292aa139 Merge pull request #2341 from Infisical/daniel/cli-run-watch-mode
feat(cli): `run` watch mode
2024-08-29 17:51:41 +04:00
4e9be8ca3c Changes 2024-08-29 17:38:00 +04:00
=
ad49e9eaf1 docs: updated doc for mongo atlas dynamic secret 2024-08-29 14:52:40 +05:30
=
fed60f7c03 feat: resolved lint fix after rebase 2024-08-29 13:28:45 +05:30
=
1bc0e3087a feat: completed atlas dynamic secret logic for ui 2024-08-29 13:26:15 +05:30
=
80a4f838a1 feat: completed mongo atlas dynamic secret backend logic 2024-08-29 13:22:25 +05:30
d31ec44f50 Merge pull request #2345 from Infisical/misc/finalize-certificate-template-and-est
finalized certificate template and EST
2024-08-29 12:47:13 +08:00
d0caef37ce Merge pull request #2349 from Infisical/mzidul-wjdhbwhufhjwebf
Add tool tip for k8s auth
2024-08-28 17:11:06 -04:00
2d26febe58 a to an 2024-08-28 17:09:47 -04:00
c23ad8ebf2 improve tooltip 2024-08-28 17:04:56 -04:00
bad068ef19 add tool tip for k8s auth 2024-08-28 16:59:14 -04:00
53430608a8 Merge pull request #2348 from Infisical/daniel/env-transform-trailing-slashes
Fix: Always remove trailing slashes from SITE_URL
2024-08-28 22:52:03 +04:00
b9071ab2b3 Fix: Always remove trailing slashes from SITE_URL 2024-08-28 22:45:08 +04:00
a556c02df6 misc: migrated est to ee and added license checks 2024-08-29 02:20:55 +08:00
bfab270d68 Merge pull request #2347 from Infisical/daniel/fix-secret-change-emails
Fix: Removed protocol parsing on secret change emails
2024-08-28 22:16:28 +04:00
8ea6a1f3d5 Fix: Removed protocol parsing 2024-08-28 22:07:31 +04:00
3c39bf6a0f Add watch interval 2024-08-28 21:11:09 +04:00
828644799f Merge pull request #2319 from Infisical/daniel/redis-dynamic-secrets
Feat: Redis support for dynamic secrets
2024-08-28 18:06:57 +04:00
411e67ae41 Finally resolved package-lock 2024-08-28 18:01:31 +04:00
4914bc4b5a Fix: Package json bugged generation 2024-08-28 18:00:05 +04:00
d7050a1947 Update package-lock.json 2024-08-28 17:58:32 +04:00
3c59422511 Fixed package 2024-08-28 17:57:31 +04:00
c81204e6d5 Test 2024-08-28 17:57:31 +04:00
880f39519f Update aws-elasticache.mdx 2024-08-28 17:57:31 +04:00
8646f6c50b Requested changes 2024-08-28 17:57:30 +04:00
437a9e6ccb AWS elasticache 2024-08-28 17:57:30 +04:00
b54139bd37 Fix 2024-08-28 17:57:30 +04:00
8a6a36ac54 Update package-lock.json 2024-08-28 17:57:20 +04:00
c6eb973da0 Uninstalled unused dependencies 2024-08-28 17:57:19 +04:00
21750a8c20 Fix: Refactored aws elasticache to separate provider 2024-08-28 17:57:19 +04:00
a598665b2f Docs: ElastiCache Docs 2024-08-28 17:57:19 +04:00
56bbf502a2 Update redis.ts 2024-08-28 17:57:19 +04:00
9975f7d83f Edition fixes 2024-08-28 17:57:19 +04:00
7ad366b363 Update licence-fns.ts 2024-08-28 17:57:19 +04:00
cca4d68d94 Fix: AWS ElastiCache support 2024-08-28 17:57:19 +04:00
b82b94db54 Docs: Redis Dynamic secrets docs 2024-08-28 17:55:04 +04:00
de9cb265e0 Feat: Redis support for dynamic secrets 2024-08-28 17:55:04 +04:00
5611b9aba1 misc: added reference to secret template functions 2024-08-28 15:53:36 +08:00
53075d503a misc: added note to secret template docs 2024-08-28 15:48:25 +08:00
e47cfa262a misc: added cloud est user guide 2024-08-28 13:59:47 +08:00
0ab7a4e713 misc: added transaction for cert template create and update 2024-08-28 13:45:25 +08:00
5138d588db Update cli.go 2024-08-28 05:35:03 +04:00
7e2d093e29 Docs: watch mode 2024-08-28 05:34:21 +04:00
2d780e0566 Feat: watch mode for run command 2024-08-28 05:22:27 +04:00
7ac4ad3194 Merge pull request #2344 from Infisical/maidul-ddqdqwdqwd3
Update health check
2024-08-27 20:09:51 -04:00
8eb234a12f Update run.go 2024-08-27 21:58:53 +04:00
85590af99e Fix: Removed more duplicate code and started using process groups to fix memory leak 2024-08-27 21:44:32 +04:00
5c7cec0c81 Update run.go 2024-08-27 20:11:27 +04:00
68f768749b Update run.go 2024-08-27 20:10:50 +04:00
2c7e342b18 Update run.go 2024-08-27 20:10:11 +04:00
632900e516 Update run.go 2024-08-27 20:10:00 +04:00
5fd975b1d7 Fix: Console error on manual cancel when not using hot reload 2024-08-27 20:09:40 +04:00
d45ac66064 Fix: Match test cases 2024-08-27 20:03:01 +04:00
47cba8ec3c Update test-TestUniversalAuth_SecretsGetWrongEnvironment 2024-08-27 19:48:20 +04:00
d4aab66da2 Update test-TestUniversalAuth_SecretsGetWrongEnvironment 2024-08-27 19:45:00 +04:00
0dc4c92c89 Feat: --watch flag for watching for secret changes 2024-08-27 19:37:11 +04:00
f49c963367 Settings for hot reloading 2024-08-27 19:36:38 +04:00
fe11b8e57e Function for locally generating ETag 2024-08-27 19:36:02 +04:00
0401f55bc3 Update onboarding.mdx 2024-08-26 13:28:37 -07:00
403e0d2d9d Update onboarding.mdx 2024-08-26 13:26:21 -07:00
e73d3f87f3 small nit 2024-08-23 14:29:29 -04:00
b53607f8e4 doc: updated ecs with agent doc to use aws auth 2024-08-24 01:51:39 +08:00
8f79d3210a feat: added raw template for agent 2024-08-24 01:48:39 +08:00
=
3ddb4cd27a feat: simplified ui for password based secret sharing 2024-08-10 22:21:17 +05:30
=
a5555c3816 feat: simplified endpoints to support password based secret sharing 2024-08-10 22:19:42 +05:30
8479c406a5 fix: fix type assersion error 2024-08-08 10:06:55 +03:00
8e0b4254b1 refactor: fix lint issues and refactor code 2024-08-08 09:56:18 +03:00
069651bdb4 fix: fix lint errors 2024-08-07 23:26:24 +03:00
9061ec2dff fix(lint): fix type errors 2024-08-07 22:59:50 +03:00
b0a5023723 feat: check if secret is expired before checking if secret has password 2024-08-07 20:55:37 +03:00
69fe5bf71d feat: only update view count when we validate the password if it's set 2024-08-07 16:52:11 +03:00
f12d4d80c6 feat: address changes on the client 2024-08-07 16:13:29 +03:00
56f2a3afa4 feat: only fetch secret if password wasn't set on initial load 2024-08-07 16:06:37 +03:00
406da1b5f0 refactor: convert usequery hook to normal fetch fn (no need for caching) 2024-08-07 08:27:17 +03:00
da45e132a3 Merge branch 'main' of github.com:Infisical/infisical into password-protect-secret-share 2024-08-06 19:49:25 +03:00
fb719a9383 fix(lint): fix some lint issues 2024-08-06 19:25:04 +03:00
3c64359597 feat: handle error logs and validate password 2024-08-06 18:36:21 +03:00
e420973dd2 feat: hashpassword and add validation endpoint 2024-08-06 17:01:13 +03:00
15cc157c5f fix(lint): make password optional 2024-08-06 15:32:48 +03:00
ad89ffe94d feat: show secret if no password was set 2024-08-06 14:42:01 +03:00
4de1713a18 fix: remove error logs 2024-08-06 14:28:02 +03:00
1917e0fdb7 feat: validate via password before showing secret 2024-08-06 14:13:03 +03:00
4b07234997 feat: update frontend queries to retrieve password 2024-08-06 14:08:40 +03:00
6a402950c3 chore: add check migration status cmd scripts 2024-08-06 12:59:46 +03:00
63333159ca feat: fetch password when fetching secrets 2024-08-06 12:58:53 +03:00
ce4ba24ef2 feat: create secret with password 2024-08-06 12:58:27 +03:00
f606e31b98 feat: apply table migrations (add password field) 2024-08-06 12:28:03 +03:00
ecdbb3eb53 feat: update type resolvers to include password 2024-08-06 12:27:16 +03:00
0321ec32fb feat: add password input 2024-08-06 12:26:23 +03:00
153 changed files with 9951 additions and 2075 deletions

View File

@ -6,9 +6,15 @@ permissions:
contents: read
jobs:
infisical-tests:
name: Run tests before deployment
# https://docs.github.com/en/actions/using-workflows/reusing-workflows#overview
uses: ./.github/workflows/run-backend-tests.yml
infisical-image:
name: Build backend image
runs-on: ubuntu-latest
needs: [infisical-tests]
steps:
- name: ☁️ Checkout source
uses: actions/checkout@v3

View File

@ -0,0 +1,35 @@
import { seedData1 } from "@app/db/seed-data";
const createPolicy = async (dto: { name: string; secretPath: string; approvers: string[]; approvals: number }) => {
const res = await testServer.inject({
method: "POST",
url: `/api/v1/secret-approvals`,
headers: {
authorization: `Bearer ${jwtAuthToken}`
},
body: {
workspaceId: seedData1.project.id,
environment: seedData1.environment.slug,
name: dto.name,
secretPath: dto.secretPath,
approvers: dto.approvers,
approvals: dto.approvals
}
});
expect(res.statusCode).toBe(200);
return res.json().approval;
};
describe("Secret approval policy router", async () => {
test("Create policy", async () => {
const policy = await createPolicy({
secretPath: "/",
approvals: 1,
approvers: [seedData1.id],
name: "test-policy"
});
expect(policy.name).toBe("test-policy");
});
});

View File

@ -1,73 +1,61 @@
import { createFolder, deleteFolder } from "e2e-test/testUtils/folders";
import { createSecretImport, deleteSecretImport } from "e2e-test/testUtils/secret-imports";
import { createSecretV2, deleteSecretV2, getSecretByNameV2, getSecretsV2 } from "e2e-test/testUtils/secrets";
import { seedData1 } from "@app/db/seed-data";
const createSecretImport = async (importPath: string, importEnv: string) => {
const res = await testServer.inject({
method: "POST",
url: `/api/v1/secret-imports`,
headers: {
authorization: `Bearer ${jwtAuthToken}`
},
body: {
workspaceId: seedData1.project.id,
environment: seedData1.environment.slug,
path: "/",
import: {
environment: importEnv,
path: importPath
}
}
});
expect(res.statusCode).toBe(200);
const payload = JSON.parse(res.payload);
expect(payload).toHaveProperty("secretImport");
return payload.secretImport;
};
const deleteSecretImport = async (id: string) => {
const res = await testServer.inject({
method: "DELETE",
url: `/api/v1/secret-imports/${id}`,
headers: {
authorization: `Bearer ${jwtAuthToken}`
},
body: {
workspaceId: seedData1.project.id,
environment: seedData1.environment.slug,
path: "/"
}
});
expect(res.statusCode).toBe(200);
const payload = JSON.parse(res.payload);
expect(payload).toHaveProperty("secretImport");
return payload.secretImport;
};
describe("Secret Import Router", async () => {
test.each([
{ importEnv: "prod", importPath: "/" }, // one in root
{ importEnv: "staging", importPath: "/" } // then create a deep one creating intermediate ones
])("Create secret import $importEnv with path $importPath", async ({ importPath, importEnv }) => {
// check for default environments
const payload = await createSecretImport(importPath, importEnv);
const payload = await createSecretImport({
authToken: jwtAuthToken,
secretPath: "/",
environmentSlug: seedData1.environment.slug,
workspaceId: seedData1.project.id,
importPath,
importEnv
});
expect(payload).toEqual(
expect.objectContaining({
id: expect.any(String),
importPath: expect.any(String),
importPath,
importEnv: expect.objectContaining({
name: expect.any(String),
slug: expect.any(String),
slug: importEnv,
id: expect.any(String)
})
})
);
await deleteSecretImport(payload.id);
await deleteSecretImport({
id: payload.id,
workspaceId: seedData1.project.id,
environmentSlug: seedData1.environment.slug,
secretPath: "/",
authToken: jwtAuthToken
});
});
test("Get secret imports", async () => {
const createdImport1 = await createSecretImport("/", "prod");
const createdImport2 = await createSecretImport("/", "staging");
const createdImport1 = await createSecretImport({
authToken: jwtAuthToken,
secretPath: "/",
environmentSlug: seedData1.environment.slug,
workspaceId: seedData1.project.id,
importPath: "/",
importEnv: "prod"
});
const createdImport2 = await createSecretImport({
authToken: jwtAuthToken,
secretPath: "/",
environmentSlug: seedData1.environment.slug,
workspaceId: seedData1.project.id,
importPath: "/",
importEnv: "staging"
});
const res = await testServer.inject({
method: "GET",
url: `/api/v1/secret-imports`,
@ -89,25 +77,60 @@ describe("Secret Import Router", async () => {
expect.arrayContaining([
expect.objectContaining({
id: expect.any(String),
importPath: expect.any(String),
importPath: "/",
importEnv: expect.objectContaining({
name: expect.any(String),
slug: expect.any(String),
slug: "prod",
id: expect.any(String)
})
}),
expect.objectContaining({
id: expect.any(String),
importPath: "/",
importEnv: expect.objectContaining({
name: expect.any(String),
slug: "staging",
id: expect.any(String)
})
})
])
);
await deleteSecretImport(createdImport1.id);
await deleteSecretImport(createdImport2.id);
await deleteSecretImport({
id: createdImport1.id,
workspaceId: seedData1.project.id,
environmentSlug: seedData1.environment.slug,
secretPath: "/",
authToken: jwtAuthToken
});
await deleteSecretImport({
id: createdImport2.id,
workspaceId: seedData1.project.id,
environmentSlug: seedData1.environment.slug,
secretPath: "/",
authToken: jwtAuthToken
});
});
test("Update secret import position", async () => {
const prodImportDetails = { path: "/", envSlug: "prod" };
const stagingImportDetails = { path: "/", envSlug: "staging" };
const createdImport1 = await createSecretImport(prodImportDetails.path, prodImportDetails.envSlug);
const createdImport2 = await createSecretImport(stagingImportDetails.path, stagingImportDetails.envSlug);
const createdImport1 = await createSecretImport({
authToken: jwtAuthToken,
secretPath: "/",
environmentSlug: seedData1.environment.slug,
workspaceId: seedData1.project.id,
importPath: prodImportDetails.path,
importEnv: prodImportDetails.envSlug
});
const createdImport2 = await createSecretImport({
authToken: jwtAuthToken,
secretPath: "/",
environmentSlug: seedData1.environment.slug,
workspaceId: seedData1.project.id,
importPath: stagingImportDetails.path,
importEnv: stagingImportDetails.envSlug
});
const updateImportRes = await testServer.inject({
method: "PATCH",
@ -161,22 +184,55 @@ describe("Secret Import Router", async () => {
expect(secretImportList.secretImports[1].id).toEqual(createdImport1.id);
expect(secretImportList.secretImports[0].id).toEqual(createdImport2.id);
await deleteSecretImport(createdImport1.id);
await deleteSecretImport(createdImport2.id);
await deleteSecretImport({
id: createdImport1.id,
workspaceId: seedData1.project.id,
environmentSlug: seedData1.environment.slug,
secretPath: "/",
authToken: jwtAuthToken
});
await deleteSecretImport({
id: createdImport2.id,
workspaceId: seedData1.project.id,
environmentSlug: seedData1.environment.slug,
secretPath: "/",
authToken: jwtAuthToken
});
});
test("Delete secret import position", async () => {
const createdImport1 = await createSecretImport("/", "prod");
const createdImport2 = await createSecretImport("/", "staging");
const deletedImport = await deleteSecretImport(createdImport1.id);
const createdImport1 = await createSecretImport({
authToken: jwtAuthToken,
secretPath: "/",
environmentSlug: seedData1.environment.slug,
workspaceId: seedData1.project.id,
importPath: "/",
importEnv: "prod"
});
const createdImport2 = await createSecretImport({
authToken: jwtAuthToken,
secretPath: "/",
environmentSlug: seedData1.environment.slug,
workspaceId: seedData1.project.id,
importPath: "/",
importEnv: "staging"
});
const deletedImport = await deleteSecretImport({
id: createdImport1.id,
workspaceId: seedData1.project.id,
environmentSlug: seedData1.environment.slug,
secretPath: "/",
authToken: jwtAuthToken
});
// check for default environments
expect(deletedImport).toEqual(
expect.objectContaining({
id: expect.any(String),
importPath: expect.any(String),
importPath: "/",
importEnv: expect.objectContaining({
name: expect.any(String),
slug: expect.any(String),
slug: "prod",
id: expect.any(String)
})
})
@ -201,6 +257,552 @@ describe("Secret Import Router", async () => {
expect(secretImportList.secretImports.length).toEqual(1);
expect(secretImportList.secretImports[0].position).toEqual(1);
await deleteSecretImport(createdImport2.id);
await deleteSecretImport({
id: createdImport2.id,
workspaceId: seedData1.project.id,
environmentSlug: seedData1.environment.slug,
secretPath: "/",
authToken: jwtAuthToken
});
});
});
// dev <- stage <- prod
describe.each([{ path: "/" }, { path: "/deep" }])(
"Secret import waterfall pattern testing - %path",
({ path: testSuitePath }) => {
beforeAll(async () => {
let prodFolder: { id: string };
let stagingFolder: { id: string };
let devFolder: { id: string };
if (testSuitePath !== "/") {
prodFolder = await createFolder({
authToken: jwtAuthToken,
environmentSlug: "prod",
workspaceId: seedData1.projectV3.id,
secretPath: "/",
name: "deep"
});
stagingFolder = await createFolder({
authToken: jwtAuthToken,
environmentSlug: "staging",
workspaceId: seedData1.projectV3.id,
secretPath: "/",
name: "deep"
});
devFolder = await createFolder({
authToken: jwtAuthToken,
environmentSlug: seedData1.environment.slug,
workspaceId: seedData1.projectV3.id,
secretPath: "/",
name: "deep"
});
}
const devImportFromStage = await createSecretImport({
authToken: jwtAuthToken,
secretPath: testSuitePath,
environmentSlug: seedData1.environment.slug,
workspaceId: seedData1.projectV3.id,
importPath: testSuitePath,
importEnv: "staging"
});
const stageImportFromProd = await createSecretImport({
authToken: jwtAuthToken,
secretPath: testSuitePath,
environmentSlug: "staging",
workspaceId: seedData1.projectV3.id,
importPath: testSuitePath,
importEnv: "prod"
});
return async () => {
await deleteSecretImport({
id: stageImportFromProd.id,
workspaceId: seedData1.projectV3.id,
environmentSlug: "staging",
secretPath: testSuitePath,
authToken: jwtAuthToken
});
await deleteSecretImport({
id: devImportFromStage.id,
workspaceId: seedData1.projectV3.id,
environmentSlug: seedData1.environment.slug,
secretPath: testSuitePath,
authToken: jwtAuthToken
});
if (prodFolder) {
await deleteFolder({
authToken: jwtAuthToken,
secretPath: "/",
id: prodFolder.id,
workspaceId: seedData1.projectV3.id,
environmentSlug: "prod"
});
}
if (stagingFolder) {
await deleteFolder({
authToken: jwtAuthToken,
secretPath: "/",
id: stagingFolder.id,
workspaceId: seedData1.projectV3.id,
environmentSlug: "staging"
});
}
if (devFolder) {
await deleteFolder({
authToken: jwtAuthToken,
secretPath: "/",
id: devFolder.id,
workspaceId: seedData1.projectV3.id,
environmentSlug: seedData1.environment.slug
});
}
};
});
test("Check one level imported secret exist", async () => {
await createSecretV2({
environmentSlug: "staging",
workspaceId: seedData1.projectV3.id,
secretPath: testSuitePath,
authToken: jwtAuthToken,
key: "STAGING_KEY",
value: "stage-value"
});
const secret = await getSecretByNameV2({
environmentSlug: seedData1.environment.slug,
workspaceId: seedData1.projectV3.id,
secretPath: testSuitePath,
authToken: jwtAuthToken,
key: "STAGING_KEY"
});
expect(secret.secretKey).toBe("STAGING_KEY");
expect(secret.secretValue).toBe("stage-value");
const listSecrets = await getSecretsV2({
environmentSlug: seedData1.environment.slug,
workspaceId: seedData1.projectV3.id,
secretPath: testSuitePath,
authToken: jwtAuthToken
});
expect(listSecrets.imports).toEqual(
expect.arrayContaining([
expect.objectContaining({
secrets: expect.arrayContaining([
expect.objectContaining({
secretKey: "STAGING_KEY",
secretValue: "stage-value"
})
])
})
])
);
await deleteSecretV2({
environmentSlug: "staging",
workspaceId: seedData1.projectV3.id,
secretPath: testSuitePath,
authToken: jwtAuthToken,
key: "STAGING_KEY"
});
});
test("Check two level imported secret exist", async () => {
await createSecretV2({
environmentSlug: "prod",
workspaceId: seedData1.projectV3.id,
secretPath: testSuitePath,
authToken: jwtAuthToken,
key: "PROD_KEY",
value: "prod-value"
});
const secret = await getSecretByNameV2({
environmentSlug: seedData1.environment.slug,
workspaceId: seedData1.projectV3.id,
secretPath: testSuitePath,
authToken: jwtAuthToken,
key: "PROD_KEY"
});
expect(secret.secretKey).toBe("PROD_KEY");
expect(secret.secretValue).toBe("prod-value");
const listSecrets = await getSecretsV2({
environmentSlug: seedData1.environment.slug,
workspaceId: seedData1.projectV3.id,
secretPath: testSuitePath,
authToken: jwtAuthToken
});
expect(listSecrets.imports).toEqual(
expect.arrayContaining([
expect.objectContaining({
secrets: expect.arrayContaining([
expect.objectContaining({
secretKey: "PROD_KEY",
secretValue: "prod-value"
})
])
})
])
);
await deleteSecretV2({
environmentSlug: "prod",
workspaceId: seedData1.projectV3.id,
secretPath: testSuitePath,
authToken: jwtAuthToken,
key: "PROD_KEY"
});
});
}
);
// dev <- stage, dev <- prod
describe.each([{ path: "/" }, { path: "/deep" }])(
"Secret import multiple destination to one source pattern testing - %path",
({ path: testSuitePath }) => {
beforeAll(async () => {
let prodFolder: { id: string };
let stagingFolder: { id: string };
let devFolder: { id: string };
if (testSuitePath !== "/") {
prodFolder = await createFolder({
authToken: jwtAuthToken,
environmentSlug: "prod",
workspaceId: seedData1.projectV3.id,
secretPath: "/",
name: "deep"
});
stagingFolder = await createFolder({
authToken: jwtAuthToken,
environmentSlug: "staging",
workspaceId: seedData1.projectV3.id,
secretPath: "/",
name: "deep"
});
devFolder = await createFolder({
authToken: jwtAuthToken,
environmentSlug: seedData1.environment.slug,
workspaceId: seedData1.projectV3.id,
secretPath: "/",
name: "deep"
});
}
const devImportFromStage = await createSecretImport({
authToken: jwtAuthToken,
secretPath: testSuitePath,
environmentSlug: seedData1.environment.slug,
workspaceId: seedData1.projectV3.id,
importPath: testSuitePath,
importEnv: "staging"
});
const devImportFromProd = await createSecretImport({
authToken: jwtAuthToken,
secretPath: testSuitePath,
environmentSlug: seedData1.environment.slug,
workspaceId: seedData1.projectV3.id,
importPath: testSuitePath,
importEnv: "prod"
});
return async () => {
await deleteSecretImport({
id: devImportFromProd.id,
workspaceId: seedData1.projectV3.id,
environmentSlug: seedData1.environment.slug,
secretPath: testSuitePath,
authToken: jwtAuthToken
});
await deleteSecretImport({
id: devImportFromStage.id,
workspaceId: seedData1.projectV3.id,
environmentSlug: seedData1.environment.slug,
secretPath: testSuitePath,
authToken: jwtAuthToken
});
if (prodFolder) {
await deleteFolder({
authToken: jwtAuthToken,
secretPath: "/",
id: prodFolder.id,
workspaceId: seedData1.projectV3.id,
environmentSlug: "prod"
});
}
if (stagingFolder) {
await deleteFolder({
authToken: jwtAuthToken,
secretPath: "/",
id: stagingFolder.id,
workspaceId: seedData1.projectV3.id,
environmentSlug: "staging"
});
}
if (devFolder) {
await deleteFolder({
authToken: jwtAuthToken,
secretPath: "/",
id: devFolder.id,
workspaceId: seedData1.projectV3.id,
environmentSlug: seedData1.environment.slug
});
}
};
});
test("Check imported secret exist", async () => {
await createSecretV2({
environmentSlug: "staging",
workspaceId: seedData1.projectV3.id,
secretPath: testSuitePath,
authToken: jwtAuthToken,
key: "STAGING_KEY",
value: "stage-value"
});
await createSecretV2({
environmentSlug: "prod",
workspaceId: seedData1.projectV3.id,
secretPath: testSuitePath,
authToken: jwtAuthToken,
key: "PROD_KEY",
value: "prod-value"
});
const secret = await getSecretByNameV2({
environmentSlug: seedData1.environment.slug,
workspaceId: seedData1.projectV3.id,
secretPath: testSuitePath,
authToken: jwtAuthToken,
key: "STAGING_KEY"
});
expect(secret.secretKey).toBe("STAGING_KEY");
expect(secret.secretValue).toBe("stage-value");
const listSecrets = await getSecretsV2({
environmentSlug: seedData1.environment.slug,
workspaceId: seedData1.projectV3.id,
secretPath: testSuitePath,
authToken: jwtAuthToken
});
expect(listSecrets.imports).toEqual(
expect.arrayContaining([
expect.objectContaining({
secrets: expect.arrayContaining([
expect.objectContaining({
secretKey: "STAGING_KEY",
secretValue: "stage-value"
})
])
}),
expect.objectContaining({
secrets: expect.arrayContaining([
expect.objectContaining({
secretKey: "PROD_KEY",
secretValue: "prod-value"
})
])
})
])
);
await deleteSecretV2({
environmentSlug: "staging",
workspaceId: seedData1.projectV3.id,
secretPath: testSuitePath,
authToken: jwtAuthToken,
key: "STAGING_KEY"
});
await deleteSecretV2({
environmentSlug: "prod",
workspaceId: seedData1.projectV3.id,
secretPath: testSuitePath,
authToken: jwtAuthToken,
key: "PROD_KEY"
});
});
}
);
// dev -> stage, prod
describe.each([{ path: "/" }, { path: "/deep" }])(
"Secret import one source to multiple destination pattern testing - %path",
({ path: testSuitePath }) => {
beforeAll(async () => {
let prodFolder: { id: string };
let stagingFolder: { id: string };
let devFolder: { id: string };
if (testSuitePath !== "/") {
prodFolder = await createFolder({
authToken: jwtAuthToken,
environmentSlug: "prod",
workspaceId: seedData1.projectV3.id,
secretPath: "/",
name: "deep"
});
stagingFolder = await createFolder({
authToken: jwtAuthToken,
environmentSlug: "staging",
workspaceId: seedData1.projectV3.id,
secretPath: "/",
name: "deep"
});
devFolder = await createFolder({
authToken: jwtAuthToken,
environmentSlug: seedData1.environment.slug,
workspaceId: seedData1.projectV3.id,
secretPath: "/",
name: "deep"
});
}
const stageImportFromDev = await createSecretImport({
authToken: jwtAuthToken,
secretPath: testSuitePath,
environmentSlug: "staging",
workspaceId: seedData1.projectV3.id,
importPath: testSuitePath,
importEnv: seedData1.environment.slug
});
const prodImportFromDev = await createSecretImport({
authToken: jwtAuthToken,
secretPath: testSuitePath,
environmentSlug: "prod",
workspaceId: seedData1.projectV3.id,
importPath: testSuitePath,
importEnv: seedData1.environment.slug
});
return async () => {
await deleteSecretImport({
id: prodImportFromDev.id,
workspaceId: seedData1.projectV3.id,
environmentSlug: "prod",
secretPath: testSuitePath,
authToken: jwtAuthToken
});
await deleteSecretImport({
id: stageImportFromDev.id,
workspaceId: seedData1.projectV3.id,
environmentSlug: "staging",
secretPath: testSuitePath,
authToken: jwtAuthToken
});
if (prodFolder) {
await deleteFolder({
authToken: jwtAuthToken,
secretPath: "/",
id: prodFolder.id,
workspaceId: seedData1.projectV3.id,
environmentSlug: "prod"
});
}
if (stagingFolder) {
await deleteFolder({
authToken: jwtAuthToken,
secretPath: "/",
id: stagingFolder.id,
workspaceId: seedData1.projectV3.id,
environmentSlug: "staging"
});
}
if (devFolder) {
await deleteFolder({
authToken: jwtAuthToken,
secretPath: "/",
id: devFolder.id,
workspaceId: seedData1.projectV3.id,
environmentSlug: seedData1.environment.slug
});
}
};
});
test("Check imported secret exist", async () => {
await createSecretV2({
environmentSlug: seedData1.environment.slug,
workspaceId: seedData1.projectV3.id,
secretPath: testSuitePath,
authToken: jwtAuthToken,
key: "STAGING_KEY",
value: "stage-value"
});
await createSecretV2({
environmentSlug: seedData1.environment.slug,
workspaceId: seedData1.projectV3.id,
secretPath: testSuitePath,
authToken: jwtAuthToken,
key: "PROD_KEY",
value: "prod-value"
});
const stagingSecret = await getSecretByNameV2({
environmentSlug: "staging",
workspaceId: seedData1.projectV3.id,
secretPath: testSuitePath,
authToken: jwtAuthToken,
key: "STAGING_KEY"
});
expect(stagingSecret.secretKey).toBe("STAGING_KEY");
expect(stagingSecret.secretValue).toBe("stage-value");
const prodSecret = await getSecretByNameV2({
environmentSlug: "prod",
workspaceId: seedData1.projectV3.id,
secretPath: testSuitePath,
authToken: jwtAuthToken,
key: "PROD_KEY"
});
expect(prodSecret.secretKey).toBe("PROD_KEY");
expect(prodSecret.secretValue).toBe("prod-value");
await deleteSecretV2({
environmentSlug: seedData1.environment.slug,
workspaceId: seedData1.projectV3.id,
secretPath: testSuitePath,
authToken: jwtAuthToken,
key: "STAGING_KEY"
});
await deleteSecretV2({
environmentSlug: seedData1.environment.slug,
workspaceId: seedData1.projectV3.id,
secretPath: testSuitePath,
authToken: jwtAuthToken,
key: "PROD_KEY"
});
});
}
);

View File

@ -0,0 +1,406 @@
import { createFolder, deleteFolder } from "e2e-test/testUtils/folders";
import { createSecretImport, deleteSecretImport } from "e2e-test/testUtils/secret-imports";
import { createSecretV2, deleteSecretV2, getSecretByNameV2, getSecretsV2 } from "e2e-test/testUtils/secrets";
import { seedData1 } from "@app/db/seed-data";
// dev <- stage <- prod
describe.each([{ secretPath: "/" }, { secretPath: "/deep" }])(
"Secret replication waterfall pattern testing - %secretPath",
({ secretPath: testSuitePath }) => {
beforeAll(async () => {
let prodFolder: { id: string };
let stagingFolder: { id: string };
let devFolder: { id: string };
if (testSuitePath !== "/") {
prodFolder = await createFolder({
authToken: jwtAuthToken,
environmentSlug: "prod",
workspaceId: seedData1.projectV3.id,
secretPath: "/",
name: "deep"
});
stagingFolder = await createFolder({
authToken: jwtAuthToken,
environmentSlug: "staging",
workspaceId: seedData1.projectV3.id,
secretPath: "/",
name: "deep"
});
devFolder = await createFolder({
authToken: jwtAuthToken,
environmentSlug: seedData1.environment.slug,
workspaceId: seedData1.projectV3.id,
secretPath: "/",
name: "deep"
});
}
const devImportFromStage = await createSecretImport({
authToken: jwtAuthToken,
secretPath: testSuitePath,
environmentSlug: seedData1.environment.slug,
workspaceId: seedData1.projectV3.id,
importPath: testSuitePath,
importEnv: "staging",
isReplication: true
});
const stageImportFromProd = await createSecretImport({
authToken: jwtAuthToken,
secretPath: testSuitePath,
environmentSlug: "staging",
workspaceId: seedData1.projectV3.id,
importPath: testSuitePath,
importEnv: "prod",
isReplication: true
});
return async () => {
await deleteSecretImport({
id: stageImportFromProd.id,
workspaceId: seedData1.projectV3.id,
environmentSlug: "staging",
secretPath: testSuitePath,
authToken: jwtAuthToken
});
await deleteSecretImport({
id: devImportFromStage.id,
workspaceId: seedData1.projectV3.id,
environmentSlug: seedData1.environment.slug,
secretPath: testSuitePath,
authToken: jwtAuthToken
});
if (prodFolder) {
await deleteFolder({
authToken: jwtAuthToken,
secretPath: "/",
id: prodFolder.id,
workspaceId: seedData1.projectV3.id,
environmentSlug: "prod"
});
}
if (stagingFolder) {
await deleteFolder({
authToken: jwtAuthToken,
secretPath: "/",
id: stagingFolder.id,
workspaceId: seedData1.projectV3.id,
environmentSlug: "staging"
});
}
if (devFolder) {
await deleteFolder({
authToken: jwtAuthToken,
secretPath: "/",
id: devFolder.id,
workspaceId: seedData1.projectV3.id,
environmentSlug: seedData1.environment.slug
});
}
};
});
test("Check one level imported secret exist", async () => {
await createSecretV2({
environmentSlug: "staging",
workspaceId: seedData1.projectV3.id,
secretPath: testSuitePath,
authToken: jwtAuthToken,
key: "STAGING_KEY",
value: "stage-value"
});
// wait for 5 second for replication to finish
await new Promise((resolve) => {
setTimeout(resolve, 5000); // time to breathe for db
});
const secret = await getSecretByNameV2({
environmentSlug: seedData1.environment.slug,
workspaceId: seedData1.projectV3.id,
secretPath: testSuitePath,
authToken: jwtAuthToken,
key: "STAGING_KEY"
});
expect(secret.secretKey).toBe("STAGING_KEY");
expect(secret.secretValue).toBe("stage-value");
const listSecrets = await getSecretsV2({
environmentSlug: seedData1.environment.slug,
workspaceId: seedData1.projectV3.id,
secretPath: testSuitePath,
authToken: jwtAuthToken
});
expect(listSecrets.imports).toEqual(
expect.arrayContaining([
expect.objectContaining({
secrets: expect.arrayContaining([
expect.objectContaining({
secretKey: "STAGING_KEY",
secretValue: "stage-value"
})
])
})
])
);
await deleteSecretV2({
environmentSlug: "staging",
workspaceId: seedData1.projectV3.id,
secretPath: testSuitePath,
authToken: jwtAuthToken,
key: "STAGING_KEY"
});
});
test("Check two level imported secret exist", async () => {
await createSecretV2({
environmentSlug: "prod",
workspaceId: seedData1.projectV3.id,
secretPath: testSuitePath,
authToken: jwtAuthToken,
key: "PROD_KEY",
value: "prod-value"
});
// wait for 5 second for replication to finish
await new Promise((resolve) => {
setTimeout(resolve, 5000); // time to breathe for db
});
const secret = await getSecretByNameV2({
environmentSlug: seedData1.environment.slug,
workspaceId: seedData1.projectV3.id,
secretPath: testSuitePath,
authToken: jwtAuthToken,
key: "PROD_KEY"
});
expect(secret.secretKey).toBe("PROD_KEY");
expect(secret.secretValue).toBe("prod-value");
const listSecrets = await getSecretsV2({
environmentSlug: seedData1.environment.slug,
workspaceId: seedData1.projectV3.id,
secretPath: testSuitePath,
authToken: jwtAuthToken
});
expect(listSecrets.imports).toEqual(
expect.arrayContaining([
expect.objectContaining({
secrets: expect.arrayContaining([
expect.objectContaining({
secretKey: "PROD_KEY",
secretValue: "prod-value"
})
])
})
])
);
await deleteSecretV2({
environmentSlug: "prod",
workspaceId: seedData1.projectV3.id,
secretPath: testSuitePath,
authToken: jwtAuthToken,
key: "PROD_KEY"
});
});
},
{ timeout: 30000 }
);
// dev <- stage, dev <- prod
describe.each([{ path: "/" }, { path: "/deep" }])(
"Secret replication 1-N pattern testing - %path",
({ path: testSuitePath }) => {
beforeAll(async () => {
let prodFolder: { id: string };
let stagingFolder: { id: string };
let devFolder: { id: string };
if (testSuitePath !== "/") {
prodFolder = await createFolder({
authToken: jwtAuthToken,
environmentSlug: "prod",
workspaceId: seedData1.projectV3.id,
secretPath: "/",
name: "deep"
});
stagingFolder = await createFolder({
authToken: jwtAuthToken,
environmentSlug: "staging",
workspaceId: seedData1.projectV3.id,
secretPath: "/",
name: "deep"
});
devFolder = await createFolder({
authToken: jwtAuthToken,
environmentSlug: seedData1.environment.slug,
workspaceId: seedData1.projectV3.id,
secretPath: "/",
name: "deep"
});
}
const devImportFromStage = await createSecretImport({
authToken: jwtAuthToken,
secretPath: testSuitePath,
environmentSlug: seedData1.environment.slug,
workspaceId: seedData1.projectV3.id,
importPath: testSuitePath,
importEnv: "staging",
isReplication: true
});
const devImportFromProd = await createSecretImport({
authToken: jwtAuthToken,
secretPath: testSuitePath,
environmentSlug: seedData1.environment.slug,
workspaceId: seedData1.projectV3.id,
importPath: testSuitePath,
importEnv: "prod",
isReplication: true
});
return async () => {
await deleteSecretImport({
id: devImportFromProd.id,
workspaceId: seedData1.projectV3.id,
environmentSlug: seedData1.environment.slug,
secretPath: testSuitePath,
authToken: jwtAuthToken
});
await deleteSecretImport({
id: devImportFromStage.id,
workspaceId: seedData1.projectV3.id,
environmentSlug: seedData1.environment.slug,
secretPath: testSuitePath,
authToken: jwtAuthToken
});
if (prodFolder) {
await deleteFolder({
authToken: jwtAuthToken,
secretPath: "/",
id: prodFolder.id,
workspaceId: seedData1.projectV3.id,
environmentSlug: "prod"
});
}
if (stagingFolder) {
await deleteFolder({
authToken: jwtAuthToken,
secretPath: "/",
id: stagingFolder.id,
workspaceId: seedData1.projectV3.id,
environmentSlug: "staging"
});
}
if (devFolder) {
await deleteFolder({
authToken: jwtAuthToken,
secretPath: "/",
id: devFolder.id,
workspaceId: seedData1.projectV3.id,
environmentSlug: seedData1.environment.slug
});
}
};
});
test("Check imported secret exist", async () => {
await createSecretV2({
environmentSlug: "staging",
workspaceId: seedData1.projectV3.id,
secretPath: testSuitePath,
authToken: jwtAuthToken,
key: "STAGING_KEY",
value: "stage-value"
});
await createSecretV2({
environmentSlug: "prod",
workspaceId: seedData1.projectV3.id,
secretPath: testSuitePath,
authToken: jwtAuthToken,
key: "PROD_KEY",
value: "prod-value"
});
// wait for 5 second for replication to finish
await new Promise((resolve) => {
setTimeout(resolve, 5000); // time to breathe for db
});
const secret = await getSecretByNameV2({
environmentSlug: seedData1.environment.slug,
workspaceId: seedData1.projectV3.id,
secretPath: testSuitePath,
authToken: jwtAuthToken,
key: "STAGING_KEY"
});
expect(secret.secretKey).toBe("STAGING_KEY");
expect(secret.secretValue).toBe("stage-value");
const listSecrets = await getSecretsV2({
environmentSlug: seedData1.environment.slug,
workspaceId: seedData1.projectV3.id,
secretPath: testSuitePath,
authToken: jwtAuthToken
});
expect(listSecrets.imports).toEqual(
expect.arrayContaining([
expect.objectContaining({
secrets: expect.arrayContaining([
expect.objectContaining({
secretKey: "STAGING_KEY",
secretValue: "stage-value"
})
])
}),
expect.objectContaining({
secrets: expect.arrayContaining([
expect.objectContaining({
secretKey: "PROD_KEY",
secretValue: "prod-value"
})
])
})
])
);
await deleteSecretV2({
environmentSlug: "staging",
workspaceId: seedData1.projectV3.id,
secretPath: testSuitePath,
authToken: jwtAuthToken,
key: "STAGING_KEY"
});
await deleteSecretV2({
environmentSlug: "prod",
workspaceId: seedData1.projectV3.id,
secretPath: testSuitePath,
authToken: jwtAuthToken,
key: "PROD_KEY"
});
});
},
{ timeout: 30000 }
);

View File

@ -0,0 +1,330 @@
import { createFolder, deleteFolder } from "e2e-test/testUtils/folders";
import { createSecretImport, deleteSecretImport } from "e2e-test/testUtils/secret-imports";
import { createSecretV2, deleteSecretV2, getSecretByNameV2, getSecretsV2 } from "e2e-test/testUtils/secrets";
import { seedData1 } from "@app/db/seed-data";
describe("Secret expansion", () => {
const projectId = seedData1.projectV3.id;
beforeAll(async () => {
const prodRootFolder = await createFolder({
authToken: jwtAuthToken,
environmentSlug: "prod",
workspaceId: projectId,
secretPath: "/",
name: "deep"
});
await createFolder({
authToken: jwtAuthToken,
environmentSlug: "prod",
workspaceId: projectId,
secretPath: "/deep",
name: "nested"
});
return async () => {
await deleteFolder({
authToken: jwtAuthToken,
secretPath: "/",
id: prodRootFolder.id,
workspaceId: projectId,
environmentSlug: "prod"
});
};
});
test("Local secret reference", async () => {
const secrets = [
{
environmentSlug: seedData1.environment.slug,
workspaceId: projectId,
secretPath: "/",
authToken: jwtAuthToken,
key: "HELLO",
value: "world"
},
{
environmentSlug: seedData1.environment.slug,
workspaceId: projectId,
secretPath: "/",
authToken: jwtAuthToken,
key: "TEST",
// eslint-disable-next-line
value: "hello ${HELLO}"
}
];
await Promise.all(secrets.map((el) => createSecretV2(el)));
const expandedSecret = await getSecretByNameV2({
environmentSlug: seedData1.environment.slug,
workspaceId: projectId,
secretPath: "/",
authToken: jwtAuthToken,
key: "TEST"
});
expect(expandedSecret.secretValue).toBe("hello world");
const listSecrets = await getSecretsV2({
environmentSlug: seedData1.environment.slug,
workspaceId: projectId,
secretPath: "/",
authToken: jwtAuthToken
});
expect(listSecrets.secrets).toEqual(
expect.arrayContaining([
expect.objectContaining({
secretKey: "TEST",
secretValue: "hello world"
})
])
);
await Promise.all(secrets.map((el) => deleteSecretV2(el)));
});
test("Cross environment secret reference", async () => {
const secrets = [
{
environmentSlug: "prod",
workspaceId: projectId,
secretPath: "/deep",
authToken: jwtAuthToken,
key: "DEEP_KEY_1",
value: "testing"
},
{
environmentSlug: "prod",
workspaceId: projectId,
secretPath: "/deep/nested",
authToken: jwtAuthToken,
key: "NESTED_KEY_1",
value: "reference"
},
{
environmentSlug: "prod",
workspaceId: projectId,
secretPath: "/deep/nested",
authToken: jwtAuthToken,
key: "NESTED_KEY_2",
// eslint-disable-next-line
value: "secret ${NESTED_KEY_1}"
},
{
environmentSlug: seedData1.environment.slug,
workspaceId: projectId,
secretPath: "/",
authToken: jwtAuthToken,
key: "KEY",
// eslint-disable-next-line
value: "hello ${prod.deep.DEEP_KEY_1} ${prod.deep.nested.NESTED_KEY_2}"
}
];
await Promise.all(secrets.map((el) => createSecretV2(el)));
const expandedSecret = await getSecretByNameV2({
environmentSlug: seedData1.environment.slug,
workspaceId: projectId,
secretPath: "/",
authToken: jwtAuthToken,
key: "KEY"
});
expect(expandedSecret.secretValue).toBe("hello testing secret reference");
const listSecrets = await getSecretsV2({
environmentSlug: seedData1.environment.slug,
workspaceId: projectId,
secretPath: "/",
authToken: jwtAuthToken
});
expect(listSecrets.secrets).toEqual(
expect.arrayContaining([
expect.objectContaining({
secretKey: "KEY",
secretValue: "hello testing secret reference"
})
])
);
await Promise.all(secrets.map((el) => deleteSecretV2(el)));
});
test("Non replicated secret import secret expansion on local reference and nested reference", async () => {
const secrets = [
{
environmentSlug: "prod",
workspaceId: projectId,
secretPath: "/deep",
authToken: jwtAuthToken,
key: "DEEP_KEY_1",
value: "testing"
},
{
environmentSlug: "prod",
workspaceId: projectId,
secretPath: "/deep/nested",
authToken: jwtAuthToken,
key: "NESTED_KEY_1",
value: "reference"
},
{
environmentSlug: "prod",
workspaceId: projectId,
secretPath: "/deep/nested",
authToken: jwtAuthToken,
key: "NESTED_KEY_2",
// eslint-disable-next-line
value: "secret ${NESTED_KEY_1} ${prod.deep.DEEP_KEY_1}"
},
{
environmentSlug: seedData1.environment.slug,
workspaceId: projectId,
secretPath: "/",
authToken: jwtAuthToken,
key: "KEY",
// eslint-disable-next-line
value: "hello world"
}
];
await Promise.all(secrets.map((el) => createSecretV2(el)));
const secretImportFromProdToDev = await createSecretImport({
environmentSlug: seedData1.environment.slug,
workspaceId: projectId,
secretPath: "/",
authToken: jwtAuthToken,
importEnv: "prod",
importPath: "/deep/nested"
});
const listSecrets = await getSecretsV2({
environmentSlug: seedData1.environment.slug,
workspaceId: projectId,
secretPath: "/",
authToken: jwtAuthToken
});
expect(listSecrets.imports).toEqual(
expect.arrayContaining([
expect.objectContaining({
secretPath: "/deep/nested",
environment: "prod",
secrets: expect.arrayContaining([
expect.objectContaining({
secretKey: "NESTED_KEY_1",
secretValue: "reference"
}),
expect.objectContaining({
secretKey: "NESTED_KEY_2",
secretValue: "secret reference testing"
})
])
})
])
);
await Promise.all(secrets.map((el) => deleteSecretV2(el)));
await deleteSecretImport({
environmentSlug: seedData1.environment.slug,
workspaceId: projectId,
authToken: jwtAuthToken,
id: secretImportFromProdToDev.id,
secretPath: "/"
});
});
test(
"Replicated secret import secret expansion on local reference and nested reference",
async () => {
const secrets = [
{
environmentSlug: "prod",
workspaceId: projectId,
secretPath: "/deep",
authToken: jwtAuthToken,
key: "DEEP_KEY_1",
value: "testing"
},
{
environmentSlug: "prod",
workspaceId: projectId,
secretPath: "/deep/nested",
authToken: jwtAuthToken,
key: "NESTED_KEY_1",
value: "reference"
},
{
environmentSlug: "prod",
workspaceId: projectId,
secretPath: "/deep/nested",
authToken: jwtAuthToken,
key: "NESTED_KEY_2",
// eslint-disable-next-line
value: "secret ${NESTED_KEY_1} ${prod.deep.DEEP_KEY_1}"
},
{
environmentSlug: seedData1.environment.slug,
workspaceId: projectId,
secretPath: "/",
authToken: jwtAuthToken,
key: "KEY",
// eslint-disable-next-line
value: "hello world"
}
];
await Promise.all(secrets.map((el) => createSecretV2(el)));
const secretImportFromProdToDev = await createSecretImport({
environmentSlug: seedData1.environment.slug,
workspaceId: projectId,
secretPath: "/",
authToken: jwtAuthToken,
importEnv: "prod",
importPath: "/deep/nested",
isReplication: true
});
// wait for 5 second for replication to finish
await new Promise((resolve) => {
setTimeout(resolve, 5000); // time to breathe for db
});
const listSecrets = await getSecretsV2({
environmentSlug: seedData1.environment.slug,
workspaceId: projectId,
secretPath: "/",
authToken: jwtAuthToken
});
expect(listSecrets.imports).toEqual(
expect.arrayContaining([
expect.objectContaining({
secretPath: `/__reserve_replication_${secretImportFromProdToDev.id}`,
environment: seedData1.environment.slug,
secrets: expect.arrayContaining([
expect.objectContaining({
secretKey: "NESTED_KEY_1",
secretValue: "reference"
}),
expect.objectContaining({
secretKey: "NESTED_KEY_2",
secretValue: "secret reference testing"
})
])
})
])
);
await Promise.all(secrets.map((el) => deleteSecretV2(el)));
await deleteSecretImport({
environmentSlug: seedData1.environment.slug,
workspaceId: projectId,
authToken: jwtAuthToken,
id: secretImportFromProdToDev.id,
secretPath: "/"
});
},
{ timeout: 10000 }
);
});

View File

@ -8,6 +8,7 @@ type TRawSecret = {
secretComment?: string;
version: number;
};
const createSecret = async (dto: { path: string; key: string; value: string; comment: string; type?: SecretType }) => {
const createSecretReqBody = {
workspaceId: seedData1.projectV3.id,

View File

@ -0,0 +1,73 @@
type TFolder = {
id: string;
name: string;
};
export const createFolder = async (dto: {
workspaceId: string;
environmentSlug: string;
secretPath: string;
name: string;
authToken: string;
}) => {
const res = await testServer.inject({
method: "POST",
url: `/api/v1/folders`,
headers: {
authorization: `Bearer ${dto.authToken}`
},
body: {
workspaceId: dto.workspaceId,
environment: dto.environmentSlug,
name: dto.name,
path: dto.secretPath
}
});
expect(res.statusCode).toBe(200);
return res.json().folder as TFolder;
};
export const deleteFolder = async (dto: {
workspaceId: string;
environmentSlug: string;
secretPath: string;
id: string;
authToken: string;
}) => {
const res = await testServer.inject({
method: "DELETE",
url: `/api/v1/folders/${dto.id}`,
headers: {
authorization: `Bearer ${dto.authToken}`
},
body: {
workspaceId: dto.workspaceId,
environment: dto.environmentSlug,
path: dto.secretPath
}
});
expect(res.statusCode).toBe(200);
return res.json().folder as TFolder;
};
export const listFolders = async (dto: {
workspaceId: string;
environmentSlug: string;
secretPath: string;
authToken: string;
}) => {
const res = await testServer.inject({
method: "GET",
url: `/api/v1/folders`,
headers: {
authorization: `Bearer ${dto.authToken}`
},
body: {
workspaceId: dto.workspaceId,
environment: dto.environmentSlug,
path: dto.secretPath
}
});
expect(res.statusCode).toBe(200);
return res.json().folders as TFolder[];
};

View File

@ -0,0 +1,93 @@
type TSecretImport = {
id: string;
importEnv: {
name: string;
slug: string;
id: string;
};
importPath: string;
};
export const createSecretImport = async (dto: {
workspaceId: string;
environmentSlug: string;
isReplication?: boolean;
secretPath: string;
importPath: string;
importEnv: string;
authToken: string;
}) => {
const res = await testServer.inject({
method: "POST",
url: `/api/v1/secret-imports`,
headers: {
authorization: `Bearer ${dto.authToken}`
},
body: {
workspaceId: dto.workspaceId,
environment: dto.environmentSlug,
isReplication: dto.isReplication,
path: dto.secretPath,
import: {
environment: dto.importEnv,
path: dto.importPath
}
}
});
expect(res.statusCode).toBe(200);
const payload = JSON.parse(res.payload);
expect(payload).toHaveProperty("secretImport");
return payload.secretImport as TSecretImport;
};
export const deleteSecretImport = async (dto: {
workspaceId: string;
environmentSlug: string;
secretPath: string;
authToken: string;
id: string;
}) => {
const res = await testServer.inject({
method: "DELETE",
url: `/api/v1/secret-imports/${dto.id}`,
headers: {
authorization: `Bearer ${dto.authToken}`
},
body: {
workspaceId: dto.workspaceId,
environment: dto.environmentSlug,
path: dto.secretPath
}
});
expect(res.statusCode).toBe(200);
const payload = JSON.parse(res.payload);
expect(payload).toHaveProperty("secretImport");
return payload.secretImport as TSecretImport;
};
export const listSecretImport = async (dto: {
workspaceId: string;
environmentSlug: string;
secretPath: string;
authToken: string;
}) => {
const res = await testServer.inject({
method: "GET",
url: `/api/v1/secret-imports`,
headers: {
authorization: `Bearer ${dto.authToken}`
},
query: {
workspaceId: dto.workspaceId,
environment: dto.environmentSlug,
path: dto.secretPath
}
});
expect(res.statusCode).toBe(200);
const payload = JSON.parse(res.payload);
expect(payload).toHaveProperty("secretImports");
return payload.secretImports as TSecretImport[];
};

View File

@ -0,0 +1,128 @@
import { SecretType } from "@app/db/schemas";
type TRawSecret = {
secretKey: string;
secretValue: string;
secretComment?: string;
version: number;
};
export const createSecretV2 = async (dto: {
workspaceId: string;
environmentSlug: string;
secretPath: string;
key: string;
value: string;
comment?: string;
authToken: string;
type?: SecretType;
}) => {
const createSecretReqBody = {
workspaceId: dto.workspaceId,
environment: dto.environmentSlug,
type: dto.type || SecretType.Shared,
secretPath: dto.secretPath,
secretKey: dto.key,
secretValue: dto.value,
secretComment: dto.comment
};
const createSecRes = await testServer.inject({
method: "POST",
url: `/api/v3/secrets/raw/${dto.key}`,
headers: {
authorization: `Bearer ${dto.authToken}`
},
body: createSecretReqBody
});
expect(createSecRes.statusCode).toBe(200);
const createdSecretPayload = JSON.parse(createSecRes.payload);
expect(createdSecretPayload).toHaveProperty("secret");
return createdSecretPayload.secret as TRawSecret;
};
export const deleteSecretV2 = async (dto: {
workspaceId: string;
environmentSlug: string;
secretPath: string;
key: string;
authToken: string;
}) => {
const deleteSecRes = await testServer.inject({
method: "DELETE",
url: `/api/v3/secrets/raw/${dto.key}`,
headers: {
authorization: `Bearer ${dto.authToken}`
},
body: {
workspaceId: dto.workspaceId,
environment: dto.environmentSlug,
secretPath: dto.secretPath
}
});
expect(deleteSecRes.statusCode).toBe(200);
const updatedSecretPayload = JSON.parse(deleteSecRes.payload);
expect(updatedSecretPayload).toHaveProperty("secret");
return updatedSecretPayload.secret as TRawSecret;
};
export const getSecretByNameV2 = async (dto: {
workspaceId: string;
environmentSlug: string;
secretPath: string;
key: string;
authToken: string;
}) => {
const response = await testServer.inject({
method: "GET",
url: `/api/v3/secrets/raw/${dto.key}`,
headers: {
authorization: `Bearer ${dto.authToken}`
},
query: {
workspaceId: dto.workspaceId,
environment: dto.environmentSlug,
secretPath: dto.secretPath,
expandSecretReferences: "true",
include_imports: "true"
}
});
expect(response.statusCode).toBe(200);
const payload = JSON.parse(response.payload);
expect(payload).toHaveProperty("secret");
return payload.secret as TRawSecret;
};
export const getSecretsV2 = async (dto: {
workspaceId: string;
environmentSlug: string;
secretPath: string;
authToken: string;
}) => {
const getSecretsResponse = await testServer.inject({
method: "GET",
url: `/api/v3/secrets/raw`,
headers: {
authorization: `Bearer ${dto.authToken}`
},
query: {
workspaceId: dto.workspaceId,
environment: dto.environmentSlug,
secretPath: dto.secretPath,
expandSecretReferences: "true",
include_imports: "true"
}
});
expect(getSecretsResponse.statusCode).toBe(200);
const getSecretsPayload = JSON.parse(getSecretsResponse.payload);
expect(getSecretsPayload).toHaveProperty("secrets");
expect(getSecretsPayload).toHaveProperty("imports");
return getSecretsPayload as {
secrets: TRawSecret[];
imports: {
secretPath: string;
environment: string;
folderId: string;
secrets: TRawSecret[];
}[];
};
};

View File

@ -11,10 +11,11 @@ import { initLogger } from "@app/lib/logger";
import { main } from "@app/server/app";
import { AuthMethod, AuthTokenType } from "@app/services/auth/auth-type";
import { mockQueue } from "./mocks/queue";
import { mockSmtpServer } from "./mocks/smtp";
import { mockKeyStore } from "./mocks/keystore";
import { initDbConnection } from "@app/db";
import { queueServiceFactory } from "@app/queue";
import { keyStoreFactory } from "@app/keystore/keystore";
import { Redis } from "ioredis";
dotenv.config({ path: path.join(__dirname, "../../.env.test"), debug: true });
export default {
@ -28,19 +29,31 @@ export default {
dbRootCert: cfg.DB_ROOT_CERT
});
const redis = new Redis(cfg.REDIS_URL);
await redis.flushdb("SYNC");
try {
await db.migrate.rollback(
{
directory: path.join(__dirname, "../src/db/migrations"),
extension: "ts",
tableName: "infisical_migrations"
},
true
);
await db.migrate.latest({
directory: path.join(__dirname, "../src/db/migrations"),
extension: "ts",
tableName: "infisical_migrations"
});
await db.seed.run({
directory: path.join(__dirname, "../src/db/seeds"),
extension: "ts"
});
const smtp = mockSmtpServer();
const queue = mockQueue();
const keyStore = mockKeyStore();
const queue = queueServiceFactory(cfg.REDIS_URL);
const keyStore = keyStoreFactory(cfg.REDIS_URL);
const server = await main({ db, smtp, logger, queue, keyStore });
// @ts-expect-error type
globalThis.testServer = server;
@ -58,10 +71,12 @@ export default {
{ expiresIn: cfg.JWT_AUTH_LIFETIME }
);
} catch (error) {
// eslint-disable-next-line
console.log("[TEST] Error setting up environment", error);
await db.destroy();
throw error;
}
// custom setup
return {
async teardown() {
@ -80,6 +95,9 @@ export default {
},
true
);
await redis.flushdb("ASYNC");
redis.disconnect();
await db.destroy();
}
};

File diff suppressed because it is too large Load Diff

View File

@ -50,6 +50,7 @@
"migration:down": "knex --knexfile ./src/db/knexfile.ts --client pg migrate:down",
"migration:list": "knex --knexfile ./src/db/knexfile.ts --client pg migrate:list",
"migration:latest": "knex --knexfile ./src/db/knexfile.ts --client pg migrate:latest",
"migration:status": "knex --knexfile ./src/db/knexfile.ts --client pg migrate:status",
"migration:rollback": "knex --knexfile ./src/db/knexfile.ts migrate:rollback",
"seed:new": "tsx ./scripts/create-seed-file.ts",
"seed": "knex --knexfile ./src/db/knexfile.ts --client pg seed:run",
@ -102,15 +103,16 @@
"tsup": "^8.0.1",
"tsx": "^4.4.0",
"typescript": "^5.3.2",
"vite-tsconfig-paths": "^4.2.2",
"vitest": "^1.2.2"
},
"dependencies": {
"@aws-sdk/client-elasticache": "^3.637.0",
"@aws-sdk/client-iam": "^3.525.0",
"@aws-sdk/client-kms": "^3.609.0",
"@aws-sdk/client-secrets-manager": "^3.504.0",
"@aws-sdk/client-sts": "^3.600.0",
"@casl/ability": "^6.5.0",
"@elastic/elasticsearch": "^8.15.0",
"@fastify/cookie": "^9.3.1",
"@fastify/cors": "^8.5.0",
"@fastify/etag": "^5.1.0",
@ -175,6 +177,8 @@
"posthog-node": "^3.6.2",
"probot": "^13.0.0",
"safe-regex": "^2.1.1",
"scim-patch": "^0.8.3",
"scim2-parse-filter": "^0.2.10",
"smee-client": "^2.0.0",
"tedious": "^18.2.1",
"tweetnacl": "^1.0.3",

View File

@ -7,6 +7,7 @@ import { TAuditLogServiceFactory } from "@app/ee/services/audit-log/audit-log-se
import { TCreateAuditLogDTO } from "@app/ee/services/audit-log/audit-log-types";
import { TAuditLogStreamServiceFactory } from "@app/ee/services/audit-log-stream/audit-log-stream-service";
import { TCertificateAuthorityCrlServiceFactory } from "@app/ee/services/certificate-authority-crl/certificate-authority-crl-service";
import { TCertificateEstServiceFactory } from "@app/ee/services/certificate-est/certificate-est-service";
import { TDynamicSecretServiceFactory } from "@app/ee/services/dynamic-secret/dynamic-secret-service";
import { TDynamicSecretLeaseServiceFactory } from "@app/ee/services/dynamic-secret-lease/dynamic-secret-lease-service";
import { TExternalKmsServiceFactory } from "@app/ee/services/external-kms/external-kms-service";
@ -36,7 +37,6 @@ import { ActorAuthMethod, ActorType } from "@app/services/auth/auth-type";
import { TAuthTokenServiceFactory } from "@app/services/auth-token/auth-token-service";
import { TCertificateServiceFactory } from "@app/services/certificate/certificate-service";
import { TCertificateAuthorityServiceFactory } from "@app/services/certificate-authority/certificate-authority-service";
import { TCertificateEstServiceFactory } from "@app/services/certificate-est/certificate-est-service";
import { TCertificateTemplateServiceFactory } from "@app/services/certificate-template/certificate-template-service";
import { TGroupProjectServiceFactory } from "@app/services/group-project/group-project-service";
import { TIdentityServiceFactory } from "@app/services/identity/identity-service";

View File

@ -115,7 +115,14 @@ export async function down(knex: Knex): Promise<void> {
// eslint-disable-next-line
// @ts-ignore because generate schema happens after this
approverId: knex(TableName.ProjectMembership)
.select("id")
.join(
TableName.SecretApprovalPolicy,
`${TableName.SecretApprovalPolicy}.id`,
`${TableName.SecretApprovalPolicyApprover}.policyId`
)
.join(TableName.Environment, `${TableName.Environment}.id`, `${TableName.SecretApprovalPolicy}.envId`)
.select(knex.ref("id").withSchema(TableName.ProjectMembership))
.where(`${TableName.ProjectMembership}.projectId`, knex.raw("??", [`${TableName.Environment}.projectId`]))
.where("userId", knex.raw("??", [`${TableName.SecretApprovalPolicyApprover}.approverUserId`]))
});
await knex.schema.alterTable(TableName.SecretApprovalPolicyApprover, (tb) => {
@ -147,13 +154,27 @@ export async function down(knex: Knex): Promise<void> {
// eslint-disable-next-line
// @ts-ignore because generate schema happens after this
committerId: knex(TableName.ProjectMembership)
.select("id")
.where("userId", knex.raw("??", [`${TableName.SecretApprovalRequest}.committerUserId`])),
.join(
TableName.SecretApprovalPolicy,
`${TableName.SecretApprovalPolicy}.id`,
`${TableName.SecretApprovalRequest}.policyId`
)
.join(TableName.Environment, `${TableName.Environment}.id`, `${TableName.SecretApprovalPolicy}.envId`)
.where(`${TableName.ProjectMembership}.projectId`, knex.raw("??", [`${TableName.Environment}.projectId`]))
.where("userId", knex.raw("??", [`${TableName.SecretApprovalRequest}.committerUserId`]))
.select(knex.ref("id").withSchema(TableName.ProjectMembership)),
// eslint-disable-next-line
// @ts-ignore because generate schema happens after this
statusChangeBy: knex(TableName.ProjectMembership)
.select("id")
.join(
TableName.SecretApprovalPolicy,
`${TableName.SecretApprovalPolicy}.id`,
`${TableName.SecretApprovalRequest}.policyId`
)
.join(TableName.Environment, `${TableName.Environment}.id`, `${TableName.SecretApprovalPolicy}.envId`)
.where(`${TableName.ProjectMembership}.projectId`, knex.raw("??", [`${TableName.Environment}.projectId`]))
.where("userId", knex.raw("??", [`${TableName.SecretApprovalRequest}.statusChangedByUserId`]))
.select(knex.ref("id").withSchema(TableName.ProjectMembership))
});
await knex.schema.alterTable(TableName.SecretApprovalRequest, (tb) => {
@ -177,8 +198,20 @@ export async function down(knex: Knex): Promise<void> {
// eslint-disable-next-line
// @ts-ignore because generate schema happens after this
member: knex(TableName.ProjectMembership)
.select("id")
.join(
TableName.SecretApprovalRequest,
`${TableName.SecretApprovalRequest}.id`,
`${TableName.SecretApprovalRequestReviewer}.requestId`
)
.join(
TableName.SecretApprovalPolicy,
`${TableName.SecretApprovalPolicy}.id`,
`${TableName.SecretApprovalRequest}.policyId`
)
.join(TableName.Environment, `${TableName.Environment}.id`, `${TableName.SecretApprovalPolicy}.envId`)
.where(`${TableName.ProjectMembership}.projectId`, knex.raw("??", [`${TableName.Environment}.projectId`]))
.where("userId", knex.raw("??", [`${TableName.SecretApprovalRequestReviewer}.reviewerUserId`]))
.select(knex.ref("id").withSchema(TableName.ProjectMembership))
});
await knex.schema.alterTable(TableName.SecretApprovalRequestReviewer, (tb) => {
tb.uuid("member").notNullable().alter();

View File

@ -0,0 +1,25 @@
import { Knex } from "knex";
import { TableName } from "../schemas";
export async function up(knex: Knex): Promise<void> {
if (await knex.schema.hasTable(TableName.SecretSharing)) {
const doesPasswordExist = await knex.schema.hasColumn(TableName.SecretSharing, "password");
if (!doesPasswordExist) {
await knex.schema.alterTable(TableName.SecretSharing, (t) => {
t.string("password").nullable();
});
}
}
}
export async function down(knex: Knex): Promise<void> {
if (await knex.schema.hasTable(TableName.SecretSharing)) {
const doesPasswordExist = await knex.schema.hasColumn(TableName.SecretSharing, "password");
if (doesPasswordExist) {
await knex.schema.alterTable(TableName.SecretSharing, (t) => {
t.dropColumn("password");
});
}
}
}

View File

@ -21,6 +21,7 @@ export const SecretSharingSchema = z.object({
expiresAfterViews: z.number().nullable().optional(),
accessType: z.string().default("anyone"),
name: z.string().nullable().optional(),
password: z.string().nullable().optional(),
lastViewedAt: z.date().nullable().optional()
});

View File

@ -17,11 +17,11 @@ export const registerAccessApprovalPolicyRouter = async (server: FastifyZodProvi
name: z.string().optional(),
secretPath: z.string().trim().default("/"),
environment: z.string(),
approverUserIds: z.string().array().min(1),
approvers: z.string().array().min(1),
approvals: z.number().min(1).default(1),
enforcementLevel: z.nativeEnum(EnforcementLevel).default(EnforcementLevel.Hard)
})
.refine((data) => data.approvals <= data.approverUserIds.length, {
.refine((data) => data.approvals <= data.approvers.length, {
path: ["approvals"],
message: "The number of approvals should be lower than the number of approvers."
}),
@ -127,11 +127,11 @@ export const registerAccessApprovalPolicyRouter = async (server: FastifyZodProvi
.trim()
.optional()
.transform((val) => (val === "" ? "/" : val)),
approverUserIds: z.string().array().min(1),
approvers: z.string().array().min(1),
approvals: z.number().min(1).default(1),
enforcementLevel: z.nativeEnum(EnforcementLevel).default(EnforcementLevel.Hard)
})
.refine((data) => data.approvals <= data.approverUserIds.length, {
.refine((data) => data.approvals <= data.approvers.length, {
path: ["approvals"],
message: "The number of approvals should be lower than the number of approvers."
}),

View File

@ -1,6 +1,6 @@
/* eslint-disable @typescript-eslint/no-unsafe-return */
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
// TODO(akhilmhdh): Fix this when licence service gets it type
// TODO(akhilmhdh): Fix this when license service gets it type
import { z } from "zod";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";

View File

@ -5,22 +5,47 @@ import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type";
export const registerScimRouter = async (server: FastifyZodProvider) => {
server.addContentTypeParser("application/scim+json", { parseAs: "string" }, (_, body, done) => {
try {
const strBody = body instanceof Buffer ? body.toString() : body;
if (!strBody) {
done(null, undefined);
return;
}
const json: unknown = JSON.parse(strBody);
done(null, json);
} catch (err) {
const error = err as Error;
done(error, undefined);
}
});
const ScimUserSchema = z.object({
schemas: z.array(z.string()),
id: z.string().trim(),
userName: z.string().trim(),
name: z
.object({
familyName: z.string().trim().optional(),
givenName: z.string().trim().optional()
})
.optional(),
emails: z
.array(
z.object({
primary: z.boolean(),
value: z.string().email(),
type: z.string().trim()
})
)
.optional(),
displayName: z.string().trim(),
active: z.boolean()
});
const ScimGroupSchema = z.object({
schemas: z.array(z.string()),
id: z.string().trim(),
displayName: z.string().trim(),
members: z
.array(
z.object({
value: z.string(),
display: z.string().optional()
})
)
.optional(),
meta: z.object({
resourceType: z.string().trim()
})
});
export const registerScimRouter = async (server: FastifyZodProvider) => {
server.route({
url: "/scim-tokens",
method: "POST",
@ -127,25 +152,7 @@ export const registerScimRouter = async (server: FastifyZodProvider) => {
}),
response: {
200: z.object({
Resources: z.array(
z.object({
id: z.string().trim(),
userName: z.string().trim(),
name: z.object({
familyName: z.string().trim(),
givenName: z.string().trim()
}),
emails: z.array(
z.object({
primary: z.boolean(),
value: z.string(),
type: z.string().trim()
})
),
displayName: z.string().trim(),
active: z.boolean()
})
),
Resources: z.array(ScimUserSchema),
itemsPerPage: z.number(),
schemas: z.array(z.string()),
startIndex: z.number(),
@ -173,30 +180,7 @@ export const registerScimRouter = async (server: FastifyZodProvider) => {
orgMembershipId: z.string().trim()
}),
response: {
201: z.object({
schemas: z.array(z.string()),
id: z.string().trim(),
userName: z.string().trim(),
name: z.object({
familyName: z.string().trim(),
givenName: z.string().trim()
}),
emails: z.array(
z.object({
primary: z.boolean(),
value: z.string(),
type: z.string().trim()
})
),
displayName: z.string().trim(),
active: z.boolean(),
groups: z.array(
z.object({
value: z.string().trim(),
display: z.string().trim()
})
)
})
200: ScimUserSchema
}
},
onRequest: verifyAuth([AuthMode.SCIM_TOKEN]),
@ -216,10 +200,12 @@ export const registerScimRouter = async (server: FastifyZodProvider) => {
body: z.object({
schemas: z.array(z.string()),
userName: z.string().trim(),
name: z.object({
familyName: z.string().trim(),
givenName: z.string().trim()
}),
name: z
.object({
familyName: z.string().trim().optional(),
givenName: z.string().trim().optional()
})
.optional(),
emails: z
.array(
z.object({
@ -229,28 +215,10 @@ export const registerScimRouter = async (server: FastifyZodProvider) => {
})
)
.optional(),
// displayName: z.string().trim(),
active: z.boolean()
active: z.boolean().default(true)
}),
response: {
200: z.object({
schemas: z.array(z.string()),
id: z.string().trim(),
userName: z.string().trim(),
name: z.object({
familyName: z.string().trim(),
givenName: z.string().trim()
}),
emails: z.array(
z.object({
primary: z.boolean(),
value: z.string().email(),
type: z.string().trim()
})
),
displayName: z.string().trim(),
active: z.boolean()
})
200: ScimUserSchema
}
},
onRequest: verifyAuth([AuthMode.SCIM_TOKEN]),
@ -260,8 +228,8 @@ export const registerScimRouter = async (server: FastifyZodProvider) => {
const user = await req.server.services.scim.createScimUser({
externalId: req.body.userName,
email: primaryEmail,
firstName: req.body.name.givenName,
lastName: req.body.name.familyName,
firstName: req.body?.name?.givenName,
lastName: req.body?.name?.familyName,
orgId: req.permission.orgId
});
@ -291,6 +259,116 @@ export const registerScimRouter = async (server: FastifyZodProvider) => {
}
});
server.route({
url: "/Users/:orgMembershipId",
method: "PUT",
schema: {
params: z.object({
orgMembershipId: z.string().trim()
}),
body: z.object({
schemas: z.array(z.string()),
id: z.string().trim(),
userName: z.string().trim(),
name: z
.object({
familyName: z.string().trim().optional(),
givenName: z.string().trim().optional()
})
.optional(),
displayName: z.string().trim(),
emails: z
.array(
z.object({
primary: z.boolean(),
value: z.string().email(),
type: z.string().trim()
})
)
.optional(),
active: z.boolean()
}),
response: {
200: z.object({
schemas: z.array(z.string()),
id: z.string().trim(),
userName: z.string().trim(),
name: z.object({
familyName: z.string().trim(),
givenName: z.string().trim()
}),
emails: z.array(
z.object({
primary: z.boolean(),
value: z.string().email(),
type: z.string().trim()
})
),
displayName: z.string().trim(),
active: z.boolean()
})
}
},
onRequest: verifyAuth([AuthMode.SCIM_TOKEN]),
handler: async (req) => {
const primaryEmail = req.body.emails?.find((email) => email.primary)?.value;
const user = await req.server.services.scim.replaceScimUser({
orgMembershipId: req.params.orgMembershipId,
orgId: req.permission.orgId,
firstName: req.body?.name?.givenName,
lastName: req.body?.name?.familyName,
active: req.body?.active,
email: primaryEmail,
externalId: req.body.userName
});
return user;
}
});
server.route({
url: "/Users/:orgMembershipId",
method: "PATCH",
schema: {
params: z.object({
orgMembershipId: z.string().trim()
}),
body: z.object({
schemas: z.array(z.string()),
Operations: z.array(
z.union([
z.object({
op: z.union([z.literal("remove"), z.literal("Remove")]),
path: z.string().trim(),
value: z
.object({
value: z.string()
})
.array()
.optional()
}),
z.object({
op: z.union([z.literal("add"), z.literal("Add"), z.literal("replace"), z.literal("Replace")]),
path: z.string().trim().optional(),
value: z.any().optional()
})
])
)
}),
response: {
200: ScimUserSchema
}
},
onRequest: verifyAuth([AuthMode.SCIM_TOKEN]),
handler: async (req) => {
const user = await req.server.services.scim.updateScimUser({
orgMembershipId: req.params.orgMembershipId,
orgId: req.permission.orgId,
operations: req.body.Operations
});
return user;
}
});
server.route({
url: "/Groups",
method: "POST",
@ -305,25 +383,10 @@ export const registerScimRouter = async (server: FastifyZodProvider) => {
display: z.string()
})
)
.optional() // okta-specific
.optional()
}),
response: {
200: z.object({
schemas: z.array(z.string()),
id: z.string().trim(),
displayName: z.string().trim(),
members: z
.array(
z.object({
value: z.string(),
display: z.string()
})
)
.optional(),
meta: z.object({
resourceType: z.string().trim()
})
})
200: ScimGroupSchema
}
},
onRequest: verifyAuth([AuthMode.SCIM_TOKEN]),
@ -344,26 +407,12 @@ export const registerScimRouter = async (server: FastifyZodProvider) => {
querystring: z.object({
startIndex: z.coerce.number().default(1),
count: z.coerce.number().default(20),
filter: z.string().trim().optional()
filter: z.string().trim().optional(),
excludedAttributes: z.string().trim().optional()
}),
response: {
200: z.object({
Resources: z.array(
z.object({
schemas: z.array(z.string()),
id: z.string().trim(),
displayName: z.string().trim(),
members: z.array(
z.object({
value: z.string(),
display: z.string()
})
),
meta: z.object({
resourceType: z.string().trim()
})
})
),
Resources: z.array(ScimGroupSchema),
itemsPerPage: z.number(),
schemas: z.array(z.string()),
startIndex: z.number(),
@ -377,7 +426,8 @@ export const registerScimRouter = async (server: FastifyZodProvider) => {
orgId: req.permission.orgId,
startIndex: req.query.startIndex,
filter: req.query.filter,
limit: req.query.count
limit: req.query.count,
isMembersExcluded: req.query.excludedAttributes === "members"
});
return groups;
@ -392,20 +442,7 @@ export const registerScimRouter = async (server: FastifyZodProvider) => {
groupId: z.string().trim()
}),
response: {
200: z.object({
schemas: z.array(z.string()),
id: z.string().trim(),
displayName: z.string().trim(),
members: z.array(
z.object({
value: z.string(),
display: z.string()
})
),
meta: z.object({
resourceType: z.string().trim()
})
})
200: ScimGroupSchema
}
},
onRequest: verifyAuth([AuthMode.SCIM_TOKEN]),
@ -414,6 +451,7 @@ export const registerScimRouter = async (server: FastifyZodProvider) => {
groupId: req.params.groupId,
orgId: req.permission.orgId
});
return group;
}
});
@ -437,25 +475,12 @@ export const registerScimRouter = async (server: FastifyZodProvider) => {
)
}),
response: {
200: z.object({
schemas: z.array(z.string()),
id: z.string().trim(),
displayName: z.string().trim(),
members: z.array(
z.object({
value: z.string(),
display: z.string()
})
),
meta: z.object({
resourceType: z.string().trim()
})
})
200: ScimGroupSchema
}
},
onRequest: verifyAuth([AuthMode.SCIM_TOKEN]),
handler: async (req) => {
const group = await req.server.services.scim.updateScimGroupNamePut({
const group = await req.server.services.scim.replaceScimGroup({
groupId: req.params.groupId,
orgId: req.permission.orgId,
...req.body
@ -476,55 +501,35 @@ export const registerScimRouter = async (server: FastifyZodProvider) => {
schemas: z.array(z.string()),
Operations: z.array(
z.union([
z.object({
op: z.union([z.literal("replace"), z.literal("Replace")]),
value: z.object({
id: z.string().trim(),
displayName: z.string().trim()
})
}),
z.object({
op: z.union([z.literal("remove"), z.literal("Remove")]),
path: z.string().trim()
path: z.string().trim(),
value: z
.object({
value: z.string()
})
.array()
.optional()
}),
z.object({
op: z.union([z.literal("add"), z.literal("Add")]),
path: z.string().trim(),
value: z.array(
z.object({
value: z.string().trim(),
display: z.string().trim().optional()
})
)
op: z.union([z.literal("add"), z.literal("Add"), z.literal("replace"), z.literal("Replace")]),
path: z.string().trim().optional(),
value: z.any()
})
])
)
}),
response: {
200: z.object({
schemas: z.array(z.string()),
id: z.string().trim(),
displayName: z.string().trim(),
members: z.array(
z.object({
value: z.string(),
display: z.string()
})
),
meta: z.object({
resourceType: z.string().trim()
})
})
200: ScimGroupSchema
}
},
onRequest: verifyAuth([AuthMode.SCIM_TOKEN]),
handler: async (req) => {
const group = await req.server.services.scim.updateScimGroupNamePatch({
const group = await req.server.services.scim.updateScimGroup({
groupId: req.params.groupId,
orgId: req.permission.orgId,
operations: req.body.Operations
});
return group;
}
});
@ -550,60 +555,4 @@ export const registerScimRouter = async (server: FastifyZodProvider) => {
return group;
}
});
server.route({
url: "/Users/:orgMembershipId",
method: "PUT",
schema: {
params: z.object({
orgMembershipId: z.string().trim()
}),
body: z.object({
schemas: z.array(z.string()),
id: z.string().trim(),
userName: z.string().trim(),
name: z.object({
familyName: z.string().trim(),
givenName: z.string().trim()
}),
displayName: z.string().trim(),
active: z.boolean()
}),
response: {
200: z.object({
schemas: z.array(z.string()),
id: z.string().trim(),
userName: z.string().trim(),
name: z.object({
familyName: z.string().trim(),
givenName: z.string().trim()
}),
emails: z.array(
z.object({
primary: z.boolean(),
value: z.string().email(),
type: z.string().trim()
})
),
displayName: z.string().trim(),
active: z.boolean(),
groups: z.array(
z.object({
value: z.string().trim(),
display: z.string().trim()
})
)
})
}
},
onRequest: verifyAuth([AuthMode.SCIM_TOKEN]),
handler: async (req) => {
const user = await req.server.services.scim.replaceScimUser({
orgMembershipId: req.params.orgMembershipId,
orgId: req.permission.orgId,
active: req.body.active
});
return user;
}
});
};

View File

@ -44,7 +44,7 @@ export const accessApprovalPolicyServiceFactory = ({
secretPath,
actorAuthMethod,
approvals,
approverUserIds,
approvers,
projectSlug,
environment,
enforcementLevel
@ -52,7 +52,7 @@ export const accessApprovalPolicyServiceFactory = ({
const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId);
if (!project) throw new BadRequestError({ message: "Project not found" });
if (approvals > approverUserIds.length)
if (approvals > approvers.length)
throw new BadRequestError({ message: "Approvals cannot be greater than approvers" });
const { permission } = await permissionService.getProjectPermission(
@ -76,7 +76,7 @@ export const accessApprovalPolicyServiceFactory = ({
secretPath,
actorAuthMethod,
permissionService,
userIds: approverUserIds
userIds: approvers
});
const accessApproval = await accessApprovalPolicyDAL.transaction(async (tx) => {
@ -91,7 +91,7 @@ export const accessApprovalPolicyServiceFactory = ({
tx
);
await accessApprovalPolicyApproverDAL.insertMany(
approverUserIds.map((userId) => ({
approvers.map((userId) => ({
approverUserId: userId,
policyId: doc.id
})),
@ -128,7 +128,7 @@ export const accessApprovalPolicyServiceFactory = ({
const updateAccessApprovalPolicy = async ({
policyId,
approverUserIds,
approvers,
secretPath,
name,
actorId,
@ -161,7 +161,7 @@ export const accessApprovalPolicyServiceFactory = ({
},
tx
);
if (approverUserIds) {
if (approvers) {
await verifyApprovers({
projectId: accessApprovalPolicy.projectId,
orgId: actorOrgId,
@ -169,12 +169,12 @@ export const accessApprovalPolicyServiceFactory = ({
secretPath: doc.secretPath!,
actorAuthMethod,
permissionService,
userIds: approverUserIds
userIds: approvers
});
await accessApprovalPolicyApproverDAL.delete({ policyId: doc.id }, tx);
await accessApprovalPolicyApproverDAL.insertMany(
approverUserIds.map((userId) => ({
approvers.map((userId) => ({
approverUserId: userId,
policyId: doc.id
})),

View File

@ -17,7 +17,7 @@ export type TCreateAccessApprovalPolicy = {
approvals: number;
secretPath: string;
environment: string;
approverUserIds: string[];
approvers: string[];
projectSlug: string;
name: string;
enforcementLevel: EnforcementLevel;
@ -26,7 +26,7 @@ export type TCreateAccessApprovalPolicy = {
export type TUpdateAccessApprovalPolicy = {
policyId: string;
approvals?: number;
approverUserIds?: string[];
approvers?: string[];
secretPath?: string;
name?: string;
enforcementLevel?: EnforcementLevel;

View File

@ -5,6 +5,7 @@ import { TableName } from "@app/db/schemas";
import { DatabaseError } from "@app/lib/errors";
import { ormify, stripUndefinedInWhere } from "@app/lib/knex";
import { logger } from "@app/lib/logger";
import { QueueName } from "@app/queue";
export type TAuditLogDALFactory = ReturnType<typeof auditLogDALFactory>;
@ -62,7 +63,9 @@ export const auditLogDALFactory = (db: TDbClient) => {
const today = new Date();
let deletedAuditLogIds: { id: string }[] = [];
let numberOfRetryOnFailure = 0;
let isRetrying = false;
logger.info(`${QueueName.DailyResourceCleanUp}: audit log started`);
do {
try {
const findExpiredLogSubQuery = (tx || db)(TableName.AuditLog)
@ -84,7 +87,9 @@ export const auditLogDALFactory = (db: TDbClient) => {
setTimeout(resolve, 10); // time to breathe for db
});
}
} while (deletedAuditLogIds.length > 0 || numberOfRetryOnFailure < MAX_RETRY_ON_FAILURE);
isRetrying = numberOfRetryOnFailure > 0;
} while (deletedAuditLogIds.length > 0 || (isRetrying && numberOfRetryOnFailure < MAX_RETRY_ON_FAILURE));
logger.info(`${QueueName.DailyResourceCleanUp}: audit log completed`);
};
return { ...auditLogOrm, pruneAuditLog, find };

View File

@ -1,16 +1,17 @@
import * as x509 from "@peculiar/x509";
import { BadRequestError, NotFoundError, UnauthorizedError } from "@app/lib/errors";
import { isCertChainValid } from "@app/services/certificate/certificate-fns";
import { TCertificateAuthorityCertDALFactory } from "@app/services/certificate-authority/certificate-authority-cert-dal";
import { TCertificateAuthorityDALFactory } from "@app/services/certificate-authority/certificate-authority-dal";
import { getCaCertChain, getCaCertChains } from "@app/services/certificate-authority/certificate-authority-fns";
import { TCertificateAuthorityServiceFactory } from "@app/services/certificate-authority/certificate-authority-service";
import { TCertificateTemplateDALFactory } from "@app/services/certificate-template/certificate-template-dal";
import { TCertificateTemplateServiceFactory } from "@app/services/certificate-template/certificate-template-service";
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
import { TProjectDALFactory } from "@app/services/project/project-dal";
import { isCertChainValid } from "../certificate/certificate-fns";
import { TCertificateAuthorityCertDALFactory } from "../certificate-authority/certificate-authority-cert-dal";
import { TCertificateAuthorityDALFactory } from "../certificate-authority/certificate-authority-dal";
import { getCaCertChain, getCaCertChains } from "../certificate-authority/certificate-authority-fns";
import { TCertificateAuthorityServiceFactory } from "../certificate-authority/certificate-authority-service";
import { TCertificateTemplateDALFactory } from "../certificate-template/certificate-template-dal";
import { TCertificateTemplateServiceFactory } from "../certificate-template/certificate-template-service";
import { TKmsServiceFactory } from "../kms/kms-service";
import { TProjectDALFactory } from "../project/project-dal";
import { TLicenseServiceFactory } from "../license/license-service";
import { convertRawCertsToPkcs7 } from "./certificate-est-fns";
type TCertificateEstServiceFactoryDep = {
@ -21,6 +22,7 @@ type TCertificateEstServiceFactoryDep = {
certificateAuthorityCertDAL: Pick<TCertificateAuthorityCertDALFactory, "find" | "findById">;
projectDAL: Pick<TProjectDALFactory, "findOne" | "updateById" | "transaction">;
kmsService: Pick<TKmsServiceFactory, "decryptWithKmsKey" | "generateKmsKey">;
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
};
export type TCertificateEstServiceFactory = ReturnType<typeof certificateEstServiceFactory>;
@ -32,7 +34,8 @@ export const certificateEstServiceFactory = ({
certificateAuthorityCertDAL,
certificateAuthorityDAL,
projectDAL,
kmsService
kmsService,
licenseService
}: TCertificateEstServiceFactoryDep) => {
const simpleReenroll = async ({
csr,
@ -48,6 +51,14 @@ export const certificateEstServiceFactory = ({
certificateTemplateId
});
const plan = await licenseService.getPlan(estConfig.orgId);
if (!plan.pkiEst) {
throw new BadRequestError({
message:
"Failed to perform EST operation - simpleReenroll due to plan restriction. Upgrade to the Enterprise plan."
});
}
if (!estConfig.isEnabled) {
throw new BadRequestError({
message: "EST is disabled"
@ -146,6 +157,14 @@ export const certificateEstServiceFactory = ({
certificateTemplateId
});
const plan = await licenseService.getPlan(estConfig.orgId);
if (!plan.pkiEst) {
throw new BadRequestError({
message:
"Failed to perform EST operation - simpleEnroll due to plan restriction. Upgrade to the Enterprise plan."
});
}
if (!estConfig.isEnabled) {
throw new BadRequestError({
message: "EST is disabled"
@ -196,6 +215,24 @@ export const certificateEstServiceFactory = ({
});
}
const estConfig = await certificateTemplateService.getEstConfiguration({
isInternal: true,
certificateTemplateId
});
const plan = await licenseService.getPlan(estConfig.orgId);
if (!plan.pkiEst) {
throw new BadRequestError({
message: "Failed to perform EST operation - caCerts due to plan restriction. Upgrade to the Enterprise plan."
});
}
if (!estConfig.isEnabled) {
throw new BadRequestError({
message: "EST is disabled"
});
}
const ca = await certificateAuthorityDAL.findById(certTemplate.caId);
if (!ca) {
throw new NotFoundError({

View File

@ -0,0 +1,226 @@
import {
CreateUserCommand,
CreateUserGroupCommand,
DeleteUserCommand,
DescribeReplicationGroupsCommand,
DescribeUserGroupsCommand,
ElastiCache,
ModifyReplicationGroupCommand,
ModifyUserGroupCommand
} from "@aws-sdk/client-elasticache";
import handlebars from "handlebars";
import { customAlphabet } from "nanoid";
import { z } from "zod";
import { BadRequestError } from "@app/lib/errors";
import { DynamicSecretAwsElastiCacheSchema, TDynamicProviderFns } from "./models";
const CreateElastiCacheUserSchema = z.object({
UserId: z.string().trim().min(1),
UserName: z.string().trim().min(1),
Engine: z.string().default("redis"),
Passwords: z.array(z.string().trim().min(1)).min(1).max(1), // Minimum password length is 16 characters, required by AWS.
AccessString: z.string().trim().min(1) // Example: "on ~* +@all"
});
const DeleteElasticCacheUserSchema = z.object({
UserId: z.string().trim().min(1)
});
type TElastiCacheRedisUser = { userId: string; password: string };
type TBasicAWSCredentials = { accessKeyId: string; secretAccessKey: string };
type TCreateElastiCacheUserInput = z.infer<typeof CreateElastiCacheUserSchema>;
type TDeleteElastiCacheUserInput = z.infer<typeof DeleteElasticCacheUserSchema>;
const ElastiCacheUserManager = (credentials: TBasicAWSCredentials, region: string) => {
const elastiCache = new ElastiCache({
region,
credentials
});
const infisicalGroup = "infisical-managed-group-elasticache";
const ensureInfisicalGroupExists = async (clusterName: string) => {
const replicationGroups = await elastiCache.send(new DescribeUserGroupsCommand());
const existingGroup = replicationGroups.UserGroups?.find((group) => group.UserGroupId === infisicalGroup);
let newlyCreatedGroup = false;
if (!existingGroup) {
const createGroupCommand = new CreateUserGroupCommand({
UserGroupId: infisicalGroup,
UserIds: ["default"],
Engine: "redis"
});
await elastiCache.send(createGroupCommand);
newlyCreatedGroup = true;
}
if (existingGroup || newlyCreatedGroup) {
const replicationGroup = (
await elastiCache.send(
new DescribeReplicationGroupsCommand({
ReplicationGroupId: clusterName
})
)
).ReplicationGroups?.[0];
if (!replicationGroup?.UserGroupIds?.includes(infisicalGroup)) {
// If the replication group doesn't have the infisical user group, we need to associate it
const modifyGroupCommand = new ModifyReplicationGroupCommand({
UserGroupIdsToAdd: [infisicalGroup],
UserGroupIdsToRemove: [],
ApplyImmediately: true,
ReplicationGroupId: clusterName
});
await elastiCache.send(modifyGroupCommand);
}
}
};
const addUserToInfisicalGroup = async (userId: string) => {
// figure out if the default user is already in the group, if it is, then we shouldn't add it again
const addUserToGroupCommand = new ModifyUserGroupCommand({
UserGroupId: infisicalGroup,
UserIdsToAdd: [userId],
UserIdsToRemove: []
});
await elastiCache.send(addUserToGroupCommand);
};
const createUser = async (creationInput: TCreateElastiCacheUserInput, clusterName: string) => {
await ensureInfisicalGroupExists(clusterName);
await elastiCache.send(new CreateUserCommand(creationInput)); // First create the user
await addUserToInfisicalGroup(creationInput.UserId); // Then add the user to the group. We know the group is already a part of the cluster because of ensureInfisicalGroupExists()
return {
userId: creationInput.UserId,
password: creationInput.Passwords[0]
};
};
const deleteUser = async (
deletionInput: TDeleteElastiCacheUserInput
): Promise<Pick<TElastiCacheRedisUser, "userId">> => {
await elastiCache.send(new DeleteUserCommand(deletionInput));
return { userId: deletionInput.UserId };
};
const verifyCredentials = async (clusterName: string) => {
await elastiCache.send(
new DescribeReplicationGroupsCommand({
ReplicationGroupId: clusterName
})
);
};
return {
createUser,
deleteUser,
verifyCredentials
};
};
const generatePassword = () => {
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_.~!*$#";
return customAlphabet(charset, 64)();
};
const generateUsername = () => {
const charset = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-";
return `inf-${customAlphabet(charset, 32)()}`; // Username must start with an ascii letter, so we prepend the username with "inf-"
};
export const AwsElastiCacheDatabaseProvider = (): TDynamicProviderFns => {
const validateProviderInputs = async (inputs: unknown) => {
const providerInputs = DynamicSecretAwsElastiCacheSchema.parse(inputs);
// We need to ensure the that the creation & revocation statements are valid and can be used to create and revoke users.
// We can't return the parsed statements here because we need to use the handlebars template to generate the username and password, before we can use the parsed statements.
CreateElastiCacheUserSchema.parse(JSON.parse(providerInputs.creationStatement));
DeleteElasticCacheUserSchema.parse(JSON.parse(providerInputs.revocationStatement));
return providerInputs;
};
const validateConnection = async (inputs: unknown) => {
const providerInputs = await validateProviderInputs(inputs);
await ElastiCacheUserManager(
{
accessKeyId: providerInputs.accessKeyId,
secretAccessKey: providerInputs.secretAccessKey
},
providerInputs.region
).verifyCredentials(providerInputs.clusterName);
return true;
};
const create = async (inputs: unknown, expireAt: number) => {
const providerInputs = await validateProviderInputs(inputs);
if (!(await validateConnection(providerInputs))) {
throw new BadRequestError({ message: "Failed to establish connection" });
}
const leaseUsername = generateUsername();
const leasePassword = generatePassword();
const leaseExpiration = new Date(expireAt).toISOString();
const creationStatement = handlebars.compile(providerInputs.creationStatement, { noEscape: true })({
username: leaseUsername,
password: leasePassword,
expiration: leaseExpiration
});
const parsedStatement = CreateElastiCacheUserSchema.parse(JSON.parse(creationStatement));
await ElastiCacheUserManager(
{
accessKeyId: providerInputs.accessKeyId,
secretAccessKey: providerInputs.secretAccessKey
},
providerInputs.region
).createUser(parsedStatement, providerInputs.clusterName);
return {
entityId: leaseUsername,
data: {
DB_USERNAME: leaseUsername,
DB_PASSWORD: leasePassword
}
};
};
const revoke = async (inputs: unknown, entityId: string) => {
const providerInputs = await validateProviderInputs(inputs);
const revokeStatement = handlebars.compile(providerInputs.revocationStatement)({ username: entityId });
const parsedStatement = DeleteElasticCacheUserSchema.parse(JSON.parse(revokeStatement));
await ElastiCacheUserManager(
{
accessKeyId: providerInputs.accessKeyId,
secretAccessKey: providerInputs.secretAccessKey
},
providerInputs.region
).deleteUser(parsedStatement);
return { entityId };
};
const renew = async (inputs: unknown, entityId: string) => {
// Do nothing
return { entityId };
};
return {
validateProviderInputs,
validateConnection,
create,
revoke,
renew
};
};

View File

@ -0,0 +1,126 @@
import { Client as ElasticSearchClient } from "@elastic/elasticsearch";
import { customAlphabet } from "nanoid";
import { z } from "zod";
import { getConfig } from "@app/lib/config/env";
import { BadRequestError } from "@app/lib/errors";
import { alphaNumericNanoId } from "@app/lib/nanoid";
import { DynamicSecretElasticSearchSchema, ElasticSearchAuthTypes, TDynamicProviderFns } from "./models";
const generatePassword = () => {
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_.~!*$#";
return customAlphabet(charset, 64)();
};
const generateUsername = () => {
return alphaNumericNanoId(32);
};
export const ElasticSearchDatabaseProvider = (): TDynamicProviderFns => {
const validateProviderInputs = async (inputs: unknown) => {
const appCfg = getConfig();
const isCloud = Boolean(appCfg.LICENSE_SERVER_KEY); // quick and dirty way to check if its cloud or not
const providerInputs = await DynamicSecretElasticSearchSchema.parseAsync(inputs);
if (
isCloud &&
// localhost
// internal ips
(providerInputs.host === "host.docker.internal" ||
providerInputs.host.match(/^10\.\d+\.\d+\.\d+/) ||
providerInputs.host.match(/^192\.168\.\d+\.\d+/))
) {
throw new BadRequestError({ message: "Invalid db host" });
}
if (providerInputs.host === "localhost" || providerInputs.host === "127.0.0.1") {
throw new BadRequestError({ message: "Invalid db host" });
}
return providerInputs;
};
const getClient = async (providerInputs: z.infer<typeof DynamicSecretElasticSearchSchema>) => {
const connection = new ElasticSearchClient({
node: {
url: new URL(`${providerInputs.host}:${providerInputs.port}`),
...(providerInputs.ca && {
ssl: {
rejectUnauthorized: false,
ca: providerInputs.ca
}
})
},
auth: {
...(providerInputs.auth.type === ElasticSearchAuthTypes.ApiKey
? {
apiKey: {
api_key: providerInputs.auth.apiKey,
id: providerInputs.auth.apiKeyId
}
}
: {
username: providerInputs.auth.username,
password: providerInputs.auth.password
})
}
});
return connection;
};
const validateConnection = async (inputs: unknown) => {
const providerInputs = await validateProviderInputs(inputs);
const connection = await getClient(providerInputs);
const infoResponse = await connection
.info()
.then(() => true)
.catch(() => false);
return infoResponse;
};
const create = async (inputs: unknown) => {
const providerInputs = await validateProviderInputs(inputs);
const connection = await getClient(providerInputs);
const username = generateUsername();
const password = generatePassword();
await connection.security.putUser({
username,
password,
full_name: "Managed by Infisical.com",
roles: providerInputs.roles
});
await connection.close();
return { entityId: username, data: { DB_USERNAME: username, DB_PASSWORD: password } };
};
const revoke = async (inputs: unknown, entityId: string) => {
const providerInputs = await validateProviderInputs(inputs);
const connection = await getClient(providerInputs);
await connection.security.deleteUser({
username: entityId
});
await connection.close();
return { entityId };
};
const renew = async (inputs: unknown, entityId: string) => {
// Do nothing
return { entityId };
};
return {
validateProviderInputs,
validateConnection,
create,
revoke,
renew
};
};

View File

@ -1,10 +1,18 @@
import { AwsElastiCacheDatabaseProvider } from "./aws-elasticache";
import { AwsIamProvider } from "./aws-iam";
import { CassandraProvider } from "./cassandra";
import { ElasticSearchDatabaseProvider } from "./elastic-search";
import { DynamicSecretProviders } from "./models";
import { MongoAtlasProvider } from "./mongo-atlas";
import { RedisDatabaseProvider } from "./redis";
import { SqlDatabaseProvider } from "./sql-database";
export const buildDynamicSecretProviders = () => ({
[DynamicSecretProviders.SqlDatabase]: SqlDatabaseProvider(),
[DynamicSecretProviders.Cassandra]: CassandraProvider(),
[DynamicSecretProviders.AwsIam]: AwsIamProvider()
[DynamicSecretProviders.AwsIam]: AwsIamProvider(),
[DynamicSecretProviders.Redis]: RedisDatabaseProvider(),
[DynamicSecretProviders.AwsElastiCache]: AwsElastiCacheDatabaseProvider(),
[DynamicSecretProviders.MongoAtlas]: MongoAtlasProvider(),
[DynamicSecretProviders.ElasticSearch]: ElasticSearchDatabaseProvider()
});

View File

@ -7,6 +7,56 @@ export enum SqlProviders {
MsSQL = "mssql"
}
export enum ElasticSearchAuthTypes {
User = "user",
ApiKey = "api-key"
}
export const DynamicSecretRedisDBSchema = z.object({
host: z.string().trim().toLowerCase(),
port: z.number(),
username: z.string().trim(), // this is often "default".
password: z.string().trim().optional(),
creationStatement: z.string().trim(),
revocationStatement: z.string().trim(),
renewStatement: z.string().trim().optional(),
ca: z.string().optional()
});
export const DynamicSecretAwsElastiCacheSchema = z.object({
clusterName: z.string().trim().min(1),
accessKeyId: z.string().trim().min(1),
secretAccessKey: z.string().trim().min(1),
region: z.string().trim(),
creationStatement: z.string().trim(),
revocationStatement: z.string().trim(),
ca: z.string().optional()
});
export const DynamicSecretElasticSearchSchema = z.object({
host: z.string().trim().min(1),
port: z.number(),
roles: z.array(z.string().trim().min(1)).min(1),
// two auth types "user, apikey"
auth: z.discriminatedUnion("type", [
z.object({
type: z.literal(ElasticSearchAuthTypes.User),
username: z.string().trim(),
password: z.string().trim()
}),
z.object({
type: z.literal(ElasticSearchAuthTypes.ApiKey),
apiKey: z.string().trim(),
apiKeyId: z.string().trim()
})
]),
ca: z.string().optional()
});
export const DynamicSecretSqlDBSchema = z.object({
client: z.nativeEnum(SqlProviders),
host: z.string().trim().toLowerCase(),
@ -44,16 +94,61 @@ export const DynamicSecretAwsIamSchema = z.object({
policyArns: z.string().trim().optional()
});
export const DynamicSecretMongoAtlasSchema = z.object({
adminPublicKey: z.string().trim().min(1).describe("Admin user public api key"),
adminPrivateKey: z.string().trim().min(1).describe("Admin user private api key"),
groupId: z
.string()
.trim()
.min(1)
.describe("Unique 24-hexadecimal digit string that identifies your project. This is same as project id"),
roles: z
.object({
collectionName: z.string().optional().describe("Collection on which this role applies."),
databaseName: z.string().min(1).describe("Database to which the user is granted access privileges."),
roleName: z
.string()
.min(1)
.describe(
' Enum: "atlasAdmin" "backup" "clusterMonitor" "dbAdmin" "dbAdminAnyDatabase" "enableSharding" "read" "readAnyDatabase" "readWrite" "readWriteAnyDatabase" "<a custom role name>".Human-readable label that identifies a group of privileges assigned to a database user. This value can either be a built-in role or a custom role.'
)
})
.array()
.min(1),
scopes: z
.object({
name: z
.string()
.min(1)
.describe(
"Human-readable label that identifies the cluster or MongoDB Atlas Data Lake that this database user can access."
),
type: z
.string()
.min(1)
.describe("Category of resource that this database user can access. Enum: CLUSTER, DATA_LAKE, STREAM")
})
.array()
});
export enum DynamicSecretProviders {
SqlDatabase = "sql-database",
Cassandra = "cassandra",
AwsIam = "aws-iam"
AwsIam = "aws-iam",
Redis = "redis",
AwsElastiCache = "aws-elasticache",
MongoAtlas = "mongo-db-atlas",
ElasticSearch = "elastic-search"
}
export const DynamicSecretProviderSchema = z.discriminatedUnion("type", [
z.object({ type: z.literal(DynamicSecretProviders.SqlDatabase), inputs: DynamicSecretSqlDBSchema }),
z.object({ type: z.literal(DynamicSecretProviders.Cassandra), inputs: DynamicSecretCassandraSchema }),
z.object({ type: z.literal(DynamicSecretProviders.AwsIam), inputs: DynamicSecretAwsIamSchema })
z.object({ type: z.literal(DynamicSecretProviders.AwsIam), inputs: DynamicSecretAwsIamSchema }),
z.object({ type: z.literal(DynamicSecretProviders.Redis), inputs: DynamicSecretRedisDBSchema }),
z.object({ type: z.literal(DynamicSecretProviders.AwsElastiCache), inputs: DynamicSecretAwsElastiCacheSchema }),
z.object({ type: z.literal(DynamicSecretProviders.MongoAtlas), inputs: DynamicSecretMongoAtlasSchema }),
z.object({ type: z.literal(DynamicSecretProviders.ElasticSearch), inputs: DynamicSecretElasticSearchSchema })
]);
export type TDynamicProviderFns = {

View File

@ -0,0 +1,146 @@
import axios, { AxiosError } from "axios";
import { customAlphabet } from "nanoid";
import { z } from "zod";
import { createDigestAuthRequestInterceptor } from "@app/lib/axios/digest-auth";
import { alphaNumericNanoId } from "@app/lib/nanoid";
import { DynamicSecretMongoAtlasSchema, TDynamicProviderFns } from "./models";
const generatePassword = (size = 48) => {
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_.~!*$#";
return customAlphabet(charset, 48)(size);
};
const generateUsername = () => {
return alphaNumericNanoId(32);
};
export const MongoAtlasProvider = (): TDynamicProviderFns => {
const validateProviderInputs = async (inputs: unknown) => {
const providerInputs = await DynamicSecretMongoAtlasSchema.parseAsync(inputs);
return providerInputs;
};
const getClient = async (providerInputs: z.infer<typeof DynamicSecretMongoAtlasSchema>) => {
const client = axios.create({
baseURL: "https://cloud.mongodb.com/api/atlas",
headers: {
Accept: "application/vnd.atlas.2023-02-01+json",
"Content-Type": "application/json"
}
});
const digestAuth = createDigestAuthRequestInterceptor(
client,
providerInputs.adminPublicKey,
providerInputs.adminPrivateKey
);
return digestAuth;
};
const validateConnection = async (inputs: unknown) => {
const providerInputs = await validateProviderInputs(inputs);
const client = await getClient(providerInputs);
const isConnected = await client({
method: "GET",
url: `v2/groups/${providerInputs.groupId}/databaseUsers`,
params: { itemsPerPage: 1 }
})
.then(() => true)
.catch((error) => {
if ((error as AxiosError).response) {
throw new Error(JSON.stringify((error as AxiosError).response?.data));
}
throw error;
});
return isConnected;
};
const create = async (inputs: unknown, expireAt: number) => {
const providerInputs = await validateProviderInputs(inputs);
const client = await getClient(providerInputs);
const username = generateUsername();
const password = generatePassword();
const expiration = new Date(expireAt).toISOString();
await client({
method: "POST",
url: `/v2/groups/${providerInputs.groupId}/databaseUsers`,
data: {
roles: providerInputs.roles,
scopes: providerInputs.scopes,
deleteAfterDate: expiration,
username,
password,
databaseName: "admin",
groupId: providerInputs.groupId
}
}).catch((error) => {
if ((error as AxiosError).response) {
throw new Error(JSON.stringify((error as AxiosError).response?.data));
}
throw error;
});
return { entityId: username, data: { DB_USERNAME: username, DB_PASSWORD: password } };
};
const revoke = async (inputs: unknown, entityId: string) => {
const providerInputs = await validateProviderInputs(inputs);
const client = await getClient(providerInputs);
const username = entityId;
const isExisting = await client({
method: "GET",
url: `/v2/groups/${providerInputs.groupId}/databaseUsers/admin/${username}`
}).catch((err) => {
if ((err as AxiosError).response?.status === 404) return false;
throw err;
});
if (isExisting) {
await client({
method: "DELETE",
url: `/v2/groups/${providerInputs.groupId}/databaseUsers/admin/${username}`
}).catch((error) => {
if ((error as AxiosError).response) {
throw new Error(JSON.stringify((error as AxiosError).response?.data));
}
throw error;
});
}
return { entityId: username };
};
const renew = async (inputs: unknown, entityId: string, expireAt: number) => {
const providerInputs = await validateProviderInputs(inputs);
const client = await getClient(providerInputs);
const username = entityId;
const expiration = new Date(expireAt).toISOString();
await client({
method: "PATCH",
url: `/v2/groups/${providerInputs.groupId}/databaseUsers/admin/${username}`,
data: {
deleteAfterDate: expiration,
databaseName: "admin",
groupId: providerInputs.groupId
}
}).catch((error) => {
if ((error as AxiosError).response) {
throw new Error(JSON.stringify((error as AxiosError).response?.data));
}
throw error;
});
return { entityId: username };
};
return {
validateProviderInputs,
validateConnection,
create,
revoke,
renew
};
};

View File

@ -0,0 +1,182 @@
import handlebars from "handlebars";
import { Redis } from "ioredis";
import { customAlphabet } from "nanoid";
import { z } from "zod";
import { getConfig } from "@app/lib/config/env";
import { BadRequestError } from "@app/lib/errors";
import { getDbConnectionHost } from "@app/lib/knex";
import { alphaNumericNanoId } from "@app/lib/nanoid";
import { DynamicSecretRedisDBSchema, TDynamicProviderFns } from "./models";
const generatePassword = () => {
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_.~!*$#";
return customAlphabet(charset, 64)();
};
const generateUsername = () => {
return alphaNumericNanoId(32);
};
const executeTransactions = async (connection: Redis, commands: string[]): Promise<(string | null)[] | null> => {
// Initiate a transaction
const pipeline = connection.multi();
// Add all commands to the pipeline
for (const command of commands) {
const args = command
.split(" ")
.map((arg) => arg.trim())
.filter((arg) => arg.length > 0);
pipeline.call(args[0], ...args.slice(1));
}
// Execute the transaction
const results = await pipeline.exec();
if (!results) {
throw new BadRequestError({ message: "Redis transaction failed: No results returned" });
}
// Check for errors in the results
const errors = results.filter(([err]) => err !== null);
if (errors.length > 0) {
throw new BadRequestError({ message: "Redis transaction failed with errors" });
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
return results.map(([_, result]) => result as string | null);
};
export const RedisDatabaseProvider = (): TDynamicProviderFns => {
const validateProviderInputs = async (inputs: unknown) => {
const appCfg = getConfig();
const isCloud = Boolean(appCfg.LICENSE_SERVER_KEY); // quick and dirty way to check if its cloud or not
const dbHost = appCfg.DB_HOST || getDbConnectionHost(appCfg.DB_CONNECTION_URI);
const providerInputs = await DynamicSecretRedisDBSchema.parseAsync(inputs);
if (
isCloud &&
// localhost
// internal ips
(providerInputs.host === "host.docker.internal" ||
providerInputs.host.match(/^10\.\d+\.\d+\.\d+/) ||
providerInputs.host.match(/^192\.168\.\d+\.\d+/))
)
throw new BadRequestError({ message: "Invalid db host" });
if (providerInputs.host === "localhost" || providerInputs.host === "127.0.0.1" || dbHost === providerInputs.host)
throw new BadRequestError({ message: "Invalid db host" });
return providerInputs;
};
const getClient = async (providerInputs: z.infer<typeof DynamicSecretRedisDBSchema>) => {
let connection: Redis | null = null;
try {
connection = new Redis({
username: providerInputs.username,
host: providerInputs.host,
port: providerInputs.port,
password: providerInputs.password,
...(providerInputs.ca && {
tls: {
rejectUnauthorized: false,
ca: providerInputs.ca
}
})
});
let result: string;
if (providerInputs.password) {
result = await connection.auth(providerInputs.username, providerInputs.password, () => {});
} else {
result = await connection.auth(providerInputs.username, () => {});
}
if (result !== "OK") {
throw new BadRequestError({ message: `Invalid credentials, Redis returned ${result} status` });
}
return connection;
} catch (err) {
if (connection) await connection.quit();
throw err;
}
};
const validateConnection = async (inputs: unknown) => {
const providerInputs = await validateProviderInputs(inputs);
const connection = await getClient(providerInputs);
const pingResponse = await connection
.ping()
.then(() => true)
.catch(() => false);
return pingResponse;
};
const create = async (inputs: unknown, expireAt: number) => {
const providerInputs = await validateProviderInputs(inputs);
const connection = await getClient(providerInputs);
const username = generateUsername();
const password = generatePassword();
const expiration = new Date(expireAt).toISOString();
const creationStatement = handlebars.compile(providerInputs.creationStatement, { noEscape: true })({
username,
password,
expiration
});
const queries = creationStatement.toString().split(";").filter(Boolean);
await executeTransactions(connection, queries);
await connection.quit();
return { entityId: username, data: { DB_USERNAME: username, DB_PASSWORD: password } };
};
const revoke = async (inputs: unknown, entityId: string) => {
const providerInputs = await validateProviderInputs(inputs);
const connection = await getClient(providerInputs);
const username = entityId;
const revokeStatement = handlebars.compile(providerInputs.revocationStatement)({ username });
const queries = revokeStatement.toString().split(";").filter(Boolean);
await executeTransactions(connection, queries);
await connection.quit();
return { entityId: username };
};
const renew = async (inputs: unknown, entityId: string, expireAt: number) => {
const providerInputs = await validateProviderInputs(inputs);
const connection = await getClient(providerInputs);
const username = entityId;
const expiration = new Date(expireAt).toISOString();
const renewStatement = handlebars.compile(providerInputs.renewStatement)({ username, expiration });
if (renewStatement) {
const queries = renewStatement.toString().split(";").filter(Boolean);
await executeTransactions(connection, queries);
}
await connection.quit();
return { entityId: username };
};
return {
validateProviderInputs,
validateConnection,
create,
revoke,
renew
};
};

View File

@ -26,8 +26,10 @@ export const getDefaultOnPremFeatures = () => {
status: null,
trial_end: null,
has_used_trial: true,
secretApproval: false,
secretApproval: true,
secretRotation: true,
caCrl: false
};
};
export const setupLicenseRequestWithStore = () => {};

View File

@ -45,18 +45,19 @@ export const getDefaultOnPremFeatures = (): TFeatureSet => ({
readLimit: 60,
writeLimit: 200,
secretsLimit: 40
}
},
pkiEst: false
});
export const setupLicenceRequestWithStore = (baseURL: string, refreshUrl: string, licenseKey: string) => {
export const setupLicenseRequestWithStore = (baseURL: string, refreshUrl: string, licenseKey: string) => {
let token: string;
const licenceReq = axios.create({
const licenseReq = axios.create({
baseURL,
timeout: 35 * 1000
// signal: AbortSignal.timeout(60 * 1000)
});
const refreshLicence = async () => {
const refreshLicense = async () => {
const appCfg = getConfig();
const {
data: { token: authToken }
@ -74,7 +75,7 @@ export const setupLicenceRequestWithStore = (baseURL: string, refreshUrl: string
return token;
};
licenceReq.interceptors.request.use(
licenseReq.interceptors.request.use(
(config) => {
if (token && config.headers) {
// eslint-disable-next-line no-param-reassign
@ -85,7 +86,7 @@ export const setupLicenceRequestWithStore = (baseURL: string, refreshUrl: string
(err) => Promise.reject(err)
);
licenceReq.interceptors.response.use(
licenseReq.interceptors.response.use(
(response) => response,
async (err) => {
const originalRequest = (err as AxiosError).config;
@ -96,15 +97,15 @@ export const setupLicenceRequestWithStore = (baseURL: string, refreshUrl: string
(originalRequest as any)._retry = true; // injected
// refresh
await refreshLicence();
await refreshLicense();
licenceReq.defaults.headers.common.Authorization = `Bearer ${token}`;
return licenceReq(originalRequest!);
licenseReq.defaults.headers.common.Authorization = `Bearer ${token}`;
return licenseReq(originalRequest!);
}
return Promise.reject(err);
}
);
return { request: licenceReq, refreshLicence };
return { request: licenseReq, refreshLicense };
};

View File

@ -16,8 +16,8 @@ import { TOrgDALFactory } from "@app/services/org/org-dal";
import { OrgPermissionActions, OrgPermissionSubjects } from "../permission/org-permission";
import { TPermissionServiceFactory } from "../permission/permission-service";
import { getDefaultOnPremFeatures, setupLicenceRequestWithStore } from "./licence-fns";
import { TLicenseDALFactory } from "./license-dal";
import { getDefaultOnPremFeatures, setupLicenseRequestWithStore } from "./license-fns";
import {
InstanceType,
TAddOrgPmtMethodDTO,
@ -64,13 +64,13 @@ export const licenseServiceFactory = ({
let onPremFeatures: TFeatureSet = getDefaultOnPremFeatures();
const appCfg = getConfig();
const licenseServerCloudApi = setupLicenceRequestWithStore(
const licenseServerCloudApi = setupLicenseRequestWithStore(
appCfg.LICENSE_SERVER_URL || "",
LICENSE_SERVER_CLOUD_LOGIN,
appCfg.LICENSE_SERVER_KEY || ""
);
const licenseServerOnPremApi = setupLicenceRequestWithStore(
const licenseServerOnPremApi = setupLicenseRequestWithStore(
appCfg.LICENSE_SERVER_URL || "",
LICENSE_SERVER_ON_PREM_LOGIN,
appCfg.LICENSE_KEY || ""
@ -79,7 +79,7 @@ export const licenseServiceFactory = ({
const init = async () => {
try {
if (appCfg.LICENSE_SERVER_KEY) {
const token = await licenseServerCloudApi.refreshLicence();
const token = await licenseServerCloudApi.refreshLicense();
if (token) instanceType = InstanceType.Cloud;
logger.info(`Instance type: ${InstanceType.Cloud}`);
isValidLicense = true;
@ -87,7 +87,7 @@ export const licenseServiceFactory = ({
}
if (appCfg.LICENSE_KEY) {
const token = await licenseServerOnPremApi.refreshLicence();
const token = await licenseServerOnPremApi.refreshLicense();
if (token) {
const {
data: { currentPlan }

View File

@ -63,6 +63,7 @@ export type TFeatureSet = {
writeLimit: number;
secretsLimit: number;
};
pkiEst: boolean;
};
export type TOrgPlansTableDTO = {

View File

@ -44,19 +44,18 @@ export const buildScimUser = ({
email,
firstName,
lastName,
groups = [],
active
active,
createdAt,
updatedAt
}: {
orgMembershipId: string;
username: string;
email?: string | null;
firstName: string | null | undefined;
lastName: string | null | undefined;
groups?: {
value: string;
display: string;
}[];
active: boolean;
createdAt: Date;
updatedAt: Date;
}): TScimUser => {
const scimUser = {
schemas: ["urn:ietf:params:scim:schemas:core:2.0:User"],
@ -78,10 +77,10 @@ export const buildScimUser = ({
]
: [],
active,
groups,
meta: {
resourceType: "User",
location: null
created: createdAt,
lastModified: updatedAt
}
};
@ -109,14 +108,18 @@ export const buildScimGroupList = ({
export const buildScimGroup = ({
groupId,
name,
members
members,
updatedAt,
createdAt
}: {
groupId: string;
name: string;
members: {
value: string;
display: string;
display?: string;
}[];
createdAt: Date;
updatedAt: Date;
}): TScimGroup => {
const scimGroup = {
schemas: ["urn:ietf:params:scim:schemas:core:2.0:Group"],
@ -125,7 +128,8 @@ export const buildScimGroup = ({
members,
meta: {
resourceType: "Group",
location: null
created: createdAt,
lastModified: updatedAt
}
};

View File

@ -1,6 +1,7 @@
import { ForbiddenError } from "@casl/ability";
import slugify from "@sindresorhus/slugify";
import jwt from "jsonwebtoken";
import { scimPatch } from "scim-patch";
import { OrgMembershipRole, OrgMembershipStatus, TableName, TOrgMemberships, TUsers } from "@app/db/schemas";
import { TGroupDALFactory } from "@app/ee/services/group/group-dal";
@ -9,7 +10,6 @@ import { TUserGroupMembershipDALFactory } from "@app/ee/services/group/user-grou
import { TScimDALFactory } from "@app/ee/services/scim/scim-dal";
import { getConfig } from "@app/lib/config/env";
import { BadRequestError, ScimRequestError, UnauthorizedError } from "@app/lib/errors";
import { logger } from "@app/lib/logger";
import { alphaNumericNanoId } from "@app/lib/nanoid";
import { TOrgPermission } from "@app/lib/types";
import { AuthTokenType } from "@app/services/auth/auth-type";
@ -32,14 +32,7 @@ import { TLicenseServiceFactory } from "../license/license-service";
import { OrgPermissionActions, OrgPermissionSubjects } from "../permission/org-permission";
import { TPermissionServiceFactory } from "../permission/permission-service";
import { TProjectUserAdditionalPrivilegeDALFactory } from "../project-user-additional-privilege/project-user-additional-privilege-dal";
import {
buildScimGroup,
buildScimGroupList,
buildScimUser,
buildScimUserList,
extractScimValueFromPath,
parseScimFilter
} from "./scim-fns";
import { buildScimGroup, buildScimGroupList, buildScimUser, buildScimUserList, parseScimFilter } from "./scim-fns";
import {
TCreateScimGroupDTO,
TCreateScimTokenDTO,
@ -64,12 +57,18 @@ type TScimServiceFactoryDep = {
scimDAL: Pick<TScimDALFactory, "create" | "find" | "findById" | "deleteById">;
userDAL: Pick<
TUserDALFactory,
"find" | "findOne" | "create" | "transaction" | "findUserEncKeyByUserIdsBatch" | "findById"
"find" | "findOne" | "create" | "transaction" | "findUserEncKeyByUserIdsBatch" | "findById" | "updateById"
>;
userAliasDAL: Pick<TUserAliasDALFactory, "findOne" | "create" | "delete">;
userAliasDAL: Pick<TUserAliasDALFactory, "findOne" | "create" | "delete" | "update">;
orgDAL: Pick<
TOrgDALFactory,
"createMembership" | "findById" | "findMembership" | "deleteMembershipById" | "transaction" | "updateMembershipById"
| "createMembership"
| "findById"
| "findMembership"
| "findMembershipWithScimFilter"
| "deleteMembershipById"
| "transaction"
| "updateMembershipById"
>;
orgMembershipDAL: Pick<TOrgMembershipDALFactory, "find" | "findOne" | "create" | "updateById" | "findById">;
projectDAL: Pick<TProjectDALFactory, "find" | "findProjectGhostUser">;
@ -193,7 +192,12 @@ export const scimServiceFactory = ({
};
// SCIM server endpoints
const listScimUsers = async ({ startIndex, limit, filter, orgId }: TListScimUsersDTO): Promise<TListScimUsers> => {
const listScimUsers = async ({
startIndex = 0,
limit = 100,
filter,
orgId
}: TListScimUsersDTO): Promise<TListScimUsers> => {
const org = await orgDAL.findById(orgId);
if (!org.scimEnabled)
@ -207,23 +211,20 @@ export const scimServiceFactory = ({
...(limit && { limit })
};
const users = await orgDAL.findMembership(
{
[`${TableName.OrgMembership}.orgId` as "id"]: orgId,
...parseScimFilter(filter)
},
findOpts
);
const users = await orgDAL.findMembershipWithScimFilter(orgId, filter, findOpts);
const scimUsers = users.map(({ id, externalId, username, firstName, lastName, email, isActive }) =>
buildScimUser({
orgMembershipId: id ?? "",
username: externalId ?? username,
firstName: firstName ?? "",
lastName: lastName ?? "",
email,
active: isActive
})
const scimUsers = users.map(
({ id, externalId, username, firstName, lastName, email, isActive, createdAt, updatedAt }) =>
buildScimUser({
orgMembershipId: id ?? "",
username: externalId ?? username,
firstName: firstName ?? "",
lastName: lastName ?? "",
email,
active: isActive,
createdAt,
updatedAt
})
);
return buildScimUserList({
@ -258,11 +259,6 @@ export const scimServiceFactory = ({
status: 403
});
const groupMembershipsInOrg = await userGroupMembershipDAL.findGroupMembershipsByUserIdInOrg(
membership.userId,
orgId
);
return buildScimUser({
orgMembershipId: membership.id,
username: membership.externalId ?? membership.username,
@ -270,10 +266,8 @@ export const scimServiceFactory = ({
firstName: membership.firstName,
lastName: membership.lastName,
active: membership.isActive,
groups: groupMembershipsInOrg.map((group) => ({
value: group.groupId,
display: group.groupName
}))
createdAt: membership.createdAt,
updatedAt: membership.updatedAt
});
};
@ -322,7 +316,7 @@ export const scimServiceFactory = ({
userId: userAlias.userId,
inviteEmail: email,
orgId,
role: OrgMembershipRole.Member,
role: OrgMembershipRole.NoAccess,
status: user.isAccepted ? OrgMembershipStatus.Accepted : OrgMembershipStatus.Invited, // if user is fully completed, then set status to accepted, otherwise set it to invited so we can update it later
isActive: true
},
@ -349,7 +343,11 @@ export const scimServiceFactory = ({
}
if (!user) {
const uniqueUsername = await normalizeUsername(`${firstName}-${lastName}`, userDAL);
const uniqueUsername = await normalizeUsername(
// external id is username
`${firstName}-${lastName}`,
userDAL
);
user = await userDAL.create(
{
username: serverCfg.trustSamlEmails ? email : uniqueUsername,
@ -430,10 +428,13 @@ export const scimServiceFactory = ({
firstName: createdUser.firstName,
lastName: createdUser.lastName,
email: createdUser.email ?? "",
active: createdOrgMembership.isActive
active: createdOrgMembership.isActive,
createdAt: createdOrgMembership.createdAt,
updatedAt: createdOrgMembership.updatedAt
});
};
// partial
const updateScimUser = async ({ orgMembershipId, orgId, operations }: TUpdateScimUserDTO) => {
const [membership] = await orgDAL
.findMembership({
@ -459,37 +460,52 @@ export const scimServiceFactory = ({
status: 403
});
let active = true;
operations.forEach((operation) => {
if (operation.op.toLowerCase() === "replace") {
if (operation.path === "active" && operation.value === "False") {
// azure scim op format
active = false;
} else if (typeof operation.value === "object" && operation.value.active === false) {
// okta scim op format
active = false;
}
}
});
if (!active) {
await orgMembershipDAL.updateById(membership.id, {
isActive: false
});
}
return buildScimUser({
const scimUser = buildScimUser({
orgMembershipId: membership.id,
username: membership.externalId ?? membership.username,
email: membership.email,
firstName: membership.firstName,
lastName: membership.lastName,
active
firstName: membership.firstName,
active: membership.isActive,
username: membership.externalId ?? membership.username,
createdAt: membership.createdAt,
updatedAt: membership.updatedAt
});
scimPatch(scimUser, operations);
const serverCfg = await getServerCfg();
await userDAL.transaction(async (tx) => {
await orgMembershipDAL.updateById(
membership.id,
{
isActive: scimUser.active
},
tx
);
const hasEmailChanged = scimUser.emails[0].value !== membership.email;
await userDAL.updateById(
membership.userId,
{
firstName: scimUser.name.givenName,
email: scimUser.emails[0].value,
lastName: scimUser.name.familyName,
isEmailVerified: hasEmailChanged ? serverCfg.trustSamlEmails : true
},
tx
);
});
return scimUser;
};
const replaceScimUser = async ({ orgMembershipId, active, orgId }: TReplaceScimUserDTO) => {
const replaceScimUser = async ({
orgMembershipId,
active,
orgId,
lastName,
firstName,
email,
externalId
}: TReplaceScimUserDTO) => {
const [membership] = await orgDAL
.findMembership({
[`${TableName.OrgMembership}.id` as "id"]: orgMembershipId,
@ -514,26 +530,47 @@ export const scimServiceFactory = ({
status: 403
});
await orgMembershipDAL.updateById(membership.id, {
isActive: active
const serverCfg = await getServerCfg();
await userDAL.transaction(async (tx) => {
await userAliasDAL.update(
{
orgId,
aliasType: UserAliasType.SAML,
userId: membership.userId
},
{
externalId
},
tx
);
await orgMembershipDAL.updateById(
membership.id,
{
isActive: active
},
tx
);
await userDAL.updateById(
membership.userId,
{
firstName,
email,
lastName,
isEmailVerified: serverCfg.trustSamlEmails
},
tx
);
});
const groupMembershipsInOrg = await userGroupMembershipDAL.findGroupMembershipsByUserIdInOrg(
membership.userId,
orgId
);
return buildScimUser({
orgMembershipId: membership.id,
username: membership.externalId ?? membership.username,
username: externalId,
email: membership.email,
firstName: membership.firstName,
lastName: membership.lastName,
active,
groups: groupMembershipsInOrg.map((group) => ({
value: group.groupId,
display: group.groupName
}))
createdAt: membership.createdAt,
updatedAt: membership.updatedAt
});
};
@ -570,7 +607,7 @@ export const scimServiceFactory = ({
return {}; // intentionally return empty object upon success
};
const listScimGroups = async ({ orgId, startIndex, limit, filter }: TListScimGroupsDTO) => {
const listScimGroups = async ({ orgId, startIndex, limit, filter, isMembersExcluded }: TListScimGroupsDTO) => {
const plan = await licenseService.getPlan(orgId);
if (!plan.groups)
throw new BadRequestError({
@ -603,6 +640,21 @@ export const scimServiceFactory = ({
);
const scimGroups: TScimGroup[] = [];
if (isMembersExcluded) {
return buildScimGroupList({
scimGroups: groups.map((group) =>
buildScimGroup({
groupId: group.id,
name: group.name,
members: [],
createdAt: group.createdAt,
updatedAt: group.updatedAt
})
),
startIndex,
limit
});
}
for await (const group of groups) {
const members = await userGroupMembershipDAL.findGroupMembershipsByGroupIdInOrg(group.id, orgId);
@ -612,7 +664,9 @@ export const scimServiceFactory = ({
members: members.map((member) => ({
value: member.orgMembershipId,
display: `${member.firstName ?? ""} ${member.lastName ?? ""}`
}))
})),
createdAt: group.createdAt,
updatedAt: group.updatedAt
});
scimGroups.push(scimGroup);
}
@ -696,7 +750,9 @@ export const scimServiceFactory = ({
members: orgMemberships.map(({ id, firstName, lastName }) => ({
value: id,
display: `${firstName} ${lastName}`
}))
})),
createdAt: newGroup.group.createdAt,
updatedAt: newGroup.group.updatedAt
});
};
@ -739,31 +795,17 @@ export const scimServiceFactory = ({
members: orgMemberships.map(({ id, firstName, lastName }) => ({
value: id,
display: `${firstName} ${lastName}`
}))
})),
createdAt: group.createdAt,
updatedAt: group.updatedAt
});
};
const updateScimGroupNamePut = async ({ groupId, orgId, displayName, members }: TUpdateScimGroupNamePutDTO) => {
const plan = await licenseService.getPlan(orgId);
if (!plan.groups)
throw new BadRequestError({
message: "Failed to update SCIM group due to plan restriction. Upgrade plan to update SCIM group."
});
const org = await orgDAL.findById(orgId);
if (!org) {
throw new ScimRequestError({
detail: "Organization Not Found",
status: 404
});
}
if (!org.scimEnabled)
throw new ScimRequestError({
detail: "SCIM is disabled for the organization",
status: 403
});
const $replaceGroupDAL = async (
groupId: string,
orgId: string,
{ displayName, members = [] }: { displayName: string; members: { value: string }[] }
) => {
const updatedGroup = await groupDAL.transaction(async (tx) => {
const [group] = await groupDAL.update(
{
@ -782,74 +824,96 @@ export const scimServiceFactory = ({
});
}
if (members) {
const orgMemberships = await orgMembershipDAL.find({
$in: {
id: members.map((member) => member.value)
}
const orgMemberships = members.length
? await orgMembershipDAL.find({
$in: {
id: members.map((member) => member.value)
}
})
: [];
const membersIdsSet = new Set(orgMemberships.map((orgMembership) => orgMembership.userId));
const userGroupMembers = await userGroupMembershipDAL.find({
groupId: group.id
});
const directMemberUserIds = userGroupMembers.filter((el) => !el.isPending).map((membership) => membership.userId);
const pendingGroupAdditionsUserIds = userGroupMembers
.filter((el) => el.isPending)
.map((pendingGroupAddition) => pendingGroupAddition.userId);
const allMembersUserIds = directMemberUserIds.concat(pendingGroupAdditionsUserIds);
const allMembersUserIdsSet = new Set(allMembersUserIds);
const toAddUserIds = orgMemberships.filter((member) => !allMembersUserIdsSet.has(member.userId as string));
const toRemoveUserIds = allMembersUserIds.filter((userId) => !membersIdsSet.has(userId));
if (toAddUserIds.length) {
await addUsersToGroupByUserIds({
group,
userIds: toAddUserIds.map((member) => member.userId as string),
userDAL,
userGroupMembershipDAL,
orgDAL,
groupProjectDAL,
projectKeyDAL,
projectDAL,
projectBotDAL,
tx
});
}
const membersIdsSet = new Set(orgMemberships.map((orgMembership) => orgMembership.userId));
const directMemberUserIds = (
await userGroupMembershipDAL.find({
groupId: group.id,
isPending: false
})
).map((membership) => membership.userId);
const pendingGroupAdditionsUserIds = (
await userGroupMembershipDAL.find({
groupId: group.id,
isPending: true
})
).map((pendingGroupAddition) => pendingGroupAddition.userId);
const allMembersUserIds = directMemberUserIds.concat(pendingGroupAdditionsUserIds);
const allMembersUserIdsSet = new Set(allMembersUserIds);
const toAddUserIds = orgMemberships.filter((member) => !allMembersUserIdsSet.has(member.userId as string));
const toRemoveUserIds = allMembersUserIds.filter((userId) => !membersIdsSet.has(userId));
if (toAddUserIds.length) {
await addUsersToGroupByUserIds({
group,
userIds: toAddUserIds.map((member) => member.userId as string),
userDAL,
userGroupMembershipDAL,
orgDAL,
groupProjectDAL,
projectKeyDAL,
projectDAL,
projectBotDAL,
tx
});
}
if (toRemoveUserIds.length) {
await removeUsersFromGroupByUserIds({
group,
userIds: toRemoveUserIds,
userDAL,
userGroupMembershipDAL,
groupProjectDAL,
projectKeyDAL,
tx
});
}
if (toRemoveUserIds.length) {
await removeUsersFromGroupByUserIds({
group,
userIds: toRemoveUserIds,
userDAL,
userGroupMembershipDAL,
groupProjectDAL,
projectKeyDAL,
tx
});
}
return group;
});
return updatedGroup;
};
const replaceScimGroup = async ({ groupId, orgId, displayName, members }: TUpdateScimGroupNamePutDTO) => {
const plan = await licenseService.getPlan(orgId);
if (!plan.groups)
throw new BadRequestError({
message: "Failed to update SCIM group due to plan restriction. Upgrade plan to update SCIM group."
});
const org = await orgDAL.findById(orgId);
if (!org) {
throw new ScimRequestError({
detail: "Organization Not Found",
status: 404
});
}
if (!org.scimEnabled)
throw new ScimRequestError({
detail: "SCIM is disabled for the organization",
status: 403
});
const updatedGroup = await $replaceGroupDAL(groupId, orgId, { displayName, members });
return buildScimGroup({
groupId: updatedGroup.id,
name: updatedGroup.name,
members
members,
updatedAt: updatedGroup.updatedAt,
createdAt: updatedGroup.createdAt
});
};
const updateScimGroupNamePatch = async ({ groupId, orgId, operations }: TUpdateScimGroupNamePatchDTO) => {
const updateScimGroup = async ({ groupId, orgId, operations }: TUpdateScimGroupNamePatchDTO) => {
const plan = await licenseService.getPlan(orgId);
if (!plan.groups)
throw new BadRequestError({
@ -871,7 +935,7 @@ export const scimServiceFactory = ({
status: 403
});
let group = await groupDAL.findOne({
const group = await groupDAL.findOne({
id: groupId,
orgId
});
@ -883,64 +947,28 @@ export const scimServiceFactory = ({
});
}
for await (const operation of operations) {
if (operation.op === "replace" || operation.op === "Replace") {
group = await groupDAL.updateById(group.id, {
name: operation.value.displayName
});
} else if (operation.op === "add" || operation.op === "Add") {
try {
const orgMemberships = await orgMembershipDAL.find({
$in: {
id: operation.value.map((member) => member.value)
}
});
await addUsersToGroupByUserIds({
group,
userIds: orgMemberships.map((membership) => membership.userId as string),
userDAL,
userGroupMembershipDAL,
orgDAL,
groupProjectDAL,
projectKeyDAL,
projectDAL,
projectBotDAL
});
} catch {
logger.info("Repeat SCIM user-group add operation");
}
} else if (operation.op === "remove" || operation.op === "Remove") {
const orgMembershipId = extractScimValueFromPath(operation.path);
if (!orgMembershipId) throw new ScimRequestError({ detail: "Invalid path value", status: 400 });
const orgMembership = await orgMembershipDAL.findById(orgMembershipId);
if (!orgMembership) throw new ScimRequestError({ detail: "Org Membership Not Found", status: 400 });
await removeUsersFromGroupByUserIds({
group,
userIds: [orgMembership.userId as string],
userDAL,
userGroupMembershipDAL,
groupProjectDAL,
projectKeyDAL
});
} else {
throw new ScimRequestError({
detail: "Invalid Operation",
status: 400
});
}
}
const members = await userGroupMembershipDAL.findGroupMembershipsByGroupIdInOrg(group.id, orgId);
return buildScimGroup({
const scimGroup = buildScimGroup({
groupId: group.id,
name: group.name,
members: members.map((member) => ({
value: member.orgMembershipId
})),
createdAt: group.createdAt,
updatedAt: group.updatedAt
});
scimPatch(scimGroup, operations);
// remove members is a weird case not following scim convention
await $replaceGroupDAL(groupId, orgId, { displayName: scimGroup.displayName, members: scimGroup.members });
const updatedScimMembers = await userGroupMembershipDAL.findGroupMembershipsByGroupIdInOrg(group.id, orgId);
return {
...scimGroup,
members: updatedScimMembers.map((member) => ({
value: member.orgMembershipId,
display: `${member.firstName ?? ""} ${member.lastName ?? ""}`
}))
});
};
};
const deleteScimGroup = async ({ groupId, orgId }: TDeleteScimGroupDTO) => {
@ -1016,8 +1044,8 @@ export const scimServiceFactory = ({
createScimGroup,
getScimGroup,
deleteScimGroup,
updateScimGroupNamePut,
updateScimGroupNamePatch,
replaceScimGroup,
updateScimGroup,
fnValidateScimToken
};
};

View File

@ -1,3 +1,5 @@
import { ScimPatchOperation } from "scim-patch";
import { TOrgPermission } from "@app/lib/types";
export type TCreateScimTokenDTO = {
@ -34,29 +36,25 @@ export type TGetScimUserDTO = {
export type TCreateScimUserDTO = {
externalId: string;
email?: string;
firstName: string;
lastName: string;
firstName?: string;
lastName?: string;
orgId: string;
};
export type TUpdateScimUserDTO = {
orgMembershipId: string;
orgId: string;
operations: {
op: string;
path?: string;
value?:
| string
| {
active: boolean;
};
}[];
operations: ScimPatchOperation[];
};
export type TReplaceScimUserDTO = {
orgMembershipId: string;
active: boolean;
orgId: string;
email?: string;
firstName?: string;
lastName?: string;
externalId: string;
};
export type TDeleteScimUserDTO = {
@ -69,6 +67,7 @@ export type TListScimGroupsDTO = {
filter?: string;
limit: number;
orgId: string;
isMembersExcluded?: boolean;
};
export type TListScimGroups = {
@ -107,31 +106,7 @@ export type TUpdateScimGroupNamePutDTO = {
export type TUpdateScimGroupNamePatchDTO = {
groupId: string;
orgId: string;
operations: (TRemoveOp | TReplaceOp | TAddOp)[];
};
// akhilmhdh: I know, this is done due to lack of time. Need to change later to support as normalized rather than like this
// Forgive akhil blame tony
type TReplaceOp = {
op: "replace" | "Replace";
value: {
id: string;
displayName: string;
};
};
type TRemoveOp = {
op: "remove" | "Remove";
path: string;
};
type TAddOp = {
op: "add" | "Add";
path: string;
value: {
value: string;
display?: string;
}[];
operations: ScimPatchOperation[];
};
export type TDeleteScimGroupDTO = {
@ -160,13 +135,10 @@ export type TScimUser = {
type: string;
}[];
active: boolean;
groups: {
value: string;
display: string;
}[];
meta: {
resourceType: string;
location: null;
created: Date;
lastModified: Date;
};
};
@ -176,10 +148,11 @@ export type TScimGroup = {
displayName: string;
members: {
value: string;
display: string;
display?: string;
}[];
meta: {
resourceType: string;
location: null;
created: Date;
lastModified: Date;
};
};

View File

@ -36,9 +36,7 @@ export const sendApprovalEmailsFn = async ({
firstName: reviewerUser.firstName,
projectName: project.name,
organizationName: project.organization.name,
approvalUrl: `${cfg.isDevelopmentMode ? "https" : "http"}://${cfg.SITE_URL}/project/${
project.id
}/approval?requestId=${secretApprovalRequest.id}`
approvalUrl: `${cfg.SITE_URL}/project/${project.id}/approval?requestId=${secretApprovalRequest.id}`
},
template: SmtpTemplates.SecretApprovalRequestNeedsReview
});

View File

@ -16,6 +16,7 @@ import {
import { DatabaseError } from "@app/lib/errors";
import { ormify, selectAllTableCols, sqlNestRelationships } from "@app/lib/knex";
import { logger } from "@app/lib/logger";
import { QueueName } from "@app/queue";
export type TSnapshotDALFactory = ReturnType<typeof snapshotDALFactory>;
@ -599,6 +600,7 @@ export const snapshotDALFactory = (db: TDbClient) => {
const pruneExcessSnapshots = async () => {
const PRUNE_FOLDER_BATCH_SIZE = 10000;
logger.info(`${QueueName.DailyResourceCleanUp}: pruning secret snapshots started`);
try {
let uuidOffset = "00000000-0000-0000-0000-000000000000";
// cleanup snapshots from current folders
@ -714,6 +716,7 @@ export const snapshotDALFactory = (db: TDbClient) => {
} catch (error) {
throw new DatabaseError({ error, name: "SnapshotPrune" });
}
logger.info(`${QueueName.DailyResourceCleanUp}: pruning secret snapshots completed`);
};
// special query for migration for secret v2

View File

@ -964,6 +964,10 @@ export const INTEGRATION = {
shouldAutoRedeploy: "Used by Render to trigger auto deploy.",
secretGCPLabel: "The label for GCP secrets.",
secretAWSTag: "The tags for AWS secrets.",
githubVisibility:
"Define where the secrets from the Github Integration should be visible. Option 'selected' lets you directly define which repositories to sync secrets to.",
githubVisibilityRepoIds:
"The repository IDs to sync secrets to when using the Github Integration. Only applicable when using Organization scope, and visibility is set to 'selected'",
kmsKeyId: "The ID of the encryption key from AWS KMS.",
shouldDisableDelete: "The flag to disable deletion of secrets in AWS Parameter Store.",
shouldMaskSecrets: "Specifies if the secrets synced from Infisical to Gitlab should be marked as 'Masked'.",

View File

@ -0,0 +1,57 @@
import crypto from "node:crypto";
import { AxiosError, AxiosInstance, AxiosRequestConfig } from "axios";
export const createDigestAuthRequestInterceptor = (
axiosInstance: AxiosInstance,
username: string,
password: string
) => {
let nc = 0;
return async (opts: AxiosRequestConfig) => {
try {
return await axiosInstance.request(opts);
} catch (err) {
const error = err as AxiosError;
const authHeader = (error?.response?.headers?.["www-authenticate"] as string) || "";
if (error?.response?.status !== 401 || !authHeader?.includes("nonce")) {
return Promise.reject(error.message);
}
if (!error.config) {
return Promise.reject(error);
}
const authDetails = authHeader.split(",").map((el) => el.split("="));
nc += 1;
const nonceCount = nc.toString(16).padStart(8, "0");
const cnonce = crypto.randomBytes(24).toString("hex");
const realm = authDetails.find((el) => el[0].toLowerCase().indexOf("realm") > -1)?.[1].replace(/"/g, "");
const nonce = authDetails.find((el) => el[0].toLowerCase().indexOf("nonce") > -1)?.[1].replace(/"/g, "");
const ha1 = crypto.createHash("md5").update(`${username}:${realm}:${password}`).digest("hex");
const path = opts.url;
const ha2 = crypto
.createHash("md5")
.update(`${opts.method ?? "GET"}:${path}`)
.digest("hex");
const response = crypto
.createHash("md5")
.update(`${ha1}:${nonce}:${nonceCount}:${cnonce}:auth:${ha2}`)
.digest("hex");
const authorization = `Digest username="${username}",realm="${realm}",nonce="${nonce}",uri="${path}",qop="auth",algorithm="MD5",response="${response}",nc="${nonceCount}",cnonce="${cnonce}"`;
if (opts.headers) {
// eslint-disable-next-line
opts.headers.authorization = authorization;
} else {
// eslint-disable-next-line
opts.headers = { authorization };
}
return axiosInstance.request(opts);
}
};
};

View File

@ -1,6 +1,7 @@
import { Logger } from "pino";
import { z } from "zod";
import { removeTrailingSlash } from "../fn";
import { zpStr } from "../zod";
export const GITLAB_URL = "https://gitlab.com";
@ -63,7 +64,9 @@ const envSchema = z
.string()
.min(32)
.default("#5VihU%rbXHcHwWwCot5L3vyPsx$7dWYw^iGk!EJg2bC*f$PD$%KCqx^R@#^LSEf"),
SITE_URL: zpStr(z.string().optional()),
// Ensure that the SITE_URL never ends with a trailing slash
SITE_URL: zpStr(z.string().transform((val) => (val ? removeTrailingSlash(val) : val))).optional(),
// Telemetry
TELEMETRY_ENABLED: zodStrBool.default("true"),
POSTHOG_HOST: zpStr(z.string().optional().default("https://app.posthog.com")),

View File

@ -0,0 +1,121 @@
import { Knex } from "knex";
import { Compare, Filter, parse } from "scim2-parse-filter";
const appendParentToGroupingOperator = (parentPath: string, filter: Filter) => {
if (filter.op !== "[]" && filter.op !== "and" && filter.op !== "or" && filter.op !== "not") {
return { ...filter, attrPath: `${parentPath}.${(filter as Compare).attrPath}` };
}
return filter;
};
export const generateKnexQueryFromScim = (
rootQuery: Knex.QueryBuilder,
rootScimFilter: string,
getAttributeField: (attr: string) => string | null
) => {
const scimRootFilterAst = parse(rootScimFilter);
const stack = [
{
scimFilterAst: scimRootFilterAst,
query: rootQuery
}
];
while (stack.length) {
const { scimFilterAst, query } = stack.pop()!;
switch (scimFilterAst.op) {
case "eq": {
const attrPath = getAttributeField(scimFilterAst.attrPath);
if (attrPath) void query.where(attrPath, scimFilterAst.compValue);
break;
}
case "pr": {
const attrPath = getAttributeField(scimFilterAst.attrPath);
if (attrPath) void query.whereNotNull(attrPath);
break;
}
case "gt": {
const attrPath = getAttributeField(scimFilterAst.attrPath);
if (attrPath) void query.where(attrPath, ">", scimFilterAst.compValue);
break;
}
case "ge": {
const attrPath = getAttributeField(scimFilterAst.attrPath);
if (attrPath) void query.where(attrPath, ">=", scimFilterAst.compValue);
break;
}
case "lt": {
const attrPath = getAttributeField(scimFilterAst.attrPath);
if (attrPath) void query.where(attrPath, "<", scimFilterAst.compValue);
break;
}
case "le": {
const attrPath = getAttributeField(scimFilterAst.attrPath);
if (attrPath) void query.where(attrPath, "<=", scimFilterAst.compValue);
break;
}
case "sw": {
const attrPath = getAttributeField(scimFilterAst.attrPath);
if (attrPath) void query.whereILike(attrPath, `${scimFilterAst.compValue}%`);
break;
}
case "ew": {
const attrPath = getAttributeField(scimFilterAst.attrPath);
if (attrPath) void query.whereILike(attrPath, `%${scimFilterAst.compValue}`);
break;
}
case "co": {
const attrPath = getAttributeField(scimFilterAst.attrPath);
if (attrPath) void query.whereILike(attrPath, `%${scimFilterAst.compValue}%`);
break;
}
case "ne": {
const attrPath = getAttributeField(scimFilterAst.attrPath);
if (attrPath) void query.whereNot(attrPath, "=", scimFilterAst.compValue);
break;
}
case "and": {
void query.andWhere((subQueryBuilder) => {
scimFilterAst.filters.forEach((el) => {
stack.push({
query: subQueryBuilder,
scimFilterAst: el
});
});
});
break;
}
case "or": {
void query.orWhere((subQueryBuilder) => {
scimFilterAst.filters.forEach((el) => {
stack.push({
query: subQueryBuilder,
scimFilterAst: el
});
});
});
break;
}
case "not": {
void query.whereNot((subQueryBuilder) => {
stack.push({
query: subQueryBuilder,
scimFilterAst: scimFilterAst.filter
});
});
break;
}
case "[]": {
void query.whereNot((subQueryBuilder) => {
stack.push({
query: subQueryBuilder,
scimFilterAst: appendParentToGroupingOperator(scimFilterAst.attrPath, scimFilterAst.valFilter)
});
});
break;
}
default:
break;
}
}
};

View File

@ -49,6 +49,21 @@ export const main = async ({ db, smtp, logger, queue, keyStore }: TMain) => {
server.setValidatorCompiler(validatorCompiler);
server.setSerializerCompiler(serializerCompiler);
server.addContentTypeParser("application/scim+json", { parseAs: "string" }, (_, body, done) => {
try {
const strBody = body instanceof Buffer ? body.toString() : body;
if (!strBody) {
done(null, undefined);
return;
}
const json: unknown = JSON.parse(strBody);
done(null, json);
} catch (err) {
const error = err as Error;
done(error, undefined);
}
});
try {
await server.register<FastifyCookieOptions>(cookie, {
secret: appCfg.COOKIE_SECRET_SIGN_KEY

View File

@ -3,6 +3,7 @@ import { Redis } from "ioredis";
import { Knex } from "knex";
import { z } from "zod";
import { registerCertificateEstRouter } from "@app/ee/routes/est/certificate-est-router";
import { registerV1EERoutes } from "@app/ee/routes/v1";
import { accessApprovalPolicyApproverDALFactory } from "@app/ee/services/access-approval-policy/access-approval-policy-approver-dal";
import { accessApprovalPolicyDALFactory } from "@app/ee/services/access-approval-policy/access-approval-policy-dal";
@ -17,6 +18,7 @@ import { auditLogStreamDALFactory } from "@app/ee/services/audit-log-stream/audi
import { auditLogStreamServiceFactory } from "@app/ee/services/audit-log-stream/audit-log-stream-service";
import { certificateAuthorityCrlDALFactory } from "@app/ee/services/certificate-authority-crl/certificate-authority-crl-dal";
import { certificateAuthorityCrlServiceFactory } from "@app/ee/services/certificate-authority-crl/certificate-authority-crl-service";
import { certificateEstServiceFactory } from "@app/ee/services/certificate-est/certificate-est-service";
import { dynamicSecretDALFactory } from "@app/ee/services/dynamic-secret/dynamic-secret-dal";
import { dynamicSecretServiceFactory } from "@app/ee/services/dynamic-secret/dynamic-secret-service";
import { buildDynamicSecretProviders } from "@app/ee/services/dynamic-secret/providers";
@ -92,7 +94,6 @@ import { certificateAuthorityDALFactory } from "@app/services/certificate-author
import { certificateAuthorityQueueFactory } from "@app/services/certificate-authority/certificate-authority-queue";
import { certificateAuthoritySecretDALFactory } from "@app/services/certificate-authority/certificate-authority-secret-dal";
import { certificateAuthorityServiceFactory } from "@app/services/certificate-authority/certificate-authority-service";
import { certificateEstServiceFactory } from "@app/services/certificate-est/certificate-est-service";
import { certificateTemplateDALFactory } from "@app/services/certificate-template/certificate-template-dal";
import { certificateTemplateEstConfigDALFactory } from "@app/services/certificate-template/certificate-template-est-config-dal";
import { certificateTemplateServiceFactory } from "@app/services/certificate-template/certificate-template-service";
@ -199,7 +200,6 @@ 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 { registerCertificateEstRouter } from "./est/certificate-est-router";
import { registerV1Routes } from "./v1";
import { registerV2Routes } from "./v2";
import { registerV3Routes } from "./v3";
@ -667,7 +667,8 @@ export const registerRoutes = async (
certificateAuthorityDAL,
permissionService,
kmsService,
projectDAL
projectDAL,
licenseService
});
const certificateEstService = certificateEstServiceFactory({
@ -677,7 +678,8 @@ export const registerRoutes = async (
certificateAuthorityCertDAL,
certificateAuthorityDAL,
projectDAL,
kmsService
kmsService,
licenseService
});
const pkiAlertService = pkiAlertServiceFactory({

View File

@ -48,7 +48,7 @@ export const registerSecretSharingRouter = async (server: FastifyZodProvider) =>
});
server.route({
method: "GET",
method: "POST",
url: "/public/:id",
config: {
rateLimit: publicEndpointLimit
@ -57,38 +57,37 @@ export const registerSecretSharingRouter = async (server: FastifyZodProvider) =>
params: z.object({
id: z.string().uuid()
}),
querystring: z.object({
hashedHex: z.string().min(1)
body: z.object({
hashedHex: z.string().min(1),
password: z.string().optional()
}),
response: {
200: SecretSharingSchema.pick({
encryptedValue: true,
iv: true,
tag: true,
expiresAt: true,
expiresAfterViews: true,
accessType: true
}).extend({
orgName: z.string().optional()
200: z.object({
isPasswordProtected: z.boolean(),
secret: SecretSharingSchema.pick({
encryptedValue: true,
iv: true,
tag: true,
expiresAt: true,
expiresAfterViews: true,
accessType: true
})
.extend({
orgName: z.string().optional()
})
.optional()
})
}
},
handler: async (req) => {
const sharedSecret = await req.server.services.secretSharing.getActiveSharedSecretById({
const sharedSecret = await req.server.services.secretSharing.getSharedSecretById({
sharedSecretId: req.params.id,
hashedHex: req.query.hashedHex,
hashedHex: req.body.hashedHex,
password: req.body.password,
orgId: req.permission?.orgId
});
if (!sharedSecret) return undefined;
return {
encryptedValue: sharedSecret.encryptedValue,
iv: sharedSecret.iv,
tag: sharedSecret.tag,
expiresAt: sharedSecret.expiresAt,
expiresAfterViews: sharedSecret.expiresAfterViews,
accessType: sharedSecret.accessType,
orgName: sharedSecret.orgName
};
return sharedSecret;
}
});
@ -101,6 +100,7 @@ export const registerSecretSharingRouter = async (server: FastifyZodProvider) =>
schema: {
body: z.object({
encryptedValue: z.string(),
password: z.string().optional(),
hashedHex: z.string(),
iv: z.string(),
tag: z.string(),
@ -131,6 +131,7 @@ export const registerSecretSharingRouter = async (server: FastifyZodProvider) =>
schema: {
body: z.object({
name: z.string().max(50).optional(),
password: z.string().optional(),
encryptedValue: z.string(),
hashedHex: z.string(),
iv: z.string(),

View File

@ -213,7 +213,6 @@ export const certificateAuthorityServiceFactory = ({
keys,
extensions: [
new x509.BasicConstraintsExtension(true, maxPathLength === -1 ? undefined : maxPathLength, true),
new x509.ExtendedKeyUsageExtension(["1.2.3.4.5.6.7", "2.3.4.5.6.7.8"], true),
// eslint-disable-next-line no-bitwise
new x509.KeyUsagesExtension(x509.KeyUsageFlags.keyCertSign | x509.KeyUsageFlags.cRLSign, true),
await x509.SubjectKeyIdentifierExtension.create(keys.publicKey)
@ -496,7 +495,6 @@ export const certificateAuthorityServiceFactory = ({
ca.maxPathLength === -1 || !ca.maxPathLength ? undefined : ca.maxPathLength,
true
),
new x509.ExtendedKeyUsageExtension(["1.2.3.4.5.6.7", "2.3.4.5.6.7.8"], true),
// eslint-disable-next-line no-bitwise
new x509.KeyUsagesExtension(x509.KeyUsageFlags.keyCertSign | x509.KeyUsageFlags.cRLSign, true),
await x509.SubjectKeyIdentifierExtension.create(caPublicKey)

View File

@ -1,3 +1,5 @@
import { Knex } from "knex";
import { TDbClient } from "@app/db";
import { TableName } from "@app/db/schemas";
import { DatabaseError } from "@app/lib/errors";
@ -30,20 +32,21 @@ export const certificateTemplateDALFactory = (db: TDbClient) => {
}
};
const getById = async (id: string) => {
const getById = async (id: string, tx?: Knex) => {
try {
const certTemplate = await db
.replicaNode()(TableName.CertificateTemplate)
const certTemplate = await (tx || db.replicaNode())(TableName.CertificateTemplate)
.join(
TableName.CertificateAuthority,
`${TableName.CertificateAuthority}.id`,
`${TableName.CertificateTemplate}.caId`
)
.join(TableName.Project, `${TableName.Project}.id`, `${TableName.CertificateAuthority}.projectId`)
.where(`${TableName.CertificateTemplate}.id`, "=", id)
.select(selectAllTableCols(TableName.CertificateTemplate))
.select(
db.ref("projectId").withSchema(TableName.CertificateAuthority),
db.ref("friendlyName").as("caName").withSchema(TableName.CertificateAuthority)
db.ref("friendlyName").as("caName").withSchema(TableName.CertificateAuthority),
db.ref("orgId").withSchema(TableName.Project)
)
.first();

View File

@ -3,6 +3,7 @@ import * as x509 from "@peculiar/x509";
import bcrypt from "bcrypt";
import { TCertificateTemplateEstConfigsUpdate } 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";
@ -32,6 +33,7 @@ type TCertificateTemplateServiceFactoryDep = {
kmsService: Pick<TKmsServiceFactory, "generateKmsKey" | "encryptWithKmsKey" | "decryptWithKmsKey">;
certificateAuthorityDAL: Pick<TCertificateAuthorityDALFactory, "findById">;
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
};
export type TCertificateTemplateServiceFactory = ReturnType<typeof certificateTemplateServiceFactory>;
@ -42,7 +44,8 @@ export const certificateTemplateServiceFactory = ({
certificateAuthorityDAL,
permissionService,
kmsService,
projectDAL
projectDAL,
licenseService
}: TCertificateTemplateServiceFactoryDep) => {
const createCertTemplate = async ({
caId,
@ -75,23 +78,28 @@ export const certificateTemplateServiceFactory = ({
ProjectPermissionSub.CertificateTemplates
);
const { id } = await certificateTemplateDAL.create({
caId,
pkiCollectionId,
name,
commonName,
subjectAlternativeName,
ttl
return certificateTemplateDAL.transaction(async (tx) => {
const { id } = await certificateTemplateDAL.create(
{
caId,
pkiCollectionId,
name,
commonName,
subjectAlternativeName,
ttl
},
tx
);
const certificateTemplate = await certificateTemplateDAL.getById(id, tx);
if (!certificateTemplate) {
throw new NotFoundError({
message: "Certificate template not found"
});
}
return certificateTemplate;
});
const certificateTemplate = await certificateTemplateDAL.getById(id);
if (!certificateTemplate) {
throw new NotFoundError({
message: "Certificate template not found"
});
}
return certificateTemplate;
};
const updateCertTemplate = async ({
@ -136,23 +144,29 @@ export const certificateTemplateServiceFactory = ({
}
}
await certificateTemplateDAL.updateById(certTemplate.id, {
caId,
pkiCollectionId,
commonName,
subjectAlternativeName,
name,
ttl
return certificateTemplateDAL.transaction(async (tx) => {
await certificateTemplateDAL.updateById(
certTemplate.id,
{
caId,
pkiCollectionId,
commonName,
subjectAlternativeName,
name,
ttl
},
tx
);
const updatedTemplate = await certificateTemplateDAL.getById(id, tx);
if (!updatedTemplate) {
throw new NotFoundError({
message: "Certificate template not found"
});
}
return updatedTemplate;
});
const updatedTemplate = await certificateTemplateDAL.getById(id);
if (!updatedTemplate) {
throw new NotFoundError({
message: "Certificate template not found"
});
}
return updatedTemplate;
};
const deleteCertTemplate = async ({ id, actorId, actorAuthMethod, actor, actorOrgId }: TDeleteCertTemplateDTO) => {
@ -215,6 +229,13 @@ export const certificateTemplateServiceFactory = ({
actor,
actorOrgId
}: TCreateEstConfigurationDTO) => {
const plan = await licenseService.getPlan(actorOrgId);
if (!plan.pkiEst) {
throw new BadRequestError({
message: "Failed to create EST configuration due to plan restriction. Upgrade to the Enterprise plan."
});
}
const certTemplate = await certificateTemplateDAL.getById(certificateTemplateId);
if (!certTemplate) {
throw new NotFoundError({
@ -285,6 +306,13 @@ export const certificateTemplateServiceFactory = ({
actor,
actorOrgId
}: TUpdateEstConfigurationDTO) => {
const plan = await licenseService.getPlan(actorOrgId);
if (!plan.pkiEst) {
throw new BadRequestError({
message: "Failed to update EST configuration due to plan restriction. Upgrade to the Enterprise plan."
});
}
const certTemplate = await certificateTemplateDAL.getById(certificateTemplateId);
if (!certTemplate) {
throw new NotFoundError({
@ -416,7 +444,8 @@ export const certificateTemplateServiceFactory = ({
isEnabled: estConfig.isEnabled,
caChain: decryptedCaChain.toString(),
hashedPassphrase: estConfig.hashedPassphrase,
projectId: certTemplate.projectId
projectId: certTemplate.projectId,
orgId: certTemplate.orgId
};
};

View File

@ -4,6 +4,8 @@ import { TDbClient } from "@app/db";
import { IdentityAuthMethod, TableName, TIdentityAccessTokens } from "@app/db/schemas";
import { DatabaseError } from "@app/lib/errors";
import { ormify, selectAllTableCols } from "@app/lib/knex";
import { logger } from "@app/lib/logger";
import { QueueName } from "@app/queue";
export type TIdentityAccessTokenDALFactory = ReturnType<typeof identityAccessTokenDALFactory>;
@ -95,6 +97,7 @@ export const identityAccessTokenDALFactory = (db: TDbClient) => {
};
const removeExpiredTokens = async (tx?: Knex) => {
logger.info(`${QueueName.DailyResourceCleanUp}: remove expired access token started`);
try {
const docs = (tx || db)(TableName.IdentityAccessToken)
.where({
@ -131,7 +134,8 @@ export const identityAccessTokenDALFactory = (db: TDbClient) => {
});
})
.delete();
return await docs;
await docs;
logger.info(`${QueueName.DailyResourceCleanUp}: remove expired access token completed`);
} catch (error) {
throw new DatabaseError({ error, name: "IdentityAccessTokenPrune" });
}

View File

@ -5,6 +5,7 @@ import { TableName } from "@app/db/schemas";
import { DatabaseError } from "@app/lib/errors";
import { ormify } from "@app/lib/knex";
import { logger } from "@app/lib/logger";
import { QueueName } from "@app/queue";
export type TIdentityUaClientSecretDALFactory = ReturnType<typeof identityUaClientSecretDALFactory>;
@ -30,7 +31,9 @@ export const identityUaClientSecretDALFactory = (db: TDbClient) => {
let deletedClientSecret: { id: string }[] = [];
let numberOfRetryOnFailure = 0;
let isRetrying = false;
logger.info(`${QueueName.DailyResourceCleanUp}: remove expired univesal auth client secret started`);
do {
try {
const findExpiredClientSecretQuery = (tx || db)(TableName.IdentityUaClientSecret)
@ -39,7 +42,7 @@ export const identityUaClientSecretDALFactory = (db: TDbClient) => {
})
.orWhere((qb) => {
void qb
.where("clientSecretNumUses", ">", 0)
.where("clientSecretNumUsesLimit", ">", 0)
.andWhere(
"clientSecretNumUses",
">=",
@ -71,7 +74,9 @@ export const identityUaClientSecretDALFactory = (db: TDbClient) => {
setTimeout(resolve, 10); // time to breathe for db
});
}
} while (deletedClientSecret.length > 0 || numberOfRetryOnFailure < MAX_RETRY_ON_FAILURE);
isRetrying = numberOfRetryOnFailure > 0;
} while (deletedClientSecret.length > 0 || (isRetrying && numberOfRetryOnFailure < MAX_RETRY_ON_FAILURE));
logger.info(`${QueueName.DailyResourceCleanUp}: remove expired univesal auth client secret completed`);
};
return { ...uaClientSecretOrm, incrementUsage, removeExpiredClientSecrets };

View File

@ -460,16 +460,21 @@ const getAppsFlyio = async ({ accessToken }: { accessToken: string }) => {
*/
const getAppsCircleCI = async ({ accessToken }: { accessToken: string }) => {
const res = (
await request.get<{ reponame: string }[]>(`${IntegrationUrls.CIRCLECI_API_URL}/v1.1/projects`, {
headers: {
"Circle-Token": accessToken,
"Accept-Encoding": "application/json"
await request.get<{ reponame: string; username: string; vcs_url: string }[]>(
`${IntegrationUrls.CIRCLECI_API_URL}/v1.1/projects`,
{
headers: {
"Circle-Token": accessToken,
"Accept-Encoding": "application/json"
}
}
})
)
).data;
const apps = res?.map((a) => ({
name: a?.reponame
const apps = res.map((a) => ({
owner: a.username, // username maps to unique organization name in CircleCI
name: a.reponame, // reponame maps to project name within an organization in CircleCI
appId: a.vcs_url.split("/").pop() // vcs_url maps to the project id in CircleCI
}));
return apps;

View File

@ -30,6 +30,7 @@ const getIntegrationSecretsV2 = async (
environment: string;
folderId: string;
depth: number;
secretPath: string;
decryptor: (value: Buffer | null | undefined) => string;
},
secretV2BridgeDAL: Pick<TSecretV2BridgeDALFactory, "find" | "findByFolderId">,
@ -306,6 +307,7 @@ export const deleteIntegrationSecrets = async ({
? await getIntegrationSecretsV2(
{
environment: integration.environment.id,
secretPath: integration.secretPath,
projectId: integration.projectId,
folderId: folder.id,
depth: 1,

View File

@ -1625,7 +1625,11 @@ const syncSecretsGitHub = async ({
await octokit.request("PUT /orgs/{org}/actions/secrets/{secret_name}", {
org: integration.owner as string,
secret_name: key,
visibility: "all",
visibility: metadata.githubVisibility ?? "all",
...(metadata.githubVisibility === "selected" && {
// we need to map the githubVisibilityRepoIds to numbers
selected_repository_ids: metadata.githubVisibilityRepoIds?.map(Number) ?? []
}),
encrypted_value: encryptedSecret,
key_id: repoPublicKey.key_id
});
@ -1925,22 +1929,62 @@ const syncSecretsCircleCI = async ({
secrets: Record<string, { value: string; comment?: string }>;
accessToken: string;
}) => {
const circleciOrganizationDetail = (
await request.get(`${IntegrationUrls.CIRCLECI_API_URL}/v2/me/collaborations`, {
const getProjectSlug = async () => {
const requestConfig = {
headers: {
"Circle-Token": accessToken,
"Accept-Encoding": "application/json"
}
})
).data[0];
};
const { slug } = circleciOrganizationDetail;
try {
const projectDetails = (
await request.get<{ slug: string }>(
`${IntegrationUrls.CIRCLECI_API_URL}/v2/project/${integration.appId}`,
requestConfig
)
).data;
return projectDetails.slug;
} catch (err) {
if (err instanceof AxiosError) {
if (err.response?.data?.message !== "Not Found") {
throw new Error("Failed to get project slug from CircleCI during first attempt.");
}
}
}
// For backwards compatibility with old CircleCI integrations where we don't keep track of the organization name, so we can't filter by organization
try {
const circleCiOrganization = (
await request.get<{ slug: string; name: string }[]>(
`${IntegrationUrls.CIRCLECI_API_URL}/v2/me/collaborations`,
requestConfig
)
).data;
// Case 1: This is a new integration where the organization name is stored under `integration.owner`
if (integration.owner) {
const org = circleCiOrganization.find((o) => o.name === integration.owner);
if (org) {
return `${org.slug}/${integration.app}`;
}
}
// Case 2: This is an old integration where the organization name is not stored, so we have to assume the first organization is the correct one
return `${circleCiOrganization[0].slug}/${integration.app}`;
} catch (err) {
throw new Error("Failed to get project slug from CircleCI during second attempt.");
}
};
const projectSlug = await getProjectSlug();
// sync secrets to CircleCI
await Promise.all(
Object.keys(secrets).map(async (key) =>
request.post(
`${IntegrationUrls.CIRCLECI_API_URL}/v2/project/${slug}/${integration.app}/envvar`,
`${IntegrationUrls.CIRCLECI_API_URL}/v2/project/${projectSlug}/envvar`,
{
name: key,
value: secrets[key].value
@ -1958,7 +2002,7 @@ const syncSecretsCircleCI = async ({
// get secrets from CircleCI
const getSecretsRes = (
await request.get<{ items: { name: string }[] }>(
`${IntegrationUrls.CIRCLECI_API_URL}/v2/project/${slug}/${integration.app}/envvar`,
`${IntegrationUrls.CIRCLECI_API_URL}/v2/project/${projectSlug}/envvar`,
{
headers: {
"Circle-Token": accessToken,
@ -1972,15 +2016,12 @@ const syncSecretsCircleCI = async ({
await Promise.all(
getSecretsRes.map(async (sec) => {
if (!(sec.name in secrets)) {
return request.delete(
`${IntegrationUrls.CIRCLECI_API_URL}/v2/project/${slug}/${integration.app}/envvar/${sec.name}`,
{
headers: {
"Circle-Token": accessToken,
"Content-Type": "application/json"
}
return request.delete(`${IntegrationUrls.CIRCLECI_API_URL}/v2/project/${projectSlug}/envvar/${sec.name}`, {
headers: {
"Circle-Token": accessToken,
"Content-Type": "application/json"
}
);
});
}
})
);

View File

@ -5,14 +5,18 @@ import { INTEGRATION } from "@app/lib/api-docs";
import { IntegrationMappingBehavior } from "../integration-auth/integration-list";
export const IntegrationMetadataSchema = z.object({
initialSyncBehavior: z.string().optional().describe(INTEGRATION.CREATE.metadata.initialSyncBehavoir),
secretPrefix: z.string().optional().describe(INTEGRATION.CREATE.metadata.secretPrefix),
secretSuffix: z.string().optional().describe(INTEGRATION.CREATE.metadata.secretSuffix),
initialSyncBehavior: z.string().optional().describe(INTEGRATION.CREATE.metadata.initialSyncBehavoir),
mappingBehavior: z
.nativeEnum(IntegrationMappingBehavior)
.optional()
.describe(INTEGRATION.CREATE.metadata.mappingBehavior),
shouldAutoRedeploy: z.boolean().optional().describe(INTEGRATION.CREATE.metadata.shouldAutoRedeploy),
secretGCPLabel: z
.object({
labelName: z.string(),
@ -20,6 +24,7 @@ export const IntegrationMetadataSchema = z.object({
})
.optional()
.describe(INTEGRATION.CREATE.metadata.secretGCPLabel),
secretAWSTag: z
.array(
z.object({
@ -29,7 +34,15 @@ export const IntegrationMetadataSchema = z.object({
)
.optional()
.describe(INTEGRATION.CREATE.metadata.secretAWSTag),
githubVisibility: z
.union([z.literal("selected"), z.literal("private"), z.literal("all")])
.optional()
.describe(INTEGRATION.CREATE.metadata.githubVisibility),
githubVisibilityRepoIds: z.array(z.string()).optional().describe(INTEGRATION.CREATE.metadata.githubVisibilityRepoIds),
kmsKeyId: z.string().optional().describe(INTEGRATION.CREATE.metadata.kmsKeyId),
shouldDisableDelete: z.boolean().optional().describe(INTEGRATION.CREATE.metadata.shouldDisableDelete),
shouldEnableDelete: z.boolean().optional().describe(INTEGRATION.CREATE.metadata.shouldEnableDelete),
shouldMaskSecrets: z.boolean().optional().describe(INTEGRATION.CREATE.metadata.shouldMaskSecrets),

View File

@ -27,6 +27,10 @@ export type TCreateIntegrationDTO = {
key: string;
value: string;
}[];
githubVisibility?: string;
githubVisibilityRepoIds?: string[];
kmsKeyId?: string;
shouldDisableDelete?: boolean;
shouldMaskSecrets?: boolean;

View File

@ -12,6 +12,7 @@ import {
} from "@app/db/schemas";
import { DatabaseError } from "@app/lib/errors";
import { buildFindFilter, ormify, selectAllTableCols, TFindFilter, TFindOpt, withTransaction } from "@app/lib/knex";
import { generateKnexQueryFromScim } from "@app/lib/knex/scim";
export type TOrgDALFactory = ReturnType<typeof orgDALFactory>;
@ -280,6 +281,67 @@ export const orgDALFactory = (db: TDbClient) => {
.select(
selectAllTableCols(TableName.OrgMembership),
db.ref("email").withSchema(TableName.Users),
db.ref("isEmailVerified").withSchema(TableName.Users),
db.ref("username").withSchema(TableName.Users),
db.ref("firstName").withSchema(TableName.Users),
db.ref("lastName").withSchema(TableName.Users),
db.ref("scimEnabled").withSchema(TableName.Organization),
db.ref("externalId").withSchema(TableName.UserAliases)
)
.where({ isGhost: false });
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;
return res;
} catch (error) {
throw new DatabaseError({ error, name: "Find one" });
}
};
const findMembershipWithScimFilter = async (
orgId: string,
scimFilter: string | undefined,
{ offset, limit, sort, tx }: TFindOpt<TOrgMemberships> = {}
) => {
try {
const query = (tx || db.replicaNode())(TableName.OrgMembership)
// eslint-disable-next-line
.where(`${TableName.OrgMembership}.orgId`, orgId)
.where((qb) => {
if (scimFilter) {
void generateKnexQueryFromScim(qb, scimFilter, (attrPath) => {
switch (attrPath) {
case "active":
return `${TableName.OrgMembership}.isActive`;
case "userName":
return `${TableName.UserAliases}.externalId`;
case "name.givenName":
return `${TableName.Users}.firstName`;
case "name.familyName":
return `${TableName.Users}.lastName`;
case "email.value":
return `${TableName.Users}.email`;
default:
return null;
}
});
}
})
.join(TableName.Users, `${TableName.Users}.id`, `${TableName.OrgMembership}.userId`)
.join(TableName.Organization, `${TableName.Organization}.id`, `${TableName.OrgMembership}.orgId`)
.leftJoin(TableName.UserAliases, function joinUserAlias() {
this.on(`${TableName.UserAliases}.userId`, "=", `${TableName.OrgMembership}.userId`)
.andOn(`${TableName.UserAliases}.orgId`, "=", `${TableName.OrgMembership}.orgId`)
.andOn(`${TableName.UserAliases}.aliasType`, "=", (tx || db).raw("?", ["saml"]));
})
.select(
selectAllTableCols(TableName.OrgMembership),
db.ref("email").withSchema(TableName.Users),
db.ref("isEmailVerified").withSchema(TableName.Users),
db.ref("username").withSchema(TableName.Users),
db.ref("firstName").withSchema(TableName.Users),
db.ref("lastName").withSchema(TableName.Users),
@ -314,6 +376,7 @@ export const orgDALFactory = (db: TDbClient) => {
updateById,
deleteById,
findMembership,
findMembershipWithScimFilter,
createMembership,
updateMembershipById,
deleteMembershipById,

View File

@ -4,6 +4,8 @@ import { TDbClient } from "@app/db";
import { TableName, TSecretFolderVersions } from "@app/db/schemas";
import { DatabaseError } from "@app/lib/errors";
import { ormify, selectAllTableCols } from "@app/lib/knex";
import { logger } from "@app/lib/logger";
import { QueueName } from "@app/queue";
export type TSecretFolderVersionDALFactory = ReturnType<typeof secretFolderVersionDALFactory>;
@ -65,6 +67,7 @@ export const secretFolderVersionDALFactory = (db: TDbClient) => {
};
const pruneExcessVersions = async () => {
logger.info(`${QueueName.DailyResourceCleanUp}: pruning secret folder versions started`);
try {
await db(TableName.SecretFolderVersion)
.with("folder_cte", (qb) => {
@ -89,6 +92,7 @@ export const secretFolderVersionDALFactory = (db: TDbClient) => {
name: "Secret Folder Version Prune"
});
}
logger.info(`${QueueName.DailyResourceCleanUp}: pruning secret folder versions completed`);
};
return { ...secretFolderVerOrm, findLatestFolderVersions, findLatestVersionByFolderId, pruneExcessVersions };

View File

@ -158,9 +158,12 @@ export const fnSecretsV2FromImports = async ({
depth?: number;
cyclicDetector?: Set<string>;
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 }>>;
expandSecretReferences?: (inputSecret: {
value?: string;
skipMultilineEncoding?: boolean | null;
secretPath: string;
environment: string;
}) => Promise<string | undefined>;
}) => {
// avoid going more than a depth
if (depth >= LEVEL_BREAK) return [];
@ -244,26 +247,21 @@ export const fnSecretsV2FromImports = async ({
});
if (expandSecretReferences) {
await Promise.all(
processedImports.map(async (processedImport) => {
const secretsGroupByKey = processedImport.secrets.reduce(
(acc, item) => {
acc[item.secretKey] = {
value: item.secretValue,
comment: item.secretComment,
skipMultilineEncoding: item.skipMultilineEncoding
};
return acc;
},
{} as Record<string, { value: string; comment?: string; skipMultilineEncoding?: boolean | null }>
);
// eslint-disable-next-line
await expandSecretReferences(secretsGroupByKey);
processedImport.secrets.forEach((decryptedSecret) => {
// eslint-disable-next-line no-param-reassign
decryptedSecret.secretValue = secretsGroupByKey[decryptedSecret.secretKey].value;
});
})
await Promise.allSettled(
processedImports.map((processedImport) =>
Promise.allSettled(
processedImport.secrets.map(async (decryptedSecret, index) => {
const expandedSecretValue = await expandSecretReferences({
value: decryptedSecret.secretValue,
secretPath: processedImport.secretPath,
environment: processedImport.environment,
skipMultilineEncoding: decryptedSecret.skipMultilineEncoding
});
// eslint-disable-next-line no-param-reassign
processedImport.secrets[index].secretValue = expandedSecretValue || "";
})
)
)
);
}

View File

@ -4,6 +4,8 @@ import { TDbClient } from "@app/db";
import { TableName, TSecretSharing } from "@app/db/schemas";
import { DatabaseError } from "@app/lib/errors";
import { ormify, selectAllTableCols } from "@app/lib/knex";
import { logger } from "@app/lib/logger";
import { QueueName } from "@app/queue";
export type TSecretSharingDALFactory = ReturnType<typeof secretSharingDALFactory>;
@ -30,6 +32,7 @@ export const secretSharingDALFactory = (db: TDbClient) => {
};
const pruneExpiredSharedSecrets = async (tx?: Knex) => {
logger.info(`${QueueName.DailyResourceCleanUp}: pruning expired shared secret started`);
try {
const today = new Date();
const docs = await (tx || db)(TableName.SecretSharing)
@ -40,6 +43,7 @@ export const secretSharingDALFactory = (db: TDbClient) => {
tag: "",
iv: ""
});
logger.info(`${QueueName.DailyResourceCleanUp}: pruning expired shared secret completed`);
return docs;
} catch (error) {
throw new DatabaseError({ error, name: "pruneExpiredSharedSecrets" });

View File

@ -1,3 +1,6 @@
import bcrypt from "bcrypt";
import { TSecretSharing } from "@app/db/schemas";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { BadRequestError, ForbiddenRequestError, NotFoundError, UnauthorizedError } from "@app/lib/errors";
import { SecretSharingAccessType } from "@app/lib/types";
@ -36,6 +39,7 @@ export const secretSharingServiceFactory = ({
iv,
tag,
name,
password,
accessType,
expiresAt,
expiresAfterViews
@ -60,8 +64,10 @@ export const secretSharingServiceFactory = ({
throw new BadRequestError({ message: "Shared secret value too long" });
}
const hashedPassword = password ? await bcrypt.hash(password, 10) : null;
const newSharedSecret = await secretSharingDAL.create({
name,
password: hashedPassword,
encryptedValue,
hashedHex,
iv,
@ -77,6 +83,7 @@ export const secretSharingServiceFactory = ({
};
const createPublicSharedSecret = async ({
password,
encryptedValue,
hashedHex,
iv,
@ -102,7 +109,9 @@ export const secretSharingServiceFactory = ({
throw new BadRequestError({ message: "Shared secret value too long" });
}
const hashedPassword = password ? await bcrypt.hash(password, 10) : null;
const newSharedSecret = await secretSharingDAL.create({
password: hashedPassword,
encryptedValue,
hashedHex,
iv,
@ -111,6 +120,7 @@ export const secretSharingServiceFactory = ({
expiresAfterViews,
accessType
});
return { id: newSharedSecret.id };
};
@ -152,7 +162,21 @@ export const secretSharingServiceFactory = ({
};
};
const getActiveSharedSecretById = async ({ sharedSecretId, hashedHex, orgId }: TGetActiveSharedSecretByIdDTO) => {
const $decrementSecretViewCount = async (sharedSecret: TSecretSharing, sharedSecretId: string) => {
const { expiresAfterViews } = sharedSecret;
if (expiresAfterViews) {
// decrement view count if view count expiry set
await secretSharingDAL.updateById(sharedSecretId, { $decr: { expiresAfterViews: 1 } });
}
await secretSharingDAL.updateById(sharedSecretId, {
lastViewedAt: new Date()
});
};
/** Get's passwordless secret. validates all secret's requested (must be fresh). */
const getSharedSecretById = async ({ sharedSecretId, hashedHex, orgId, password }: TGetActiveSharedSecretByIdDTO) => {
const sharedSecret = await secretSharingDAL.findOne({
id: sharedSecretId,
hashedHex
@ -169,6 +193,8 @@ export const secretSharingServiceFactory = ({
if (accessType === SecretSharingAccessType.Organization && orgId !== sharedSecret.orgId)
throw new UnauthorizedError();
// all secrets pass through here, meaning we check if its expired first and then check if it needs verification
// or can be safely sent to the client.
if (expiresAt !== null && expiresAt < new Date()) {
// check lifetime expiry
await secretSharingDAL.softDeleteById(sharedSecretId);
@ -185,21 +211,29 @@ export const secretSharingServiceFactory = ({
});
}
if (expiresAfterViews) {
// decrement view count if view count expiry set
await secretSharingDAL.updateById(sharedSecretId, { $decr: { expiresAfterViews: 1 } });
const isPasswordProtected = Boolean(sharedSecret.password);
const hasProvidedPassword = Boolean(password);
if (isPasswordProtected) {
if (hasProvidedPassword) {
const isMatch = await bcrypt.compare(password as string, sharedSecret.password as string);
if (!isMatch) throw new UnauthorizedError({ message: "Invalid credentials" });
} else {
return { isPasswordProtected };
}
}
await secretSharingDAL.updateById(sharedSecretId, {
lastViewedAt: new Date()
});
// decrement when we are sure the user will view secret.
await $decrementSecretViewCount(sharedSecret, sharedSecretId);
return {
...sharedSecret,
orgName:
sharedSecret.accessType === SecretSharingAccessType.Organization && orgId === sharedSecret.orgId
? orgName
: undefined
isPasswordProtected,
secret: {
...sharedSecret,
orgName:
sharedSecret.accessType === SecretSharingAccessType.Organization && orgId === sharedSecret.orgId
? orgName
: undefined
}
};
};
@ -216,6 +250,6 @@ export const secretSharingServiceFactory = ({
createPublicSharedSecret,
getSharedSecrets,
deleteSharedSecretById,
getActiveSharedSecretById
getSharedSecretById
};
};

View File

@ -15,6 +15,7 @@ export type TSharedSecretPermission = {
orgId: string;
accessType?: SecretSharingAccessType;
name?: string;
password?: string;
};
export type TCreatePublicSharedSecretDTO = {
@ -24,6 +25,7 @@ export type TCreatePublicSharedSecretDTO = {
tag: string;
expiresAt: string;
expiresAfterViews?: number;
password?: string;
accessType: SecretSharingAccessType;
};
@ -31,6 +33,11 @@ export type TGetActiveSharedSecretByIdDTO = {
sharedSecretId: string;
hashedHex: string;
orgId?: string;
password?: string;
};
export type TValidateActiveSharedSecretDTO = TGetActiveSharedSecretByIdDTO & {
password: string;
};
export type TCreateSharedSecretDTO = TSharedSecretPermission & TCreatePublicSharedSecretDTO;

View File

@ -377,150 +377,116 @@ type TInterpolateSecretArg = {
folderDAL: Pick<TSecretFolderDALFactory, "findBySecretPath">;
};
const MAX_SECRET_REFERENCE_DEPTH = 10;
export const expandSecretReferencesFactory = ({
projectId,
decryptSecretValue: decryptSecret,
secretDAL,
folderDAL
}: TInterpolateSecretArg) => {
const fetchSecretFactory = () => {
const secretCache: Record<string, Record<string, string>> = {};
const secretCache: Record<string, Record<string, string>> = {};
const getCacheUniqueKey = (environment: string, secretPath: string) => `${environment}-${secretPath}`;
return async (secRefEnv: string, secRefPath: string[], secRefKey: string) => {
const referredSecretPathURL = path.join("/", ...secRefPath);
const uniqueKey = `${secRefEnv}-${referredSecretPathURL}`;
const fetchSecret = async (environment: string, secretPath: string, secretKey: string) => {
const cacheKey = getCacheUniqueKey(environment, secretPath);
if (secretCache?.[uniqueKey]) {
return secretCache[uniqueKey][secRefKey];
}
if (secretCache?.[cacheKey]) {
return secretCache[cacheKey][secretKey] || "";
}
const folder = await folderDAL.findBySecretPath(projectId, secRefEnv, referredSecretPathURL);
if (!folder) return "";
const secrets = await secretDAL.findByFolderId(folder.id);
const folder = await folderDAL.findBySecretPath(projectId, environment, secretPath);
if (!folder) return "";
const secrets = await secretDAL.findByFolderId(folder.id);
const decryptedSecret = secrets.reduce<Record<string, string>>((prev, secret) => {
// eslint-disable-next-line
prev[secret.key] = decryptSecret(secret.encryptedValue) || "";
return prev;
}, {});
const decryptedSecret = secrets.reduce<Record<string, string>>((prev, secret) => {
// eslint-disable-next-line no-param-reassign
prev[secret.key] = decryptSecret(secret.encryptedValue) || "";
return prev;
}, {});
secretCache[uniqueKey] = decryptedSecret;
secretCache[cacheKey] = decryptedSecret;
return secretCache[uniqueKey][secRefKey];
};
return secretCache[cacheKey][secretKey] || "";
};
const recursivelyExpandSecret = async (
expandedSec: Record<string, string | undefined>,
interpolatedSec: Record<string, string | undefined>,
fetchSecret: (env: string, secPath: string[], secKey: string) => Promise<string>,
recursionChainBreaker: Record<string, boolean>,
key: string
): Promise<string | undefined> => {
if (expandedSec?.[key] !== undefined) {
return expandedSec[key];
}
if (recursionChainBreaker?.[key]) {
return "";
}
// eslint-disable-next-line
recursionChainBreaker[key] = true;
const recursivelyExpandSecret = async (dto: { value?: string; secretPath: string; environment: string }) => {
if (!dto.value) return "";
let interpolatedValue = interpolatedSec[key];
if (!interpolatedValue) {
// eslint-disable-next-line no-console
console.error(`Couldn't find referenced value - ${key}`);
return "";
}
const stack = [{ ...dto, depth: 0 }];
let expandedValue = dto.value;
const refs = interpolatedValue.match(INTERPOLATION_SYNTAX_REG);
if (refs) {
for (const interpolationSyntax of refs) {
const interpolationKey = interpolationSyntax.slice(2, interpolationSyntax.length - 1);
const entities = interpolationKey.trim().split(".");
while (stack.length) {
const { value, secretPath, environment, depth } = stack.pop()!;
// eslint-disable-next-line no-continue
if (depth > MAX_SECRET_REFERENCE_DEPTH) continue;
const refs = value?.match(INTERPOLATION_SYNTAX_REG);
if (entities.length === 1) {
// eslint-disable-next-line
const val = await recursivelyExpandSecret(
expandedSec,
interpolatedSec,
fetchSecret,
recursionChainBreaker,
interpolationKey
);
if (val) {
interpolatedValue = interpolatedValue.replaceAll(interpolationSyntax, val);
}
// eslint-disable-next-line
continue;
}
if (refs) {
for (const interpolationSyntax of refs) {
const interpolationKey = interpolationSyntax.slice(2, interpolationSyntax.length - 1);
const entities = interpolationKey.trim().split(".");
if (entities.length > 1) {
const secRefEnv = entities[0];
const secRefPath = entities.slice(1, entities.length - 1);
const secRefKey = entities[entities.length - 1];
// eslint-disable-next-line no-continue
if (!entities.length) continue;
// eslint-disable-next-line
const val = await fetchSecret(secRefEnv, secRefPath, secRefKey);
if (val) {
interpolatedValue = interpolatedValue.replaceAll(interpolationSyntax, val);
if (entities.length === 1) {
const [secretKey] = entities;
// eslint-disable-next-line no-continue,no-await-in-loop
const referedValue = await fetchSecret(environment, secretPath, secretKey);
const cacheKey = getCacheUniqueKey(environment, secretPath);
secretCache[cacheKey][secretKey] = referedValue;
if (INTERPOLATION_SYNTAX_REG.test(referedValue)) {
stack.push({
value: referedValue,
secretPath,
environment,
depth: depth + 1
});
}
expandedValue = expandedValue.replaceAll(interpolationSyntax, referedValue);
} else {
const secretReferenceEnvironment = entities[0];
const secretReferencePath = path.join("/", ...entities.slice(1, entities.length - 1));
const secretReferenceKey = entities[entities.length - 1];
// eslint-disable-next-line no-await-in-loop
const referedValue = await fetchSecret(secretReferenceEnvironment, secretReferencePath, secretReferenceKey);
const cacheKey = getCacheUniqueKey(secretReferenceEnvironment, secretReferencePath);
secretCache[cacheKey][secretReferenceKey] = referedValue;
if (INTERPOLATION_SYNTAX_REG.test(referedValue)) {
stack.push({
value: referedValue,
secretPath: secretReferencePath,
environment: secretReferenceEnvironment,
depth: depth + 1
});
}
expandedValue = expandedValue.replaceAll(interpolationSyntax, referedValue);
}
}
}
}
// eslint-disable-next-line
expandedSec[key] = interpolatedValue;
return interpolatedValue;
return expandedValue;
};
const fetchSecret = fetchSecretFactory();
const expandSecrets = async (
inputSecrets: Record<string, { value?: string; comment?: string; skipMultilineEncoding?: boolean | null }>
) => {
const expandedSecrets: Record<string, string | undefined> = {};
const toBeExpandedSecrets: Record<string, string | undefined> = {};
const expandSecret = async (inputSecret: {
value?: string;
skipMultilineEncoding?: boolean | null;
secretPath: string;
environment: string;
}) => {
if (!inputSecret.value) return inputSecret.value;
Object.keys(inputSecrets).forEach((key) => {
if (inputSecrets[key].value?.match(INTERPOLATION_SYNTAX_REG)) {
toBeExpandedSecrets[key] = inputSecrets[key].value;
} else {
expandedSecrets[key] = inputSecrets[key].value;
}
});
const shouldExpand = Boolean(inputSecret.value?.match(INTERPOLATION_SYNTAX_REG));
if (!shouldExpand) return inputSecret.value;
for (const key of Object.keys(inputSecrets)) {
if (expandedSecrets?.[key]) {
// should not do multi line encoding if user has set it to skip
// eslint-disable-next-line
inputSecrets[key].value = inputSecrets[key].skipMultilineEncoding
? formatMultiValueEnv(expandedSecrets[key])
: expandedSecrets[key];
// eslint-disable-next-line
continue;
}
// this is to avoid recursion loop. So the graph should be direct graph rather than cyclic
// so for any recursion building if there is an entity two times same key meaning it will be looped
const recursionChainBreaker: Record<string, boolean> = {};
// eslint-disable-next-line
const expandedVal = await recursivelyExpandSecret(
expandedSecrets,
toBeExpandedSecrets,
fetchSecret,
recursionChainBreaker,
key
);
// eslint-disable-next-line
inputSecrets[key].value = inputSecrets[key].skipMultilineEncoding
? formatMultiValueEnv(expandedVal)
: expandedVal;
}
return inputSecrets;
const expandedSecretValue = await recursivelyExpandSecret(inputSecret);
return inputSecret.skipMultilineEncoding ? formatMultiValueEnv(expandedSecretValue) : expandedSecretValue;
};
return expandSecrets;
return expandSecret;
};
export const reshapeBridgeSecret = (

View File

@ -521,27 +521,22 @@ export const secretV2BridgeServiceFactory = ({
if (shouldExpandSecretReferences) {
const secretsGroupByPath = groupBy(filteredSecrets, (i) => i.secretPath);
for (const secretPathKey in secretsGroupByPath) {
if (Object.hasOwn(secretsGroupByPath, secretPathKey)) {
const secretsGroupByKey = secretsGroupByPath[secretPathKey].reduce(
(acc, item) => {
acc[item.secretKey] = {
value: item.secretValue,
comment: item.secretComment,
skipMultilineEncoding: item.skipMultilineEncoding
};
return acc;
},
{} as Record<string, { value?: string; comment?: string; skipMultilineEncoding?: boolean | null }>
);
// eslint-disable-next-line
await expandSecretReferences(secretsGroupByKey);
secretsGroupByPath[secretPathKey].forEach((decryptedSecret) => {
// eslint-disable-next-line no-param-reassign
decryptedSecret.secretValue = secretsGroupByKey[decryptedSecret.secretKey].value || "";
});
}
}
await Promise.allSettled(
Object.keys(secretsGroupByPath).map((groupedPath) =>
Promise.allSettled(
secretsGroupByPath[groupedPath].map(async (decryptedSecret, index) => {
const expandedSecretValue = await expandSecretReferences({
value: decryptedSecret.secretValue,
secretPath: groupedPath,
environment,
skipMultilineEncoding: decryptedSecret.skipMultilineEncoding
});
// eslint-disable-next-line no-param-reassign
secretsGroupByPath[groupedPath][index].secretValue = expandedSecretValue || "";
})
)
)
);
}
if (!includeImports) {
@ -693,12 +688,14 @@ export const secretV2BridgeServiceFactory = ({
? secretManagerDecryptor({ cipherTextBlob: secret.encryptedValue }).toString()
: "";
if (shouldExpandSecretReferences && secretValue) {
const secretReferenceExpandedRecord = {
[secret.key]: { value: secretValue }
};
// eslint-disable-next-line
await expandSecretReferences(secretReferenceExpandedRecord);
secretValue = secretReferenceExpandedRecord[secret.key].value;
const expandedSecretValue = await expandSecretReferences({
environment,
secretPath: path,
value: secretValue,
skipMultilineEncoding: secret.skipMultilineEncoding
});
secretValue = expandedSecretValue || "";
}
return reshapeBridgeSecret(projectId, environment, path, {

View File

@ -4,6 +4,8 @@ import { TDbClient } from "@app/db";
import { TableName, TSecretVersionsV2, TSecretVersionsV2Update } from "@app/db/schemas";
import { BadRequestError, DatabaseError } from "@app/lib/errors";
import { ormify, selectAllTableCols } from "@app/lib/knex";
import { logger } from "@app/lib/logger";
import { QueueName } from "@app/queue";
export type TSecretVersionV2DALFactory = ReturnType<typeof secretVersionV2BridgeDALFactory>;
@ -87,6 +89,7 @@ export const secretVersionV2BridgeDALFactory = (db: TDbClient) => {
};
const pruneExcessVersions = async () => {
logger.info(`${QueueName.DailyResourceCleanUp}: pruning secret version v2 started`);
try {
await db(TableName.SecretVersionV2)
.with("version_cte", (qb) => {
@ -112,6 +115,7 @@ export const secretVersionV2BridgeDALFactory = (db: TDbClient) => {
name: "Secret Version Prune"
});
}
logger.info(`${QueueName.DailyResourceCleanUp}: pruning secret version v2 completed`);
};
return {

View File

@ -196,6 +196,13 @@ export const recursivelyGetSecretPaths = ({
return getPaths;
};
// used to convert multi line ones to quotes ones with \n
const formatMultiValueEnv = (val?: string) => {
if (!val) return "";
if (!val.match("\n")) return val;
return `"${val.replace(/\n/g, "\\n")}"`;
};
type TInterpolateSecretArg = {
projectId: string;
secretEncKey: string;
@ -203,162 +210,128 @@ type TInterpolateSecretArg = {
folderDAL: Pick<TSecretFolderDALFactory, "findBySecretPath">;
};
const MAX_SECRET_REFERENCE_DEPTH = 5;
const INTERPOLATION_SYNTAX_REG = /\${([^}]+)}/g;
export const interpolateSecrets = ({ projectId, secretEncKey, secretDAL, folderDAL }: TInterpolateSecretArg) => {
const fetchSecretsCrossEnv = () => {
const fetchCache: Record<string, Record<string, string>> = {};
const secretCache: Record<string, Record<string, string>> = {};
const getCacheUniqueKey = (environment: string, secretPath: string) => `${environment}-${secretPath}`;
return async (secRefEnv: string, secRefPath: string[], secRefKey: string) => {
const secRefPathUrl = path.join("/", ...secRefPath);
const uniqKey = `${secRefEnv}-${secRefPathUrl}`;
const fetchSecret = async (environment: string, secretPath: string, secretKey: string) => {
const cacheKey = getCacheUniqueKey(environment, secretPath);
const uniqKey = `${environment}-${cacheKey}`;
if (fetchCache?.[uniqKey]) {
return fetchCache[uniqKey][secRefKey];
}
if (secretCache?.[uniqKey]) {
return secretCache[uniqKey][secretKey] || "";
}
const folder = await folderDAL.findBySecretPath(projectId, secRefEnv, secRefPathUrl);
if (!folder) return "";
const secrets = await secretDAL.findByFolderId(folder.id);
const folder = await folderDAL.findBySecretPath(projectId, environment, secretPath);
if (!folder) return "";
const secrets = await secretDAL.findByFolderId(folder.id);
const decryptedSec = secrets.reduce<Record<string, string>>((prev, secret) => {
const secretKey = decryptSymmetric128BitHexKeyUTF8({
ciphertext: secret.secretKeyCiphertext,
iv: secret.secretKeyIV,
tag: secret.secretKeyTag,
key: secretEncKey
});
const secretValue = decryptSymmetric128BitHexKeyUTF8({
ciphertext: secret.secretValueCiphertext,
iv: secret.secretValueIV,
tag: secret.secretValueTag,
key: secretEncKey
});
const decryptedSec = secrets.reduce<Record<string, string>>((prev, secret) => {
const decryptedSecretKey = decryptSymmetric128BitHexKeyUTF8({
ciphertext: secret.secretKeyCiphertext,
iv: secret.secretKeyIV,
tag: secret.secretKeyTag,
key: secretEncKey
});
const decryptedSecretValue = decryptSymmetric128BitHexKeyUTF8({
ciphertext: secret.secretValueCiphertext,
iv: secret.secretValueIV,
tag: secret.secretValueTag,
key: secretEncKey
});
// eslint-disable-next-line
prev[secretKey] = secretValue;
return prev;
}, {});
// eslint-disable-next-line
prev[decryptedSecretKey] = decryptedSecretValue;
return prev;
}, {});
fetchCache[uniqKey] = decryptedSec;
secretCache[uniqKey] = decryptedSec;
return fetchCache[uniqKey][secRefKey];
};
return secretCache[uniqKey][secretKey] || "";
};
const recursivelyExpandSecret = async (
expandedSec: Record<string, string>,
interpolatedSec: Record<string, string>,
fetchCrossEnv: (env: string, secPath: string[], secKey: string) => Promise<string>,
recursionChainBreaker: Record<string, boolean>,
key: string
) => {
if (expandedSec?.[key] !== undefined) {
return expandedSec[key];
}
if (recursionChainBreaker?.[key]) {
return "";
}
// eslint-disable-next-line
recursionChainBreaker[key] = true;
const recursivelyExpandSecret = async ({
value,
secretPath,
environment,
depth = 0
}: {
value?: string;
secretPath: string;
environment: string;
depth?: number;
}) => {
if (!value) return "";
if (depth > MAX_SECRET_REFERENCE_DEPTH) return "";
let interpolatedValue = interpolatedSec[key];
if (!interpolatedValue) {
// eslint-disable-next-line no-console
console.error(`Couldn't find referenced value - ${key}`);
return "";
}
const refs = interpolatedValue.match(INTERPOLATION_SYNTAX_REG);
const refs = value.match(INTERPOLATION_SYNTAX_REG);
let expandedValue = value;
if (refs) {
for (const interpolationSyntax of refs) {
const interpolationKey = interpolationSyntax.slice(2, interpolationSyntax.length - 1);
const entities = interpolationKey.trim().split(".");
if (entities.length === 1) {
const val = await recursivelyExpandSecret(
expandedSec,
interpolatedSec,
fetchCrossEnv,
recursionChainBreaker,
interpolationKey
);
if (val) {
interpolatedValue = interpolatedValue.replaceAll(interpolationSyntax, val);
}
const [secretKey] = entities;
// eslint-disable-next-line
continue;
let referenceValue = await fetchSecret(environment, secretPath, secretKey);
if (INTERPOLATION_SYNTAX_REG.test(referenceValue)) {
// eslint-disable-next-line
referenceValue = await recursivelyExpandSecret({
environment,
secretPath,
value: referenceValue,
depth: depth + 1
});
}
const cacheKey = getCacheUniqueKey(environment, secretPath);
secretCache[cacheKey][secretKey] = referenceValue;
expandedValue = expandedValue.replaceAll(interpolationSyntax, referenceValue);
}
if (entities.length > 1) {
const secRefEnv = entities[0];
const secRefPath = entities.slice(1, entities.length - 1);
const secRefKey = entities[entities.length - 1];
const secretReferenceEnvironment = entities[0];
const secretReferencePath = path.join("/", ...entities.slice(1, entities.length - 1));
const secretReferenceKey = entities[entities.length - 1];
const val = await fetchCrossEnv(secRefEnv, secRefPath, secRefKey);
if (val) {
interpolatedValue = interpolatedValue.replaceAll(interpolationSyntax, val);
// eslint-disable-next-line
let referenceValue = await fetchSecret(secretReferenceEnvironment, secretReferencePath, secretReferenceKey);
if (INTERPOLATION_SYNTAX_REG.test(referenceValue)) {
// eslint-disable-next-line
referenceValue = await recursivelyExpandSecret({
environment: secretReferenceEnvironment,
secretPath: secretReferencePath,
value: referenceValue,
depth: depth + 1
});
}
const cacheKey = getCacheUniqueKey(secretReferenceEnvironment, secretReferencePath);
secretCache[cacheKey][secretReferenceKey] = referenceValue;
expandedValue = expandedValue.replaceAll(interpolationSyntax, referenceValue);
}
}
}
// eslint-disable-next-line
expandedSec[key] = interpolatedValue;
return interpolatedValue;
return expandedValue;
};
// used to convert multi line ones to quotes ones with \n
const formatMultiValueEnv = (val?: string) => {
if (!val) return "";
if (!val.match("\n")) return val;
return `"${val.replace(/\n/g, "\\n")}"`;
const expandSecret = async (inputSecret: {
value?: string;
skipMultilineEncoding?: boolean | null;
secretPath: string;
environment: string;
}) => {
if (!inputSecret.value) return inputSecret.value;
const shouldExpand = Boolean(inputSecret.value?.match(INTERPOLATION_SYNTAX_REG));
if (!shouldExpand) return inputSecret.value;
const expandedSecretValue = await recursivelyExpandSecret(inputSecret);
return inputSecret.skipMultilineEncoding ? formatMultiValueEnv(expandedSecretValue) : expandedSecretValue;
};
const expandSecrets = async (
secrets: Record<string, { value: string; comment?: string; skipMultilineEncoding?: boolean | null }>
) => {
const expandedSec: Record<string, string> = {};
const interpolatedSec: Record<string, string> = {};
const crossSecEnvFetch = fetchSecretsCrossEnv();
Object.keys(secrets).forEach((key) => {
if (secrets[key].value.match(INTERPOLATION_SYNTAX_REG)) {
interpolatedSec[key] = secrets[key].value;
} else {
expandedSec[key] = secrets[key].value;
}
});
for (const key of Object.keys(secrets)) {
if (expandedSec?.[key]) {
// should not do multi line encoding if user has set it to skip
// eslint-disable-next-line
secrets[key].value = secrets[key].skipMultilineEncoding
? formatMultiValueEnv(expandedSec[key])
: expandedSec[key];
// eslint-disable-next-line
continue;
}
// this is to avoid recursion loop. So the graph should be direct graph rather than cyclic
// so for any recursion building if there is an entity two times same key meaning it will be looped
const recursionChainBreaker: Record<string, boolean> = {};
const expandedVal = await recursivelyExpandSecret(
expandedSec,
interpolatedSec,
crossSecEnvFetch,
recursionChainBreaker,
key
);
// eslint-disable-next-line
secrets[key].value = secrets[key].skipMultilineEncoding ? formatMultiValueEnv(expandedVal) : expandedVal;
}
return secrets;
};
return expandSecrets;
return expandSecret;
};
export const decryptSecretRaw = (

View File

@ -258,6 +258,7 @@ export const secretQueueFactory = ({
const getIntegrationSecretsV2 = async (dto: {
projectId: string;
environment: string;
secretPath: string;
folderId: string;
depth: number;
decryptor: (value: Buffer | null | undefined) => string;
@ -269,30 +270,36 @@ export const secretQueueFactory = ({
);
return content;
}
// process secrets in current folder
const secrets = await secretV2BridgeDAL.findByFolderId(dto.folderId);
secrets.forEach((secret) => {
const secretKey = secret.key;
const secretValue = dto.decryptor(secret.encryptedValue);
content[secretKey] = { value: secretValue };
if (secret.encryptedComment) {
const commentValue = dto.decryptor(secret.encryptedComment);
content[secretKey].comment = commentValue;
}
content[secretKey].skipMultilineEncoding = Boolean(secret.skipMultilineEncoding);
});
const expandSecretReferences = expandSecretReferencesFactory({
decryptSecretValue: dto.decryptor,
secretDAL: secretV2BridgeDAL,
folderDAL,
projectId: dto.projectId
});
// process secrets in current folder
const secrets = await secretV2BridgeDAL.findByFolderId(dto.folderId);
await Promise.allSettled(
secrets.map(async (secret) => {
const secretKey = secret.key;
const secretValue = dto.decryptor(secret.encryptedValue);
const expandedSecretValue = await expandSecretReferences({
environment: dto.environment,
secretPath: dto.secretPath,
skipMultilineEncoding: secret.skipMultilineEncoding,
value: secretValue
});
content[secretKey] = { value: expandedSecretValue || "" };
if (secret.encryptedComment) {
const commentValue = dto.decryptor(secret.encryptedComment);
content[secretKey].comment = commentValue;
}
content[secretKey].skipMultilineEncoding = Boolean(secret.skipMultilineEncoding);
})
);
await expandSecretReferences(content);
// check if current folder has any imports from other folders
const secretImports = await secretImportDAL.find({ folderId: dto.folderId, isReplication: false });
@ -329,6 +336,7 @@ export const secretQueueFactory = ({
const getIntegrationSecrets = async (dto: {
projectId: string;
environment: string;
secretPath: string;
folderId: string;
key: string;
depth: number;
@ -341,46 +349,52 @@ export const secretQueueFactory = ({
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
});
const secretValue = decryptSymmetric128BitHexKeyUTF8({
ciphertext: secret.secretValueCiphertext,
iv: secret.secretValueIV,
tag: secret.secretValueTag,
key: dto.key
});
content[secretKey] = { value: secretValue };
if (secret.secretCommentCiphertext && secret.secretCommentIV && secret.secretCommentTag) {
const commentValue = decryptSymmetric128BitHexKeyUTF8({
ciphertext: secret.secretCommentCiphertext,
iv: secret.secretCommentIV,
tag: secret.secretCommentTag,
key: dto.key
});
content[secretKey].comment = commentValue;
}
content[secretKey].skipMultilineEncoding = Boolean(secret.skipMultilineEncoding);
});
const expandSecrets = interpolateSecrets({
const expandSecretReferences = interpolateSecrets({
projectId: dto.projectId,
secretEncKey: dto.key,
folderDAL,
secretDAL
});
await expandSecrets(content);
// process secrets in current folder
const secrets = await secretDAL.findByFolderId(dto.folderId);
await Promise.allSettled(
secrets.map(async (secret) => {
const secretKey = decryptSymmetric128BitHexKeyUTF8({
ciphertext: secret.secretKeyCiphertext,
iv: secret.secretKeyIV,
tag: secret.secretKeyTag,
key: dto.key
});
const secretValue = decryptSymmetric128BitHexKeyUTF8({
ciphertext: secret.secretValueCiphertext,
iv: secret.secretValueIV,
tag: secret.secretValueTag,
key: dto.key
});
const expandedSecretValue = await expandSecretReferences({
environment: dto.environment,
secretPath: dto.secretPath,
skipMultilineEncoding: secret.skipMultilineEncoding,
value: secretValue
});
content[secretKey] = { value: expandedSecretValue || "" };
if (secret.secretCommentCiphertext && secret.secretCommentIV && secret.secretCommentTag) {
const commentValue = decryptSymmetric128BitHexKeyUTF8({
ciphertext: secret.secretCommentCiphertext,
iv: secret.secretCommentIV,
tag: secret.secretCommentTag,
key: dto.key
});
content[secretKey].comment = commentValue;
}
content[secretKey].skipMultilineEncoding = Boolean(secret.skipMultilineEncoding);
})
);
// check if current folder has any imports from other folders
const secretImport = await secretImportDAL.find({ folderId: dto.folderId, isReplication: false });
@ -404,7 +418,8 @@ export const secretQueueFactory = ({
projectId: dto.projectId,
folderId: folder.id,
key: dto.key,
depth: dto.depth + 1
depth: dto.depth + 1,
secretPath: dto.secretPath
});
// add the imported secrets to the current folder secrets
@ -686,6 +701,7 @@ export const secretQueueFactory = ({
projectId,
folderId: folder.id,
depth: 1,
secretPath,
decryptor: (value) => (value ? secretManagerDecryptor({ cipherTextBlob: value }).toString() : "")
})
: await getIntegrationSecrets({
@ -693,7 +709,8 @@ export const secretQueueFactory = ({
projectId,
folderId: folder.id,
key: botKey as string,
depth: 1
depth: 1,
secretPath
});
for (const integration of toBeSyncedIntegrations) {

View File

@ -482,7 +482,7 @@ export const secretServiceFactory = ({
projectId,
environmentSlug: folder.environment.slug
});
// TODO(akhilmhdh-pg): licence check, posthog service and snapshot
// TODO(akhilmhdh-pg): license check, posthog service and snapshot
return { ...deletedSecret[0], _id: deletedSecret[0].id, workspace: projectId, environment, secretPath: path };
};
@ -1047,74 +1047,47 @@ export const secretServiceFactory = ({
};
});
const expandSecret = interpolateSecrets({
folderDAL,
projectId,
secretDAL,
secretEncKey: botKey
});
if (expandSecretReferences) {
const expandSecrets = interpolateSecrets({
folderDAL,
projectId,
secretDAL,
secretEncKey: botKey
});
const batchSecretsExpand = async (
secretBatch: {
secretKey: string;
secretValue: string;
secretComment?: string;
secretPath: string;
skipMultilineEncoding: boolean | null | undefined;
}[]
) => {
// Group secrets by secretPath
const secretsByPath: Record<
string,
{
secretKey: string;
secretValue: string;
secretComment?: string;
skipMultilineEncoding: boolean | null | undefined;
}[]
> = {};
secretBatch.forEach((secret) => {
if (!secretsByPath[secret.secretPath]) {
secretsByPath[secret.secretPath] = [];
}
secretsByPath[secret.secretPath].push(secret);
});
// Expand secrets for each group
for (const secPath in secretsByPath) {
if (!Object.hasOwn(secretsByPath, path)) {
// eslint-disable-next-line no-continue
continue;
}
const secretRecord: Record<
string,
{ value: string; comment?: string; skipMultilineEncoding: boolean | null | undefined }
> = {};
secretsByPath[secPath].forEach((decryptedSecret) => {
secretRecord[decryptedSecret.secretKey] = {
value: decryptedSecret.secretValue,
comment: decryptedSecret.secretComment,
skipMultilineEncoding: decryptedSecret.skipMultilineEncoding
};
});
await expandSecrets(secretRecord);
secretsByPath[secPath].forEach((decryptedSecret) => {
// eslint-disable-next-line no-param-reassign
decryptedSecret.secretValue = secretRecord[decryptedSecret.secretKey].value;
});
}
};
// expand secrets
await batchSecretsExpand(filteredSecrets);
// expand imports by batch
await Promise.all(processedImports.map((processedImport) => batchSecretsExpand(processedImport.secrets)));
const secretsGroupByPath = groupBy(filteredSecrets, (i) => i.secretPath);
await Promise.allSettled(
Object.keys(secretsGroupByPath).map((groupedPath) =>
Promise.allSettled(
secretsGroupByPath[groupedPath].map(async (decryptedSecret, index) => {
const expandedSecretValue = await expandSecret({
value: decryptedSecret.secretValue,
secretPath: groupedPath,
environment,
skipMultilineEncoding: decryptedSecret.skipMultilineEncoding
});
// eslint-disable-next-line no-param-reassign
secretsGroupByPath[groupedPath][index].secretValue = expandedSecretValue || "";
})
)
)
);
await Promise.allSettled(
processedImports.map((processedImport) =>
Promise.allSettled(
processedImport.secrets.map(async (decryptedSecret, index) => {
const expandedSecretValue = await expandSecret({
value: decryptedSecret.secretValue,
secretPath: path,
environment,
skipMultilineEncoding: decryptedSecret.skipMultilineEncoding
});
// eslint-disable-next-line no-param-reassign
processedImport.secrets[index].secretValue = expandedSecretValue || "";
})
)
)
);
}
return {
@ -1177,40 +1150,19 @@ export const secretServiceFactory = ({
const decryptedSecret = decryptSecretRaw(encryptedSecret, botKey);
if (expandSecretReferences) {
const expandSecrets = interpolateSecrets({
const expandSecret = interpolateSecrets({
folderDAL,
projectId,
secretDAL,
secretEncKey: botKey
});
const expandSingleSecret = async (secret: {
secretKey: string;
secretValue: string;
secretComment?: string;
secretPath: string;
skipMultilineEncoding: boolean | null | undefined;
}) => {
const secretRecord: Record<
string,
{ value: string; comment?: string; skipMultilineEncoding: boolean | null | undefined }
> = {
[secret.secretKey]: {
value: secret.secretValue,
comment: secret.secretComment,
skipMultilineEncoding: secret.skipMultilineEncoding
}
};
await expandSecrets(secretRecord);
// Update the secret with the expanded value
// eslint-disable-next-line no-param-reassign
secret.secretValue = secretRecord[secret.secretKey].value;
};
// Expand the secret
await expandSingleSecret(decryptedSecret);
const expandedSecretValue = await expandSecret({
environment,
secretPath: path,
value: decryptedSecret.secretValue,
skipMultilineEncoding: decryptedSecret.skipMultilineEncoding
});
decryptedSecret.secretValue = expandedSecretValue || "";
}
return decryptedSecret;

View File

@ -4,6 +4,8 @@ import { TDbClient } from "@app/db";
import { TableName, TSecretVersions, TSecretVersionsUpdate } from "@app/db/schemas";
import { BadRequestError, DatabaseError } from "@app/lib/errors";
import { ormify, selectAllTableCols } from "@app/lib/knex";
import { logger } from "@app/lib/logger";
import { QueueName } from "@app/queue";
export type TSecretVersionDALFactory = ReturnType<typeof secretVersionDALFactory>;
@ -112,6 +114,7 @@ export const secretVersionDALFactory = (db: TDbClient) => {
};
const pruneExcessVersions = async () => {
logger.info(`${QueueName.DailyResourceCleanUp}: pruning secret version v1 started`);
try {
await db(TableName.SecretVersion)
.with("version_cte", (qb) => {
@ -137,6 +140,7 @@ export const secretVersionDALFactory = (db: TDbClient) => {
name: "Secret Version Prune"
});
}
logger.info(`${QueueName.DailyResourceCleanUp}: pruning secret version v1 completed`);
};
return {

View File

@ -1,4 +1,4 @@
import tsconfigPaths from "vite-tsconfig-paths"; // only if you are using custom tsconfig paths
import path from "path";
import { defineConfig } from "vitest/config";
export default defineConfig({
@ -15,7 +15,14 @@ export default defineConfig({
useAtomics: true,
isolate: false
}
},
alias: {
"./license-fns": path.resolve(__dirname, "./src/ee/services/license/__mocks__/license-fns")
}
},
plugins: [tsconfigPaths()] // only if you are using custom tsconfig paths,
resolve: {
alias: {
"@app": path.resolve(__dirname, "./src")
}
}
});

View File

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

View File

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

View File

@ -19,10 +19,11 @@ Every new joiner has an onboarding buddy who should ideally be in the the same t
1. Join the weekly all-hands meeting. It typically happens on Monday's at 8:30am PT.
2. Ship something together on day one even if tiny! It feels great to hit the ground running, with a development environment all ready to go.
3. Check out the [Areas of Responsibility (AoR) Table](https://docs.google.com/spreadsheets/d/1RnXlGFg83Sgu0dh7ycuydsSobmFfI3A0XkGw7vrVxEI/edit?usp=sharing). This is helpful to know who you can ask about particular areas of Infisical. Feel free to add yourself to the areas you'd be most interesting to dive into.
4. Read the [Infisical Strategy Doc](https://docs.google.com/document/d/1oy_NP1Q_Zt1oqxLpyNkLIGmhAI3N28AmZq6dDIOONSQ/edit?usp=sharing).
4. Read the [Infisical Strategy Doc](https://docs.google.com/document/d/1RaJd3RoS2QpWLFHlgfHaXnHqCCwRt6mCGZkbJ75J_D0/edit?usp=sharing).
5. Update your LinkedIn profile with one of [Infisical's official banners](https://drive.google.com/drive/u/0/folders/1oSNWjbpRl9oNYwxM_98IqzKs9fAskrb2) (if you want to). You can also coordinate your social posts in the #marketing Slack channel, so that we can boost it from Infisical's official social media accounts.
6. Over the first few weeks, feel free to schedule 1:1s with folks on the team to get to know them a bit better.
7. Change your Slack username in the users channel to `[NAME] (Infisical)`.
8. Go through the [technical overview](https://infisical.com/docs/internals/overview) of Infisical.
9. Request a company credit card (Maidul will be able to help with that).

View File

@ -0,0 +1,144 @@
---
title: "AWS ElastiCache"
description: "Learn how to dynamically generate AWS ElastiCache user credentials."
---
The Infisical AWS ElastiCache dynamic secret allows you to generate AWS ElastiCache credentials on demand based on configured role.
## Prerequisites
2. Create an AWS IAM user with the following permissions:
```json
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "",
"Effect": "Allow",
"Action": [
"elasticache:DescribeUsers",
"elasticache:ModifyUser",
"elasticache:CreateUser",
"elasticache:CreateUserGroup",
"elasticache:DeleteUser",
"elasticache:DescribeReplicationGroups",
"elasticache:DescribeUserGroups",
"elasticache:ModifyReplicationGroup",
"elasticache:ModifyUserGroup"
],
"Resource": "arn:aws:elasticache:<region>:<account-id>:user:*"
}
]
}
```
3. Create an access key ID and secret access key for the user you created in the previous step. You will need these to configure the Infisical dynamic secret.
<Note>
New leases may take up-to a couple of minutes before ElastiCache has the chance to complete their configuration.
It is recommended to use a retry strategy when establishing new ElastiCache connections.
This may prevent errors when trying to use a password that isn't yet live on the targeted ElastiCache cluster.
While a leasing is being created, you will be unable to create new leases for the same dynamic secret.
</Note>
<Note>
Please ensure that your ElastiCache cluster has transit encryption enabled and set to required. This is required for the dynamic secret to work.
</Note>
## Set up Dynamic Secrets with AWS ElastiCache
<Steps>
<Step title="Open Secret Overview Dashboard">
Open the Secret Overview dashboard and select the environment in which you would like to add a dynamic secret.
</Step>
<Step title="Click on the 'Add Dynamic Secret' button">
![Add Dynamic Secret Button](../../../images/platform/dynamic-secrets/add-dynamic-secret-button-redis.png)
</Step>
<Step title="Select 'AWS ElastiCache'">
![Dynamic Secret Modal](../../../images/platform/dynamic-secrets/dynamic-secret-modal-aws-elasti-cache.png)
</Step>
<Step title="Provide the inputs for dynamic secret parameters">
<ParamField path="Secret Name" type="string" required>
Name by which you want the secret to be referenced
</ParamField>
<ParamField path="Default TTL" type="string" required>
Default time-to-live for a generated secret (it is possible to modify this value when a secret is generate)
</ParamField>
<ParamField path="Max TTL" type="string" required>
Maximum time-to-live for a generated secret.
</ParamField>
<ParamField path="Region" type="string" required>
The region that the ElastiCache cluster is located in. _(e.g. us-east-1)_
</ParamField>
<ParamField path="Access Key ID" type="string" required>
This is the access key ID of the AWS IAM user you created in the prerequisites. This will be used to provision and manage the dynamic secret leases.
</ParamField>
<ParamField path="Secret Access Key" type="string" required>
This is the secret access key of the AWS IAM user you created in the prerequisites. This will be used to provision and manage the dynamic secret leases.
</ParamField>
<ParamField path="CA(SSL)" type="string">
A CA may be required if your DB requires it for incoming connections. This is often the case when connecting to a managed service.
</ParamField>
</Step>
<Step title="(Optional) Modify ElastiCache Statements">
If you want to provide specific privileges for the generated dynamic credentials, you can modify the ElastiCache statement to your needs. This is useful if you want to only give access to a specific table(s).
![Modify ElastiCache Statements Modal](/images/platform/dynamic-secrets/modify-elasticache-statement.png)
</Step>
<Step title="Click `Submit`">
After submitting the form, you will see a dynamic secret created in the dashboard.
<Note>
If this step fails, you may have to add the CA certificate.
</Note>
</Step>
<Step title="Generate dynamic secrets">
Once you've successfully configured the dynamic secret, you're ready to generate on-demand credentials.
To do this, simply click on the 'Generate' button which appears when hovering over the dynamic secret item.
Alternatively, you can initiate the creation of a new lease by selecting 'New Lease' from the dynamic secret lease list section.
![Dynamic Secret](/images/platform/dynamic-secrets/dynamic-secret-generate-redis.png)
![Dynamic Secret](/images/platform/dynamic-secrets/dynamic-secret-lease-empty-redis.png)
When generating these secrets, it's important to specify a Time-to-Live (TTL) duration. This will dictate how long the credentials are valid for.
![Provision Lease](/images/platform/dynamic-secrets/provision-lease-redis.png)
<Tip>
Ensure that the TTL for the lease fall within the maximum TTL defined when configuring the dynamic secret.
</Tip>
Once you click the `Submit` button, a new secret lease will be generated and the credentials from it will be shown to you.
![Provision Lease](/images/platform/dynamic-secrets/lease-values-redis.png)
</Step>
</Steps>
## Audit or Revoke Leases
Once you have created one or more leases, you will be able to access them by clicking on the respective dynamic secret item on the dashboard.
This will allow you see the expiration time of the lease or delete a lease before it's set time to live.
![Provision Lease](/images/platform/dynamic-secrets/lease-data-redis.png)
## Renew Leases
To extend the life of the generated dynamic secret leases past its initial time to live, simply click on the **Renew** as illustrated below.
![Provision Lease](/images/platform/dynamic-secrets/dynamic-secret-lease-renew-redis.png)
<Warning>
Lease renewals cannot exceed the maximum TTL set when configuring the dynamic secret
</Warning>

View File

@ -1,6 +1,6 @@
---
title: "AWS IAM"
description: "How to dynamically generate AWS IAM Users."
description: "Learn how to dynamically generate AWS IAM Users."
---
The Infisical AWS IAM dynamic secret allows you to generate AWS IAM Users on demand based on configured AWS policy.

View File

@ -1,6 +1,6 @@
---
title: "Cassandra"
description: "How to dynamically generate Cassandra database users."
description: "Learn how to dynamically generate Cassandra database user credentials"
---
The Infisical Cassandra dynamic secret allows you to generate Cassandra database credentials on demand based on configured role.

View File

@ -0,0 +1,127 @@
---
title: "Elasticsearch"
description: "Learn how to dynamically generate Elasticsearch user credentials."
---
The Infisical Elasticsearch dynamic secret allows you to generate Elasticsearch credentials on demand based on configured role.
## Prerequisites
1. Create a role with at least `manage_security` and `monitor` permissions.
2. Assign the newly created role to your API key or user that you'll use later in the dynamic secret configuration.
<Note>
For testing purposes, you can also use a highly privileged role like `superuser`, that will have full control over the cluster. This is not recommended in production environments following the principle of least privilege.
</Note>
## Set up Dynamic Secrets with Elasticsearch
<Steps>
<Step title="Open Secret Overview Dashboard">
Open the Secret Overview dashboard and select the environment in which you would like to add a dynamic secret.
</Step>
<Step title="Click on the 'Add Dynamic Secret' button">
![Add Dynamic Secret Button](../../../images/platform/dynamic-secrets/add-dynamic-secret-button-redis.png)
</Step>
<Step title="Select 'Elasticsearch'">
![Dynamic Secret Modal](../../../images/platform/dynamic-secrets/dynamic-secret-modal-elastic-search.png)
</Step>
<Step title="Provide the inputs for dynamic secret parameters">
<ParamField path="Secret Name" type="string" required>
Name by which you want the secret to be referenced
</ParamField>
<ParamField path="Default TTL" type="string" required>
Default time-to-live for a generated secret (it is possible to modify this value when a secret is generate)
</ParamField>
<ParamField path="Max TTL" type="string" required>
Maximum time-to-live for a generated secret.
</ParamField>
<ParamField path="Host" type="string" required>
Your Elasticsearch host. This is the endpoint that your instance runs on. _(Example: https://your-cluster-ip)_
</ParamField>
<ParamField path="Port" type="string" required>
The port that your Elasticsearch instance is running on. _(Example: 9200)_
</ParamField>
<ParamField path="Roles" type="string[]" required>
The roles that the new user that is created when a lease is provisioned will be assigned to. This is a required field. This defaults to `superuser`, which is highly privileged. It is recommended to create a new role with the least privileges required for the lease.
</ParamField>
<ParamField path="Authentication Method" type="API Key | Username/Password" required>
Select the authentication method you want to use to connect to your Elasticsearch instance.
</ParamField>
<ParamField path="Username" type="string" required>
The username of the user that will be used to provision new dynamic secret leases. Only required if you selected the `Username/Password` authentication method.
</ParamField>
<ParamField path="Password" type="string" required>
The password of the user that will be used to provision new dynamic secret leases. Only required if you selected the `Username/Password` authentication method.
</ParamField>
<ParamField path="API Key ID" required>
The ID of the API key that will be used to provision new dynamic secret leases. Only required if you selected the `API Key` authentication method.
</ParamField>
<ParamField path="API Key" required>
The API key that will be used to provision new dynamic secret leases. Only required if you selected the `API Key` authentication method.
</ParamField>
<ParamField path="CA(SSL)" type="string">
A CA may be required if your DB requires it for incoming connections. This is often the case when connecting to a managed service.
</ParamField>
![Dynamic Secret Setup Modal](../../../images/platform/dynamic-secrets/dynamic-secret-input-modal-elastic-search.png)
</Step>
<Step title="Click `Submit`">
After submitting the form, you will see a dynamic secret created in the dashboard.
<Note>
If this step fails, you may have to add the CA certificate.
</Note>
</Step>
<Step title="Generate dynamic secrets">
Once you've successfully configured the dynamic secret, you're ready to generate on-demand credentials.
To do this, simply click on the 'Generate' button which appears when hovering over the dynamic secret item.
Alternatively, you can initiate the creation of a new lease by selecting 'New Lease' from the dynamic secret lease list section.
![Dynamic Secret](/images/platform/dynamic-secrets/dynamic-secret-generate-redis.png)
![Dynamic Secret](/images/platform/dynamic-secrets/dynamic-secret-lease-empty-redis.png)
When generating these secrets, it's important to specify a Time-to-Live (TTL) duration. This will dictate how long the credentials are valid for.
![Provision Lease](/images/platform/dynamic-secrets/provision-lease-redis.png)
<Tip>
Ensure that the TTL for the lease fall within the maximum TTL defined when configuring the dynamic secret.
</Tip>
Once you click the `Submit` button, a new secret lease will be generated and the credentials from it will be shown to you.
![Provision Lease](/images/platform/dynamic-secrets/lease-values-elastic-search.png)
</Step>
</Steps>
## Audit or Revoke Leases
Once you have created one or more leases, you will be able to access them by clicking on the respective dynamic secret item on the dashboard.
This will allow you see the expiration time of the lease or delete a lease before it's set time to live.
![Provision Lease](/images/platform/dynamic-secrets/lease-data-redis.png)
## Renew Leases
To extend the life of the generated dynamic secret leases past its initial time to live, simply click on the **Renew** as illustrated below.
![Provision Lease](/images/platform/dynamic-secrets/dynamic-secret-lease-renew-redis.png)
<Warning>
Lease renewals cannot exceed the maximum TTL set when configuring the dynamic secret
</Warning>

View File

@ -0,0 +1,114 @@
---
title: "Mongo Atlas"
description: "Learn how to dynamically generate Mongo Atlas Database user credentials."
---
The Infisical Mongo Atlas dynamic secret allows you to generate Mongo Atlas Database credentials on demand based on configured role.
## Prerequisite
Create a project scopped API Key with the required permission in your Mongo Atlas following the [official doc](https://www.mongodb.com/docs/atlas/configure-api-access/#grant-programmatic-access-to-a-project).
<Info>
The API Key must have permission to manage users in the project.
</Info>
## Set up Dynamic Secrets with Mongo Atlas
<Steps>
<Step title="Open Secret Overview Dashboard">
Open the Secret Overview dashboard and select the environment in which you would like to add a dynamic secret.
</Step>
<Step title="Click on the 'Add Dynamic Secret' button">
![Add Dynamic Secret Button](../../../images/platform/dynamic-secrets/add-dynamic-secret-button.png)
</Step>
<Step title="Select Mongo Atlas">
![Dynamic Secret Modal](../../../images/platform/dynamic-secrets/dynamic-secret-atlas-modal.png)
</Step>
<Step title="Provide the inputs for dynamic secret parameters">
<ParamField path="Secret Name" type="string" required>
Name by which you want the secret to be referenced
</ParamField>
<ParamField path="Default TTL" type="string" required>
Default time-to-live for a generated secret (it is possible to modify this value when a secret is generate)
</ParamField>
<ParamField path="Max TTL" type="string" required>
Maximum time-to-live for a generated secret
</ParamField>
<ParamField path="Admin public key" type="string" required>
The public key of your generated Atlas API Key. This acts as a username.
</ParamField>
<ParamField path="Admin private key" type="string" required>
The private key of your generated Atlas API Key. This acts as a password.
</ParamField>
<ParamField path="Group ID" type="number" required>
Unique 24-hexadecimal digit string that identifies your project. This is same as project id
</ParamField>
<ParamField path="Roles" type="string" required>
List that provides the pairings of one role with one applicable database.
- **Database Name**: Database to which the user is granted access privileges.
- **Collection**: Collection on which this role applies.
- **Role Name**: Human-readable label that identifies a group of privileges assigned to a database user. This value can either be a built-in role or a custom role.
- Enum: `atlasAdmin` `backup` `clusterMonitor` `dbAdmin` `dbAdminAnyDatabase` `enableSharding` `read` `readAnyDatabase` `readWrite` `readWriteAnyDatabase` `<a custom role name>`.
</ParamField>
![Dynamic Secret Setup Modal](../../../images/platform/dynamic-secrets/dynamic-secret-modal-atlas.png)
</Step>
<Step title="(Optional) Modify Access Scope">
List that contains clusters, MongoDB Atlas Data Lakes, and MongoDB Atlas Streams Instances that this database user can access. If omitted, MongoDB Cloud grants the database user access to all the clusters, MongoDB Atlas Data Lakes, and MongoDB Atlas Streams Instances in the project.
![Modify Scope Modal](../../../images/platform/dynamic-secrets/advanced-option-atlas.png)
- **Label**: Human-readable label that identifies the cluster or MongoDB Atlas Data Lake that this database user can access.
- **Type**: Category of resource that this database user can access.
</Step>
<Step title="Click 'Submit'">
After submitting the form, you will see a dynamic secret created in the dashboard.
<Note>
If this step fails, you may have to add the CA certficate.
</Note>
![Dynamic Secret](../../../images/platform/dynamic-secrets/dynamic-secret.png)
</Step>
<Step title="Generate dynamic secrets">
Once you've successfully configured the dynamic secret, you're ready to generate on-demand credentials.
To do this, simply click on the 'Generate' button which appears when hovering over the dynamic secret item.
Alternatively, you can initiate the creation of a new lease by selecting 'New Lease' from the dynamic secret lease list section.
![Dynamic Secret](/images/platform/dynamic-secrets/dynamic-secret-generate.png)
![Dynamic Secret](/images/platform/dynamic-secrets/dynamic-secret-lease-empty.png)
When generating these secrets, it's important to specify a Time-to-Live (TTL) duration. This will dictate how long the credentials are valid for.
![Provision Lease](/images/platform/dynamic-secrets/provision-lease.png)
<Tip>
Ensure that the TTL for the lease fall within the maximum TTL defined when configuring the dynamic secret.
</Tip>
Once you click the `Submit` button, a new secret lease will be generated and the credentials for it will be shown to you.
![Provision Lease](/images/platform/dynamic-secrets/lease-values.png)
</Step>
</Steps>
## Audit or Revoke Leases
Once you have created one or more leases, you will be able to access them by clicking on the respective dynamic secret item on the dashboard.
This will allow you see the expiration time of the lease or delete a lease before it's set time to live.
![Provision Lease](/images/platform/dynamic-secrets/lease-data.png)
## Renew Leases
To extend the life of the generated dynamic secret leases past its initial time to live, simply click on the **Renew** as illustrated below.
![Provision Lease](/images/platform/dynamic-secrets/dynamic-secret-lease-renew.png)
<Warning>
Lease renewals cannot exceed the maximum TTL set when configuring the dynamic secret
</Warning>

View File

@ -1,6 +1,6 @@
---
title: "MS SQL"
description: "How to dynamically generate MS SQL database users."
description: "Learn how to dynamically generate MS SQL database user credentials."
---
The Infisical MS SQL dynamic secret allows you to generate Microsoft SQL server database credentials on demand based on configured role.

View File

@ -1,6 +1,6 @@
---
title: "MySQL"
description: "Learn how to dynamically generate MySQL Database user passwords."
description: "Learn how to dynamically generate MySQL Database user credentials."
---
The Infisical MySQL dynamic secret allows you to generate MySQL Database credentials on demand based on configured role.

View File

@ -1,6 +1,6 @@
---
title: "Oracle"
description: "Learn how to dynamically generate Oracle Database user passwords."
description: "Learn how to dynamically generate Oracle Database user credentials."
---
The Infisical Oracle dynamic secret allows you to generate Oracle Database credentials on demand based on configured role.

View File

@ -32,4 +32,5 @@ Dynamic secrets are particularly useful in environments with stringent security
2. [MySQL](./mysql)
3. [Cassandra](./cassandra)
4. [Oracle](./oracle)
6. [Redis](./redis)
5. [AWS IAM](./aws-iam)

View File

@ -1,6 +1,6 @@
---
title: "PostgreSQL"
description: "How to dynamically generate PostgreSQL database users."
description: "Learn how to dynamically generate PostgreSQL database users."
---
The Infisical PostgreSQL dynamic secret allows you to generate PostgreSQL database credentials on demand based on configured role.

View File

@ -0,0 +1,106 @@
---
title: "Redis"
description: "Learn how to dynamically generate Redis Database user credentials."
---
The Infisical Redis dynamic secret allows you to generate Redis Database credentials on demand based on configured role.
## Prerequisite
Create a user with the required permission in your Redis instance. This user will be used to create new accounts on-demand.
## Set up Dynamic Secrets with Redis
<Steps>
<Step title="Open Secret Overview Dashboard">
Open the Secret Overview dashboard and select the environment in which you would like to add a dynamic secret.
</Step>
<Step title="Click on the 'Add Dynamic Secret' button">
![Add Dynamic Secret Button](../../../images/platform/dynamic-secrets/add-dynamic-secret-button-redis.png)
</Step>
<Step title="Select 'Redis'">
![Dynamic Secret Modal](../../../images/platform/dynamic-secrets/dynamic-secret-modal-redis.png)
</Step>
<Step title="Provide the inputs for dynamic secret parameters">
<ParamField path="Secret Name" type="string" required>
Name by which you want the secret to be referenced
</ParamField>
<ParamField path="Default TTL" type="string" required>
Default time-to-live for a generated secret (it is possible to modify this value when a secret is generate)
</ParamField>
<ParamField path="Max TTL" type="string" required>
Maximum time-to-live for a generated secret.
</ParamField>
<ParamField path="Host" type="string" required>
The database host, this can be an IP address or a domain name as long as Infisical can reach it.
</ParamField>
<ParamField path="Port" type="number" required>
The database port, this is the port that the Redis instance is listening on.
</ParamField>
<ParamField path="User" type="string" required>
Redis username that will be used to create new users on-demand. This is often 'default' or 'admin'.
</ParamField>
<ParamField path="Password" type="string" optional>
Password that will be used to create dynamic secrets. This is required if your Redis instance is password protected.
</ParamField>
<ParamField path="CA(SSL)" type="string">
A CA may be required if your DB requires it for incoming connections. This is often the case when connecting to a managed service.
</ParamField>
</Step>
<Step title="(Optional) Modify Redis Statements">
If you want to provide specific privileges for the generated dynamic credentials, you can modify the Redis statement to your needs. This is useful if you want to only give access to a specific table(s).
![Modify Redis Statements Modal](/images/platform/dynamic-secrets/modify-redis-statement.png)
</Step>
<Step title="Click `Submit`">
After submitting the form, you will see a dynamic secret created in the dashboard.
<Note>
If this step fails, you may have to add the CA certificate.
</Note>
</Step>
<Step title="Generate dynamic secrets">
Once you've successfully configured the dynamic secret, you're ready to generate on-demand credentials.
To do this, simply click on the 'Generate' button which appears when hovering over the dynamic secret item.
Alternatively, you can initiate the creation of a new lease by selecting 'New Lease' from the dynamic secret lease list section.
![Dynamic Secret](/images/platform/dynamic-secrets/dynamic-secret-generate-redis.png)
![Dynamic Secret](/images/platform/dynamic-secrets/dynamic-secret-lease-empty-redis.png)
When generating these secrets, it's important to specify a Time-to-Live (TTL) duration. This will dictate how long the credentials are valid for.
![Provision Lease](/images/platform/dynamic-secrets/provision-lease-redis.png)
<Tip>
Ensure that the TTL for the lease fall within the maximum TTL defined when configuring the dynamic secret.
</Tip>
Once you click the `Submit` button, a new secret lease will be generated and the credentials from it will be shown to you.
![Provision Lease](/images/platform/dynamic-secrets/lease-values-redis.png)
</Step>
</Steps>
## Audit or Revoke Leases
Once you have created one or more leases, you will be able to access them by clicking on the respective dynamic secret item on the dashboard.
This will allow you see the expiration time of the lease or delete a lease before it's set time to live.
![Provision Lease](/images/platform/dynamic-secrets/lease-data-redis.png)
## Renew Leases
To extend the life of the generated dynamic secret leases past its initial time to live, simply click on the **Renew** as illustrated below.
![Provision Lease](/images/platform/dynamic-secrets/dynamic-secret-lease-renew-redis.png)
<Warning>
Lease renewals cannot exceed the maximum TTL set when configuring the dynamic secret
</Warning>

View File

@ -21,7 +21,8 @@ These endpoints are exposed on port 8443 under the .well-known/est path e.g.
- You need to have an existing [CA hierarchy](/documentation/platform/pki/private-ca).
- The client devices need to have a bootstrap/pre-installed certificate.
- The client devices must trust the server certificates used by Infisical's EST server. If the devices are new or lack existing trust configurations, you need to manually establish trust for the appropriate certificates. When using Infisical Cloud, this means establishing trust for certificates issued by AWS.
- The client devices must trust the server certificates used by Infisical's EST server. If the devices are new or lack existing trust configurations, you need to manually establish trust for the appropriate certificates.
- For Infisical Cloud users, the devices must be configured to trust the [Amazon root CA certificates](https://www.amazontrust.com/repository).
## Guide to configuring EST
@ -42,7 +43,7 @@ These endpoints are exposed on port 8443 under the .well-known/est path e.g.
4. Once the configuration of enrollment options is completed, a new **EST Label** field appears in the enrollment settings. This is the value to use as label in the URL when configuring the connection of EST clients to Infisical.
![est enrollment modal create](/images/platform/pki/est/template-enrollment-est-label.png)
For demonstration, the complete URL of the supported EST endpoints will look like the following:
The complete URL of the supported EST endpoints will look like the following:
- https://app.infisical.com:8443/.well-known/est/f110f308-9888-40ab-b228-237b12de8b96/cacerts
- https://app.infisical.com:8443/.well-known/est/f110f308-9888-40ab-b228-237b12de8b96/simpleenroll

View File

@ -0,0 +1,247 @@
---
title: "Kubernetes Issuer"
sidebarTitle: "Certificates for Kubernetes"
description: "Learn how to automatically provision and manage TLS certificates for in Kubernetes using Infisical PKI"
---
## Concept
The Infisical PKI Issuer is an installable Kubernetes [cert-manager](https://cert-manager.io/) controller that uses Infisical PKI to sign certificate requests. The issuer is perfect for getting X.509 certificates for ingresses and other Kubernetes resources and capable of automatically renewing certificates as needed.
As part of the workflow, you install `cert-manager`, the Infisical PKI Issuer, and configure resources to represent the connection details to your Infisical PKI and the certificates you wish to issue. Each issued certificate and corresponding private key is made available in a Kubernetes secret.
We recommend reading the [cert-manager documentation](https://cert-manager.io/docs/) for a fuller understanding of all the moving parts.
## Workflow
A typical workflow for using the Infisical PKI Issuer to issue certificates for your Kubernetes resources consists of the following steps:
1. Creating a machine identity in Infisical.
2. Creating a Kubernetes secret to store the credentials of the machine identity.
3. Installing `cert-manager` into your Kubernetes cluster.
4. Installing the Infisical PKI Issuer controller into your Kubernetes cluster.
5. Creating an `Issuer` or `ClusterIssuer` resource in your Kubernetes cluster to represent the Infisical PKI issuer you wish to use.
6. Creating a `Certificate` resource in your Kubernetes cluster to represent a certificate you wish to issue. As part of this step, you specify the Kubernetes `Secret` to create and store the issued certificate and private key.
7. Consuming the issued certificate across your Kubernetes resources from the specified Kubernetes `Secret`.
## Guide
In the following steps, we explore how to install the Infisical PKI Issuer using [kubectl](https://github.com/kubernetes/kubectl) and use it to obtain certificates for your Kubernetes resources.
<Steps>
<Step title="Create an identity in Infisical">
Follow the instructions [here](/documentation/platform/identities/universal-auth) to configure a [machine identity](/documentation/platform/identities/machine-identities) in Infisical with Universal Auth.
By the end of this step, you should have a **Client ID** and **Client Secret** on hand as part of the Universal Auth configuration for the Infisical PKI Issuer to authenticate with Infisical; this will be useful in steps 4 and 5.
<Note>
Currently, the Infisical PKI Issuer only supports authenticating with Infisical via the [Universal Auth](/documentation/platform/identities/universal-auth) authentication method.
We're planning to add support for [Kubernetes Auth](/documentation/platform/identities/kubernetes-auth) in the near future.
</Note>
</Step>
<Step title="Install cert-manager">
Install `cert-manager` into your Kubernetes cluster by following the instructions [here](https://cert-manager.io/docs/installation/) or by running the following command:
```bash
kubectl apply -f https://github.com/cert-manager/cert-manager/releases/download/v1.15.3/cert-manager.yaml
```
</Step>
<Step title="Install the Issuer Controller">
Install the Infisical PKI Issuer controller into your Kubernetes cluster by running the following command:
```bash
kubectl apply -f https://raw.githubusercontent.com/Infisical/infisical-issuer/main/build/install.yaml
```
</Step>
<Step title="Create Kubernetes Secret for Infisical PKI Issuer">
Start by creating a Kubernetes `Secret` containing the **Client Secret** from step 1. As mentioned previously, this will be used by the Infisical PKI issuer to authenticate with Infisical.
<Tabs>
<Tab title="kubectl command">
```bash
kubectl create secret generic issuer-infisical-client-secret \
--namespace <namespace_you_want_to_issue_certificates_in> \
--from-literal=clientSecret=<client_secret>
```
</Tab>
<Tab title="Configuration file">
```yaml secret-issuer.yaml
apiVersion: v1
kind: Secret
metadata:
name: issuer-infisical-client-secret
namespace: <namespace_you_want_to_issue_certificates_in>
data:
clientSecret: <client_secret>
```
```bash
kubectl apply -f secret-issuer.yaml
```
</Tab>
</Tabs>
</Step>
<Step title="Create Infisical PKI Issuer">
Next, create the Infisical PKI Issuer by filling out `url`, `clientId`, either `caId` or `certificateTemplateId`, and applying the following configuration file for the `Issuer` resource.
This configuration file specifies the connection details to your Infisical PKI CA to be used for issuing certificates.
```yaml infisical-issuer.yaml
apiVersion: infisical-issuer.infisical.com/v1alpha1
kind: Issuer
metadata:
name: issuer-infisical
namespace: <namespace_you_want_to_issue_certificates_in>
spec:
url: "https://app.infisical.com" # the URL of your Infisical instance
caId: <ca_id> # the ID of the CA you want to use to issue certificates
certificateTemplateId: <certificate_template_id> # the ID of the certificate template you want to use to issue certificates against
authentication:
universalAuth:
clientId: <client_id> # the Client ID from step 1
secretRef: # reference to the Secret created in step 4
name: "issuer-infisical-client-secret"
key: "clientSecret"
```
```
kubectl apply -f infisical-issuer.yaml
```
<Warning>
The Infisical PKI Issuer supports issuing certificates against a specific CA or a specific certificate template.
For this reason, you should only fill in the `caId` or the `certificateTemplateId` field but not both.
We recommend using the `certificateTemplateId` field to issue certificates against a specific [certificate template](/documentation/platform/pki/certificate-templates)
since templates let you enforce constraints on issued certificates and may have alerting policies bound to them.
</Warning>
You can check that the issuer was created successfully by running the following command:
```bash
kubectl get issuers.infisical-issuer.infisical.com -n <namespace_of_issuer> -o wide
```
```bash
NAME AGE
issuer-infisical 21h
```
<Note>
An `Issuer` is a namespaced resource, and it is not possible to issue certificates from an `Issuer` in a different namespace.
This means you will need to create an `Issuer` in each namespace you wish to obtain `Certificates` in.
If you want to create a single `Issuer` that can be consumed in multiple namespaces, you should consider creating a `ClusterIssuer` resource. This is almost identical to the `Issuer` resource, however is non-namespaced so it can be used to issue `Certificates` across all namespaces.
You can read more about the `Issuer` and `ClusterIssuer` resources [here](https://cert-manager.io/docs/configuration/).
</Note>
</Step>
<Step title="Create Certificate">
Finally, create a `Certificate` by applying the following configuration file.
This configuration file specifies the details of the (end-entity/leaf) certificate to be issued.
```yaml certificate-issuer.yaml
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: certificate-by-issuer
namespace: <namespace_you_want_to_issue_certificates_in>
spec:
commonName: certificate-by-issuer.example.com # the common name for the certificate
secretName: certificate-by-issuer # the name of the Kubernetes Secret to create and store the certificate and private key in
issuerRef:
name: issuer-infisical
group: infisical-issuer.infisical.com
kind: Issuer
privateKey: # the algorithm and key size to use
algorithm: ECDSA
size: 256
duration: 48h # the ttl for the certificate
renewBefore: 12h # the time before the certificate expiry that the certificate should be automatically renewed
```
The above sample configuration file specifies a certificate to be issued with the common name `certificate-by-issuer.example.com` and ECDSA private key using the P-256 curve, valid for 48 hours; the certificate will be automatically renewed by `cert-manager` 12 hours before expiry.
The certificate is issued by the issuer `issuer-infisical` created in the previous step and the resulting certificate and private key will be stored in a secret named `certificate-by-issuer`.
Note that the full list of the fields supported on the `Certificate` resource can be found in the API reference documentation [here](https://cert-manager.io/docs/reference/api-docs/#cert-manager.io/v1.CertificateSpec).
You can check that the certificate was created successfully by running the following command:
```bash
kubectl get certificates -n <namespace_of_your_certificate> -o wide
```
```bash
NAME READY SECRET ISSUER STATUS AGE
certificate-by-issuer True certificate-by-issuer issuer-infisical Certificate is up to date and has not expired 20h
```
</Step>
<Step title="Use Certificate in Kubernetes Secret">
Since the actual certificate and private key are stored in a Kubernetes secret, we can check that the secret was created successfully by running the following command:
```bash
kubectl get secret certificate-by-issuer -n <namespace_of_your_certificate>
```
```bash
NAME TYPE DATA AGE
certificate-by-issuer kubernetes.io/tls 2 26h
```
We can `describe` the secret to get more information about it:
```bash
kubectl describe secret certificate-by-issuer -n default
```
```bash
Name: certificate-by-issuer
Namespace: default
Labels: controller.cert-manager.io/fao=true
Annotations: cert-manager.io/alt-names:
cert-manager.io/certificate-name: certificate-by-issuer
cert-manager.io/common-name: certificate-by-issuer.example.com
cert-manager.io/ip-sans:
cert-manager.io/issuer-group: infisical-issuer.infisical.com
cert-manager.io/issuer-kind: Issuer
cert-manager.io/issuer-name: issuer-infisical
cert-manager.io/uri-sans:
Type: kubernetes.io/tls
Data
====
tls.key: 227 bytes
tls.crt: 912 bytes
```
We can decode the certificate and print it out using `openssl`:
```bash
kubectl get secret certificate-by-issuer -n default -o jsonpath='{.data.tls\.crt}' | base64 --decode | openssl x509 -text -noout
```
In any case, the certificate is ready to be used as Kubernetes Secret by your Kubernetes resources.
</Step>
</Steps>
## FAQ
<AccordionGroup>
<Accordion title="What fields can be configured on the Certificate resource?">
The full list of the fields supported on the `Certificate` resource can be found in the API reference documentation [here](https://cert-manager.io/docs/reference/api-docs/#cert-manager.io/v1.CertificateSpec).
<Note>
Currently, not all fields are supported by the Infisical PKI Issuer.
</Note>
</Accordion>
<Accordion title="Can certificates be renewed automatically?">
Yes. `cert-manager` will automatically renew certificates according to the `renewBefore` threshold of expiry as
specified in the corresponding `Certificate` resource.
You can read more about the `renewBefore` field [here](https://cert-manager.io/docs/reference/api-docs/#cert-manager.io/v1.CertificateSpec).
</Accordion>
</AccordionGroup>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 691 KiB

After

Width:  |  Height:  |  Size: 700 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 709 KiB

After

Width:  |  Height:  |  Size: 710 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 715 KiB

After

Width:  |  Height:  |  Size: 696 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 200 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 197 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 173 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 133 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 136 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 157 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 151 KiB

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