1
0
mirror of https://github.com/Infisical/infisical.git synced 2025-03-21 15:01:38 +00:00

Compare commits

..

698 Commits

Author SHA1 Message Date
d6b7045461 Merge pull request from Infisical/fix/address-client-side-error-secret-approval-page
fix: add loading screen for user context
2024-09-19 02:59:18 +08:00
bd9c9ea1f4 fix: add loading screen for user context 2024-09-19 02:33:03 +08:00
5740d2b4e4 Merge pull request from Infisical/daniel/integration-ui-improvements
feat: integration details page with logging
2024-09-17 14:29:26 +04:00
09887a7405 Update ConfiguredIntegrationItem.tsx 2024-09-16 23:05:38 +04:00
38ee3a005e Requested changes 2024-09-16 22:26:36 +04:00
10e7999334 Merge pull request from Infisical/misc/address-slack-env-related-error
misc: addressed slack env config validation error
2024-09-17 02:16:07 +08:00
8c458588ab misc: removed from .env.example 2024-09-17 01:25:16 +08:00
2381a2e4ba misc: addressed slack env config validation error 2024-09-17 01:19:45 +08:00
9ef8812205 Merge pull request from Infisical/misc/added-handling-of-no-project-access
misc: added handling of no project access for redirects
2024-09-17 01:07:35 +08:00
37a204e49e misc: addressed review comment 2024-09-16 23:27:10 +08:00
11927f341a Merge pull request from Infisical/daniel/aws-sm-secrets-prefix
feat(integrations): aws secrets manager secrets prefixing support
2024-09-16 18:24:40 +04:00
6fc17a4964 Update license-fns.ts 2024-09-16 18:15:35 +04:00
eb00232db6 Merge pull request from Infisical/misc/allow-direct-project-assignment-even-with-group
misc: allow direct project assignment even with group access
2024-09-16 22:04:43 +08:00
4fd245e493 Merge pull request from meetcshah19/meet/allow-unlimited-users
Don't enforce max user and identity limits
2024-09-16 19:27:02 +05:30
d92c57d051 misc: allow direct project assignment even with group access 2024-09-16 21:35:45 +08:00
beaef1feb0 Merge pull request from Infisical/daniel/fix-project-role-desc-update
fix: updating role description
2024-09-16 16:47:21 +04:00
033fd5e7a4 fix: updating role description 2024-09-16 16:42:11 +04:00
f49f3c926c misc: added handling of no project access for redirects 2024-09-16 20:00:54 +08:00
280d44f1e5 Merge pull request from Infisical/fix/addressed-group-view-issue-in-approval-creation
fix: address group view issue encountered during policy creation
2024-09-16 19:40:03 +08:00
4eea0dc544 fix(integrations): improved github repos fetching 2024-09-16 15:37:44 +04:00
8a33f1a591 feat(integrations): aws secrets manager prefix support 2024-09-16 15:36:41 +04:00
74653e7ed1 Minor ui improvements 2024-09-16 13:56:23 +04:00
56ff11d63f fix: address group view issue encountered during approval creation 2024-09-16 14:17:14 +08:00
1ecce285f0 Merge pull request from scott-ray-wilson/secret-env-access-warning
Fix: Restricted Secret Environment UI Corrections
2024-09-15 19:08:23 -04:00
b5c9b6a1bd fix: hide envs without read permission in secret main page nav header dropdown 2024-09-15 12:36:42 -07:00
e12ac6c07e fix: hide envs without read permission in the env filter dropdown 2024-09-15 12:29:24 -07:00
8a0b1bb427 Update IntegrationAuditLogsSection.tsx 2024-09-15 20:34:08 +04:00
1f6faadf81 Cleanup 2024-09-15 20:24:23 +04:00
8f3b7e1698 feat: audit logs event metadata & remapping support 2024-09-15 20:01:43 +04:00
24c460c695 feat: integration details page 2024-09-15 20:00:43 +04:00
8acceab1e7 fix: updated last used to be considered last success sync 2024-09-15 19:57:56 +04:00
d60aba9339 fix: added missing integration metadata attributes 2024-09-15 19:57:36 +04:00
3a228f7521 feat: improved audit logs 2024-09-15 19:57:02 +04:00
3f7ac0f142 feat: integration synced log event 2024-09-15 19:52:43 +04:00
63cf535ebb feat: platform-level actor for logs 2024-09-15 19:52:13 +04:00
69a2a46c47 Update organization-router.ts 2024-09-15 19:51:54 +04:00
d081077273 feat: integration sync logs 2024-09-15 19:51:38 +04:00
75034f9350 feat: more expendable audit logs 2024-09-15 19:50:03 +04:00
eacd7b0c6a feat: made audit logs more searchable with better filters 2024-09-15 19:49:35 +04:00
5bad77083c feat: more expendable audit logs 2024-09-15 19:49:07 +04:00
ea480c222b update default to 20 per page 2024-09-14 23:26:30 -04:00
1fb644af4a include secret path in dependency array 2024-09-14 07:01:02 -07:00
a6f4a95821 Merge pull request from Infisical/cancel-button-fix
fixed inactive cancel button
2024-09-14 09:52:01 -04:00
8578208f2d fix: hide environments that users does not have read access too 2024-09-14 06:50:45 -07:00
fc4189ba0f fixed inactive cancel button 2024-09-13 21:31:08 -07:00
b9ecf42fb6 fix: unlimited users and identities only for enterprise and remove frontend check 2024-09-14 05:54:50 +05:30
008e18638f Merge pull request from Infisical/daniel/fix-invalid-role-creation
fix(project-roles): creation of invalid project roles
2024-09-13 16:42:02 -04:00
ac3b9c25dd Update permissions.mdx 2024-09-14 00:33:52 +04:00
f4997dec12 Update project-role-service.ts 2024-09-13 23:59:08 +04:00
fcf405c630 docs(permissions): creation of project roles with invalid permissions 2024-09-13 23:56:19 +04:00
efc6876260 fix(api): creation of project roles with invalid permissions 2024-09-13 23:55:56 +04:00
1025759efb Feat: Integration Audit Logs 2024-09-13 21:00:47 +04:00
8bab6d87bb Merge pull request from scott-ray-wilson/secrets-pagination-fix
Fix: Account for secret import count in secrets offset
2024-09-13 07:37:42 -07:00
39a49f12f5 fix: account for secret import count in secrets offset 2024-09-13 07:27:52 -07:00
cfd841ea08 Merge pull request from meetcshah19/meet/add-empty-value-log-gcp
chore: add log on empty value being pushed to gcp
2024-09-13 19:53:38 +05:30
4d67c03e3e Merge pull request from scott-ray-wilson/secrets-pagination
Feature: Secrets Overview Page Pagination/Optimizations
2024-09-13 09:56:48 -04:00
8826bc5d60 fix: include imports in secret pagination, and rectify tag/value search not working for secrets 2024-09-13 06:25:13 -07:00
03fdce67f1 Merge pull request from akhilmhdh/fix/saml-entra
fix: resolved entra failing
2024-09-13 09:08:07 -04:00
72f3f7980e Merge pull request from Infisical/misc/address-minor-cert-lint-issues
misc: addressed minor cert lint issues
2024-09-13 20:57:40 +08:00
f1aa2fbd84 chore: better log string 2024-09-13 15:34:12 +05:30
=
217de6250f feat: pagination for main secret page 2024-09-13 14:12:53 +05:30
f742bd01d9 refactor to useCallback select instead of queryFn 2024-09-12 22:47:23 -07:00
3fe53d5183 remove unused import 2024-09-12 22:08:16 -07:00
a5f5f803df feature: secret overview page pagination/optimizations 2024-09-12 21:44:38 -07:00
c37e3ba635 misc: addressed comments 2024-09-13 12:44:12 +08:00
55279e5e41 Merge pull request from Infisical/pki-docs-improvement
Update README (Expand on PKI / New Features)
2024-09-12 20:16:41 -07:00
88fb37e8c6 Made changes as per review 2024-09-12 20:14:25 -07:00
6271dcc25d Fix mint.json openapi link back 2024-09-12 20:02:40 -07:00
0f7faa6bfe Update README to include newer features, expand on PKI, separate PKI endpoints into separate section in API reference 2024-09-12 19:58:55 -07:00
4ace339d5b Update README to include newer features, expand on PKI, separate PKI endpoints into separate section in API reference 2024-09-12 19:57:37 -07:00
=
e8c0d1ece9 fix: resolved entra failing 2024-09-13 07:18:49 +05:30
bb1977976c Merge pull request from Infisical/maidful-edwdwqdhwjq
revert PR 
2024-09-12 20:43:38 -04:00
bb3da75870 Minor text updates 2024-09-12 17:26:56 -07:00
088e888560 Merge pull request from scott-ray-wilson/identity-pagination-fix
Fix: Apply Project Identity Pagination Prior to Left Join of Roles
2024-09-12 20:23:03 -04:00
180241fdf0 revert PR 2024-09-13 00:15:26 +00:00
93f27a7ee8 improvement: make limit conditional 2024-09-12 16:19:22 -07:00
ed3bc8dd27 fix: apply project identity offset/limit separate from left joins 2024-09-12 16:11:58 -07:00
8dc4809ec8 Merge pull request from akhilmhdh/ui/combobox
UI/combobox
2024-09-12 18:50:43 -04:00
a55d64e430 chore: add log on empty value being pushed to gcp 2024-09-13 03:52:09 +05:30
02d54da74a resolve change requests 2024-09-12 15:22:05 -07:00
=
d660168700 fix: org invite check only when needed 2024-09-13 00:35:48 +05:30
=
1c75fc84f0 feat: added a temporary combobox for identity addition to project 2024-09-13 00:35:48 +05:30
f63da87c7f Merge remote-tracking branch 'origin/main' into misc/address-minor-cert-lint-issues 2024-09-13 01:46:00 +08:00
53b9fe2dec Merge pull request from Infisical/feat/add-key-usages-for-template-and-cert
feat: add support for configuring certificate key usage and extended key usage
2024-09-13 00:55:19 +08:00
87dc0eed7e fix: addressed tslint errors 2024-09-12 23:25:26 +08:00
f2dd6f94a4 Merge pull request from scott-ray-wilson/identity-pagination
Feature: Project and Org Identities Table Additions: Pagination, Search and Sort
2024-09-12 11:22:45 -04:00
ac26ae3893 misc: addressed minor cert lint issues 2024-09-12 23:16:49 +08:00
4c65e9910a resolve merge conflict 2024-09-12 08:03:10 -07:00
5e5ab29ab9 Feat: Integration UI improvements 2024-09-12 13:09:00 +04:00
5150c102e6 Merge pull request from Infisical/daniel/invite-multiple-members-to-project
feat: invite multiple members to projects with role assignment
2024-09-12 11:16:41 +04:00
41c29d41e1 Update AddMemberModal.tsx 2024-09-12 11:13:39 +04:00
4de33190a9 Rebase fixes 2024-09-12 11:12:45 +04:00
7cfecb39e4 Update AddMemberModal.tsx 2024-09-12 11:08:25 +04:00
7524b83c29 Delete project-membership-fns.ts 2024-09-12 11:08:25 +04:00
7a41cdf51b Fix: type errors 2024-09-12 11:08:25 +04:00
17d99cb2cf fix: circular dependencies and query invalidation 2024-09-12 11:07:41 +04:00
bd0da0ff74 Update AddMemberModal.tsx 2024-09-12 11:03:20 +04:00
d2a54234f4 Rebase with Akhi 2024-09-12 11:03:20 +04:00
626262461a feat: assign roles when inviting members to project 2024-09-12 11:03:20 +04:00
93ba29e57f Feat: Invite multiple users to project with multiple roles 2024-09-12 11:03:20 +04:00
1581aa088d Update org-admin-service.ts 2024-09-12 11:03:20 +04:00
ceab951bca feat: remove project role from workspace user encryption computation 2024-09-12 11:03:20 +04:00
2e3dcc50ae API doc 2024-09-12 11:03:20 +04:00
a79087670e misc: addressed comments and doc changes 2024-09-12 13:27:39 +08:00
7b04c08fc7 Merge pull request from meetcshah19/meet/fix-org-selection
fix: redirect to selected org if already present
2024-09-12 10:14:56 +05:30
70842b8e5e Merge pull request from akhilmhdh/debug/entra-saml-logpoint
feat: debug added log points for entra failing saml
2024-09-11 19:00:36 -04:00
36e3e4c1b5 fix: redirect to selected org if already present 2024-09-12 03:37:55 +05:30
ce9b66ef14 address feedback suggestions 2024-09-11 12:40:27 -07:00
=
1384c8e855 feat: debug added log points for entra failing saml 2024-09-12 00:19:16 +05:30
f213c75ede Merge pull request from Infisical/misc/slack-integration-doc-and-ui-updates
misc: added cloud users guide for slack and channel dropdown fix
2024-09-11 14:36:15 -04:00
6ade708e19 misc: added cloud users guide for slack and other ui updates 2024-09-12 02:23:57 +08:00
ce3af41ebc Merge pull request from Infisical/daniel/permission-visualization
feat: user details page audit logs & groups visualization
2024-09-11 21:45:15 +04:00
e442f10fa5 Fix merge conflicts 2024-09-11 10:38:47 -07:00
2e8ad18285 Merge remote-tracking branch 'origin' into daniel/permission-visualization 2024-09-11 10:32:17 -07:00
f03ca7f916 Minor adjustments 2024-09-11 10:30:16 -07:00
bfa533e9d2 misc: api property description 2024-09-11 22:59:19 +08:00
a8759e7410 feat: added support for custom extended key usages 2024-09-11 22:38:36 +08:00
af1905a39e Merge pull request from meetcshah19/meet/fix-email-capitalization
Send lower case emails to backend
2024-09-11 20:07:14 +05:30
16182a9d1d feature: project and org identity pagination, search and sort 2024-09-11 07:22:08 -07:00
1321aa712f Merge pull request from Infisical/feat/native-slack-integration
feat: native slack integration
2024-09-11 09:36:25 -04:00
c1f61f2db4 feat: added custom key usages support for sign endpoint 2024-09-11 20:26:33 +08:00
5ad00130ea Merge pull request from akhilmhdh/feat/org-project-invite
Manager users without waiting for confirmation of mail
2024-09-11 13:06:28 +04:00
ea5e8e29e6 Requested changes 2024-09-11 12:45:14 +04:00
e7f89bdfef doc: add note for private channels 2024-09-11 13:50:40 +08:00
d23a7e41f3 misc: addressed comments 2024-09-11 13:29:43 +08:00
=
52a885716d feat: changes on review comments 2024-09-11 10:46:49 +05:30
3fc907f076 fix: send lower case emails to backend 2024-09-11 04:38:00 +05:30
eaf10483c0 Merge pull request from Infisical/fix-azure-saml-map-docs
Fix Stated Map for Azure SAML Attributes
2024-09-10 16:46:40 -04:00
dcd0234fb5 Fix stated map for azure saml attributes 2024-09-10 13:16:36 -07:00
4dda270e8e Requested changes 2024-09-10 23:29:23 +04:00
4e6b289e1b misc: integrated custom key usages for issue-cert endpoint 2024-09-11 01:57:16 +08:00
c1cb85b49f Merge pull request from akhilmhdh/fix/secret-reference-pass
Secret reference skip if not found
2024-09-10 13:17:56 -04:00
=
ed71e651f6 fix: secret reference skip if not found 2024-09-10 22:23:40 +05:30
6fab7d9507 Merge remote-tracking branch 'origin/main' into feat/add-key-usages-for-template-and-cert 2024-09-11 00:22:04 +08:00
1a11dd954b Merge pull request from Infisical/misc/allow-wildcard-san-value
misc: allow wildcard SAN domain value for certificates
2024-09-11 00:19:43 +08:00
5d3574d3f6 Merge pull request from Infisical/cert-template-enforcement
Certificate Template Enforcement Option + PKI UX Improvements
2024-09-10 09:19:37 -07:00
aa42aa05aa misc: updated docs 2024-09-11 00:13:44 +08:00
7a36badb23 misc: addressed review comments 2024-09-11 00:11:19 +08:00
9ce6fd3f8e Made required adjustments based on review 2024-09-10 08:18:31 -07:00
a549c8b9e3 Merge pull request from Infisical/daniel/cli-run-watch-mode
feat(cli): `run` command watch mode
2024-09-10 10:39:06 -04:00
1c749c84f2 misc: key usages setup 2024-09-10 21:42:41 +08:00
1bc1feb843 Merge pull request from sanyarajan/patch-1
Remove reference to Okta in Azure SAML setup
2024-09-10 08:46:36 -04:00
80ca115ccd Merge pull request from Infisical/daniel/cli-stale-session
fix: stale session after logging into CLI
2024-09-10 08:27:16 -04:00
5a6bb90870 Remove reference to Okta in Azure SAML setup 2024-09-10 12:25:11 +02:00
de7a693a6a Merge pull request from Infisical/daniel/rabbitmq-dynamic-secrets
feat(dynamic-secrets): Rabbit MQ
2024-09-10 12:54:56 +05:30
096417281e Update rabbit-mq.ts 2024-09-10 11:21:52 +04:00
763a96faf8 Update rabbit-mq.ts 2024-09-10 11:21:52 +04:00
870eaf9301 docs(dynamic-secrets): rabbit mq 2024-09-10 11:21:52 +04:00
10abf192a1 chore(docs): cleanup incorrectly formatted images 2024-09-10 11:21:52 +04:00
508f697bdd feat(dynamic-secrets): RabbitMQ 2024-09-10 11:21:52 +04:00
8ea8a6f72e Fix: ElasticSearch provider typo 2024-09-10 11:17:35 +04:00
54e6f4b607 Requested changes 2024-09-10 11:07:25 +04:00
ea3b3c5cec Merge pull request from Infisical/misc/update-kms-of-existing-params-for-integration
misc: ensure that selected kms key in aws param integration is followed
2024-09-10 12:51:06 +08:00
a8fd83652d Update docs for PKI issuer secret target output 2024-09-09 19:55:02 -07:00
45f3675337 Merge pull request from Infisical/misc/support-glob-patterns-oidc
misc: support glob patterns for OIDC
2024-09-09 18:22:51 -04:00
87a9a87dcd Show cert template ID on manage policies modal 2024-09-09 14:35:46 -07:00
0b882ece8c Update certificate / template docs 2024-09-09 14:22:26 -07:00
e005e94165 Merge remote-tracking branch 'origin' into cert-template-enforcement 2024-09-09 12:47:06 -07:00
0e07eaaa01 Fix cert template enforcement migration check 2024-09-09 12:45:33 -07:00
e10e313af3 Finish cert template enforcement 2024-09-09 12:42:56 -07:00
e6c0bbb25b fix: stale session after logging into CLI 2024-09-09 23:15:58 +04:00
2b39d9e6c4 Merge pull request from Infisical/pki-issuer-docs
Documentation for Infisical PKI Issuer for K8s Cert-Manager
2024-09-09 14:33:15 -04:00
cf42279e5b misc: allow wildcard san domain value for certificates 2024-09-10 01:20:31 +08:00
fbc4b47198 misc: ensure that selected kms key in aws param integration is applied 2024-09-09 22:23:22 +08:00
4baa6b1d3d Merge pull request from akhilmhdh/dynamic-secret/mongodb
Dynamic secret/mongodb
2024-09-09 19:50:03 +05:30
74ee77f41e Merge pull request from Infisical/misc/throw-saml-sso-errors-properly
misc: throw SAML or SSO errors properly
2024-09-09 08:57:57 -04:00
ee1b12173a misc: throw saml sso errors properly 2024-09-09 19:32:18 +08:00
1bfbc7047c Merge pull request from srijan-paul/patch-1
fix: small typo (`fasitfy` -> `fastify`)
2024-09-09 15:31:16 +04:00
=
a410d560a7 feat: removed an image 2024-09-09 16:40:14 +05:30
=
99e150cc1d feat: updated doc with requested changes 2024-09-09 16:32:49 +05:30
=
e7191c2f71 feat: made project role multi support for org invite 2024-09-09 16:17:59 +05:30
=
f6deb0969a feat: added atlas warning to doc 2024-09-09 15:24:30 +05:30
=
1163e41e64 docs: dynamic secret mongodb\ 2024-09-09 15:00:21 +05:30
=
a0f93f995e feat: dynamic secret mongodb ui 2024-09-09 15:00:01 +05:30
=
50fcf97a36 feat: dynamic secret api changes for mongodb 2024-09-09 14:59:34 +05:30
8e68d21115 misc: support glob patterns for oidc 2024-09-09 17:17:12 +08:00
372b6cbaea fix: audit log fixes 2024-09-09 10:42:39 +04:00
26add7bfd1 fix: remove delete project membership option 2024-09-09 10:42:10 +04:00
364302a691 Merge pull request from akhilmhdh/docs/fluent-bit-log-stream
feat: added doc for audit log stream via fluentbit
2024-09-08 15:08:46 -04:00
c8dc29d59b revise audit log stream PR 2024-09-08 15:04:30 -04:00
f3d207ab5c feat: better user visualization 2024-09-08 20:20:34 +04:00
e1cd632546 improvements to user group ui 2024-09-08 20:20:10 +04:00
655ee4f118 Update mutations.tsx 2024-09-08 20:19:50 +04:00
34a2452bf5 feat: fetch all user group memberships 2024-09-08 20:19:10 +04:00
7846a81636 chore: new group with project memberships type 2024-09-08 19:28:17 +04:00
6bdf3455f5 Update mutations.tsx 2024-09-08 19:27:31 +04:00
556ae168dd feat: fetch specific user group memberships 2024-09-08 19:25:48 +04:00
7b19d2aa6a feat: audit logs on organization-level support 2024-09-08 19:24:04 +04:00
bda9bb3d61 fix: rename list audit logs and include project 2024-09-08 19:21:17 +04:00
4b66a9343c feat: audit logs section 2024-09-08 19:20:32 +04:00
4930d7fc02 feat: user groups section 2024-09-08 19:20:18 +04:00
ad644db512 feat: audit logs on organization-level 2024-09-08 19:19:55 +04:00
=
3707b75349 feat: added doc for audit log stream via fluentbit 2024-09-08 20:33:47 +05:30
ffaf145317 misc: removed unused table usage 2024-09-08 17:04:41 +08:00
17b0d0081d misc: moved away from dedicated slack admin config 2024-09-08 17:00:50 +08:00
ecf177fecc misc: added root workflow integration structure 2024-09-08 13:49:32 +08:00
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
=
eb7c804bb9 feat(ui): made corresponding changes in api call made from frontend 2024-09-06 23:33:57 +05:30
=
9d7bfae519 feat: made default role on project invite as no access to org level 2024-09-06 23:33:12 +05:30
=
1292b5bf56 feat(api): manage users in org and project level without waiting for confirmation 2024-09-06 23:31:55 +05:30
f09e18a706 Merge pull request 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
12154c869f fix: small typo (fasitfy -> fastify 2024-09-06 18:10:17 +05:30
8d66272ab2 Merge pull request 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 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 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 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
dbc5b5a3d1 doc: native slack integration 2024-09-05 18:28:38 +08: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
1bd66a614b misc: added channels count validator 2024-09-05 02:36:27 +08:00
802a9cf83c misc: formatting changes 2024-09-05 01:42:33 +08:00
9e95fdbb58 misc: added proper error message hints 2024-09-05 01:20:12 +08:00
803f56cfe5 misc: added placeholder 2024-09-05 00:46:00 +08:00
b163a6c5ad feat: integration to access request approval 2024-09-05 00:42:21 +08:00
ddc119ceb6 Merge remote-tracking branch 'origin/main' into feat/native-slack-integration 2024-09-05 00:36:44 +08:00
302e068c74 Merge pull request 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 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
09e621539e misc: finalized labels 2024-09-04 23:54:19 +08:00
dab3e2efad fix(access-requests): policy creation and edits 2024-09-04 19:46:44 +04:00
5e0b78b104 Requested changes 2024-09-04 19:34:51 +04:00
27852607d1 Merge remote-tracking branch 'origin/main' into feat/native-slack-integration 2024-09-04 23:10:15 +08:00
956719f797 feat: admin slack configuration 2024-09-04 23:06:30 +08:00
04cbbccd25 Merge pull request 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 from Infisical/daniel/elastisearch-dynamic-secrets
feat(dynamic-secrets): elastic search support
2024-09-04 15:23:23 +04:00
e6a920caa3 Merge pull request from mukulpadwal/bugfix/incorrect-alignment-of-logo-on-login-page
FIX : padding-and-alignment-login-page
2024-09-04 16:15:36 +05:30
71b8c59050 feat: slack channel suggestions 2024-09-04 18:03:07 +08:00
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
15c5fe4095 misc: slack integration reinstall 2024-09-04 15:44:58 +08:00
91ebcca0fd Update run.go 2024-09-04 10:44:39 +04:00
9cf917de07 Merge pull request from Infisical/daniel/redirect-node-docs
feat(integrations): Add visibility support to Github Integration
2024-09-04 10:32:13 +04:00
0826b40e2a Fixes and requested changes 2024-09-04 10:18:17 +04:00
911b62c63a Update run.go 2024-09-04 10:05:57 +04:00
5343c7af00 misc: added auto redirect to workflow settings tab 2024-09-04 02:22:53 +08:00
8c03c160a9 misc: implemented secret approval request and project audit logs 2024-09-04 01:48:08 +08:00
604b0467f9 feat: finalized integration selection in project settings 2024-09-04 00:34:03 +08:00
a2b555dd81 feat: finished org-level integration management flow 2024-09-03 22:08:31 +08:00
ce7bb82f02 Merge pull request from akhilmhdh/feat/test-import
Feat/test import
2024-09-03 09:33:26 -04:00
7cd092c0cf Merge pull request 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 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
9120367562 misc: audit logs for slack integration management 2024-09-02 23:15:00 +08:00
a74c37c18b Merge pull request from akhilmhdh/dynamic-secret/atlas
MongoDB atlas dynamic secret
2024-09-02 10:39:34 -04:00
f509464947 slack integration reinstall 2024-09-02 21:05:30 +08:00
07fd489982 feat: slack integration deletion 2024-09-02 20:34:13 +08:00
f6d3831d6d feat: finished slack integration update 2024-09-02 20:13:01 +08: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
d604ef2480 feat: integrated secret approval request 2024-09-02 15:38:05 +08:00
45e471b16a FIX : padding-and-alignment-login-page 2024-08-31 16:25:54 +05:30
fe096772e0 feat: initial installation flow 2024-08-31 02:56:02 +08:00
7c540b6be8 Merge pull request 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
35a63b8cc6 Fix: Fixed merge related changes 2024-08-29 22:54:49 +04:00
2a4596d415 Merge branch 'main' into daniel/cli-run-watch-mode 2024-08-29 22:37:35 +04:00
35e476d916 Fix: Runtime bugs 2024-08-29 22:35:21 +04:00
b975996158 Merge pull request 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 from Infisical/feat/raw-agent-template
feat: added raw template for agent
2024-08-29 14:24:50 -04:00
c9911aa841 Merge pull request from Infisical/vmatsiiako-patch-handbook-1
Update onboarding.mdx
2024-08-29 14:22:30 -04:00
32cd0d8af8 Merge pull request 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 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 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 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 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 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 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 from Infisical/maidul-ddqdqwdqwd3
Update health check
2024-08-27 20:09:51 -04:00
3ab6eb62c8 update health check 2024-08-27 20:03:36 -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
79680b6a73 Merge pull request from Infisical/misc/added-timeout-for-hijacked-est-connection
misc: added timeout for est connection
2024-08-27 23:31:07 +08:00
58838c541f misc: added timeout for est connection 2024-08-27 23:26:56 +08:00
03cc71cfed Merge pull request from Infisical/feature/est-simpleenroll
Certificate EST protocol (simpleenroll, simplereenroll, cacerts)
2024-08-27 13:42:58 +08:00
02529106c9 Merge pull request from akhilmhdh/fix/scim-error
fix: resolved scim group update failing
2024-08-26 16:34:27 -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
=
d939ff289d fix: resolved scim group update failing 2024-08-27 01:50:26 +05:30
d1816c3051 Merge pull request from Infisical/daniel/azure-devops-docs
Docs: Azure DevOps Integration
2024-08-26 23:49:23 +04:00
cb350788c0 Update create.tsx 2024-08-26 23:21:56 +04:00
cd58768d6f Updated images 2024-08-26 23:20:51 +04:00
dcd6f4d55d Fix: Updated Azure DevOps integration styling 2024-08-26 23:12:00 +04:00
3c828614b8 Fix: Azure DevOps Label naming typos 2024-08-26 22:44:11 +04:00
09e7988596 Docs: Azure DevOps Integration 2024-08-26 22:43:49 +04:00
f40df19334 misc: finalized est config schema 2024-08-27 02:15:01 +08:00
76c9d3488b Merge remote-tracking branch 'origin/main' into feature/est-simpleenroll 2024-08-27 02:13:59 +08:00
0809da33e0 misc: improved docs and added support for curl clients 2024-08-27 02:05:35 +08:00
b528eec4bb Merge pull request from Infisical/daniel/secret-change-emails
Feat: Email notification on secret change requests
2024-08-26 21:50:30 +04:00
5179103680 Update SecretApprovalRequest.tsx 2024-08-26 21:46:05 +04:00
25a9e5f58a Update SecretApprovalRequest.tsx 2024-08-26 21:42:47 +04:00
8ddfe7b6e9 Update secret-approval-request-fns.ts 2024-08-26 20:53:43 +04:00
c23f21d57a Update SecretApprovalRequest.tsx 2024-08-26 20:21:05 +04:00
1242a43d98 Feat: Open approval with ID in URL 2024-08-26 20:04:06 +04:00
1655ca27d1 Fix: Creation of secret approval policies 2024-08-26 20:02:58 +04:00
2bcead03b0 Feat: Send secret change request emails to approvers 2024-08-26 19:55:04 +04:00
41ab1972ce Feat: Find project and include org dal 2024-08-26 19:54:48 +04:00
b00fff6922 Update index.ts 2024-08-26 19:54:15 +04:00
97b01ca5f8 Feat: Send secret change request emails to approvers 2024-08-26 19:54:01 +04:00
c2bd6f5ef3 Feat: Send secret change request emails to approvers 2024-08-26 19:53:49 +04:00
18efc9a6de Include more user details 2024-08-26 19:53:17 +04:00
436ccb25fb Merge pull request from Infisical/daniel/presist-selfhosting-domains
Feat: Persistent self-hosting domains on `infisical login`
2024-08-26 18:04:25 +04:00
8f08a352dd Merge pull request from Infisical/daniel/azure-devops-integration
Feat: Azure DevOps Integration
2024-08-26 18:04:04 +04:00
00f86cfd00 misc: addressed review comments 2024-08-26 21:10:29 +08:00
3944aafb11 Use slices 2024-08-26 15:18:45 +04:00
a6b852fab9 Fix: Type errors / cleanup 2024-08-26 15:13:18 +04:00
2a043afe11 Cleanup 2024-08-26 15:13:18 +04:00
df8f2cf9ab Update integration-sync-secret.ts 2024-08-26 15:13:18 +04:00
a18015b1e5 Fix: Use unique parameter for passing devops org name
Used to be teamId, now it's azureDevopsOrgName.
2024-08-26 15:13:18 +04:00
8b80622d2f Cleanup 2024-08-26 15:13:18 +04:00
c0fd0a56f3 Update integration-list.ts 2024-08-26 15:13:18 +04:00
326764dd41 Feat: Azure DevOps Integration 2024-08-26 15:13:18 +04:00
1f24d02c5e Fix: Do not save duplicate domains 2024-08-26 15:08:51 +04:00
c130fbddd9 Merge pull request from Infisical/daniel/specify-roles-and-projects
Feat: Select roles & projects when inviting members to organization
2024-08-26 15:00:39 +04:00
f560534493 Replace custom pkcs7 fns with module 2024-08-25 20:21:53 -07:00
10a97f4522 update python docs to point to new repo 2024-08-25 18:02:51 -04:00
7a2f0214f3 Feat: Persist self-hosting domains on infisical login 2024-08-24 13:18:03 +04:00
a2b994ab23 Requested changes 2024-08-24 11:00:05 +04:00
e73d3f87f3 small nit 2024-08-23 14:29:29 -04:00
c4715124dc Merge pull request from Infisical/fix/resolve-name-null-null
fix: this pr addresses null null name issue with invited users
2024-08-23 14:01:27 -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
67c1cb9bf1 fix: this pr addresses null null name issue with invited users 2024-08-23 15:40:06 +08:00
68b1984a76 Merge pull request from Infisical/crl-update
CRL Distribution Point URLs + Support for Multiple CRLs per CA
2024-08-22 23:56:55 -07:00
ba45e83880 Clean 2024-08-22 23:37:36 -07:00
28ecc37163 Update org-service.ts 2024-08-23 03:10:14 +04:00
a6a2e2bae0 Update AddOrgMemberModal.tsx 2024-08-23 02:25:15 +04:00
d8bbfacae0 UI improvements 2024-08-23 02:25:15 +04:00
58549c398f Update project-service.ts 2024-08-23 02:25:15 +04:00
842ed62bec Rename 2024-08-23 02:25:15 +04:00
06d8800ee0 Feat: Specify organization role and projects when inviting users to org 2024-08-23 02:25:15 +04:00
2ecfd1bb7e Update auth-signup-type.ts 2024-08-23 02:25:15 +04:00
783d4c7bd6 Update org-dal.ts 2024-08-23 02:25:15 +04:00
fbf3f26abd Refactored org invites to allow for multiple users and to handle project invites 2024-08-23 02:25:15 +04:00
1d09693041 Update org-types.ts 2024-08-23 02:25:15 +04:00
626e37e3d0 Moved project membership creation to project membership fns 2024-08-23 02:25:15 +04:00
07fd67b328 Add metadata to SMTP email 2024-08-23 02:25:15 +04:00
3f1f018adc Update telemetry-types.ts 2024-08-23 02:25:15 +04:00
fe04e6d20c Remove *.*.posthog.com 2024-08-23 02:25:15 +04:00
d7171a1617 Removed unused code 2024-08-23 02:25:15 +04:00
384a0daa31 Update types.ts 2024-08-23 02:25:15 +04:00
c5c949e034 Multi user org invites 2024-08-23 02:25:15 +04:00
c2c9edf156 Update types.ts 2024-08-23 02:25:15 +04:00
c8248ef4e9 Fix: Skip org selection when user only has one org 2024-08-23 02:25:15 +04:00
9f6a6a7b7c Automatic timed toggle 2024-08-23 02:25:15 +04:00
121b642d50 Added new metadata parameter for signup 2024-08-23 02:25:15 +04:00
59b16f647e Update AddOrgMemberModal.tsx 2024-08-23 02:25:15 +04:00
2ab5932693 Update OrgMembersSection.tsx 2024-08-23 02:25:15 +04:00
8dfcef3900 Seperate component for Org Invite Links 2024-08-23 02:25:15 +04:00
8ca70eec44 Refactor add users to org handlers 2024-08-23 02:25:14 +04:00
60df59c7f0 Multi-user organization invites structure 2024-08-23 02:25:14 +04:00
e231c531a6 Update index.ts 2024-08-23 02:25:14 +04:00
d48bb910fa JWT invite lifetime (1 day) 2024-08-23 02:25:14 +04:00
1317266415 Merge remote-tracking branch 'origin' into feature/est-simpleenroll 2024-08-22 14:54:34 -07:00
f0938330a7 Merge pull request from Infisical/daniel/disallow-user-creation-on-member-group-fix
Fix: Disallow org members to invite new members
2024-08-22 17:33:46 -04:00
e1bb0ac3ad Update org-permission.ts 2024-08-23 01:21:57 +04:00
f54d930de2 Fix: Disallow org members to invite new members 2024-08-23 01:13:45 +04:00
288f47f4bd Update API reference CRL docs 2024-08-22 12:30:48 -07:00
b090ebfd41 Update API reference CRL docs 2024-08-22 12:26:13 -07:00
67773bff5e Update wording on external parent ca 2024-08-22 12:18:00 -07:00
8ef1cfda04 Update docs for CRL 2024-08-22 12:16:37 -07:00
2a79d5ba36 Fix merge conflicts 2024-08-22 12:01:43 -07:00
0cb95f36ff Finish updating CRL impl 2024-08-22 11:55:19 -07:00
4a1dfda41f Merge pull request from Infisical/maidul-udfysfgj32
Remove service token depreciation notice
2024-08-22 14:29:55 -04:00
c238b7b6ae remove service token notice 2024-08-22 13:57:40 -04:00
288d7e88ae misc: made SSL header key configurable via env 2024-08-23 01:38:12 +08:00
83d314ba32 Merge pull request from Infisical/install-external-ca
Install Intermediate CA with External Parent CA
2024-08-22 09:41:52 -07:00
b94a0ffa6c Merge pull request from akhilmhdh/fix/build-mismatch-lines
feat: added backend build sourcemap for line matching
2024-08-22 09:34:12 -04:00
f88389bf9e misc: added general format 2024-08-22 21:05:34 +08:00
2e88c5e2c5 misc: improved url examples in est doc 2024-08-22 21:02:38 +08:00
73f3b8173e doc: added guide for EST usage' 2024-08-22 20:44:21 +08:00
=
b60e404243 feat: added backend build sourcemap for line matching 2024-08-22 15:18:33 +05:30
aa5b88ff04 misc: removed enrollment options from CA page 2024-08-22 15:40:36 +08:00
b7caff88cf feat: finished up EST cacerts 2024-08-22 15:39:53 +08:00
10120e1825 Merge pull request from akhilmhdh/feat/debounce-last-used
feat: added identity and service token postgres update debounced
2024-08-22 00:50:54 -04:00
31e66c18e7 Merge pull request from Infisical/maidul-deuyfgwyu
Set default to host sts endpoint for aws auth
2024-08-21 22:38:49 -04:00
fb06f5a3bc default to host sts for aws auth 2024-08-21 22:29:30 -04:00
1515dd8a71 Merge pull request from akhilmhdh/feat/ui-patch-v1
feat: resolved ui issues related to permission based hiding
2024-08-21 13:50:24 -04:00
=
da18a12648 fix: resovled create failing in bulk create too 2024-08-21 23:13:42 +05:30
=
49a0d3cec6 feat: resolved ui issues related to permission based hiding 2024-08-21 23:01:23 +05:30
=
e821a11271 feat: added identity and service token postgres update debounced 2024-08-21 22:21:31 +05:30
af4428acec Add external parent ca support to docs 2024-08-20 22:43:29 -07:00
61370cc6b2 Finish allow installing intermediate CA with external parent CA 2024-08-20 21:44:41 -07:00
cf3b2ebbca Merge pull request from Infisical/daniel/read-secrets-notification-removal
Fix: Remove notification when unable to read secrets from environment
2024-08-20 21:56:41 +04:00
e970cc0f47 Fix: Error notification when user does not have access to read from certain environments 2024-08-20 21:41:02 +04:00
bd5cd03aeb Merge pull request from Infisical/daniel/access-requests-group-support
feat(core): Group support for access requests
2024-08-20 21:20:01 +04:00
760a1e917a feat: added simplereenroll 2024-08-20 23:56:27 +08:00
c46e4d7fc1 fix: scim cleanup 2024-08-20 19:50:42 +04:00
1f3896231a fix: remove privileges when user loses access to project/org 2024-08-20 19:50:42 +04:00
4323f6fa8f Update 20240724101056_access-request-groups.ts 2024-08-20 19:50:42 +04:00
65db91d491 Update 20240724101056_access-request-groups.ts 2024-08-20 19:50:42 +04:00
ae5b57f69f Update 20240724101056_access-request-groups.ts 2024-08-20 19:50:42 +04:00
b717de4f78 Update 20240724101056_access-request-groups.ts 2024-08-20 19:50:42 +04:00
1216d218c1 fix: rollback access approval requests requestedBy 2024-08-20 19:50:42 +04:00
209004ec6d fix: rollback access approval requests requestedBy 2024-08-20 19:50:42 +04:00
c865d12849 Update 20240724101056_access-request-groups.ts 2024-08-20 19:50:42 +04:00
c921c28185 Update AccessPolicyModal.tsx 2024-08-20 19:50:42 +04:00
3647943c80 Feat: Access requests group support 2024-08-20 19:50:42 +04:00
4bf5381060 Feat: Access requests group support 2024-08-20 19:50:42 +04:00
a10c358f83 Feat: Access requests group support 2024-08-20 19:50:42 +04:00
d3c63b5699 Access approval request 2024-08-20 19:50:42 +04:00
c64334462f Access approval policy 2024-08-20 19:50:42 +04:00
c497e19b99 Routers 2024-08-20 19:50:42 +04:00
2aeae616de Migration 2024-08-20 19:50:42 +04:00
e0e21530e2 Schemas 2024-08-20 19:50:41 +04:00
2d7ff66246 Merge branch 'feature/est-simpleenroll' of https://github.com/Infisical/infisical into feature/est-simpleenroll 2024-08-20 15:31:58 +08:00
179497e830 misc: moved est logic to service 2024-08-20 15:31:10 +08:00
4c08c80e5b Merge remote-tracking branch 'origin' into feature/est-simpleenroll 2024-08-19 14:53:04 -07:00
7b4b802a9b Merge pull request from Infisical/daniel/sdk-docs-updates
Fix: Include imports SDK docs
2024-08-20 01:21:28 +04:00
95cf3cf6cc Docs: Add expand secret references to single secret sdk docs 2024-08-20 01:12:42 +04:00
d021b414cf Fix: Include imports SDK docs 2024-08-20 01:09:53 +04:00
bed75c36dd Merge pull request from Infisical/feature/certificate-template
feat: certificate templates
2024-08-19 11:48:03 -07:00
7d6af64904 misc: added proxy header for amazon mtls client cert 2024-08-20 01:53:47 +08:00
16519f9486 feat: added reading SANs from CSR 2024-08-20 01:39:40 +08:00
bb27d38a12 misc: ui form adjustments 2024-08-19 21:39:00 +08:00
5b26928751 misc: added audit logs 2024-08-19 20:25:07 +08:00
f425e7e48f misc: addressed alignment issue 2024-08-19 19:50:09 +08:00
4601f46afb misc: finalized variable naming 2024-08-19 19:33:46 +08:00
692bdc060c misc: updated est configuration to be binded to certificate template 2024-08-19 19:26:20 +08:00
3a4f8c2e54 Merge branch 'feature/certificate-template' into feature/est-simpleenroll 2024-08-19 17:04:22 +08:00
04cb499f0f doc: finalized sample request response values 2024-08-19 16:53:17 +08:00
189a610f52 doc: add cert template api usage 2024-08-19 16:40:18 +08:00
00039ba0e4 misc: addressed PR feedback regarding audit logs and endpoint structure 2024-08-19 16:15:43 +08:00
abdcb95a8f Merge remote-tracking branch 'origin/main' into feature/certificate-template 2024-08-19 14:51:42 +08:00
47ea4ae9a6 Fix merge conflicts 2024-08-18 12:05:15 -07:00
903b2c3dc6 Merge pull request from Infisical/handbook-update
added talking-to-customers.mdx to handbook
2024-08-18 12:55:20 -04:00
c795b3b3a0 added talking-to-customers.mdx to handbook 2024-08-17 22:49:53 -07:00
0d8ff1828e Merge pull request from Infisical/certificate-alerting
Alerting System for expiring CA + Certificates
2024-08-17 22:45:24 -07:00
30d6af7760 Make PR review adjustments 2024-08-17 21:23:25 -07:00
44b42359da Merge remote-tracking branch 'origin' into certificate-alerting 2024-08-17 19:47:37 -07:00
38373722e3 Merge remote-tracking branch 'origin' into certificate-alerting 2024-08-17 19:46:23 -07:00
7ec68ca9a1 Update expiry badge display for certs 2024-08-17 19:44:47 -07:00
a49d5b121b Merge pull request from akhilmhdh/fix/#2288
fix: resolved add all members to project failing when there is pending users in organization
2024-08-16 20:52:43 -04:00
901ff7a605 Merge pull request from akhilmhdh/feat/license-check-approval-api
feat: license check in secret approval api level
2024-08-16 14:07:03 -04:00
ba4aa15c92 doc: added platform docs for certificate template 2024-08-17 01:04:29 +08:00
=
a00103aa1e feat: license check in secret approval api level 2024-08-16 22:34:10 +05:30
0c17cc3577 doc: API references 2024-08-17 00:15:11 +08:00
51d84a47b9 misc: added certificate templates to permissions 2024-08-16 23:30:29 +08:00
d529670a52 misc: addressed failing github actions 2024-08-16 23:14:45 +08:00
ed0463e3e4 misc: added audit logs for certificate template 2024-08-16 23:08:01 +08:00
20db0a255c Merge pull request from Infisical/ca-renewal
CA Renewal (Same Key Pair)
2024-08-16 10:37:44 -04:00
6fe1d77375 misc: added descriptive tooltips 2024-08-16 21:09:35 +08:00
f90855e7a5 misc: add tracking of certificate template ID 2024-08-16 20:17:52 +08:00
97f5c33aea feat: added collection selection for cert template 2024-08-16 19:40:23 +08:00
=
34c2200269 fix: resolved add all members to project failing when there is pending users 2024-08-16 16:20:38 +05:30
69925721cc Merge branch 'certificate-alerting' into feature/certificate-template 2024-08-16 18:42:12 +08:00
0961d2f1c6 feat: subject alternative name policy enforcement 2024-08-16 17:24:33 +08:00
b9bd518aa6 feat: initial enforcement of template policy 2024-08-16 16:32:47 +08:00
692c9b5d9c Merge pull request from dthree/patch-1
fix: added missing word
2024-08-16 00:24:05 -04:00
DC
32046ca880 fix: added missing word 2024-08-15 20:17:40 -07:00
590dbbcb04 Merge pull request from Infisical/maidul-iqdgqwuygd
Add DISABLE_AUDIT_LOG_GENERATION
2024-08-15 22:44:08 -04:00
27d2af4979 Add DISABLE_AUDIT_LOG_GENERATION
Added `DISABLE_AUDIT_LOG_GENERATION` which when set to true will prevent the creation of audit logs in Infisical.

This will be used for load testing purposes and help verify if audit logs are a bottle neck for performance
2024-08-15 22:39:49 -04:00
a1e6c6f7d5 Merge pull request from akhilmhdh/feat/replication-test
fix: switched sync integration to have redis lock
2024-08-15 15:18:42 -04:00
=
cc94a3366a feat: made requested changes for integration sync 2024-08-15 23:38:16 +05:30
6a6c084b8a Add description to PKI collection 2024-08-15 11:03:58 -07:00
7baa3b4cbe Add PKI collection to issue cert modal 2024-08-15 10:41:37 -07:00
=
6cab7504fc fix: switched sync integration to have redis lock 2024-08-15 22:04:32 +05:30
ca3d8c5594 Bring PR up to speed with ca renewal changes 2024-08-15 09:31:21 -07:00
28a2a6c41a feat: initial integration of cert template management 2024-08-15 21:06:37 +08:00
05efd95472 feat: completed certificate template schema endpoints 2024-08-15 15:46:35 +08:00
fa31f87479 Merge pull request from Infisical/doc/add-dynamic-secrets-to-api-reference
doc: add dynamic secrets to api references
2024-08-15 15:37:42 +08:00
b176f13392 doc: add dynamic secrets to api references 2024-08-15 15:21:49 +08:00
f4384bb01e feat: initial structure 2024-08-15 14:49:30 +08:00
4570de09ae Merge pull request from akhilmhdh/feat/replication-test
feat: resolved getSecretByName empty value from imported in kms arch
2024-08-14 16:05:01 -04:00
=
4feff5b4ca feat: resolved getSecretByName empty value from imported in kms arch 2024-08-15 01:24:54 +05:30
6081e2927e Merge pull request from rhythmbhiwani/fix-pagination-disappear
Fixed Pagination Disappearing on Secret Sharing Page
2024-08-14 14:54:37 -04:00
0b42f29916 Merge pull request from akhilmhdh/feat/replication-test
feat: added log point for aws tag and check for delete secret in bridge
2024-08-14 12:25:28 -04:00
=
b60d0992f4 feat: added log point for aws tag and check for delete secret in bridge 2024-08-14 21:42:07 +05:30
146c4284a2 feat: integrated to est routes 2024-08-14 20:52:21 +08:00
a8a68f600c Merge pull request from akhilmhdh/feat/replication-test
feat(ui): resolved a race condition in ui
2024-08-13 14:48:08 -04:00
=
742f5f6621 feat(ui): resolved a race condition in ui 2024-08-14 00:13:55 +05:30
5ae33b9f3b misc: minor UI updates 2024-08-14 01:10:25 +08:00
1f38b92ec6 feat: finished up integration for est config management 2024-08-14 01:00:31 +08:00
f3cd7efe0e Merge pull request from akhilmhdh/feat/replication-test
feat: added more endpoints for delete
2024-08-13 12:41:54 -04:00
2b16c19b70 improve logs for aws ssm debug 2024-08-13 12:40:02 -04:00
=
943b540383 feat: added more endpoints for delete 2024-08-13 21:48:03 +05:30
e180021aa6 Merge pull request from akhilmhdh/feat/replication-test
feat: added debug points to test ssm integration in replication
2024-08-13 11:23:25 -04:00
f2a49a79f0 feat: initial simpleenroll setup (mvp) 2024-08-13 23:22:47 +08:00
=
8e08c443ad feat: added log to print operation based keys 2024-08-13 20:50:19 +05:30
=
dae26daeeb feat: added debug points to test ssm integration in replication 2024-08-13 20:40:53 +05:30
170f8d9add Merge pull request from Infisical/misc/addressed-reported-cli-behaviors
misc: addressed reported flaws with CLI usage
2024-08-13 12:49:20 +08:00
8d41ef198a Merge pull request from akhilmhdh/feat/client-secret-cleanup
fix: resolved secret approval broken due to tag name removal
2024-08-12 16:51:55 -04:00
=
69d60a227a fix: resolved secret approval broken due to tag name removal 2024-08-13 02:16:57 +05:30
c8eefcfbf9 Merge pull request from akhilmhdh/feat/client-secret-cleanup
feat: switched to ssm update as overwrite with tag as seperate operation
2024-08-12 16:38:57 -04:00
=
53cec754cc feat: switched to ssm update as overwrite with tag as seperate operation 2024-08-13 02:04:55 +05:30
5db3e177eb Fixed Pagination Disappearing on Secret Sharing Page 2024-08-13 02:01:25 +05:30
3fcc3ccff4 fix spending money tpyo 2024-08-12 12:41:15 -04:00
df07d7b6d7 update spending docs 2024-08-12 11:34:32 -04:00
28a655bef1 Merge pull request from akhilmhdh/feat/client-secret-cleanup
Client secret cleanup on resource cleanup queue
2024-08-12 11:01:46 -04:00
=
5f2cd04f46 feat: removed not needed condition 2024-08-12 20:29:05 +05:30
=
897ce1f267 chore: new reviewable command in root make file to check all the entities lint and type error 2024-08-12 13:19:55 +05:30
=
6afc17b84b feat: implemented universal auth client secret cleanup in resource cleanup queue 2024-08-12 13:19:25 +05:30
9017a5e838 Update spending-money.mdx 2024-08-12 01:29:45 -04:00
cb8e4d884e add equipment details to handbook 2024-08-11 23:17:58 -04:00
16807c3dd6 update k8s helm chart image tag 2024-08-11 13:09:22 -04:00
61791e385c update chart version of k8 2024-08-11 10:34:23 -04:00
bbd7bfb0f5 Merge pull request from MohamadTahir/fix-operator-bugs
Bug Fixes
2024-08-11 10:33:26 -04:00
4de8c48b2c Merge pull request from Ayush-Dutt-Sharma/ayush/minor-bug-#2269
replaced "creditnals" to "credentials"
2024-08-11 19:18:43 +05:30
a4bbe2c612 fix the client site url & the creation of new variable instead of updating the previous initiated variable 2024-08-11 16:46:48 +03:00
541a2e7d05 replaced "creditnals" to "credentials" 2024-08-11 14:10:49 +05:30
=
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
ea4e51d826 Merge pull request from Ayush-Dutt-Sharma/ayush/bug-2267-backend
better logging and while loop for ask propmt again
2024-08-10 19:48:05 +05:30
3bc920c593 better logging and while loop for ask propmt again 2024-08-10 15:36:43 +05:30
f4244c6d4d Finish docs for pki alerting + expose endpoints 2024-08-09 19:22:47 -07:00
e1b9965f01 Add frontend audit log ui for pki alerting / collection 2024-08-09 09:28:15 -07:00
705b4f7513 Add audit logging for pki alerts / collections 2024-08-09 09:07:02 -07:00
df38c761ad Merge pull request from akhilmhdh/fix/migration-switch-batch-insert
Secret migration switched to chunking based batch insert
2024-08-09 11:46:19 -04:00
=
32a84471f2 feat: added a new batch insert operation to convert inserts into chunks and updated secret migration 2024-08-09 21:02:26 +05:30
fc4a20caf2 Rename pki alerting structures 2024-08-09 08:27:53 -07:00
ea14df2cbd Merge pull request from akhilmhdh/fix/tag-filter-secret-api
Tag based filtering for secret endpoint
2024-08-09 20:33:43 +05:30
6bd6cac366 Merge pull request from Infisical/misc/addressed-misleading-google-saml-setup
misc: addressed misleading docs and placeholder values for Google SAML
2024-08-09 07:43:56 -07:00
45294253aa Merge pull request from GLEF1X/bugfix/yaml-exporting
fix(cli): make yaml exporting reliable and standardized
2024-08-09 10:01:26 -04:00
635fbdc80b misc: addressedm misleading docs and placeholder values for google saml 2024-08-09 21:29:33 +08:00
d20c48b7cf Merge pull request from Ayush-Dutt-Sharma/ayush/document-fixes
kubernetes operators integration doc fix
2024-08-09 14:59:50 +05:30
=
1fc18fe23b feat: added name in attach tag 2024-08-09 14:38:15 +05:30
99403e122b kubernetes operators integration doc fix 2024-08-09 14:33:29 +05:30
5176e70437 rephrase error messages 2024-08-08 18:15:13 -04:00
82b2b0af97 Merge pull request from akhilmhdh/feat/secret-get-personal
fix: resolved cli failign to get overriden secret in get command
2024-08-08 15:08:39 -04:00
e313c866a2 remove backup test for temp 2024-08-08 14:25:12 -04:00
2d81606049 update test with typo fix 2024-08-08 14:03:52 -04:00
718f4ef129 Merge pull request from Infisical/maidu-2321e
remove INFISICAL_VAULT_FILE_PASSPHRASE because it is being auto generated now
2024-08-08 13:52:05 -04:00
a42f3b3763 remove INFISICAL_VAULT_FILE_PASSPHRASE because it is being auto generated now 2024-08-08 13:50:34 -04:00
f7d882a6fc Merge pull request from akhilmhdh/fix/backup
Resolved keyring dataset too big by keeping only the encryption key
2024-08-08 13:19:50 -04:00
385afdfcf8 generate random string fn 2024-08-08 13:03:45 -04:00
281d703cc3 removeed vault use command and auto generated passphrase 2024-08-08 13:02:08 -04:00
6f56ed5474 add missing error logs on secrets backup 2024-08-08 13:01:14 -04:00
=
809e4eeba1 fix: resolved cli failign to get overriden secret in get command 2024-08-08 21:23:04 +05:30
=
254446c895 fix: resolved keyring dataset too big by keeping only the encryption key 2024-08-08 13:04:33 +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
bb52e2beb4 Update secret-tag-router.ts 2024-08-08 00:31:41 -04:00
2739b08e59 revert bb934ef7b1c47195b2ff65a335712add791cb59c 2024-08-07 22:15:06 -04:00
ba5e877a3b Revert "add base64 package"
This reverts commit 4892eea009ee1ed73c27d783d2dc4e7adc735d11.
2024-08-07 22:14:08 -04:00
d2752216f6 Merge pull request from Infisical/revert-2252-maidul-dhusduqwdhj
Revert "Patch CLI auto select file vault "
2024-08-07 22:13:00 -04:00
d91fb0db02 Revert "Patch CLI auto select file vault " 2024-08-07 22:12:50 -04:00
556e4d62c4 Added pki collection table, pki alert modal 2024-08-07 17:09:07 -07:00
4892eea009 add base64 package 2024-08-07 19:06:25 -04:00
09c6fcb73b Merge pull request from Infisical/maidul-dhusduqwdhj
Patch CLI auto select file vault
2024-08-07 19:03:38 -04:00
79181a1e3d remove os 2024-08-07 23:03:14 +00:00
bb934ef7b1 set vault type when auto selection enabled 2024-08-07 23:02:35 +00:00
cd9316537d prevent auto saving passphrase to disk 2024-08-07 18:56:15 -04:00
942e5f2f65 update phrase 2024-08-07 18:35:57 -04:00
353d231a4e Patch CLI auto select file vault
# Description 📣

When we auto select file vault, we also need to set it's type. When we set the type, we don't need to fall back to file vault in the `GetValueInKeyring` and `DeleteValueInKeyring` because `currentVaultBackend` will be `file`.

Also rephrased the text asking the user to eneter a passphrase.
2024-08-07 18:35:07 -04: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
68e05b7198 add debug log to print keyring error 2024-08-07 14:51:55 -04:00
b0a5023723 feat: check if secret is expired before checking if secret has password 2024-08-07 20:55:37 +03:00
4f998e3940 Merge pull request from akhilmhdh/fix/replication
fix: resolved replication secret not getting deleted
2024-08-07 11:57:14 -04:00
=
1248840dc8 fix: resolved replication secret not getting deleted 2024-08-07 21:23:22 +05:30
64c8125e4b add external secrets operator mention in k8s docs 2024-08-07 11:13:02 -04:00
1690a9429c Begin cert alerting 2024-08-07 07:08:47 -07: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
=
c109fbab3e feat: removed tag name used in queries 2024-08-07 13:24:22 +05:30
=
15fb01089b feat: name removal in tag respective changes in frontend 2024-08-07 13:15:53 +05:30
=
6f4be3e25a feat: removed name from tag and stricter slugification for tag endpoint 2024-08-07 13:14:39 +05:30
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
8d33647739 Merge pull request from Infisical/maidul-sqhdqwdgvqwjf
patch findProjectUserWorkspaceKey
2024-08-06 22:12:03 +05:30
d1c142e5b1 patch findProjectUserWorkspaceKey 2024-08-06 12:39:06 -04:00
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
bb1cad0c5b Merge pull request from Infisical/misc/add-org-level-rate-limit
misc: moved to license-plan-based rate limits
2024-08-06 10:42:57 -04:00
2a1cfe15b4 update text when secrets deleted after integ delete 2024-08-06 10:07:41 -04:00
e420973dd2 feat: hashpassword and add validation endpoint 2024-08-06 17:01:13 +03:00
881d70bc64 Merge pull request from Infisical/feat/enabled-secrets-deletion-on-integ-removal
feat: added secrets deletion feature on integration removal
2024-08-06 09:54:15 -04:00
14c1b4f07b misc: hide not found text when flag plain is enabled 2024-08-06 21:21:45 +08:00
3028bdd424 misc: made local workspace file not required if using auth token 2024-08-06 21:06:14 +08:00
15cc157c5f fix(lint): make password optional 2024-08-06 15:32:48 +03:00
902a0b0ed4 Merge pull request from akhilmhdh/fix/missing-coment-field 2024-08-06 08:18:18 -04:00
ad89ffe94d feat: show secret if no password was set 2024-08-06 14:42:01 +03:00
ba92192537 misc: removed creation limits completely 2024-08-06 19:41:09 +08:00
4de1713a18 fix: remove error logs 2024-08-06 14:28:02 +03:00
26ed8df73c misc: finalized list of license rate limits 2024-08-06 19:14:49 +08: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
c1decab912 misc: addressed comments 2024-08-06 18:58:07 +08:00
=
216c073290 fix: missing comment key in updated project 2024-08-06 16:14:25 +05:30
=
8626bce632 feat: added tag support for secret operation in cli 2024-08-06 15:36:03 +05:30
=
c5a2b0321f feat: completed secret v3 raw to support tag based filtering 2024-08-06 15:35:00 +05:30
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
1070954bdd misc: used destructuring 2024-08-06 02:05:13 +08:00
cc689d3178 feat: added secrets deletion feature on integration removal 2024-08-06 01:52:58 +08:00
0f23b7e1d3 misc: added check for undefined orgId 2024-08-03 02:10:47 +08:00
33193a47ae misc: updated default onprem rate limits 2024-08-03 01:52:04 +08:00
1ad286ca87 misc: name updates and more comments 2024-08-02 22:58:53 +08:00
be7c11a3f5 Merge remote-tracking branch 'origin/main' into misc/add-org-level-rate-limit 2024-08-02 22:42:23 +08:00
55a6740714 misc: moved to plan-based rate limit 2024-08-02 21:37:48 +08:00
dbe771dba0 refactor: remove unnecessary comment 2024-07-30 05:30:13 -04:00
273fd6c98f refactor: remove deprecated errors package
- Replace errors.Wrap with fmt.Errorf and %w verb
2024-07-30 05:23:43 -04:00
18aac6508b fix(cli): make yaml exporting reliable and standardized 2024-07-29 22:38:10 -04:00
687 changed files with 37380 additions and 10102 deletions
.env.example
.github/workflows
.gitignoreMakefileREADME.md
backend
e2e-test
package-lock.jsonpackage.json
scripts
src
@types
db
ee
routes
services
keystore
lib
queue
server
services
access-token-queue
auth-token
auth
certificate-authority
certificate-template
certificate
group-project
identity-access-token
identity-aws-auth
identity-oidc-auth
identity-project
identity-ua
identity
integration-auth
integration
kms
org-admin
org
pki-alert
pki-collection
project-bot
project-membership
project-role
project
resource-cleanup
secret-folder
secret-import
secret-sharing
secret-tag
secret-v2-bridge
secret
service-token
slack
smtp
super-admin
telemetry
user
workflow-integration
vitest.e2e.config.ts
cli
company
docker-compose.dev.yml
docs
api-reference/endpoints
cli/commands
documentation
images
integrations
platform
dynamic-secrets
pki
workflow-integrations/slack-integration
sso/google-saml
integrations
internals
mint.json
sdks
self-hosting
frontend
next.config.jspackage-lock.jsonpackage.json
public/data
src
components
context
ProjectPermissionContext
UserContext
WorkspaceContext
hooks
layouts/AppLayout
pages
integrations
aws-parameter-store
aws-secret-manager
azure-devops
circleci
details
github
login
org/[id]/overview
project/[id]/pki-collections/[collectionId]
signupinvite.tsx
views
IntegrationsPage
Login
Org
Project
SecretApprovalPage/components
SecretMainPage
SecretOverviewPage
SecretOverviewPage.tsx
components
CreateSecretForm
SecretOverviewTableRow
Settings
ShareSecretPage/components
ShareSecretPublicPage/components
ViewSecretPublicPage
admin/DashboardPage
helm-charts/secrets-operator
k8-operator/controllers
nginx
standalone-entrypoint.sh

@ -70,3 +70,5 @@ NEXT_PUBLIC_CAPTCHA_SITE_KEY=
PLAIN_API_KEY=
PLAIN_WISH_LABEL_IDS=
SSL_CLIENT_CERTIFICATE_HEADER_KEY=

@ -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

@ -50,6 +50,6 @@ jobs:
CLI_TESTS_ENV_SLUG: ${{ secrets.CLI_TESTS_ENV_SLUG }}
CLI_TESTS_USER_EMAIL: ${{ secrets.CLI_TESTS_USER_EMAIL }}
CLI_TESTS_USER_PASSWORD: ${{ secrets.CLI_TESTS_USER_PASSWORD }}
INFISICAL_VAULT_FILE_PASSPHRASE: ${{ secrets.CLI_TESTS_INFISICAL_VAULT_FILE_PASSPHRASE }}
# INFISICAL_VAULT_FILE_PASSPHRASE: ${{ secrets.CLI_TESTS_INFISICAL_VAULT_FILE_PASSPHRASE }}
run: go test -v -count=1 ./test

1
.gitignore vendored

@ -63,6 +63,7 @@ yarn-error.log*
# Editor specific
.vscode/*
.idea/*
frontend-build

@ -15,3 +15,16 @@ up-prod:
down:
docker compose -f docker-compose.dev.yml down
reviewable-ui:
cd frontend && \
npm run lint:fix && \
npm run type:check
reviewable-api:
cd backend && \
npm run lint:fix && \
npm run type:check
reviewable: reviewable-ui reviewable-api

File diff suppressed because one or more lines are too long

@ -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");
});
});

@ -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"
});
});
}
);

@ -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 }
);

@ -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 }
);
});

@ -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,

@ -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[];
};

@ -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[];
};

@ -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[];
}[];
};
};

@ -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();
}
};

5480
backend/package-lock.json generated

File diff suppressed because it is too large Load Diff

@ -34,9 +34,9 @@
"test": "echo \"Error: no test specified\" && exit 1",
"dev": "tsx watch --clear-screen=false ./src/main.ts | pino-pretty --colorize --colorizeObjects --singleLine",
"dev:docker": "nodemon",
"build": "tsup",
"build": "tsup --sourcemap",
"build:frontend": "npm run build --prefix ../frontend",
"start": "node dist/main.mjs",
"start": "node --enable-source-maps dist/main.mjs",
"type:check": "tsc --noEmit",
"lint:fix": "eslint --fix --ext js,ts ./src",
"lint": "eslint 'src/**/*.ts'",
@ -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",
@ -78,6 +79,7 @@
"@types/picomatch": "^2.3.3",
"@types/prompt-sync": "^4.2.3",
"@types/resolve": "^1.20.6",
"@types/safe-regex": "^1.1.6",
"@types/uuid": "^9.0.7",
"@typescript-eslint/eslint-plugin": "^6.20.0",
"@typescript-eslint/parser": "^6.20.0",
@ -101,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",
@ -121,12 +124,15 @@
"@fastify/swagger": "^8.14.0",
"@fastify/swagger-ui": "^2.1.0",
"@node-saml/passport-saml": "^4.0.4",
"@octokit/plugin-retry": "^5.0.5",
"@octokit/rest": "^20.0.2",
"@octokit/webhooks-types": "^7.3.1",
"@peculiar/asn1-schema": "^2.3.8",
"@peculiar/x509": "^1.10.0",
"@peculiar/x509": "^1.12.1",
"@serdnam/pino-cloudwatch-transport": "^1.0.4",
"@sindresorhus/slugify": "1.1.0",
"@slack/oauth": "^3.0.1",
"@slack/web-api": "^7.3.4",
"@team-plain/typescript-sdk": "^4.6.1",
"@ucast/mongo2js": "^1.3.4",
"ajv": "^8.12.0",
@ -154,6 +160,7 @@
"ldapjs": "^3.0.7",
"libsodium-wrappers": "^0.7.13",
"lodash.isequal": "^4.5.0",
"mongodb": "^6.8.1",
"ms": "^2.1.3",
"mysql2": "^3.9.8",
"nanoid": "^3.3.4",
@ -169,8 +176,12 @@
"pg-query-stream": "^4.5.3",
"picomatch": "^3.0.1",
"pino": "^8.16.2",
"pkijs": "^3.2.4",
"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",

@ -7,14 +7,33 @@ const prompt = promptSync({
sigint: true
});
type ComponentType = 1 | 2 | 3;
console.log(`
Component List
--------------
0. Exit
1. Service component
2. DAL component
3. Router component
`);
const componentType = parseInt(prompt("Select a component: "), 10);
function getComponentType(): ComponentType {
while (true) {
const input = prompt("Select a component (0-3): ");
const componentType = parseInt(input, 10);
if (componentType === 0) {
console.log("Exiting the program. Goodbye!");
process.exit(0);
} else if (componentType === 1 || componentType === 2 || componentType === 3) {
return componentType;
} else {
console.log("Invalid input. Please enter 0, 1, 2, or 3.");
}
}
}
const componentType = getComponentType();
if (componentType === 1) {
const componentName = prompt("Enter service name: ");

@ -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";
@ -18,6 +19,7 @@ import { TOidcConfigServiceFactory } from "@app/ee/services/oidc/oidc-config-ser
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { TProjectUserAdditionalPrivilegeServiceFactory } from "@app/ee/services/project-user-additional-privilege/project-user-additional-privilege-service";
import { TRateLimitServiceFactory } from "@app/ee/services/rate-limit/rate-limit-service";
import { RateLimitConfiguration } from "@app/ee/services/rate-limit/rate-limit-types";
import { TSamlConfigServiceFactory } from "@app/ee/services/saml-config/saml-config-service";
import { TScimServiceFactory } from "@app/ee/services/scim/scim-service";
import { TSecretApprovalPolicyServiceFactory } from "@app/ee/services/secret-approval-policy/secret-approval-policy-service";
@ -35,6 +37,7 @@ 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 { 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";
import { TIdentityAccessTokenServiceFactory } from "@app/services/identity-access-token/identity-access-token-service";
@ -51,6 +54,8 @@ import { TIntegrationAuthServiceFactory } from "@app/services/integration-auth/i
import { TOrgRoleServiceFactory } from "@app/services/org/org-role-service";
import { TOrgServiceFactory } from "@app/services/org/org-service";
import { TOrgAdminServiceFactory } from "@app/services/org-admin/org-admin-service";
import { TPkiAlertServiceFactory } from "@app/services/pki-alert/pki-alert-service";
import { TPkiCollectionServiceFactory } from "@app/services/pki-collection/pki-collection-service";
import { TProjectServiceFactory } from "@app/services/project/project-service";
import { TProjectBotServiceFactory } from "@app/services/project-bot/project-bot-service";
import { TProjectEnvServiceFactory } from "@app/services/project-env/project-env-service";
@ -65,12 +70,14 @@ import { TSecretReplicationServiceFactory } from "@app/services/secret-replicati
import { TSecretSharingServiceFactory } from "@app/services/secret-sharing/secret-sharing-service";
import { TSecretTagServiceFactory } from "@app/services/secret-tag/secret-tag-service";
import { TServiceTokenServiceFactory } from "@app/services/service-token/service-token-service";
import { TSlackServiceFactory } from "@app/services/slack/slack-service";
import { TSuperAdminServiceFactory } from "@app/services/super-admin/super-admin-service";
import { TTelemetryServiceFactory } from "@app/services/telemetry/telemetry-service";
import { TUserDALFactory } from "@app/services/user/user-dal";
import { TUserServiceFactory } from "@app/services/user/user-service";
import { TUserEngagementServiceFactory } from "@app/services/user-engagement/user-engagement-service";
import { TWebhookServiceFactory } from "@app/services/webhook/webhook-service";
import { TWorkflowIntegrationServiceFactory } from "@app/services/workflow-integration/workflow-integration-service";
declare module "fastify" {
interface FastifyRequest {
@ -89,6 +96,7 @@ declare module "fastify" {
id: string;
orgId: string;
};
rateLimits: RateLimitConfiguration;
// passport data
passportUser: {
isUserCompleted: string;
@ -114,6 +122,7 @@ declare module "fastify" {
group: TGroupServiceFactory;
groupProject: TGroupProjectServiceFactory;
apiKey: TApiKeyServiceFactory;
pkiAlert: TPkiAlertServiceFactory;
project: TProjectServiceFactory;
projectMembership: TProjectMembershipServiceFactory;
projectEnv: TProjectEnvServiceFactory;
@ -151,8 +160,11 @@ declare module "fastify" {
auditLog: TAuditLogServiceFactory;
auditLogStream: TAuditLogStreamServiceFactory;
certificate: TCertificateServiceFactory;
certificateTemplate: TCertificateTemplateServiceFactory;
certificateAuthority: TCertificateAuthorityServiceFactory;
certificateAuthorityCrl: TCertificateAuthorityCrlServiceFactory;
certificateEst: TCertificateEstServiceFactory;
pkiCollection: TPkiCollectionServiceFactory;
secretScanning: TSecretScanningServiceFactory;
license: TLicenseServiceFactory;
trustedIp: TTrustedIpServiceFactory;
@ -167,6 +179,8 @@ declare module "fastify" {
userEngagement: TUserEngagementServiceFactory;
externalKms: TExternalKmsServiceFactory;
orgAdmin: TOrgAdminServiceFactory;
slack: TSlackServiceFactory;
workflowIntegration: TWorkflowIntegrationServiceFactory;
};
// this is exclusive use for middlewares in which we need to inject data
// everywhere else access using service layer

@ -53,6 +53,12 @@ import {
TCertificateSecretsUpdate,
TCertificatesInsert,
TCertificatesUpdate,
TCertificateTemplateEstConfigs,
TCertificateTemplateEstConfigsInsert,
TCertificateTemplateEstConfigsUpdate,
TCertificateTemplates,
TCertificateTemplatesInsert,
TCertificateTemplatesUpdate,
TDynamicSecretLeases,
TDynamicSecretLeasesInsert,
TDynamicSecretLeasesUpdate,
@ -161,6 +167,15 @@ import {
TOrgRoles,
TOrgRolesInsert,
TOrgRolesUpdate,
TPkiAlerts,
TPkiAlertsInsert,
TPkiAlertsUpdate,
TPkiCollectionItems,
TPkiCollectionItemsInsert,
TPkiCollectionItemsUpdate,
TPkiCollections,
TPkiCollectionsInsert,
TPkiCollectionsUpdate,
TProjectBots,
TProjectBotsInsert,
TProjectBotsUpdate,
@ -178,6 +193,9 @@ import {
TProjectRolesUpdate,
TProjects,
TProjectsInsert,
TProjectSlackConfigs,
TProjectSlackConfigsInsert,
TProjectSlackConfigsUpdate,
TProjectsUpdate,
TProjectUserAdditionalPrivilege,
TProjectUserAdditionalPrivilegeInsert,
@ -284,6 +302,9 @@ import {
TServiceTokens,
TServiceTokensInsert,
TServiceTokensUpdate,
TSlackIntegrations,
TSlackIntegrationsInsert,
TSlackIntegrationsUpdate,
TSuperAdmin,
TSuperAdminInsert,
TSuperAdminUpdate,
@ -307,7 +328,10 @@ import {
TUsersUpdate,
TWebhooks,
TWebhooksInsert,
TWebhooksUpdate
TWebhooksUpdate,
TWorkflowIntegrations,
TWorkflowIntegrationsInsert,
TWorkflowIntegrationsUpdate
} from "@app/db/schemas";
import {
TSecretV2TagJunction,
@ -355,6 +379,16 @@ declare module "knex/types/tables" {
TCertificateAuthorityCrlUpdate
>;
[TableName.Certificate]: KnexOriginal.CompositeTableType<TCertificates, TCertificatesInsert, TCertificatesUpdate>;
[TableName.CertificateTemplate]: KnexOriginal.CompositeTableType<
TCertificateTemplates,
TCertificateTemplatesInsert,
TCertificateTemplatesUpdate
>;
[TableName.CertificateTemplateEstConfig]: KnexOriginal.CompositeTableType<
TCertificateTemplateEstConfigs,
TCertificateTemplateEstConfigsInsert,
TCertificateTemplateEstConfigsUpdate
>;
[TableName.CertificateBody]: KnexOriginal.CompositeTableType<
TCertificateBodies,
TCertificateBodiesInsert,
@ -365,6 +399,17 @@ declare module "knex/types/tables" {
TCertificateSecretsInsert,
TCertificateSecretsUpdate
>;
[TableName.PkiAlert]: KnexOriginal.CompositeTableType<TPkiAlerts, TPkiAlertsInsert, TPkiAlertsUpdate>;
[TableName.PkiCollection]: KnexOriginal.CompositeTableType<
TPkiCollections,
TPkiCollectionsInsert,
TPkiCollectionsUpdate
>;
[TableName.PkiCollectionItem]: KnexOriginal.CompositeTableType<
TPkiCollectionItems,
TPkiCollectionItemsInsert,
TPkiCollectionItemsUpdate
>;
[TableName.UserGroupMembership]: KnexOriginal.CompositeTableType<
TUserGroupMembership,
TUserGroupMembershipInsert,
@ -740,5 +785,20 @@ declare module "knex/types/tables" {
TKmsKeyVersionsInsert,
TKmsKeyVersionsUpdate
>;
[TableName.SlackIntegrations]: KnexOriginal.CompositeTableType<
TSlackIntegrations,
TSlackIntegrationsInsert,
TSlackIntegrationsUpdate
>;
[TableName.ProjectSlackConfigs]: KnexOriginal.CompositeTableType<
TProjectSlackConfigs,
TProjectSlackConfigsInsert,
TProjectSlackConfigsUpdate
>;
[TableName.WorkflowIntegrations]: KnexOriginal.CompositeTableType<
TWorkflowIntegrations,
TWorkflowIntegrationsInsert,
TWorkflowIntegrationsUpdate
>;
}
}

@ -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();

@ -0,0 +1,294 @@
import { Knex } from "knex";
import { TableName } from "../schemas";
export async function up(knex: Knex): Promise<void> {
// ---------- ACCESS APPROVAL POLICY APPROVER ------------
const hasApproverUserId = await knex.schema.hasColumn(TableName.AccessApprovalPolicyApprover, "approverUserId");
const hasApproverId = await knex.schema.hasColumn(TableName.AccessApprovalPolicyApprover, "approverId");
if (!hasApproverUserId) {
// add the new fields
await knex.schema.alterTable(TableName.AccessApprovalPolicyApprover, (tb) => {
// if (hasApproverId) tb.setNullable("approverId");
tb.uuid("approverUserId");
tb.foreign("approverUserId").references("id").inTable(TableName.Users).onDelete("CASCADE");
});
// convert project membership id => user id
await knex(TableName.AccessApprovalPolicyApprover).update({
// eslint-disable-next-line
// @ts-ignore because generate schema happens after this
approverUserId: knex(TableName.ProjectMembership)
.select("userId")
.where("id", knex.raw("??", [`${TableName.AccessApprovalPolicyApprover}.approverId`]))
});
// drop the old field
await knex.schema.alterTable(TableName.AccessApprovalPolicyApprover, (tb) => {
if (hasApproverId) tb.dropColumn("approverId");
tb.uuid("approverUserId").notNullable().alter();
});
}
// ---------- ACCESS APPROVAL REQUEST ------------
const hasAccessApprovalRequestTable = await knex.schema.hasTable(TableName.AccessApprovalRequest);
const hasRequestedByUserId = await knex.schema.hasColumn(TableName.AccessApprovalRequest, "requestedByUserId");
const hasRequestedBy = await knex.schema.hasColumn(TableName.AccessApprovalRequest, "requestedBy");
if (hasAccessApprovalRequestTable) {
// new fields
await knex.schema.alterTable(TableName.AccessApprovalRequest, (tb) => {
if (!hasRequestedByUserId) {
tb.uuid("requestedByUserId");
tb.foreign("requestedByUserId").references("id").inTable(TableName.Users).onDelete("SET NULL");
}
});
// copy the assigned project membership => user id to new fields
await knex(TableName.AccessApprovalRequest).update({
// eslint-disable-next-line
// @ts-ignore because generate schema happens after this
requestedByUserId: knex(TableName.ProjectMembership)
.select("userId")
.where("id", knex.raw("??", [`${TableName.AccessApprovalRequest}.requestedBy`]))
});
// drop old fields
await knex.schema.alterTable(TableName.AccessApprovalRequest, (tb) => {
if (hasRequestedBy) {
// DROP AT A LATER TIME
// tb.dropColumn("requestedBy");
// ADD ALLOW NULLABLE FOR NOW
tb.uuid("requestedBy").nullable().alter();
}
tb.uuid("requestedByUserId").notNullable().alter();
});
}
// ---------- ACCESS APPROVAL REQUEST REVIEWER ------------
const hasMemberId = await knex.schema.hasColumn(TableName.AccessApprovalRequestReviewer, "member");
const hasReviewerUserId = await knex.schema.hasColumn(TableName.AccessApprovalRequestReviewer, "reviewerUserId");
if (!hasReviewerUserId) {
// new fields
await knex.schema.alterTable(TableName.AccessApprovalRequestReviewer, (tb) => {
// if (hasMemberId) tb.setNullable("member");
tb.uuid("reviewerUserId");
tb.foreign("reviewerUserId").references("id").inTable(TableName.Users).onDelete("SET NULL");
});
// copy project membership => user id to new fields
await knex(TableName.AccessApprovalRequestReviewer).update({
// eslint-disable-next-line
// @ts-ignore because generate schema happens after this
reviewerUserId: knex(TableName.ProjectMembership)
.select("userId")
.where("id", knex.raw("??", [`${TableName.AccessApprovalRequestReviewer}.member`]))
});
// drop table
await knex.schema.alterTable(TableName.AccessApprovalRequestReviewer, (tb) => {
if (hasMemberId) {
// DROP AT A LATER TIME
// tb.dropColumn("member");
// ADD ALLOW NULLABLE FOR NOW
tb.uuid("member").nullable().alter();
}
tb.uuid("reviewerUserId").notNullable().alter();
});
}
// ---------- PROJECT USER ADDITIONAL PRIVILEGE ------------
const projectUserAdditionalPrivilegeHasProjectMembershipId = await knex.schema.hasColumn(
TableName.ProjectUserAdditionalPrivilege,
"projectMembershipId"
);
const projectUserAdditionalPrivilegeHasUserId = await knex.schema.hasColumn(
TableName.ProjectUserAdditionalPrivilege,
"userId"
);
if (!projectUserAdditionalPrivilegeHasUserId) {
await knex.schema.alterTable(TableName.ProjectUserAdditionalPrivilege, (tb) => {
tb.uuid("userId");
tb.foreign("userId").references("id").inTable(TableName.Users).onDelete("CASCADE");
tb.string("projectId");
tb.foreign("projectId").references("id").inTable(TableName.Project).onDelete("CASCADE");
});
await knex(TableName.ProjectUserAdditionalPrivilege)
.update({
// eslint-disable-next-line
// @ts-ignore because generate schema happens after this
userId: knex(TableName.ProjectMembership)
.select("userId")
.where("id", knex.raw("??", [`${TableName.ProjectUserAdditionalPrivilege}.projectMembershipId`])),
// eslint-disable-next-line
// @ts-ignore because generate schema happens after this
projectId: knex(TableName.ProjectMembership)
.select("projectId")
.where("id", knex.raw("??", [`${TableName.ProjectUserAdditionalPrivilege}.projectMembershipId`]))
})
.whereNotNull("projectMembershipId");
await knex.schema.alterTable(TableName.ProjectUserAdditionalPrivilege, (tb) => {
tb.uuid("userId").notNullable().alter();
tb.string("projectId").notNullable().alter();
});
}
if (projectUserAdditionalPrivilegeHasProjectMembershipId) {
await knex.schema.alterTable(TableName.ProjectUserAdditionalPrivilege, (tb) => {
// DROP AT A LATER TIME
// tb.dropColumn("projectMembershipId");
// ADD ALLOW NULLABLE FOR NOW
tb.uuid("projectMembershipId").nullable().alter();
});
}
}
export async function down(knex: Knex): Promise<void> {
// We remove project user additional privileges first, because it may delete records in the database where the project membership is not found.
// The project membership won't be found on records created by group members. In those cades we just delete the record and continue.
// When the additionl privilege record is deleted, it will cascade delete the access request created by the group member.
// ---------- PROJECT USER ADDITIONAL PRIVILEGE ------------
const hasUserId = await knex.schema.hasColumn(TableName.ProjectUserAdditionalPrivilege, "userId");
const hasProjectMembershipId = await knex.schema.hasColumn(
TableName.ProjectUserAdditionalPrivilege,
"projectMembershipId"
);
// If it doesn't have the userId field, then the up migration has not run
if (!hasUserId) {
return;
}
await knex.schema.alterTable(TableName.ProjectUserAdditionalPrivilege, (tb) => {
if (!hasProjectMembershipId) {
tb.uuid("projectMembershipId");
tb.foreign("projectMembershipId").references("id").inTable(TableName.ProjectMembership).onDelete("CASCADE");
}
});
if (!hasProjectMembershipId) {
// First, update records where a matching project membership exists
await knex(TableName.ProjectUserAdditionalPrivilege).update({
// eslint-disable-next-line
// @ts-ignore because generate schema happens after this
projectMembershipId: knex(TableName.ProjectMembership)
.select("id")
.where("userId", knex.raw("??", [`${TableName.ProjectUserAdditionalPrivilege}.userId`]))
});
await knex(TableName.AccessApprovalRequest).update({
// eslint-disable-next-line
// @ts-ignore because generate schema happens after this
projectMembershipId: knex(TableName.ProjectMembership)
.select("id")
.where("userId", knex.raw("??", [`${TableName.SecretApprovalRequest}.userId`]))
});
await knex.schema.alterTable(TableName.ProjectUserAdditionalPrivilege, (tb) => {
tb.dropColumn("userId");
tb.dropColumn("projectId");
tb.uuid("projectMembershipId").notNullable().alter();
});
}
// Then, delete records where no matching project membership was found
await knex(TableName.ProjectUserAdditionalPrivilege).whereNull("projectMembershipId").delete();
await knex(TableName.AccessApprovalRequest).whereNull("requestedBy").delete();
// ---------- ACCESS APPROVAL POLICY APPROVER ------------
const hasApproverUserId = await knex.schema.hasColumn(TableName.AccessApprovalPolicyApprover, "approverUserId");
const hasApproverId = await knex.schema.hasColumn(TableName.AccessApprovalPolicyApprover, "approverId");
if (hasApproverUserId) {
await knex.schema.alterTable(TableName.AccessApprovalPolicyApprover, (tb) => {
if (!hasApproverId) {
tb.uuid("approverId");
tb.foreign("approverId").references("id").inTable(TableName.ProjectMembership).onDelete("CASCADE");
}
});
if (!hasApproverId) {
await knex(TableName.AccessApprovalPolicyApprover).update({
// eslint-disable-next-line
// @ts-ignore because generate schema happens after this
approverId: knex(TableName.ProjectMembership)
.select("id")
.where("userId", knex.raw("??", [`${TableName.AccessApprovalPolicyApprover}.approverUserId`]))
});
await knex.schema.alterTable(TableName.AccessApprovalPolicyApprover, (tb) => {
tb.dropColumn("approverUserId");
tb.uuid("approverId").notNullable().alter();
});
}
// ---------- ACCESS APPROVAL REQUEST ------------
const hasAccessApprovalRequestTable = await knex.schema.hasTable(TableName.AccessApprovalRequest);
const hasRequestedByUserId = await knex.schema.hasColumn(TableName.AccessApprovalRequest, "requestedByUserId");
const hasRequestedBy = await knex.schema.hasColumn(TableName.AccessApprovalRequest, "requestedBy");
if (hasAccessApprovalRequestTable) {
await knex.schema.alterTable(TableName.AccessApprovalRequest, (tb) => {
if (!hasRequestedBy) {
tb.uuid("requestedBy");
tb.foreign("requestedBy").references("id").inTable(TableName.ProjectMembership).onDelete("CASCADE");
}
});
// Try to find a project membership based on the AccessApprovalRequest.requestedByUserId and AccessApprovalRequest.policyId(reference to AccessApprovalRequestPolicy).envId(reference to Environment).projectId(reference to Project)
// If a project membership is found, set the AccessApprovalRequest.requestedBy to the project membership id
// If a project membership is not found, remove the AccessApprovalRequest record
await knex(TableName.AccessApprovalRequest).update({
// eslint-disable-next-line
// @ts-ignore because generate schema happens after this
requestedBy: knex(TableName.ProjectMembership)
.select("id")
.where("userId", knex.raw("??", [`${TableName.AccessApprovalRequest}.requestedByUserId`]))
});
// Then, delete records where no matching project membership was found
await knex(TableName.AccessApprovalRequest).whereNull("requestedBy").delete();
await knex.schema.alterTable(TableName.AccessApprovalRequest, (tb) => {
if (hasRequestedByUserId) {
tb.dropColumn("requestedByUserId");
}
if (hasRequestedBy) tb.uuid("requestedBy").notNullable().alter();
});
}
// ---------- ACCESS APPROVAL REQUEST REVIEWER ------------
const hasMemberId = await knex.schema.hasColumn(TableName.AccessApprovalRequestReviewer, "member");
const hasReviewerUserId = await knex.schema.hasColumn(TableName.AccessApprovalRequestReviewer, "reviewerUserId");
if (hasReviewerUserId) {
if (!hasMemberId) {
await knex.schema.alterTable(TableName.AccessApprovalRequestReviewer, (tb) => {
tb.uuid("member");
tb.foreign("member").references("id").inTable(TableName.ProjectMembership).onDelete("CASCADE");
});
}
await knex(TableName.AccessApprovalRequestReviewer).update({
// eslint-disable-next-line
// @ts-ignore because generate schema happens after this
member: knex(TableName.ProjectMembership)
.select("id")
.where("userId", knex.raw("??", [`${TableName.AccessApprovalRequestReviewer}.reviewerUserId`]))
});
await knex.schema.alterTable(TableName.AccessApprovalRequestReviewer, (tb) => {
tb.dropColumn("reviewerUserId");
tb.uuid("member").notNullable().alter();
});
}
}
}

@ -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");
});
}
}
}

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

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

@ -0,0 +1,62 @@
import { Knex } from "knex";
import { TableName } from "../schemas";
import { createOnUpdateTrigger, dropOnUpdateTrigger } from "../utils";
export async function up(knex: Knex): Promise<void> {
if (!(await knex.schema.hasTable(TableName.PkiCollection))) {
await knex.schema.createTable(TableName.PkiCollection, (t) => {
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
t.timestamps(true, true, true);
t.string("projectId").notNullable();
t.foreign("projectId").references("id").inTable(TableName.Project).onDelete("CASCADE");
t.string("name").notNullable();
t.string("description").notNullable();
});
}
await createOnUpdateTrigger(knex, TableName.PkiCollection);
if (!(await knex.schema.hasTable(TableName.PkiCollectionItem))) {
await knex.schema.createTable(TableName.PkiCollectionItem, (t) => {
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
t.timestamps(true, true, true);
t.uuid("pkiCollectionId").notNullable();
t.foreign("pkiCollectionId").references("id").inTable(TableName.PkiCollection).onDelete("CASCADE");
t.uuid("caId").nullable();
t.foreign("caId").references("id").inTable(TableName.CertificateAuthority).onDelete("CASCADE");
t.uuid("certId").nullable();
t.foreign("certId").references("id").inTable(TableName.Certificate).onDelete("CASCADE");
});
}
await createOnUpdateTrigger(knex, TableName.PkiCollectionItem);
if (!(await knex.schema.hasTable(TableName.PkiAlert))) {
await knex.schema.createTable(TableName.PkiAlert, (t) => {
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
t.timestamps(true, true, true);
t.string("projectId").notNullable();
t.foreign("projectId").references("id").inTable(TableName.Project).onDelete("CASCADE");
t.uuid("pkiCollectionId").notNullable();
t.foreign("pkiCollectionId").references("id").inTable(TableName.PkiCollection).onDelete("CASCADE");
t.string("name").notNullable();
t.integer("alertBeforeDays").notNullable();
t.string("recipientEmails").notNullable();
t.unique(["name", "projectId"]);
});
}
await createOnUpdateTrigger(knex, TableName.PkiAlert);
}
export async function down(knex: Knex): Promise<void> {
await knex.schema.dropTableIfExists(TableName.PkiAlert);
await dropOnUpdateTrigger(knex, TableName.PkiAlert);
await knex.schema.dropTableIfExists(TableName.PkiCollectionItem);
await dropOnUpdateTrigger(knex, TableName.PkiCollectionItem);
await knex.schema.dropTableIfExists(TableName.PkiCollection);
await dropOnUpdateTrigger(knex, TableName.PkiCollection);
}

@ -0,0 +1,55 @@
import { Knex } from "knex";
import { TableName } from "../schemas";
import { createOnUpdateTrigger, dropOnUpdateTrigger } from "../utils";
export async function up(knex: Knex): Promise<void> {
const hasCertificateTemplateTable = await knex.schema.hasTable(TableName.CertificateTemplate);
if (!hasCertificateTemplateTable) {
await knex.schema.createTable(TableName.CertificateTemplate, (tb) => {
tb.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
tb.uuid("caId").notNullable();
tb.foreign("caId").references("id").inTable(TableName.CertificateAuthority).onDelete("CASCADE");
tb.uuid("pkiCollectionId");
tb.foreign("pkiCollectionId").references("id").inTable(TableName.PkiCollection).onDelete("SET NULL");
tb.string("name").notNullable();
tb.string("commonName").notNullable();
tb.string("subjectAlternativeName").notNullable();
tb.string("ttl").notNullable();
tb.timestamps(true, true, true);
});
await createOnUpdateTrigger(knex, TableName.CertificateTemplate);
}
const doesCertificateTableHaveTemplateId = await knex.schema.hasColumn(
TableName.Certificate,
"certificateTemplateId"
);
if (!doesCertificateTableHaveTemplateId) {
await knex.schema.alterTable(TableName.Certificate, (tb) => {
tb.uuid("certificateTemplateId");
tb.foreign("certificateTemplateId").references("id").inTable(TableName.CertificateTemplate).onDelete("SET NULL");
});
}
}
export async function down(knex: Knex): Promise<void> {
const doesCertificateTableHaveTemplateId = await knex.schema.hasColumn(
TableName.Certificate,
"certificateTemplateId"
);
if (doesCertificateTableHaveTemplateId) {
await knex.schema.alterTable(TableName.Certificate, (t) => {
t.dropColumn("certificateTemplateId");
});
}
const hasCertificateTemplateTable = await knex.schema.hasTable(TableName.CertificateTemplate);
if (hasCertificateTemplateTable) {
await knex.schema.dropTable(TableName.CertificateTemplate);
await dropOnUpdateTrigger(knex, TableName.CertificateTemplate);
}
}

@ -0,0 +1,26 @@
import { Knex } from "knex";
import { TableName } from "../schemas";
import { createOnUpdateTrigger, dropOnUpdateTrigger } from "../utils";
export async function up(knex: Knex): Promise<void> {
const hasEstConfigTable = await knex.schema.hasTable(TableName.CertificateTemplateEstConfig);
if (!hasEstConfigTable) {
await knex.schema.createTable(TableName.CertificateTemplateEstConfig, (tb) => {
tb.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
tb.uuid("certificateTemplateId").notNullable().unique();
tb.foreign("certificateTemplateId").references("id").inTable(TableName.CertificateTemplate).onDelete("CASCADE");
tb.binary("encryptedCaChain").notNullable();
tb.string("hashedPassphrase").notNullable();
tb.boolean("isEnabled").notNullable();
tb.timestamps(true, true, true);
});
await createOnUpdateTrigger(knex, TableName.CertificateTemplateEstConfig);
}
}
export async function down(knex: Knex): Promise<void> {
await knex.schema.dropTableIfExists(TableName.CertificateTemplateEstConfig);
await dropOnUpdateTrigger(knex, TableName.CertificateTemplateEstConfig);
}

@ -0,0 +1,36 @@
import { Knex } from "knex";
import { TableName } from "../schemas";
export async function up(knex: Knex): Promise<void> {
if (await knex.schema.hasTable(TableName.CertificateAuthorityCrl)) {
const hasCaSecretIdColumn = await knex.schema.hasColumn(TableName.CertificateAuthorityCrl, "caSecretId");
if (!hasCaSecretIdColumn) {
await knex.schema.alterTable(TableName.CertificateAuthorityCrl, (t) => {
t.uuid("caSecretId").nullable();
t.foreign("caSecretId").references("id").inTable(TableName.CertificateAuthoritySecret).onDelete("CASCADE");
});
await knex.raw(`
UPDATE "${TableName.CertificateAuthorityCrl}" crl
SET "caSecretId" = (
SELECT sec.id
FROM "${TableName.CertificateAuthoritySecret}" sec
WHERE sec."caId" = crl."caId"
)
`);
await knex.schema.alterTable(TableName.CertificateAuthorityCrl, (t) => {
t.uuid("caSecretId").notNullable().alter();
});
}
}
}
export async function down(knex: Knex): Promise<void> {
if (await knex.schema.hasTable(TableName.CertificateAuthorityCrl)) {
await knex.schema.alterTable(TableName.CertificateAuthorityCrl, (t) => {
t.dropColumn("caSecretId");
});
}
}

@ -0,0 +1,96 @@
import { Knex } from "knex";
import { TableName } from "../schemas";
import { createOnUpdateTrigger, dropOnUpdateTrigger } from "../utils";
export async function up(knex: Knex): Promise<void> {
if (!(await knex.schema.hasTable(TableName.WorkflowIntegrations))) {
await knex.schema.createTable(TableName.WorkflowIntegrations, (tb) => {
tb.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
tb.string("integration").notNullable();
tb.string("slug").notNullable();
tb.uuid("orgId").notNullable();
tb.foreign("orgId").references("id").inTable(TableName.Organization).onDelete("CASCADE");
tb.string("description");
tb.unique(["orgId", "slug"]);
tb.timestamps(true, true, true);
});
await createOnUpdateTrigger(knex, TableName.WorkflowIntegrations);
}
if (!(await knex.schema.hasTable(TableName.SlackIntegrations))) {
await knex.schema.createTable(TableName.SlackIntegrations, (tb) => {
tb.uuid("id", { primaryKey: true }).notNullable();
tb.foreign("id").references("id").inTable(TableName.WorkflowIntegrations).onDelete("CASCADE");
tb.string("teamId").notNullable();
tb.string("teamName").notNullable();
tb.string("slackUserId").notNullable();
tb.string("slackAppId").notNullable();
tb.binary("encryptedBotAccessToken").notNullable();
tb.string("slackBotId").notNullable();
tb.string("slackBotUserId").notNullable();
tb.timestamps(true, true, true);
});
await createOnUpdateTrigger(knex, TableName.SlackIntegrations);
}
if (!(await knex.schema.hasTable(TableName.ProjectSlackConfigs))) {
await knex.schema.createTable(TableName.ProjectSlackConfigs, (tb) => {
tb.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
tb.string("projectId").notNullable().unique();
tb.foreign("projectId").references("id").inTable(TableName.Project).onDelete("CASCADE");
tb.uuid("slackIntegrationId").notNullable();
tb.foreign("slackIntegrationId").references("id").inTable(TableName.SlackIntegrations).onDelete("CASCADE");
tb.boolean("isAccessRequestNotificationEnabled").notNullable().defaultTo(false);
tb.string("accessRequestChannels").notNullable().defaultTo("");
tb.boolean("isSecretRequestNotificationEnabled").notNullable().defaultTo(false);
tb.string("secretRequestChannels").notNullable().defaultTo("");
tb.timestamps(true, true, true);
});
await createOnUpdateTrigger(knex, TableName.ProjectSlackConfigs);
}
const doesSuperAdminHaveSlackClientId = await knex.schema.hasColumn(TableName.SuperAdmin, "encryptedSlackClientId");
const doesSuperAdminHaveSlackClientSecret = await knex.schema.hasColumn(
TableName.SuperAdmin,
"encryptedSlackClientSecret"
);
await knex.schema.alterTable(TableName.SuperAdmin, (tb) => {
if (!doesSuperAdminHaveSlackClientId) {
tb.binary("encryptedSlackClientId");
}
if (!doesSuperAdminHaveSlackClientSecret) {
tb.binary("encryptedSlackClientSecret");
}
});
}
export async function down(knex: Knex): Promise<void> {
await knex.schema.dropTableIfExists(TableName.ProjectSlackConfigs);
await dropOnUpdateTrigger(knex, TableName.ProjectSlackConfigs);
await knex.schema.dropTableIfExists(TableName.SlackIntegrations);
await dropOnUpdateTrigger(knex, TableName.SlackIntegrations);
await knex.schema.dropTableIfExists(TableName.WorkflowIntegrations);
await dropOnUpdateTrigger(knex, TableName.WorkflowIntegrations);
const doesSuperAdminHaveSlackClientId = await knex.schema.hasColumn(TableName.SuperAdmin, "encryptedSlackClientId");
const doesSuperAdminHaveSlackClientSecret = await knex.schema.hasColumn(
TableName.SuperAdmin,
"encryptedSlackClientSecret"
);
await knex.schema.alterTable(TableName.SuperAdmin, (tb) => {
if (doesSuperAdminHaveSlackClientId) {
tb.dropColumn("encryptedSlackClientId");
}
if (doesSuperAdminHaveSlackClientSecret) {
tb.dropColumn("encryptedSlackClientSecret");
}
});
}

@ -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.CertificateAuthority)) {
const hasRequireTemplateForIssuanceColumn = await knex.schema.hasColumn(
TableName.CertificateAuthority,
"requireTemplateForIssuance"
);
if (!hasRequireTemplateForIssuanceColumn) {
await knex.schema.alterTable(TableName.CertificateAuthority, (t) => {
t.boolean("requireTemplateForIssuance").notNullable().defaultTo(false);
});
}
}
}
export async function down(knex: Knex): Promise<void> {
if (await knex.schema.hasTable(TableName.CertificateAuthority)) {
await knex.schema.alterTable(TableName.CertificateAuthority, (t) => {
t.dropColumn("requireTemplateForIssuance");
});
}
}

@ -0,0 +1,85 @@
import { Knex } from "knex";
import { CertKeyUsage } from "@app/services/certificate/certificate-types";
import { TableName } from "../schemas";
export async function up(knex: Knex): Promise<void> {
// Certificate template
const hasKeyUsagesCol = await knex.schema.hasColumn(TableName.CertificateTemplate, "keyUsages");
const hasExtendedKeyUsagesCol = await knex.schema.hasColumn(TableName.CertificateTemplate, "extendedKeyUsages");
await knex.schema.alterTable(TableName.CertificateTemplate, (tb) => {
if (!hasKeyUsagesCol) {
tb.specificType("keyUsages", "text[]");
}
if (!hasExtendedKeyUsagesCol) {
tb.specificType("extendedKeyUsages", "text[]");
}
});
if (!hasKeyUsagesCol) {
await knex(TableName.CertificateTemplate).update({
keyUsages: [CertKeyUsage.DIGITAL_SIGNATURE, CertKeyUsage.KEY_ENCIPHERMENT]
});
}
if (!hasExtendedKeyUsagesCol) {
await knex(TableName.CertificateTemplate).update({
extendedKeyUsages: []
});
}
// Certificate
const doesCertTableHaveKeyUsages = await knex.schema.hasColumn(TableName.Certificate, "keyUsages");
const doesCertTableHaveExtendedKeyUsages = await knex.schema.hasColumn(TableName.Certificate, "extendedKeyUsages");
await knex.schema.alterTable(TableName.Certificate, (tb) => {
if (!doesCertTableHaveKeyUsages) {
tb.specificType("keyUsages", "text[]");
}
if (!doesCertTableHaveExtendedKeyUsages) {
tb.specificType("extendedKeyUsages", "text[]");
}
});
if (!doesCertTableHaveKeyUsages) {
await knex(TableName.Certificate).update({
keyUsages: [CertKeyUsage.DIGITAL_SIGNATURE, CertKeyUsage.KEY_ENCIPHERMENT]
});
}
if (!doesCertTableHaveExtendedKeyUsages) {
await knex(TableName.Certificate).update({
extendedKeyUsages: []
});
}
}
export async function down(knex: Knex): Promise<void> {
// Certificate Template
const hasKeyUsagesCol = await knex.schema.hasColumn(TableName.CertificateTemplate, "keyUsages");
const hasExtendedKeyUsagesCol = await knex.schema.hasColumn(TableName.CertificateTemplate, "extendedKeyUsages");
await knex.schema.alterTable(TableName.CertificateTemplate, (t) => {
if (hasKeyUsagesCol) {
t.dropColumn("keyUsages");
}
if (hasExtendedKeyUsagesCol) {
t.dropColumn("extendedKeyUsages");
}
});
// Certificate
const doesCertTableHaveKeyUsages = await knex.schema.hasColumn(TableName.Certificate, "keyUsages");
const doesCertTableHaveExtendedKeyUsages = await knex.schema.hasColumn(TableName.Certificate, "extendedKeyUsages");
await knex.schema.alterTable(TableName.Certificate, (t) => {
if (doesCertTableHaveKeyUsages) {
t.dropColumn("keyUsages");
}
if (doesCertTableHaveExtendedKeyUsages) {
t.dropColumn("extendedKeyUsages");
}
});
}

@ -9,10 +9,10 @@ import { TImmutableDBKeys } from "./models";
export const AccessApprovalPoliciesApproversSchema = z.object({
id: z.string().uuid(),
approverId: z.string().uuid(),
policyId: z.string().uuid(),
createdAt: z.date(),
updatedAt: z.date()
updatedAt: z.date(),
approverUserId: z.string().uuid()
});
export type TAccessApprovalPoliciesApprovers = z.infer<typeof AccessApprovalPoliciesApproversSchema>;

@ -9,11 +9,12 @@ import { TImmutableDBKeys } from "./models";
export const AccessApprovalRequestsReviewersSchema = z.object({
id: z.string().uuid(),
member: z.string().uuid(),
member: z.string().uuid().nullable().optional(),
status: z.string(),
requestId: z.string().uuid(),
createdAt: z.date(),
updatedAt: z.date()
updatedAt: z.date(),
reviewerUserId: z.string().uuid()
});
export type TAccessApprovalRequestsReviewers = z.infer<typeof AccessApprovalRequestsReviewersSchema>;

@ -11,12 +11,13 @@ export const AccessApprovalRequestsSchema = z.object({
id: z.string().uuid(),
policyId: z.string().uuid(),
privilegeId: z.string().uuid().nullable().optional(),
requestedBy: z.string().uuid(),
requestedBy: z.string().uuid().nullable().optional(),
isTemporary: z.boolean(),
temporaryRange: z.string().nullable().optional(),
permissions: z.unknown(),
createdAt: z.date(),
updatedAt: z.date()
updatedAt: z.date(),
requestedByUserId: z.string().uuid()
});
export type TAccessApprovalRequests = z.infer<typeof AccessApprovalRequestsSchema>;

@ -28,7 +28,8 @@ export const CertificateAuthoritiesSchema = z.object({
keyAlgorithm: z.string(),
notBefore: z.date().nullable().optional(),
notAfter: z.date().nullable().optional(),
activeCaCertId: z.string().uuid().nullable().optional()
activeCaCertId: z.string().uuid().nullable().optional(),
requireTemplateForIssuance: z.boolean().default(false)
});
export type TCertificateAuthorities = z.infer<typeof CertificateAuthoritiesSchema>;

@ -14,7 +14,8 @@ export const CertificateAuthorityCrlSchema = z.object({
createdAt: z.date(),
updatedAt: z.date(),
caId: z.string().uuid(),
encryptedCrl: zodBuffer
encryptedCrl: zodBuffer,
caSecretId: z.string().uuid()
});
export type TCertificateAuthorityCrl = z.infer<typeof CertificateAuthorityCrlSchema>;

@ -0,0 +1,29 @@
// Code generated by automation script, DO NOT EDIT.
// Automated by pulling database and generating zod schema
// To update. Just run npm run generate:schema
// Written by akhilmhdh.
import { z } from "zod";
import { zodBuffer } from "@app/lib/zod";
import { TImmutableDBKeys } from "./models";
export const CertificateTemplateEstConfigsSchema = z.object({
id: z.string().uuid(),
certificateTemplateId: z.string().uuid(),
encryptedCaChain: zodBuffer,
hashedPassphrase: z.string(),
isEnabled: z.boolean(),
createdAt: z.date(),
updatedAt: z.date()
});
export type TCertificateTemplateEstConfigs = z.infer<typeof CertificateTemplateEstConfigsSchema>;
export type TCertificateTemplateEstConfigsInsert = Omit<
z.input<typeof CertificateTemplateEstConfigsSchema>,
TImmutableDBKeys
>;
export type TCertificateTemplateEstConfigsUpdate = Partial<
Omit<z.input<typeof CertificateTemplateEstConfigsSchema>, TImmutableDBKeys>
>;

@ -0,0 +1,26 @@
// Code generated by automation script, DO NOT EDIT.
// Automated by pulling database and generating zod schema
// To update. Just run npm run generate:schema
// Written by akhilmhdh.
import { z } from "zod";
import { TImmutableDBKeys } from "./models";
export const CertificateTemplatesSchema = z.object({
id: z.string().uuid(),
caId: z.string().uuid(),
pkiCollectionId: z.string().uuid().nullable().optional(),
name: z.string(),
commonName: z.string(),
subjectAlternativeName: z.string(),
ttl: z.string(),
createdAt: z.date(),
updatedAt: z.date(),
keyUsages: z.string().array().nullable().optional(),
extendedKeyUsages: z.string().array().nullable().optional()
});
export type TCertificateTemplates = z.infer<typeof CertificateTemplatesSchema>;
export type TCertificateTemplatesInsert = Omit<z.input<typeof CertificateTemplatesSchema>, TImmutableDBKeys>;
export type TCertificateTemplatesUpdate = Partial<Omit<z.input<typeof CertificateTemplatesSchema>, TImmutableDBKeys>>;

@ -21,7 +21,10 @@ export const CertificatesSchema = z.object({
revokedAt: z.date().nullable().optional(),
revocationReason: z.number().nullable().optional(),
altNames: z.string().default("").nullable().optional(),
caCertId: z.string().uuid()
caCertId: z.string().uuid(),
certificateTemplateId: z.string().uuid().nullable().optional(),
keyUsages: z.string().array().nullable().optional(),
extendedKeyUsages: z.string().array().nullable().optional()
});
export type TCertificates = z.infer<typeof CertificatesSchema>;

@ -14,6 +14,8 @@ export * from "./certificate-authority-crl";
export * from "./certificate-authority-secret";
export * from "./certificate-bodies";
export * from "./certificate-secrets";
export * from "./certificate-template-est-configs";
export * from "./certificate-templates";
export * from "./certificates";
export * from "./dynamic-secret-leases";
export * from "./dynamic-secrets";
@ -52,11 +54,15 @@ export * from "./org-bots";
export * from "./org-memberships";
export * from "./org-roles";
export * from "./organizations";
export * from "./pki-alerts";
export * from "./pki-collection-items";
export * from "./pki-collections";
export * from "./project-bots";
export * from "./project-environments";
export * from "./project-keys";
export * from "./project-memberships";
export * from "./project-roles";
export * from "./project-slack-configs";
export * from "./project-user-additional-privilege";
export * from "./project-user-membership-roles";
export * from "./projects";
@ -96,6 +102,7 @@ export * from "./secret-versions-v2";
export * from "./secrets";
export * from "./secrets-v2";
export * from "./service-tokens";
export * from "./slack-integrations";
export * from "./super-admin";
export * from "./trusted-ips";
export * from "./user-actions";
@ -104,3 +111,4 @@ export * from "./user-encryption-keys";
export * from "./user-group-membership";
export * from "./users";
export * from "./webhooks";
export * from "./workflow-integrations";

@ -3,12 +3,17 @@ import { z } from "zod";
export enum TableName {
Users = "users",
CertificateAuthority = "certificate_authorities",
CertificateTemplateEstConfig = "certificate_template_est_configs",
CertificateAuthorityCert = "certificate_authority_certs",
CertificateAuthoritySecret = "certificate_authority_secret",
CertificateAuthorityCrl = "certificate_authority_crl",
Certificate = "certificates",
CertificateBody = "certificate_bodies",
CertificateSecret = "certificate_secrets",
CertificateTemplate = "certificate_templates",
PkiAlert = "pki_alerts",
PkiCollection = "pki_collections",
PkiCollectionItem = "pki_collection_items",
Groups = "groups",
GroupProjectMembership = "group_project_memberships",
GroupProjectMembershipRole = "group_project_membership_roles",
@ -109,7 +114,10 @@ export enum TableName {
InternalKms = "internal_kms",
InternalKmsKeyVersion = "internal_kms_key_version",
// @depreciated
KmsKeyVersion = "kms_key_versions"
KmsKeyVersion = "kms_key_versions",
WorkflowIntegrations = "workflow_integrations",
SlackIntegrations = "slack_integrations",
ProjectSlackConfigs = "project_slack_configs"
}
export type TImmutableDBKeys = "id" | "createdAt" | "updatedAt";

@ -0,0 +1,23 @@
// Code generated by automation script, DO NOT EDIT.
// Automated by pulling database and generating zod schema
// To update. Just run npm run generate:schema
// Written by akhilmhdh.
import { z } from "zod";
import { TImmutableDBKeys } from "./models";
export const PkiAlertsSchema = z.object({
id: z.string().uuid(),
createdAt: z.date(),
updatedAt: z.date(),
projectId: z.string(),
pkiCollectionId: z.string().uuid(),
name: z.string(),
alertBeforeDays: z.number(),
recipientEmails: z.string()
});
export type TPkiAlerts = z.infer<typeof PkiAlertsSchema>;
export type TPkiAlertsInsert = Omit<z.input<typeof PkiAlertsSchema>, TImmutableDBKeys>;
export type TPkiAlertsUpdate = Partial<Omit<z.input<typeof PkiAlertsSchema>, TImmutableDBKeys>>;

@ -0,0 +1,21 @@
// Code generated by automation script, DO NOT EDIT.
// Automated by pulling database and generating zod schema
// To update. Just run npm run generate:schema
// Written by akhilmhdh.
import { z } from "zod";
import { TImmutableDBKeys } from "./models";
export const PkiCollectionItemsSchema = z.object({
id: z.string().uuid(),
createdAt: z.date(),
updatedAt: z.date(),
pkiCollectionId: z.string().uuid(),
caId: z.string().uuid().nullable().optional(),
certId: z.string().uuid().nullable().optional()
});
export type TPkiCollectionItems = z.infer<typeof PkiCollectionItemsSchema>;
export type TPkiCollectionItemsInsert = Omit<z.input<typeof PkiCollectionItemsSchema>, TImmutableDBKeys>;
export type TPkiCollectionItemsUpdate = Partial<Omit<z.input<typeof PkiCollectionItemsSchema>, TImmutableDBKeys>>;

@ -0,0 +1,21 @@
// Code generated by automation script, DO NOT EDIT.
// Automated by pulling database and generating zod schema
// To update. Just run npm run generate:schema
// Written by akhilmhdh.
import { z } from "zod";
import { TImmutableDBKeys } from "./models";
export const PkiCollectionsSchema = z.object({
id: z.string().uuid(),
createdAt: z.date(),
updatedAt: z.date(),
projectId: z.string(),
name: z.string(),
description: z.string()
});
export type TPkiCollections = z.infer<typeof PkiCollectionsSchema>;
export type TPkiCollectionsInsert = Omit<z.input<typeof PkiCollectionsSchema>, TImmutableDBKeys>;
export type TPkiCollectionsUpdate = Partial<Omit<z.input<typeof PkiCollectionsSchema>, TImmutableDBKeys>>;

@ -0,0 +1,24 @@
// Code generated by automation script, DO NOT EDIT.
// Automated by pulling database and generating zod schema
// To update. Just run npm run generate:schema
// Written by akhilmhdh.
import { z } from "zod";
import { TImmutableDBKeys } from "./models";
export const ProjectSlackConfigsSchema = z.object({
id: z.string().uuid(),
projectId: z.string(),
slackIntegrationId: z.string().uuid(),
isAccessRequestNotificationEnabled: z.boolean().default(false),
accessRequestChannels: z.string().default(""),
isSecretRequestNotificationEnabled: z.boolean().default(false),
secretRequestChannels: z.string().default(""),
createdAt: z.date(),
updatedAt: z.date()
});
export type TProjectSlackConfigs = z.infer<typeof ProjectSlackConfigsSchema>;
export type TProjectSlackConfigsInsert = Omit<z.input<typeof ProjectSlackConfigsSchema>, TImmutableDBKeys>;
export type TProjectSlackConfigsUpdate = Partial<Omit<z.input<typeof ProjectSlackConfigsSchema>, TImmutableDBKeys>>;

@ -10,7 +10,7 @@ import { TImmutableDBKeys } from "./models";
export const ProjectUserAdditionalPrivilegeSchema = z.object({
id: z.string().uuid(),
slug: z.string(),
projectMembershipId: z.string().uuid(),
projectMembershipId: z.string().uuid().nullable().optional(),
isTemporary: z.boolean().default(false),
temporaryMode: z.string().nullable().optional(),
temporaryRange: z.string().nullable().optional(),
@ -18,7 +18,9 @@ export const ProjectUserAdditionalPrivilegeSchema = z.object({
temporaryAccessEndTime: z.date().nullable().optional(),
permissions: z.unknown(),
createdAt: z.date(),
updatedAt: z.date()
updatedAt: z.date(),
userId: z.string().uuid(),
projectId: z.string()
});
export type TProjectUserAdditionalPrivilege = z.infer<typeof ProjectUserAdditionalPrivilegeSchema>;

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

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

@ -9,7 +9,6 @@ import { TImmutableDBKeys } from "./models";
export const SecretTagsSchema = z.object({
id: z.string().uuid(),
name: z.string(),
slug: z.string(),
color: z.string().nullable().optional(),
createdAt: z.date(),

@ -0,0 +1,27 @@
// Code generated by automation script, DO NOT EDIT.
// Automated by pulling database and generating zod schema
// To update. Just run npm run generate:schema
// Written by akhilmhdh.
import { z } from "zod";
import { zodBuffer } from "@app/lib/zod";
import { TImmutableDBKeys } from "./models";
export const SlackIntegrationsSchema = z.object({
id: z.string().uuid(),
teamId: z.string(),
teamName: z.string(),
slackUserId: z.string(),
slackAppId: z.string(),
encryptedBotAccessToken: zodBuffer,
slackBotId: z.string(),
slackBotUserId: z.string(),
createdAt: z.date(),
updatedAt: z.date()
});
export type TSlackIntegrations = z.infer<typeof SlackIntegrationsSchema>;
export type TSlackIntegrationsInsert = Omit<z.input<typeof SlackIntegrationsSchema>, TImmutableDBKeys>;
export type TSlackIntegrationsUpdate = Partial<Omit<z.input<typeof SlackIntegrationsSchema>, TImmutableDBKeys>>;

@ -5,6 +5,8 @@
import { z } from "zod";
import { zodBuffer } from "@app/lib/zod";
import { TImmutableDBKeys } from "./models";
export const SuperAdminSchema = z.object({
@ -19,7 +21,9 @@ export const SuperAdminSchema = z.object({
trustLdapEmails: z.boolean().default(false).nullable().optional(),
trustOidcEmails: z.boolean().default(false).nullable().optional(),
defaultAuthOrgId: z.string().uuid().nullable().optional(),
enabledLoginMethods: z.string().array().nullable().optional()
enabledLoginMethods: z.string().array().nullable().optional(),
encryptedSlackClientId: zodBuffer.nullable().optional(),
encryptedSlackClientSecret: zodBuffer.nullable().optional()
});
export type TSuperAdmin = z.infer<typeof SuperAdminSchema>;

@ -0,0 +1,22 @@
// Code generated by automation script, DO NOT EDIT.
// Automated by pulling database and generating zod schema
// To update. Just run npm run generate:schema
// Written by akhilmhdh.
import { z } from "zod";
import { TImmutableDBKeys } from "./models";
export const WorkflowIntegrationsSchema = z.object({
id: z.string().uuid(),
integration: z.string(),
slug: z.string(),
orgId: z.string().uuid(),
description: z.string().nullable().optional(),
createdAt: z.date(),
updatedAt: z.date()
});
export type TWorkflowIntegrations = z.infer<typeof WorkflowIntegrationsSchema>;
export type TWorkflowIntegrationsInsert = Omit<z.input<typeof WorkflowIntegrationsSchema>, TImmutableDBKeys>;
export type TWorkflowIntegrationsUpdate = Partial<Omit<z.input<typeof WorkflowIntegrationsSchema>, TImmutableDBKeys>>;

@ -0,0 +1,173 @@
import bcrypt from "bcrypt";
import { z } from "zod";
import { getConfig } from "@app/lib/config/env";
import { BadRequestError, UnauthorizedError } from "@app/lib/errors";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
export const registerCertificateEstRouter = async (server: FastifyZodProvider) => {
const appCfg = getConfig();
// add support for CSR bodies
server.addContentTypeParser("application/pkcs10", { parseAs: "string" }, (_, body, done) => {
try {
let csrBody = body as string;
// some EST clients send CSRs in PEM format and some in base64 format
// for CSRs sent in PEM, we leave them as is
// for CSRs sent in base64, we preprocess them to remove new lines and spaces
if (!csrBody.includes("BEGIN CERTIFICATE REQUEST")) {
csrBody = csrBody.replace(/\n/g, "").replace(/ /g, "");
}
done(null, csrBody);
} catch (err) {
const error = err as Error;
done(error, undefined);
}
});
// Authenticate EST client using Passphrase
server.addHook("onRequest", async (req, res) => {
const { authorization } = req.headers;
const urlFragments = req.url.split("/");
// cacerts endpoint should not have any authentication
if (urlFragments[urlFragments.length - 1] === "cacerts") {
return;
}
if (!authorization) {
const wwwAuthenticateHeader = "WWW-Authenticate";
const errAuthRequired = "Authentication required";
await res.hijack();
// definitive connection timeout to clean-up open connections and prevent memory leak
res.raw.setTimeout(10 * 1000, () => {
res.raw.end();
});
res.raw.setHeader(wwwAuthenticateHeader, `Basic realm="infisical"`);
res.raw.setHeader("Content-Length", 0);
res.raw.statusCode = 401;
// Write the error message to the response without ending the connection
res.raw.write(errAuthRequired);
// flush headers
res.raw.flushHeaders();
return;
}
const certificateTemplateId = urlFragments.slice(-2)[0];
const estConfig = await server.services.certificateTemplate.getEstConfiguration({
isInternal: true,
certificateTemplateId
});
if (!estConfig.isEnabled) {
throw new BadRequestError({
message: "EST is disabled"
});
}
const rawCredential = authorization?.split(" ").pop();
if (!rawCredential) {
throw new UnauthorizedError({ message: "Missing HTTP credentials" });
}
// expected format is user:password
const basicCredential = atob(rawCredential);
const password = basicCredential.split(":").pop();
if (!password) {
throw new BadRequestError({
message: "No password provided"
});
}
const isPasswordValid = await bcrypt.compare(password, estConfig.hashedPassphrase);
if (!isPasswordValid) {
throw new UnauthorizedError({
message: "Invalid credentials"
});
}
});
server.route({
method: "POST",
url: "/:certificateTemplateId/simpleenroll",
config: {
rateLimit: writeLimit
},
schema: {
body: z.string().min(1),
params: z.object({
certificateTemplateId: z.string().min(1)
}),
response: {
200: z.string()
}
},
handler: async (req, res) => {
void res.header("Content-Type", "application/pkcs7-mime; smime-type=certs-only");
void res.header("Content-Transfer-Encoding", "base64");
return server.services.certificateEst.simpleEnroll({
csr: req.body,
certificateTemplateId: req.params.certificateTemplateId,
sslClientCert: req.headers[appCfg.SSL_CLIENT_CERTIFICATE_HEADER_KEY] as string
});
}
});
server.route({
method: "POST",
url: "/:certificateTemplateId/simplereenroll",
config: {
rateLimit: writeLimit
},
schema: {
body: z.string().min(1),
params: z.object({
certificateTemplateId: z.string().min(1)
}),
response: {
200: z.string()
}
},
handler: async (req, res) => {
void res.header("Content-Type", "application/pkcs7-mime; smime-type=certs-only");
void res.header("Content-Transfer-Encoding", "base64");
return server.services.certificateEst.simpleReenroll({
csr: req.body,
certificateTemplateId: req.params.certificateTemplateId,
sslClientCert: req.headers[appCfg.SSL_CLIENT_CERTIFICATE_HEADER_KEY] as string
});
}
});
server.route({
method: "GET",
url: "/:certificateTemplateId/cacerts",
config: {
rateLimit: readLimit
},
schema: {
params: z.object({
certificateTemplateId: z.string().min(1)
}),
response: {
200: z.string()
}
},
handler: async (req, res) => {
void res.header("Content-Type", "application/pkcs7-mime; smime-type=certs-only");
void res.header("Content-Transfer-Encoding", "base64");
return server.services.certificateEst.getCaCerts({
certificateTemplateId: req.params.certificateTemplateId
});
}
});
};

@ -56,7 +56,16 @@ export const registerAccessApprovalPolicyRouter = async (server: FastifyZodProvi
}),
response: {
200: z.object({
approvals: sapPubSchema.extend({ approvers: z.string().array(), secretPath: z.string().optional() }).array()
approvals: sapPubSchema
.extend({
userApprovers: z
.object({
userId: z.string()
})
.array(),
secretPath: z.string().optional().nullable()
})
.array()
})
}
},
@ -69,6 +78,7 @@ export const registerAccessApprovalPolicyRouter = async (server: FastifyZodProvi
actorOrgId: req.permission.orgId,
projectSlug: req.query.projectSlug
});
return { approvals };
}
});

@ -1,10 +1,19 @@
import { z } from "zod";
import { AccessApprovalRequestsReviewersSchema, AccessApprovalRequestsSchema } from "@app/db/schemas";
import { AccessApprovalRequestsReviewersSchema, AccessApprovalRequestsSchema, UsersSchema } from "@app/db/schemas";
import { ApprovalStatus } from "@app/ee/services/access-approval-request/access-approval-request-types";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type";
const approvalRequestUser = z.object({ userId: z.string() }).merge(
UsersSchema.pick({
email: true,
firstName: true,
lastName: true,
username: true
})
);
export const registerAccessApprovalRequestRouter = async (server: FastifyZodProvider) => {
server.route({
url: "/",
@ -104,10 +113,11 @@ export const registerAccessApprovalRequestRouter = async (server: FastifyZodProv
}),
reviewers: z
.object({
member: z.string(),
userId: z.string(),
status: z.string()
})
.array()
.array(),
requestedByUser: approvalRequestUser
}).array()
})
}

@ -1,86 +1,55 @@
/* eslint-disable @typescript-eslint/no-floating-promises */
import { z } from "zod";
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
import { CERTIFICATE_AUTHORITIES } from "@app/lib/api-docs";
import { CA_CRLS } from "@app/lib/api-docs";
import { readLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type";
export const registerCaCrlRouter = async (server: FastifyZodProvider) => {
server.route({
method: "GET",
url: "/:caId/crl",
url: "/:crlId",
config: {
rateLimit: readLimit
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: {
description: "Get CRL of the CA",
description: "Get CRL in DER format (deprecated)",
params: z.object({
caId: z.string().trim().describe(CERTIFICATE_AUTHORITIES.GET_CRL.caId)
crlId: z.string().trim().describe(CA_CRLS.GET.crlId)
}),
response: {
200: z.object({
crl: z.string().describe(CERTIFICATE_AUTHORITIES.GET_CRL.crl)
})
200: z.instanceof(Buffer)
}
},
handler: async (req) => {
const { crl, ca } = await server.services.certificateAuthorityCrl.getCaCrl({
caId: req.params.caId,
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId
});
handler: async (req, res) => {
const { crl } = await server.services.certificateAuthorityCrl.getCrlById(req.params.crlId);
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
projectId: ca.projectId,
event: {
type: EventType.GET_CA_CRL,
metadata: {
caId: ca.id,
dn: ca.dn
}
}
});
res.header("Content-Type", "application/pkix-crl");
return {
crl
};
return Buffer.from(crl);
}
});
// server.route({
// method: "GET",
// url: "/:caId/crl/rotate",
// config: {
// rateLimit: writeLimit
// },
// onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
// schema: {
// description: "Rotate CRL of the CA",
// params: z.object({
// caId: z.string().trim()
// }),
// response: {
// 200: z.object({
// message: z.string()
// })
// }
// },
// handler: async (req) => {
// await server.services.certificateAuthority.rotateCaCrl({
// caId: req.params.caId,
// actor: req.permission.type,
// actorId: req.permission.id,
// actorAuthMethod: req.permission.authMethod,
// actorOrgId: req.permission.orgId
// });
// return {
// message: "Successfully rotated CA CRL"
// };
// }
// });
server.route({
method: "GET",
url: "/:crlId/der",
config: {
rateLimit: readLimit
},
schema: {
description: "Get CRL in DER format",
params: z.object({
crlId: z.string().trim().describe(CA_CRLS.GET.crlId)
}),
response: {
200: z.instanceof(Buffer)
}
},
handler: async (req, res) => {
const { crl } = await server.services.certificateAuthorityCrl.getCrlById(req.params.crlId);
res.header("Content-Type", "application/pkix-crl");
return Buffer.from(crl);
}
});
};

@ -131,7 +131,7 @@ export const registerDynamicSecretLeaseRouter = async (server: FastifyZodProvide
.default("/")
.transform(removeTrailingSlash)
.describe(DYNAMIC_SECRET_LEASES.RENEW.path),
environmentSlug: z.string().min(1).describe(DYNAMIC_SECRET_LEASES.RENEW.ttl)
environmentSlug: z.string().min(1).describe(DYNAMIC_SECRET_LEASES.RENEW.environmentSlug)
}),
response: {
200: z.object({

@ -61,7 +61,7 @@ export const registerV1EERoutes = async (server: FastifyZodProvider) => {
await server.register(
async (pkiRouter) => {
await pkiRouter.register(registerCaCrlRouter, { prefix: "/ca" });
await pkiRouter.register(registerCaCrlRouter, { prefix: "/crl" });
},
{ prefix: "/pki" }
);

@ -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";

@ -101,6 +101,7 @@ export const registerProjectRoleRouter = async (server: FastifyZodProvider) => {
message: "Slug must be a valid"
}),
name: z.string().trim().optional().describe(PROJECT_ROLE.UPDATE.name),
description: z.string().trim().optional().describe(PROJECT_ROLE.UPDATE.description),
permissions: ProjectPermissionSchema.array().describe(PROJECT_ROLE.UPDATE.permissions).optional()
}),
response: {

@ -122,6 +122,10 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
})
.merge(
z.object({
project: z.object({
name: z.string(),
slug: z.string()
}),
event: z.object({
type: z.string(),
metadata: z.any()
@ -138,16 +142,20 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const auditLogs = await server.services.auditLog.listProjectAuditLogs({
const auditLogs = await server.services.auditLog.listAuditLogs({
actorId: req.permission.id,
actorOrgId: req.permission.orgId,
actorAuthMethod: req.permission.authMethod,
projectId: req.params.workspaceId,
...req.query,
endDate: req.query.endDate,
startDate: req.query.startDate || getLastMidnightDateISO(),
auditLogActor: req.query.actor,
actor: req.permission.type
actor: req.permission.type,
filter: {
...req.query,
projectId: req.params.workspaceId,
endDate: req.query.endDate,
startDate: req.query.startDate || getLastMidnightDateISO(),
auditLogActorId: req.query.actor,
eventType: req.query.eventType ? [req.query.eventType] : undefined
}
});
return { auditLogs };
}

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

@ -100,17 +100,34 @@ export const registerSamlRouter = async (server: FastifyZodProvider) => {
async (req, profile, cb) => {
try {
if (!profile) throw new BadRequestError({ message: "Missing profile" });
const email = profile?.email ?? (profile?.emailAddress as string); // emailRippling is added because in Rippling the field `email` reserved
const email =
profile?.email ??
// entra sends data in this format
(profile["http://schemas.xmlsoap.org/ws/2005/05/identity/claims/email"] as string) ??
(profile?.emailAddress as string); // emailRippling is added because in Rippling the field `email` reserved\
if (!email || !profile.firstName) {
throw new BadRequestError({ message: "Invalid request. Missing email or first name" });
const firstName = (profile.firstName ??
// entra sends data in this format
profile["http://schemas.xmlsoap.org/ws/2005/05/identity/claims/firstName"]) as string;
const lastName =
profile.lastName ?? profile["http://schemas.xmlsoap.org/ws/2005/05/identity/claims/lastName"];
if (!email || !firstName) {
logger.info(
{
err: new Error("Invalid saml request. Missing email or first name"),
profile
},
`email: ${email} firstName: ${profile.firstName as string}`
);
}
const { isUserCompleted, providerAuthToken } = await server.services.saml.samlLogin({
externalId: profile.nameID,
email,
firstName: profile.firstName as string,
lastName: profile.lastName as string,
firstName,
lastName: lastName as string,
relayState: (req.body as { RelayState?: string }).RelayState,
authProvider: (req as unknown as FastifyRequest).ssoConfig?.authProvider as string,
orgId: (req as unknown as FastifyRequest).ssoConfig?.orgId as string
@ -118,7 +135,7 @@ export const registerSamlRouter = async (server: FastifyZodProvider) => {
cb(null, { isUserCompleted, providerAuthToken });
} catch (error) {
logger.error(error);
cb(null, {});
cb(error as Error);
}
},
() => {}

@ -5,19 +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";
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.addContentTypeParser("application/scim+json", { parseAs: "string" }, (_, body, done) => {
try {
const strBody = body instanceof Buffer ? body.toString() : body;
const json: unknown = JSON.parse(strBody);
done(null, json);
} catch (err) {
const error = err as Error;
done(error, undefined);
}
});
server.route({
url: "/scim-tokens",
method: "POST",
@ -124,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(),
@ -170,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]),
@ -213,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({
@ -226,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]),
@ -257,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
});
@ -288,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",
@ -302,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]),
@ -341,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(),
@ -374,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;
@ -389,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]),
@ -411,6 +451,7 @@ export const registerScimRouter = async (server: FastifyZodProvider) => {
groupId: req.params.groupId,
orgId: req.permission.orgId
});
return group;
}
});
@ -434,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
@ -474,54 +502,34 @@ export const registerScimRouter = async (server: FastifyZodProvider) => {
Operations: z.array(
z.union([
z.object({
op: z.literal("replace"),
value: z.object({
id: z.string().trim(),
displayName: z.string().trim()
})
}),
z.object({
op: z.literal("remove"),
path: z.string().trim()
}),
z.object({
op: z.literal("add"),
op: z.union([z.literal("remove"), z.literal("Remove")]),
path: z.string().trim(),
value: z.array(
z.object({
value: z.string().trim(),
display: z.string().trim().optional()
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()
})
])
)
}),
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;
}
});
@ -547,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;
}
});
};

@ -1,9 +1,9 @@
import { Knex } from "knex";
import { TDbClient } from "@app/db";
import { TableName, TAccessApprovalPolicies } from "@app/db/schemas";
import { AccessApprovalPoliciesSchema, TableName, TAccessApprovalPolicies } from "@app/db/schemas";
import { DatabaseError } from "@app/lib/errors";
import { buildFindFilter, mergeOneToManyRelation, ormify, selectAllTableCols, TFindFilter } from "@app/lib/knex";
import { buildFindFilter, ormify, selectAllTableCols, sqlNestRelationships, TFindFilter } from "@app/lib/knex";
export type TAccessApprovalPolicyDALFactory = ReturnType<typeof accessApprovalPolicyDALFactory>;
@ -15,12 +15,12 @@ export const accessApprovalPolicyDALFactory = (db: TDbClient) => {
// eslint-disable-next-line
.where(buildFindFilter(filter))
.join(TableName.Environment, `${TableName.AccessApprovalPolicy}.envId`, `${TableName.Environment}.id`)
.join(
.leftJoin(
TableName.AccessApprovalPolicyApprover,
`${TableName.AccessApprovalPolicy}.id`,
`${TableName.AccessApprovalPolicyApprover}.policyId`
)
.select(tx.ref("approverId").withSchema(TableName.AccessApprovalPolicyApprover))
.select(tx.ref("approverUserId").withSchema(TableName.AccessApprovalPolicyApprover))
.select(tx.ref("name").withSchema(TableName.Environment).as("envName"))
.select(tx.ref("slug").withSchema(TableName.Environment).as("envSlug"))
.select(tx.ref("id").withSchema(TableName.Environment).as("envId"))
@ -35,18 +35,30 @@ export const accessApprovalPolicyDALFactory = (db: TDbClient) => {
const doc = await accessApprovalPolicyFindQuery(tx || db.replicaNode(), {
[`${TableName.AccessApprovalPolicy}.id` as "id"]: id
});
const formatedDoc = mergeOneToManyRelation(
doc,
"id",
({ approverId, envId, envName: name, envSlug: slug, ...el }) => ({
...el,
envId,
environment: { id: envId, name, slug }
const formattedDoc = sqlNestRelationships({
data: doc,
key: "id",
parentMapper: (data) => ({
environment: {
id: data.envId,
name: data.envName,
slug: data.envSlug
},
projectId: data.projectId,
...AccessApprovalPoliciesSchema.parse(data)
}),
({ approverId }) => approverId,
"approvers"
);
return formatedDoc?.[0];
childrenMapper: [
{
key: "approverUserId",
label: "userApprovers" as const,
mapper: ({ approverUserId }) => ({
userId: approverUserId
})
}
]
});
return formattedDoc?.[0];
} catch (error) {
throw new DatabaseError({ error, name: "FindById" });
}
@ -55,18 +67,32 @@ export const accessApprovalPolicyDALFactory = (db: TDbClient) => {
const find = async (filter: TFindFilter<TAccessApprovalPolicies & { projectId: string }>, tx?: Knex) => {
try {
const docs = await accessApprovalPolicyFindQuery(tx || db.replicaNode(), filter);
const formatedDoc = mergeOneToManyRelation(
docs,
"id",
({ approverId, envId, envName: name, envSlug: slug, ...el }) => ({
...el,
envId,
environment: { id: envId, name, slug }
const formattedDocs = sqlNestRelationships({
data: docs,
key: "id",
parentMapper: (data) => ({
environment: {
id: data.envId,
name: data.envName,
slug: data.envSlug
},
projectId: data.projectId,
...AccessApprovalPoliciesSchema.parse(data)
// secretPath: data.secretPath || undefined,
}),
({ approverId }) => approverId,
"approvers"
);
return formatedDoc.map((policy) => ({ ...policy, secretPath: policy.secretPath || undefined }));
childrenMapper: [
{
key: "approverUserId",
label: "userApprovers" as const,
mapper: ({ approverUserId }) => ({
userId: approverUserId
})
}
]
});
return formattedDocs;
} catch (error) {
throw new DatabaseError({ error, name: "Find" });
}

@ -34,8 +34,7 @@ export const accessApprovalPolicyServiceFactory = ({
accessApprovalPolicyApproverDAL,
permissionService,
projectEnvDAL,
projectDAL,
projectMembershipDAL
projectDAL
}: TSecretApprovalPolicyServiceFactoryDep) => {
const createAccessApprovalPolicy = async ({
name,
@ -70,15 +69,6 @@ export const accessApprovalPolicyServiceFactory = ({
const env = await projectEnvDAL.findOne({ slug: environment, projectId: project.id });
if (!env) throw new BadRequestError({ message: "Environment not found" });
const secretApprovers = await projectMembershipDAL.find({
projectId: project.id,
$in: { id: approvers }
});
if (secretApprovers.length !== approvers.length) {
throw new BadRequestError({ message: "Approver not found in project" });
}
await verifyApprovers({
projectId: project.id,
orgId: actorOrgId,
@ -86,7 +76,7 @@ export const accessApprovalPolicyServiceFactory = ({
secretPath,
actorAuthMethod,
permissionService,
userIds: secretApprovers.map((approver) => approver.userId)
userIds: approvers
});
const accessApproval = await accessApprovalPolicyDAL.transaction(async (tx) => {
@ -101,8 +91,8 @@ export const accessApprovalPolicyServiceFactory = ({
tx
);
await accessApprovalPolicyApproverDAL.insertMany(
secretApprovers.map(({ id }) => ({
approverId: id,
approvers.map((userId) => ({
approverUserId: userId,
policyId: doc.id
})),
tx
@ -172,15 +162,6 @@ export const accessApprovalPolicyServiceFactory = ({
tx
);
if (approvers) {
// Find the workspace project memberships of the users passed in the approvers array
const secretApprovers = await projectMembershipDAL.find(
{
projectId: accessApprovalPolicy.projectId,
$in: { id: approvers }
},
{ tx }
);
await verifyApprovers({
projectId: accessApprovalPolicy.projectId,
orgId: actorOrgId,
@ -188,15 +169,13 @@ export const accessApprovalPolicyServiceFactory = ({
secretPath: doc.secretPath!,
actorAuthMethod,
permissionService,
userIds: secretApprovers.map((approver) => approver.userId)
userIds: approvers
});
if (secretApprovers.length !== approvers.length)
throw new BadRequestError({ message: "Approvals cannot be greater than approvers" });
await accessApprovalPolicyApproverDAL.delete({ policyId: doc.id }, tx);
await accessApprovalPolicyApproverDAL.insertMany(
secretApprovers.map(({ id }) => ({
approverId: id,
approvers.map((userId) => ({
approverUserId: userId,
policyId: doc.id
})),
tx

@ -1,7 +1,7 @@
import { Knex } from "knex";
import { TDbClient } from "@app/db";
import { AccessApprovalRequestsSchema, TableName, TAccessApprovalRequests } from "@app/db/schemas";
import { AccessApprovalRequestsSchema, TableName, TAccessApprovalRequests, TUsers } from "@app/db/schemas";
import { DatabaseError } from "@app/lib/errors";
import { ormify, selectAllTableCols, sqlNestRelationships, TFindFilter } from "@app/lib/knex";
@ -40,6 +40,12 @@ export const accessApprovalRequestDALFactory = (db: TDbClient) => {
`${TableName.AccessApprovalPolicyApprover}.policyId`
)
.join<TUsers>(
db(TableName.Users).as("requestedByUser"),
`${TableName.AccessApprovalRequest}.requestedByUserId`,
`requestedByUser.id`
)
.leftJoin(TableName.Environment, `${TableName.AccessApprovalPolicy}.envId`, `${TableName.Environment}.id`)
.select(selectAllTableCols(TableName.AccessApprovalRequest))
@ -52,7 +58,7 @@ export const accessApprovalRequestDALFactory = (db: TDbClient) => {
db.ref("envId").withSchema(TableName.AccessApprovalPolicy).as("policyEnvId")
)
.select(db.ref("approverId").withSchema(TableName.AccessApprovalPolicyApprover))
.select(db.ref("approverUserId").withSchema(TableName.AccessApprovalPolicyApprover))
.select(
db.ref("projectId").withSchema(TableName.Environment),
@ -61,15 +67,20 @@ export const accessApprovalRequestDALFactory = (db: TDbClient) => {
)
.select(
db.ref("member").withSchema(TableName.AccessApprovalRequestReviewer).as("reviewerMemberId"),
db.ref("reviewerUserId").withSchema(TableName.AccessApprovalRequestReviewer).as("reviewerUserId"),
db.ref("status").withSchema(TableName.AccessApprovalRequestReviewer).as("reviewerStatus")
)
// TODO: ADD SUPPORT FOR GROUPS!!!!
.select(
db
.ref("projectMembershipId")
.withSchema(TableName.ProjectUserAdditionalPrivilege)
.as("privilegeMembershipId"),
db.ref("email").withSchema("requestedByUser").as("requestedByUserEmail"),
db.ref("username").withSchema("requestedByUser").as("requestedByUserUsername"),
db.ref("firstName").withSchema("requestedByUser").as("requestedByUserFirstName"),
db.ref("lastName").withSchema("requestedByUser").as("requestedByUserLastName"),
db.ref("userId").withSchema(TableName.ProjectUserAdditionalPrivilege).as("privilegeUserId"),
db.ref("projectId").withSchema(TableName.ProjectUserAdditionalPrivilege).as("privilegeMembershipId"),
db.ref("isTemporary").withSchema(TableName.ProjectUserAdditionalPrivilege).as("privilegeIsTemporary"),
db.ref("temporaryMode").withSchema(TableName.ProjectUserAdditionalPrivilege).as("privilegeTemporaryMode"),
db.ref("temporaryRange").withSchema(TableName.ProjectUserAdditionalPrivilege).as("privilegeTemporaryRange"),
@ -102,9 +113,18 @@ export const accessApprovalRequestDALFactory = (db: TDbClient) => {
enforcementLevel: doc.policyEnforcementLevel,
envId: doc.policyEnvId
},
requestedByUser: {
userId: doc.requestedByUserId,
email: doc.requestedByUserEmail,
firstName: doc.requestedByUserFirstName,
lastName: doc.requestedByUserLastName,
username: doc.requestedByUserUsername
},
privilege: doc.privilegeId
? {
membershipId: doc.privilegeMembershipId,
userId: doc.privilegeUserId,
projectId: doc.projectId,
isTemporary: doc.privilegeIsTemporary,
temporaryMode: doc.privilegeTemporaryMode,
temporaryRange: doc.privilegeTemporaryRange,
@ -118,11 +138,11 @@ export const accessApprovalRequestDALFactory = (db: TDbClient) => {
}),
childrenMapper: [
{
key: "reviewerMemberId",
key: "reviewerUserId",
label: "reviewers" as const,
mapper: ({ reviewerMemberId: member, reviewerStatus: status }) => (member ? { member, status } : undefined)
mapper: ({ reviewerUserId: userId, reviewerStatus: status }) => (userId ? { userId, status } : undefined)
},
{ key: "approverId", label: "approvers" as const, mapper: ({ approverId }) => approverId }
{ key: "approverUserId", label: "approvers" as const, mapper: ({ approverUserId }) => approverUserId }
]
});
@ -146,30 +166,65 @@ export const accessApprovalRequestDALFactory = (db: TDbClient) => {
`${TableName.AccessApprovalPolicy}.id`
)
.join<TUsers>(
db(TableName.Users).as("requestedByUser"),
`${TableName.AccessApprovalRequest}.requestedByUserId`,
`requestedByUser.id`
)
.join(
TableName.AccessApprovalPolicyApprover,
`${TableName.AccessApprovalPolicy}.id`,
`${TableName.AccessApprovalPolicyApprover}.policyId`
)
.join<TUsers>(
db(TableName.Users).as("accessApprovalPolicyApproverUser"),
`${TableName.AccessApprovalPolicyApprover}.approverUserId`,
"accessApprovalPolicyApproverUser.id"
)
.leftJoin(
TableName.AccessApprovalRequestReviewer,
`${TableName.AccessApprovalRequest}.id`,
`${TableName.AccessApprovalRequestReviewer}.requestId`
)
.leftJoin<TUsers>(
db(TableName.Users).as("accessApprovalReviewerUser"),
`${TableName.AccessApprovalRequestReviewer}.reviewerUserId`,
`accessApprovalReviewerUser.id`
)
.leftJoin(TableName.Environment, `${TableName.AccessApprovalPolicy}.envId`, `${TableName.Environment}.id`)
.select(selectAllTableCols(TableName.AccessApprovalRequest))
.select(
tx.ref("member").withSchema(TableName.AccessApprovalRequestReviewer).as("reviewerMemberId"),
tx.ref("approverUserId").withSchema(TableName.AccessApprovalPolicyApprover),
tx.ref("email").withSchema("accessApprovalPolicyApproverUser").as("approverEmail"),
tx.ref("username").withSchema("accessApprovalPolicyApproverUser").as("approverUsername"),
tx.ref("firstName").withSchema("accessApprovalPolicyApproverUser").as("approverFirstName"),
tx.ref("lastName").withSchema("accessApprovalPolicyApproverUser").as("approverLastName"),
tx.ref("email").withSchema("requestedByUser").as("requestedByUserEmail"),
tx.ref("username").withSchema("requestedByUser").as("requestedByUserUsername"),
tx.ref("firstName").withSchema("requestedByUser").as("requestedByUserFirstName"),
tx.ref("lastName").withSchema("requestedByUser").as("requestedByUserLastName"),
tx.ref("reviewerUserId").withSchema(TableName.AccessApprovalRequestReviewer),
tx.ref("status").withSchema(TableName.AccessApprovalRequestReviewer).as("reviewerStatus"),
tx.ref("email").withSchema("accessApprovalReviewerUser").as("reviewerEmail"),
tx.ref("username").withSchema("accessApprovalReviewerUser").as("reviewerUsername"),
tx.ref("firstName").withSchema("accessApprovalReviewerUser").as("reviewerFirstName"),
tx.ref("lastName").withSchema("accessApprovalReviewerUser").as("reviewerLastName"),
tx.ref("id").withSchema(TableName.AccessApprovalPolicy).as("policyId"),
tx.ref("name").withSchema(TableName.AccessApprovalPolicy).as("policyName"),
tx.ref("projectId").withSchema(TableName.Environment),
tx.ref("slug").withSchema(TableName.Environment).as("environment"),
tx.ref("secretPath").withSchema(TableName.AccessApprovalPolicy).as("policySecretPath"),
tx.ref("enforcementLevel").withSchema(TableName.AccessApprovalPolicy).as("policyEnforcementLevel"),
tx.ref("approvals").withSchema(TableName.AccessApprovalPolicy).as("policyApprovals"),
tx.ref("approverId").withSchema(TableName.AccessApprovalPolicyApprover)
tx.ref("approvals").withSchema(TableName.AccessApprovalPolicy).as("policyApprovals")
);
const findById = async (id: string, tx?: Knex) => {
@ -189,15 +244,45 @@ export const accessApprovalRequestDALFactory = (db: TDbClient) => {
approvals: el.policyApprovals,
secretPath: el.policySecretPath,
enforcementLevel: el.policyEnforcementLevel
},
requestedByUser: {
userId: el.requestedByUserId,
email: el.requestedByUserEmail,
firstName: el.requestedByUserFirstName,
lastName: el.requestedByUserLastName,
username: el.requestedByUserUsername
}
}),
childrenMapper: [
{
key: "reviewerMemberId",
key: "reviewerUserId",
label: "reviewers" as const,
mapper: ({ reviewerMemberId: member, reviewerStatus: status }) => (member ? { member, status } : undefined)
mapper: ({
reviewerUserId: userId,
reviewerStatus: status,
reviewerEmail: email,
reviewerLastName: lastName,
reviewerUsername: username,
reviewerFirstName: firstName
}) => (userId ? { userId, status, email, firstName, lastName, username } : undefined)
},
{ key: "approverId", label: "approvers" as const, mapper: ({ approverId }) => approverId }
{
key: "approverUserId",
label: "approvers" as const,
mapper: ({
approverUserId,
approverEmail: email,
approverUsername: username,
approverLastName: lastName,
approverFirstName: firstName
}) => ({
userId: approverUserId,
email,
firstName,
lastName,
username
})
}
]
});
if (!formatedDoc?.[0]) return;
@ -235,7 +320,7 @@ export const accessApprovalRequestDALFactory = (db: TDbClient) => {
.where(`${TableName.Environment}.projectId`, projectId)
.select(selectAllTableCols(TableName.AccessApprovalRequest))
.select(db.ref("status").withSchema(TableName.AccessApprovalRequestReviewer).as("reviewerStatus"))
.select(db.ref("member").withSchema(TableName.AccessApprovalRequestReviewer).as("reviewerMemberId"));
.select(db.ref("reviewerUserId").withSchema(TableName.AccessApprovalRequestReviewer).as("reviewerUserId"));
const formattedRequests = sqlNestRelationships({
data: accessRequests,
@ -245,9 +330,10 @@ export const accessApprovalRequestDALFactory = (db: TDbClient) => {
}),
childrenMapper: [
{
key: "reviewerMemberId",
key: "reviewerUserId",
label: "reviewers" as const,
mapper: ({ reviewerMemberId: member, reviewerStatus: status }) => (member ? { member, status } : undefined)
mapper: ({ reviewerUserId: reviewer, reviewerStatus: status }) =>
reviewer ? { reviewer, status } : undefined
}
]
});

@ -5,9 +5,13 @@ import { ProjectMembershipRole } from "@app/db/schemas";
import { getConfig } from "@app/lib/config/env";
import { BadRequestError, UnauthorizedError } from "@app/lib/errors";
import { alphaNumericNanoId } from "@app/lib/nanoid";
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
import { TProjectDALFactory } from "@app/services/project/project-dal";
import { TProjectEnvDALFactory } from "@app/services/project-env/project-env-dal";
import { TProjectMembershipDALFactory } from "@app/services/project-membership/project-membership-dal";
import { TProjectSlackConfigDALFactory } from "@app/services/slack/project-slack-config-dal";
import { triggerSlackNotification } from "@app/services/slack/slack-fns";
import { SlackTriggerFeature } from "@app/services/slack/slack-types";
import { SmtpTemplates, TSmtpService } from "@app/services/smtp/smtp-service";
import { TUserDALFactory } from "@app/services/user/user-dal";
@ -33,7 +37,10 @@ type TSecretApprovalRequestServiceFactoryDep = {
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
accessApprovalPolicyApproverDAL: Pick<TAccessApprovalPolicyApproverDALFactory, "find">;
projectEnvDAL: Pick<TProjectEnvDALFactory, "findOne">;
projectDAL: Pick<TProjectDALFactory, "checkProjectUpgradeStatus" | "findProjectBySlug">;
projectDAL: Pick<
TProjectDALFactory,
"checkProjectUpgradeStatus" | "findProjectBySlug" | "findProjectWithOrg" | "findById"
>;
accessApprovalRequestDAL: Pick<
TAccessApprovalRequestDALFactory,
| "create"
@ -52,7 +59,12 @@ type TSecretApprovalRequestServiceFactoryDep = {
>;
projectMembershipDAL: Pick<TProjectMembershipDALFactory, "findById">;
smtpService: Pick<TSmtpService, "sendMail">;
userDAL: Pick<TUserDALFactory, "findUserByProjectMembershipId" | "findUsersByProjectMembershipIds">;
userDAL: Pick<
TUserDALFactory,
"findUserByProjectMembershipId" | "findUsersByProjectMembershipIds" | "find" | "findById"
>;
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
projectSlackConfigDAL: Pick<TProjectSlackConfigDALFactory, "getIntegrationDetailsByProject">;
};
export type TAccessApprovalRequestServiceFactory = ReturnType<typeof accessApprovalRequestServiceFactory>;
@ -68,7 +80,9 @@ export const accessApprovalRequestServiceFactory = ({
accessApprovalPolicyApproverDAL,
additionalPrivilegeDAL,
smtpService,
userDAL
userDAL,
kmsService,
projectSlackConfigDAL
}: TSecretApprovalRequestServiceFactoryDep) => {
const createAccessApprovalRequest = async ({
isTemporary,
@ -94,7 +108,7 @@ export const accessApprovalRequestServiceFactory = ({
);
if (!membership) throw new UnauthorizedError({ message: "You are not a member of this project" });
const requestedByUser = await userDAL.findUserByProjectMembershipId(membership.id);
const requestedByUser = await userDAL.findById(actorId);
if (!requestedByUser) throw new UnauthorizedError({ message: "User not found" });
await projectDAL.checkProjectUpgradeStatus(project.id);
@ -114,13 +128,15 @@ export const accessApprovalRequestServiceFactory = ({
policyId: policy.id
});
const approverUsers = await userDAL.findUsersByProjectMembershipIds(
approvers.map((approver) => approver.approverId)
);
const approverUsers = await userDAL.find({
$in: {
id: approvers.map((approver) => approver.approverUserId)
}
});
const duplicateRequests = await accessApprovalRequestDAL.find({
policyId: policy.id,
requestedBy: membership.id,
requestedByUserId: actorId,
permissions: JSON.stringify(requestedPermissions),
isTemporary
});
@ -153,7 +169,7 @@ export const accessApprovalRequestServiceFactory = ({
const approvalRequest = await accessApprovalRequestDAL.create(
{
policyId: policy.id,
requestedBy: membership.id,
requestedByUserId: actorId,
temporaryRange: temporaryRange || null,
permissions: JSON.stringify(requestedPermissions),
isTemporary
@ -161,13 +177,36 @@ export const accessApprovalRequestServiceFactory = ({
tx
);
const requesterFullName = `${requestedByUser.firstName} ${requestedByUser.lastName}`;
const approvalUrl = `${cfg.SITE_URL}/project/${project.id}/approval`;
await triggerSlackNotification({
projectId: project.id,
projectSlackConfigDAL,
projectDAL,
kmsService,
notification: {
type: SlackTriggerFeature.ACCESS_REQUEST,
payload: {
projectName: project.name,
requesterFullName,
isTemporary,
requesterEmail: requestedByUser.email as string,
secretPath,
environment: envSlug,
permissions: accessTypes,
approvalUrl
}
}
});
await smtpService.sendMail({
recipients: approverUsers.filter((approver) => approver.email).map((approver) => approver.email!),
subjectLine: "Access Approval Request",
substitutions: {
projectName: project.name,
requesterFullName: `${requestedByUser.firstName} ${requestedByUser.lastName}`,
requesterFullName,
requesterEmail: requestedByUser.email,
isTemporary,
...(isTemporary && {
@ -176,7 +215,7 @@ export const accessApprovalRequestServiceFactory = ({
secretPath,
environment: envSlug,
permissions: accessTypes,
approvalUrl: `${cfg.SITE_URL}/project/${project.id}/approval`
approvalUrl
},
template: SmtpTemplates.AccessApprovalRequest
});
@ -212,7 +251,7 @@ export const accessApprovalRequestServiceFactory = ({
let requests = await accessApprovalRequestDAL.findRequestsWithPrivilegeByPolicyIds(policies.map((p) => p.id));
if (authorProjectMembershipId) {
requests = requests.filter((request) => request.requestedBy === authorProjectMembershipId);
requests = requests.filter((request) => request.requestedByUserId === actorId);
}
if (envSlug) {
@ -246,8 +285,8 @@ export const accessApprovalRequestServiceFactory = ({
if (
!hasRole(ProjectMembershipRole.Admin) &&
accessApprovalRequest.requestedBy !== membership.id && // The request wasn't made by the current user
!policy.approvers.find((approverId) => approverId === membership.id) // The request isn't performed by an assigned approver
accessApprovalRequest.requestedByUserId !== actorId && // The request wasn't made by the current user
!policy.approvers.find((approver) => approver.userId === actorId) // The request isn't performed by an assigned approver
) {
throw new UnauthorizedError({ message: "You are not authorized to approve this request" });
}
@ -273,7 +312,7 @@ export const accessApprovalRequestServiceFactory = ({
const review = await accessApprovalRequestReviewerDAL.findOne(
{
requestId: accessApprovalRequest.id,
member: membership.id
reviewerUserId: actorId
},
tx
);
@ -282,7 +321,7 @@ export const accessApprovalRequestServiceFactory = ({
{
status,
requestId: accessApprovalRequest.id,
member: membership.id
reviewerUserId: actorId
},
tx
);
@ -303,7 +342,8 @@ export const accessApprovalRequestServiceFactory = ({
// Permanent access
const privilege = await additionalPrivilegeDAL.create(
{
projectMembershipId: accessApprovalRequest.requestedBy,
userId: accessApprovalRequest.requestedByUserId,
projectId: accessApprovalRequest.projectId,
slug: `requested-privilege-${slugify(alphaNumericNanoId(12))}`,
permissions: JSON.stringify(accessApprovalRequest.permissions)
},
@ -317,7 +357,8 @@ export const accessApprovalRequestServiceFactory = ({
const privilege = await additionalPrivilegeDAL.create(
{
projectMembershipId: accessApprovalRequest.requestedBy,
userId: accessApprovalRequest.requestedByUserId,
projectId: accessApprovalRequest.projectId,
slug: `requested-privilege-${slugify(alphaNumericNanoId(12))}`,
permissions: JSON.stringify(accessApprovalRequest.permissions),
isTemporary: true,

@ -2,10 +2,11 @@ import { ForbiddenError } from "@casl/ability";
import { RawAxiosRequestHeaders } from "axios";
import { SecretKeyEncoding } from "@app/db/schemas";
import { getConfig } from "@app/lib/config/env";
import { request } from "@app/lib/config/request";
import { infisicalSymmetricDecrypt, infisicalSymmetricEncypt } from "@app/lib/crypto/encryption";
import { BadRequestError } from "@app/lib/errors";
import { validateLocalIps } from "@app/lib/validator";
import { blockLocalAndPrivateIpAddresses } from "@app/lib/validator";
import { AUDIT_LOG_STREAM_TIMEOUT } from "../audit-log/audit-log-queue";
import { TLicenseServiceFactory } from "../license/license-service";
@ -44,6 +45,7 @@ export const auditLogStreamServiceFactory = ({
}: TCreateAuditLogStreamDTO) => {
if (!actorOrgId) throw new BadRequestError({ message: "Missing org id from token" });
const appCfg = getConfig();
const plan = await licenseService.getPlan(actorOrgId);
if (!plan.auditLogStreams)
throw new BadRequestError({
@ -59,7 +61,9 @@ export const auditLogStreamServiceFactory = ({
);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Settings);
validateLocalIps(url);
if (appCfg.isCloud) {
blockLocalAndPrivateIpAddresses(url);
}
const totalStreams = await auditLogStreamDAL.find({ orgId: actorOrgId });
if (totalStreams.length >= plan.auditLogStreamLimit) {
@ -131,7 +135,8 @@ export const auditLogStreamServiceFactory = ({
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Settings);
if (url) validateLocalIps(url);
const appCfg = getConfig();
if (url && appCfg.isCloud) blockLocalAndPrivateIpAddresses(url);
// testing connection first
const streamHeaders: RawAxiosRequestHeaders = { "Content-Type": "application/json" };

@ -1,10 +1,14 @@
import { Knex } from "knex";
import { TDbClient } from "@app/db";
import { TableName } from "@app/db/schemas";
import { AuditLogsSchema, TableName } from "@app/db/schemas";
import { DatabaseError } from "@app/lib/errors";
import { ormify, stripUndefinedInWhere } from "@app/lib/knex";
import { ormify, selectAllTableCols, stripUndefinedInWhere } from "@app/lib/knex";
import { logger } from "@app/lib/logger";
import { QueueName } from "@app/queue";
import { ActorType } from "@app/services/auth/auth-type";
import { EventType } from "./audit-log-types";
export type TAuditLogDALFactory = ReturnType<typeof auditLogDALFactory>;
@ -24,7 +28,24 @@ export const auditLogDALFactory = (db: TDbClient) => {
const auditLogOrm = ormify(db, TableName.AuditLog);
const find = async (
{ orgId, projectId, userAgentType, startDate, endDate, limit = 20, offset = 0, actor, eventType }: TFindQuery,
{
orgId,
projectId,
userAgentType,
startDate,
endDate,
limit = 20,
offset = 0,
actorId,
actorType,
eventType,
eventMetadata
}: Omit<TFindQuery, "actor" | "eventType"> & {
actorId?: string;
actorType?: ActorType;
eventType?: EventType[];
eventMetadata?: Record<string, string>;
},
tx?: Knex
) => {
try {
@ -32,23 +53,57 @@ export const auditLogDALFactory = (db: TDbClient) => {
.where(
stripUndefinedInWhere({
projectId,
orgId,
eventType,
actor,
[`${TableName.AuditLog}.orgId`]: orgId,
userAgentType
})
)
.leftJoin(TableName.Project, `${TableName.AuditLog}.projectId`, `${TableName.Project}.id`)
.select(selectAllTableCols(TableName.AuditLog))
.select(
db.ref("name").withSchema(TableName.Project).as("projectName"),
db.ref("slug").withSchema(TableName.Project).as("projectSlug")
)
.limit(limit)
.offset(offset)
.orderBy("createdAt", "desc");
.orderBy(`${TableName.AuditLog}.createdAt`, "desc");
if (actorId) {
void sqlQuery.whereRaw(`"actorMetadata"->>'userId' = ?`, [actorId]);
}
if (eventMetadata && Object.keys(eventMetadata).length) {
Object.entries(eventMetadata).forEach(([key, value]) => {
void sqlQuery.whereRaw(`"eventMetadata"->>'${key}' = ?`, [value]);
});
}
if (actorType) {
void sqlQuery.where("actor", actorType);
}
if (eventType?.length) {
void sqlQuery.whereIn("eventType", eventType);
}
if (startDate) {
void sqlQuery.where("createdAt", ">=", startDate);
void sqlQuery.where(`${TableName.AuditLog}.createdAt`, ">=", startDate);
}
if (endDate) {
void sqlQuery.where("createdAt", "<=", endDate);
void sqlQuery.where(`${TableName.AuditLog}.createdAt`, "<=", endDate);
}
const docs = await sqlQuery;
return docs;
return docs.map((doc) => ({
...AuditLogsSchema.parse(doc),
project: {
name: doc.projectName,
slug: doc.projectSlug
}
}));
} catch (error) {
throw new DatabaseError({ error });
}
@ -62,7 +117,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)
@ -75,15 +132,18 @@ export const auditLogDALFactory = (db: TDbClient) => {
.del()
.returning("id");
numberOfRetryOnFailure = 0; // reset
// eslint-disable-next-line no-await-in-loop
await new Promise((resolve) => {
setTimeout(resolve, 100); // time to breathe for db
});
} catch (error) {
numberOfRetryOnFailure += 1;
logger.error(error, "Failed to delete audit log on pruning");
} finally {
// eslint-disable-next-line no-await-in-loop
await new Promise((resolve) => {
setTimeout(resolve, 10); // time to breathe for db
});
}
} while (deletedAuditLogIds.length > 0 && numberOfRetryOnFailure < MAX_RETRY_ON_FAILURE);
isRetrying = numberOfRetryOnFailure > 0;
} while (deletedAuditLogIds.length > 0 || (isRetrying && numberOfRetryOnFailure < MAX_RETRY_ON_FAILURE));
logger.info(`${QueueName.DailyResourceCleanUp}: audit log completed`);
};
return { ...auditLogOrm, pruneAuditLog, find };

@ -1,7 +1,9 @@
import { ForbiddenError } from "@casl/ability";
import { getConfig } from "@app/lib/config/env";
import { BadRequestError } from "@app/lib/errors";
import { OrgPermissionActions, OrgPermissionSubjects } from "../permission/org-permission";
import { TPermissionServiceFactory } from "../permission/permission-service";
import { ProjectPermissionActions, ProjectPermissionSub } from "../permission/project-permission";
import { TAuditLogDALFactory } from "./audit-log-dal";
@ -10,7 +12,7 @@ import { EventType, TCreateAuditLogDTO, TListProjectAuditLogDTO } from "./audit-
type TAuditLogServiceFactoryDep = {
auditLogDAL: TAuditLogDALFactory;
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission" | "getOrgPermission">;
auditLogQueue: TAuditLogQueueServiceFactory;
};
@ -21,38 +23,47 @@ export const auditLogServiceFactory = ({
auditLogQueue,
permissionService
}: TAuditLogServiceFactoryDep) => {
const listProjectAuditLogs = async ({
userAgentType,
eventType,
offset,
limit,
endDate,
startDate,
actor,
actorId,
actorOrgId,
actorAuthMethod,
projectId,
auditLogActor
}: TListProjectAuditLogDTO) => {
const { permission } = await permissionService.getProjectPermission(
actor,
actorId,
projectId,
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.AuditLogs);
const listAuditLogs = async ({ actorAuthMethod, actorId, actorOrgId, actor, filter }: TListProjectAuditLogDTO) => {
if (filter.projectId) {
const { permission } = await permissionService.getProjectPermission(
actor,
actorId,
filter.projectId,
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.AuditLogs);
} else {
const { permission } = await permissionService.getOrgPermission(
actor,
actorId,
actorOrgId,
actorAuthMethod,
actorOrgId
);
/**
* NOTE (dangtony98): Update this to organization-level audit log permission check once audit logs are moved
* to the organization level
*/
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Member);
}
// If project ID is not provided, then we need to return all the audit logs for the organization itself.
const auditLogs = await auditLogDAL.find({
startDate,
endDate,
limit,
offset,
eventType,
userAgentType,
actor: auditLogActor,
projectId
startDate: filter.startDate,
endDate: filter.endDate,
limit: filter.limit,
offset: filter.offset,
eventType: filter.eventType,
userAgentType: filter.userAgentType,
actorId: filter.auditLogActorId,
actorType: filter.actorType,
eventMetadata: filter.eventMetadata,
...(filter.projectId ? { projectId: filter.projectId } : { orgId: actorOrgId })
});
return auditLogs.map(({ eventType: logEventType, actor: eActor, actorMetadata, eventMetadata, ...el }) => ({
...el,
event: { type: logEventType, metadata: eventMetadata },
@ -61,6 +72,10 @@ export const auditLogServiceFactory = ({
};
const createAuditLog = async (data: TCreateAuditLogDTO) => {
const appCfg = getConfig();
if (appCfg.DISABLE_AUDIT_LOG_GENERATION) {
return;
}
// add all cases in which project id or org id cannot be added
if (data.event.type !== EventType.LOGIN_IDENTITY_UNIVERSAL_AUTH) {
if (!data.projectId && !data.orgId) throw new BadRequestError({ message: "Must either project id or org id" });
@ -71,6 +86,6 @@ export const auditLogServiceFactory = ({
return {
createAuditLog,
listProjectAuditLogs
listAuditLogs
};
};

@ -2,21 +2,26 @@ import { TProjectPermission } from "@app/lib/types";
import { ActorType } from "@app/services/auth/auth-type";
import { CaStatus } from "@app/services/certificate-authority/certificate-authority-types";
import { TIdentityTrustedIp } from "@app/services/identity/identity-types";
import { PkiItemType } from "@app/services/pki-collection/pki-collection-types";
export type TListProjectAuditLogDTO = {
auditLogActor?: string;
projectId: string;
eventType?: string;
startDate?: string;
endDate?: string;
userAgentType?: string;
limit?: number;
offset?: number;
} & TProjectPermission;
filter: {
userAgentType?: UserAgentType;
eventType?: EventType[];
offset?: number;
limit: number;
endDate?: string;
startDate?: string;
projectId?: string;
auditLogActorId?: string;
actorType?: ActorType;
eventMetadata?: Record<string, string>;
};
} & Omit<TProjectPermission, "projectId">;
export type TCreateAuditLogDTO = {
event: Event;
actor: UserActor | IdentityActor | ServiceActor | ScimClientActor;
actor: UserActor | IdentityActor | ServiceActor | ScimClientActor | PlatformActor;
orgId?: string;
projectId?: string;
} & BaseAuthData;
@ -136,13 +141,25 @@ export enum EventType {
GET_CA_CERT = "get-certificate-authority-cert",
SIGN_INTERMEDIATE = "sign-intermediate",
IMPORT_CA_CERT = "import-certificate-authority-cert",
GET_CA_CRL = "get-certificate-authority-crl",
GET_CA_CRLS = "get-certificate-authority-crls",
ISSUE_CERT = "issue-cert",
SIGN_CERT = "sign-cert",
GET_CA_CERTIFICATE_TEMPLATES = "get-ca-certificate-templates",
GET_CERT = "get-cert",
DELETE_CERT = "delete-cert",
REVOKE_CERT = "revoke-cert",
GET_CERT_BODY = "get-cert-body",
CREATE_PKI_ALERT = "create-pki-alert",
GET_PKI_ALERT = "get-pki-alert",
UPDATE_PKI_ALERT = "update-pki-alert",
DELETE_PKI_ALERT = "delete-pki-alert",
CREATE_PKI_COLLECTION = "create-pki-collection",
GET_PKI_COLLECTION = "get-pki-collection",
UPDATE_PKI_COLLECTION = "update-pki-collection",
DELETE_PKI_COLLECTION = "delete-pki-collection",
GET_PKI_COLLECTION_ITEMS = "get-pki-collection-items",
ADD_PKI_COLLECTION_ITEM = "add-pki-collection-item",
DELETE_PKI_COLLECTION_ITEM = "delete-pki-collection-item",
CREATE_KMS = "create-kms",
UPDATE_KMS = "update-kms",
DELETE_KMS = "delete-kms",
@ -150,7 +167,22 @@ export enum EventType {
UPDATE_PROJECT_KMS = "update-project-kms",
GET_PROJECT_KMS_BACKUP = "get-project-kms-backup",
LOAD_PROJECT_KMS_BACKUP = "load-project-kms-backup",
ORG_ADMIN_ACCESS_PROJECT = "org-admin-accessed-project"
ORG_ADMIN_ACCESS_PROJECT = "org-admin-accessed-project",
CREATE_CERTIFICATE_TEMPLATE = "create-certificate-template",
UPDATE_CERTIFICATE_TEMPLATE = "update-certificate-template",
DELETE_CERTIFICATE_TEMPLATE = "delete-certificate-template",
GET_CERTIFICATE_TEMPLATE = "get-certificate-template",
CREATE_CERTIFICATE_TEMPLATE_EST_CONFIG = "create-certificate-template-est-config",
UPDATE_CERTIFICATE_TEMPLATE_EST_CONFIG = "update-certificate-template-est-config",
GET_CERTIFICATE_TEMPLATE_EST_CONFIG = "get-certificate-template-est-config",
ATTEMPT_CREATE_SLACK_INTEGRATION = "attempt-create-slack-integration",
ATTEMPT_REINSTALL_SLACK_INTEGRATION = "attempt-reinstall-slack-integration",
GET_SLACK_INTEGRATION = "get-slack-integration",
UPDATE_SLACK_INTEGRATION = "update-slack-integration",
DELETE_SLACK_INTEGRATION = "delete-slack-integration",
GET_PROJECT_SLACK_CONFIG = "get-project-slack-config",
UPDATE_PROJECT_SLACK_CONFIG = "update-project-slack-config",
INTEGRATION_SYNCED = "integration-synced"
}
interface UserActorMetadata {
@ -171,6 +203,8 @@ interface IdentityActorMetadata {
interface ScimClientActorMetadata {}
interface PlatformActorMetadata {}
export interface UserActor {
type: ActorType.USER;
metadata: UserActorMetadata;
@ -181,6 +215,11 @@ export interface ServiceActor {
metadata: ServiceActorMetadata;
}
export interface PlatformActor {
type: ActorType.PLATFORM;
metadata: PlatformActorMetadata;
}
export interface IdentityActor {
type: ActorType.IDENTITY;
metadata: IdentityActorMetadata;
@ -191,7 +230,7 @@ export interface ScimClientActor {
metadata: ScimClientActorMetadata;
}
export type Actor = UserActor | ServiceActor | IdentityActor | ScimClientActor;
export type Actor = UserActor | ServiceActor | IdentityActor | ScimClientActor | PlatformActor;
interface GetSecretsEvent {
type: EventType.GET_SECRETS;
@ -340,6 +379,7 @@ interface DeleteIntegrationEvent {
targetServiceId?: string;
path?: string;
region?: string;
shouldDeleteIntegrationSecrets?: boolean;
};
}
@ -1146,8 +1186,8 @@ interface ImportCaCert {
};
}
interface GetCaCrl {
type: EventType.GET_CA_CRL;
interface GetCaCrls {
type: EventType.GET_CA_CRLS;
metadata: {
caId: string;
dn: string;
@ -1172,6 +1212,14 @@ interface SignCert {
};
}
interface GetCaCertificateTemplates {
type: EventType.GET_CA_CERTIFICATE_TEMPLATES;
metadata: {
caId: string;
dn: string;
};
}
interface GetCert {
type: EventType.GET_CERT;
metadata: {
@ -1208,6 +1256,95 @@ interface GetCertBody {
};
}
interface CreatePkiAlert {
type: EventType.CREATE_PKI_ALERT;
metadata: {
pkiAlertId: string;
pkiCollectionId: string;
name: string;
alertBeforeDays: number;
recipientEmails: string;
};
}
interface GetPkiAlert {
type: EventType.GET_PKI_ALERT;
metadata: {
pkiAlertId: string;
};
}
interface UpdatePkiAlert {
type: EventType.UPDATE_PKI_ALERT;
metadata: {
pkiAlertId: string;
pkiCollectionId?: string;
name?: string;
alertBeforeDays?: number;
recipientEmails?: string;
};
}
interface DeletePkiAlert {
type: EventType.DELETE_PKI_ALERT;
metadata: {
pkiAlertId: string;
};
}
interface CreatePkiCollection {
type: EventType.CREATE_PKI_COLLECTION;
metadata: {
pkiCollectionId: string;
name: string;
};
}
interface GetPkiCollection {
type: EventType.GET_PKI_COLLECTION;
metadata: {
pkiCollectionId: string;
};
}
interface UpdatePkiCollection {
type: EventType.UPDATE_PKI_COLLECTION;
metadata: {
pkiCollectionId: string;
name?: string;
};
}
interface DeletePkiCollection {
type: EventType.DELETE_PKI_COLLECTION;
metadata: {
pkiCollectionId: string;
};
}
interface GetPkiCollectionItems {
type: EventType.GET_PKI_COLLECTION_ITEMS;
metadata: {
pkiCollectionId: string;
};
}
interface AddPkiCollectionItem {
type: EventType.ADD_PKI_COLLECTION_ITEM;
metadata: {
pkiCollectionItemId: string;
pkiCollectionId: string;
type: PkiItemType;
itemId: string;
};
}
interface DeletePkiCollectionItem {
type: EventType.DELETE_PKI_COLLECTION_ITEM;
metadata: {
pkiCollectionItemId: string;
pkiCollectionId: string;
};
}
interface CreateKmsEvent {
type: EventType.CREATE_KMS;
metadata: {
@ -1264,6 +1401,46 @@ interface LoadProjectKmsBackupEvent {
metadata: Record<string, string>; // no metadata yet
}
interface CreateCertificateTemplate {
type: EventType.CREATE_CERTIFICATE_TEMPLATE;
metadata: {
certificateTemplateId: string;
caId: string;
pkiCollectionId?: string;
name: string;
commonName: string;
subjectAlternativeName: string;
ttl: string;
};
}
interface GetCertificateTemplate {
type: EventType.GET_CERTIFICATE_TEMPLATE;
metadata: {
certificateTemplateId: string;
};
}
interface UpdateCertificateTemplate {
type: EventType.UPDATE_CERTIFICATE_TEMPLATE;
metadata: {
certificateTemplateId: string;
caId: string;
pkiCollectionId?: string;
name: string;
commonName: string;
subjectAlternativeName: string;
ttl: string;
};
}
interface DeleteCertificateTemplate {
type: EventType.DELETE_CERTIFICATE_TEMPLATE;
metadata: {
certificateTemplateId: string;
};
}
interface OrgAdminAccessProjectEvent {
type: EventType.ORG_ADMIN_ACCESS_PROJECT;
metadata: {
@ -1274,6 +1451,96 @@ interface OrgAdminAccessProjectEvent {
}; // no metadata yet
}
interface CreateCertificateTemplateEstConfig {
type: EventType.CREATE_CERTIFICATE_TEMPLATE_EST_CONFIG;
metadata: {
certificateTemplateId: string;
isEnabled: boolean;
};
}
interface UpdateCertificateTemplateEstConfig {
type: EventType.UPDATE_CERTIFICATE_TEMPLATE_EST_CONFIG;
metadata: {
certificateTemplateId: string;
isEnabled: boolean;
};
}
interface GetCertificateTemplateEstConfig {
type: EventType.GET_CERTIFICATE_TEMPLATE_EST_CONFIG;
metadata: {
certificateTemplateId: string;
};
}
interface AttemptCreateSlackIntegration {
type: EventType.ATTEMPT_CREATE_SLACK_INTEGRATION;
metadata: {
slug: string;
description?: string;
};
}
interface AttemptReinstallSlackIntegration {
type: EventType.ATTEMPT_REINSTALL_SLACK_INTEGRATION;
metadata: {
id: string;
};
}
interface UpdateSlackIntegration {
type: EventType.UPDATE_SLACK_INTEGRATION;
metadata: {
id: string;
slug: string;
description?: string;
};
}
interface DeleteSlackIntegration {
type: EventType.DELETE_SLACK_INTEGRATION;
metadata: {
id: string;
};
}
interface GetSlackIntegration {
type: EventType.GET_SLACK_INTEGRATION;
metadata: {
id: string;
};
}
interface UpdateProjectSlackConfig {
type: EventType.UPDATE_PROJECT_SLACK_CONFIG;
metadata: {
id: string;
slackIntegrationId: string;
isAccessRequestNotificationEnabled: boolean;
accessRequestChannels: string;
isSecretRequestNotificationEnabled: boolean;
secretRequestChannels: string;
};
}
interface GetProjectSlackConfig {
type: EventType.GET_PROJECT_SLACK_CONFIG;
metadata: {
id: string;
};
}
interface IntegrationSyncedEvent {
type: EventType.INTEGRATION_SYNCED;
metadata: {
integrationId: string;
lastSyncJobId: string;
lastUsed: Date;
syncMessage: string;
isSynced: boolean;
};
}
export type Event =
| GetSecretsEvent
| GetSecretEvent
@ -1372,13 +1639,25 @@ export type Event =
| GetCaCert
| SignIntermediate
| ImportCaCert
| GetCaCrl
| GetCaCrls
| IssueCert
| SignCert
| GetCaCertificateTemplates
| GetCert
| DeleteCert
| RevokeCert
| GetCertBody
| CreatePkiAlert
| GetPkiAlert
| UpdatePkiAlert
| DeletePkiAlert
| CreatePkiCollection
| GetPkiCollection
| UpdatePkiCollection
| DeletePkiCollection
| GetPkiCollectionItems
| AddPkiCollectionItem
| DeletePkiCollectionItem
| CreateKmsEvent
| UpdateKmsEvent
| DeleteKmsEvent
@ -1386,4 +1665,19 @@ export type Event =
| UpdateProjectKmsEvent
| GetProjectKmsBackupEvent
| LoadProjectKmsBackupEvent
| OrgAdminAccessProjectEvent;
| OrgAdminAccessProjectEvent
| CreateCertificateTemplate
| UpdateCertificateTemplate
| GetCertificateTemplate
| DeleteCertificateTemplate
| CreateCertificateTemplateEstConfig
| UpdateCertificateTemplateEstConfig
| GetCertificateTemplateEstConfig
| AttemptCreateSlackIntegration
| AttemptReinstallSlackIntegration
| UpdateSlackIntegration
| DeleteSlackIntegration
| GetSlackIntegration
| UpdateProjectSlackConfig
| GetProjectSlackConfig
| IntegrationSyncedEvent;

@ -2,24 +2,24 @@ import { ForbiddenError } from "@casl/ability";
import * as x509 from "@peculiar/x509";
import { TCertificateAuthorityCrlDALFactory } from "@app/ee/services/certificate-authority-crl/certificate-authority-crl-dal";
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
// import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
import { BadRequestError } from "@app/lib/errors";
import { BadRequestError, NotFoundError } from "@app/lib/errors";
import { TCertificateAuthorityDALFactory } from "@app/services/certificate-authority/certificate-authority-dal";
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
import { TProjectDALFactory } from "@app/services/project/project-dal";
import { getProjectKmsCertificateKeyId } from "@app/services/project/project-fns";
import { TGetCrl } from "./certificate-authority-crl-types";
import { TGetCaCrlsDTO, TGetCrlById } from "./certificate-authority-crl-types";
type TCertificateAuthorityCrlServiceFactoryDep = {
certificateAuthorityDAL: Pick<TCertificateAuthorityDALFactory, "findById">;
certificateAuthorityCrlDAL: Pick<TCertificateAuthorityCrlDALFactory, "findOne">;
certificateAuthorityCrlDAL: Pick<TCertificateAuthorityCrlDALFactory, "find" | "findById">;
projectDAL: Pick<TProjectDALFactory, "findOne" | "updateById" | "transaction">;
kmsService: Pick<TKmsServiceFactory, "decryptWithKmsKey" | "generateKmsKey">;
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
// licenseService: Pick<TLicenseServiceFactory, "getPlan">;
};
export type TCertificateAuthorityCrlServiceFactory = ReturnType<typeof certificateAuthorityCrlServiceFactory>;
@ -29,13 +29,42 @@ export const certificateAuthorityCrlServiceFactory = ({
certificateAuthorityCrlDAL,
projectDAL,
kmsService,
permissionService,
licenseService
permissionService // licenseService
}: TCertificateAuthorityCrlServiceFactoryDep) => {
/**
* Return the Certificate Revocation List (CRL) for CA with id [caId]
* Return CRL with id [crlId]
*/
const getCaCrl = async ({ caId, actorId, actorAuthMethod, actor, actorOrgId }: TGetCrl) => {
const getCrlById = async (crlId: TGetCrlById) => {
const caCrl = await certificateAuthorityCrlDAL.findById(crlId);
if (!caCrl) throw new NotFoundError({ message: "CRL not found" });
const ca = await certificateAuthorityDAL.findById(caCrl.caId);
const keyId = await getProjectKmsCertificateKeyId({
projectId: ca.projectId,
projectDAL,
kmsService
});
const kmsDecryptor = await kmsService.decryptWithKmsKey({
kmsId: keyId
});
const decryptedCrl = await kmsDecryptor({ cipherTextBlob: caCrl.encryptedCrl });
const crl = new x509.X509Crl(decryptedCrl);
return {
ca,
caCrl,
crl: crl.rawData
};
};
/**
* Returns a list of CRL ids for CA with id [caId]
*/
const getCaCrls = async ({ caId, actorId, actorAuthMethod, actor, actorOrgId }: TGetCaCrlsDTO) => {
const ca = await certificateAuthorityDAL.findById(caId);
if (!ca) throw new BadRequestError({ message: "CA not found" });
@ -52,15 +81,14 @@ export const certificateAuthorityCrlServiceFactory = ({
ProjectPermissionSub.CertificateAuthorities
);
const plan = await licenseService.getPlan(actorOrgId);
if (!plan.caCrl)
throw new BadRequestError({
message:
"Failed to get CA certificate revocation list (CRL) due to plan restriction. Upgrade plan to get the CA CRL."
});
// const plan = await licenseService.getPlan(actorOrgId);
// if (!plan.caCrl)
// throw new BadRequestError({
// message:
// "Failed to get CA certificate revocation lists (CRLs) due to plan restriction. Upgrade plan to get the CA CRL."
// });
const caCrl = await certificateAuthorityCrlDAL.findOne({ caId: ca.id });
if (!caCrl) throw new BadRequestError({ message: "CRL not found" });
const caCrls = await certificateAuthorityCrlDAL.find({ caId: ca.id }, { sort: [["createdAt", "desc"]] });
const keyId = await getProjectKmsCertificateKeyId({
projectId: ca.projectId,
@ -72,15 +100,23 @@ export const certificateAuthorityCrlServiceFactory = ({
kmsId: keyId
});
const decryptedCrl = await kmsDecryptor({ cipherTextBlob: caCrl.encryptedCrl });
const crl = new x509.X509Crl(decryptedCrl);
const decryptedCrls = await Promise.all(
caCrls.map(async (caCrl) => {
const decryptedCrl = await kmsDecryptor({ cipherTextBlob: caCrl.encryptedCrl });
const crl = new x509.X509Crl(decryptedCrl);
const base64crl = crl.toString("base64");
const crlPem = `-----BEGIN X509 CRL-----\n${base64crl.match(/.{1,64}/g)?.join("\n")}\n-----END X509 CRL-----`;
const base64crl = crl.toString("base64");
const crlPem = `-----BEGIN X509 CRL-----\n${base64crl.match(/.{1,64}/g)?.join("\n")}\n-----END X509 CRL-----`;
return {
id: caCrl.id,
crl: crlPem
};
})
);
return {
crl: crlPem,
ca
ca,
crls: decryptedCrls
};
};
@ -166,7 +202,8 @@ export const certificateAuthorityCrlServiceFactory = ({
// };
return {
getCaCrl
getCrlById,
getCaCrls
// rotateCaCrl
};
};

@ -1,5 +1,7 @@
import { TProjectPermission } from "@app/lib/types";
export type TGetCrl = {
export type TGetCrlById = string;
export type TGetCaCrlsDTO = {
caId: string;
} & Omit<TProjectPermission, "projectId">;

@ -0,0 +1,24 @@
import { Certificate, ContentInfo, EncapsulatedContentInfo, SignedData } from "pkijs";
export const convertRawCertsToPkcs7 = (rawCertificate: ArrayBuffer[]) => {
const certs = rawCertificate.map((rawCert) => Certificate.fromBER(rawCert));
const cmsSigned = new SignedData({
encapContentInfo: new EncapsulatedContentInfo({
eContentType: "1.2.840.113549.1.7.1" // not encrypted and not compressed data
}),
certificates: certs
});
const cmsContent = new ContentInfo({
contentType: "1.2.840.113549.1.7.2", // SignedData
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
content: cmsSigned.toSchema()
});
const derBuffer = cmsContent.toSchema().toBER(false);
const base64Pkcs7 = Buffer.from(derBuffer)
.toString("base64")
.replace(/(.{64})/g, "$1\n"); // we add a linebreak for CURL clients
return base64Pkcs7;
};

@ -0,0 +1,268 @@
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 { TLicenseServiceFactory } from "../license/license-service";
import { convertRawCertsToPkcs7 } from "./certificate-est-fns";
type TCertificateEstServiceFactoryDep = {
certificateAuthorityService: Pick<TCertificateAuthorityServiceFactory, "signCertFromCa">;
certificateTemplateService: Pick<TCertificateTemplateServiceFactory, "getEstConfiguration">;
certificateTemplateDAL: Pick<TCertificateTemplateDALFactory, "findById">;
certificateAuthorityDAL: Pick<TCertificateAuthorityDALFactory, "findById">;
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>;
export const certificateEstServiceFactory = ({
certificateAuthorityService,
certificateTemplateService,
certificateTemplateDAL,
certificateAuthorityCertDAL,
certificateAuthorityDAL,
projectDAL,
kmsService,
licenseService
}: TCertificateEstServiceFactoryDep) => {
const simpleReenroll = async ({
csr,
certificateTemplateId,
sslClientCert
}: {
csr: string;
certificateTemplateId: string;
sslClientCert: string;
}) => {
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 - simpleReenroll due to plan restriction. Upgrade to the Enterprise plan."
});
}
if (!estConfig.isEnabled) {
throw new BadRequestError({
message: "EST is disabled"
});
}
const certTemplate = await certificateTemplateDAL.findById(certificateTemplateId);
const leafCertificate = decodeURIComponent(sslClientCert).match(
/-----BEGIN CERTIFICATE-----[\s\S]+?-----END CERTIFICATE-----/g
)?.[0];
if (!leafCertificate) {
throw new UnauthorizedError({ message: "Missing client certificate" });
}
const cert = new x509.X509Certificate(leafCertificate);
// We have to assert that the client certificate provided can be traced back to the Root CA
const caCertChains = await getCaCertChains({
caId: certTemplate.caId,
certificateAuthorityCertDAL,
certificateAuthorityDAL,
projectDAL,
kmsService
});
const verifiedChains = await Promise.all(
caCertChains.map((chain) => {
const caCert = new x509.X509Certificate(chain.certificate);
const caChain =
chain.certificateChain
.match(/-----BEGIN CERTIFICATE-----[\s\S]+?-----END CERTIFICATE-----/g)
?.map((c) => new x509.X509Certificate(c)) || [];
return isCertChainValid([cert, caCert, ...caChain]);
})
);
if (!verifiedChains.some(Boolean)) {
throw new BadRequestError({
message: "Invalid client certificate: unable to build a valid certificate chain"
});
}
// We ensure that the Subject and SubjectAltNames of the CSR and the existing certificate are exactly the same
const csrObj = new x509.Pkcs10CertificateRequest(csr);
if (csrObj.subject !== cert.subject) {
throw new BadRequestError({
message: "Subject mismatch"
});
}
let csrSanSet: Set<string> = new Set();
const csrSanExtension = csrObj.extensions.find((ext) => ext.type === "2.5.29.17");
if (csrSanExtension) {
const sanNames = new x509.GeneralNames(csrSanExtension.value);
csrSanSet = new Set([...sanNames.items.map((name) => `${name.type}-${name.value}`)]);
}
let certSanSet: Set<string> = new Set();
const certSanExtension = cert.extensions.find((ext) => ext.type === "2.5.29.17");
if (certSanExtension) {
const sanNames = new x509.GeneralNames(certSanExtension.value);
certSanSet = new Set([...sanNames.items.map((name) => `${name.type}-${name.value}`)]);
}
if (csrSanSet.size !== certSanSet.size || ![...csrSanSet].every((element) => certSanSet.has(element))) {
throw new BadRequestError({
message: "Subject alternative names mismatch"
});
}
const { certificate } = await certificateAuthorityService.signCertFromCa({
isInternal: true,
certificateTemplateId,
csr
});
return convertRawCertsToPkcs7([certificate.rawData]);
};
const simpleEnroll = async ({
csr,
certificateTemplateId,
sslClientCert
}: {
csr: string;
certificateTemplateId: string;
sslClientCert: string;
}) => {
/* We first have to assert that the client certificate provided can be traced back to the attached
CA chain in the EST configuration
*/
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 - simpleEnroll due to plan restriction. Upgrade to the Enterprise plan."
});
}
if (!estConfig.isEnabled) {
throw new BadRequestError({
message: "EST is disabled"
});
}
const caCerts = estConfig.caChain
.match(/-----BEGIN CERTIFICATE-----[\s\S]+?-----END CERTIFICATE-----/g)
?.map((cert) => {
return new x509.X509Certificate(cert);
});
if (!caCerts) {
throw new BadRequestError({ message: "Failed to parse certificate chain" });
}
const leafCertificate = decodeURIComponent(sslClientCert).match(
/-----BEGIN CERTIFICATE-----[\s\S]+?-----END CERTIFICATE-----/g
)?.[0];
if (!leafCertificate) {
throw new BadRequestError({ message: "Missing client certificate" });
}
const certObj = new x509.X509Certificate(leafCertificate);
if (!(await isCertChainValid([certObj, ...caCerts]))) {
throw new BadRequestError({ message: "Invalid certificate chain" });
}
const { certificate } = await certificateAuthorityService.signCertFromCa({
isInternal: true,
certificateTemplateId,
csr
});
return convertRawCertsToPkcs7([certificate.rawData]);
};
/**
* Return the CA certificate and CA certificate chain for the CA bound to
* the certificate template with id [certificateTemplateId] as part of EST protocol
*/
const getCaCerts = async ({ certificateTemplateId }: { certificateTemplateId: string }) => {
const certTemplate = await certificateTemplateDAL.findById(certificateTemplateId);
if (!certTemplate) {
throw new NotFoundError({
message: "Certificate template not found"
});
}
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({
message: "Certificate Authority not found"
});
}
const { caCert, caCertChain } = await getCaCertChain({
caCertId: ca.activeCaCertId as string,
certificateAuthorityDAL,
certificateAuthorityCertDAL,
projectDAL,
kmsService
});
const certificates = caCertChain
.match(/-----BEGIN CERTIFICATE-----[\s\S]+?-----END CERTIFICATE-----/g)
?.map((cert) => new x509.X509Certificate(cert));
if (!certificates) {
throw new BadRequestError({ message: "Failed to parse certificate chain" });
}
const caCertificate = new x509.X509Certificate(caCert);
return convertRawCertsToPkcs7([caCertificate.rawData, ...certificates.map((cert) => cert.rawData)]);
};
return {
simpleEnroll,
simpleReenroll,
getCaCerts
};
};

@ -98,6 +98,7 @@ export const dynamicSecretServiceFactory = ({
if (!isConnected) throw new BadRequestError({ message: "Provider connection failed" });
const encryptedInput = infisicalSymmetricEncypt(JSON.stringify(inputs));
const dynamicSecretCfg = await dynamicSecretDAL.create({
type: provider.type,
version: 1,

@ -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
};
};

@ -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 ElasticSearchProvider = (): 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
};
};

@ -1,10 +1,22 @@
import { AwsElastiCacheDatabaseProvider } from "./aws-elasticache";
import { AwsIamProvider } from "./aws-iam";
import { CassandraProvider } from "./cassandra";
import { ElasticSearchProvider } from "./elastic-search";
import { DynamicSecretProviders } from "./models";
import { MongoAtlasProvider } from "./mongo-atlas";
import { MongoDBProvider } from "./mongo-db";
import { RabbitMqProvider } from "./rabbit-mq";
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.MongoDB]: MongoDBProvider(),
[DynamicSecretProviders.ElasticSearch]: ElasticSearchProvider(),
[DynamicSecretProviders.RabbitMq]: RabbitMqProvider()
});

@ -7,6 +7,75 @@ 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 DynamicSecretRabbitMqSchema = z.object({
host: z.string().trim().min(1),
port: z.number(),
tags: z.array(z.string().trim()).default([]),
username: z.string().trim().min(1),
password: z.string().trim().min(1),
ca: z.string().optional(),
virtualHost: z.object({
name: z.string().trim().min(1),
permissions: z.object({
read: z.string().trim().min(1),
write: z.string().trim().min(1),
configure: z.string().trim().min(1)
})
})
});
export const DynamicSecretSqlDBSchema = z.object({
client: z.nativeEnum(SqlProviders),
host: z.string().trim().toLowerCase(),
@ -44,16 +113,81 @@ 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 const DynamicSecretMongoDBSchema = z.object({
host: z.string().min(1).trim().toLowerCase(),
port: z.number().optional(),
username: z.string().min(1).trim(),
password: z.string().min(1).trim(),
database: z.string().min(1).trim(),
ca: z.string().min(1).optional(),
roles: z
.string()
.array()
.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.'
)
});
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",
MongoDB = "mongo-db",
RabbitMq = "rabbit-mq"
}
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 }),
z.object({ type: z.literal(DynamicSecretProviders.MongoDB), inputs: DynamicSecretMongoDBSchema }),
z.object({ type: z.literal(DynamicSecretProviders.RabbitMq), inputs: DynamicSecretRabbitMqSchema })
]);
export type TDynamicProviderFns = {

@ -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
};
};

@ -0,0 +1,116 @@
import { MongoClient } from "mongodb";
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 { DynamicSecretMongoDBSchema, TDynamicProviderFns } from "./models";
const generatePassword = (size = 48) => {
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_.~!*$#";
return customAlphabet(charset, 48)(size);
};
const generateUsername = () => {
return alphaNumericNanoId(32);
};
export const MongoDBProvider = (): TDynamicProviderFns => {
const validateProviderInputs = async (inputs: unknown) => {
const appCfg = getConfig();
const providerInputs = await DynamicSecretMongoDBSchema.parseAsync(inputs);
if (
appCfg.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 DynamicSecretMongoDBSchema>) => {
const isSrv = !providerInputs.port;
const uri = isSrv
? `mongodb+srv://${providerInputs.host}`
: `mongodb://${providerInputs.host}:${providerInputs.port}`;
const client = new MongoClient(uri, {
auth: {
username: providerInputs.username,
password: providerInputs.password
},
directConnection: !isSrv,
ca: providerInputs.ca
});
return client;
};
const validateConnection = async (inputs: unknown) => {
const providerInputs = await validateProviderInputs(inputs);
const client = await getClient(providerInputs);
const isConnected = await client
.db(providerInputs.database)
.command({ ping: 1 })
.then(() => true);
await client.close();
return isConnected;
};
const create = async (inputs: unknown) => {
const providerInputs = await validateProviderInputs(inputs);
const client = await getClient(providerInputs);
const username = generateUsername();
const password = generatePassword();
const db = client.db(providerInputs.database);
await db.command({
createUser: username,
pwd: password,
roles: providerInputs.roles
});
await client.close();
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 db = client.db(providerInputs.database);
await db.command({
dropUser: username
});
await client.close();
return { entityId: username };
};
const renew = async (_inputs: unknown, entityId: string) => {
return { entityId };
};
return {
validateProviderInputs,
validateConnection,
create,
revoke,
renew
};
};

@ -0,0 +1,172 @@
import axios, { Axios } from "axios";
import https from "https";
import { customAlphabet } from "nanoid";
import { z } from "zod";
import { getConfig } from "@app/lib/config/env";
import { BadRequestError } from "@app/lib/errors";
import { removeTrailingSlash } from "@app/lib/fn";
import { logger } from "@app/lib/logger";
import { alphaNumericNanoId } from "@app/lib/nanoid";
import { DynamicSecretRabbitMqSchema, TDynamicProviderFns } from "./models";
const generatePassword = () => {
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_.~!*$#";
return customAlphabet(charset, 64)();
};
const generateUsername = () => {
return alphaNumericNanoId(32);
};
type TCreateRabbitMQUser = {
axiosInstance: Axios;
createUser: {
username: string;
password: string;
tags: string[];
};
virtualHost: {
name: string;
permissions: {
read: string;
write: string;
configure: string;
};
};
};
type TDeleteRabbitMqUser = {
axiosInstance: Axios;
usernameToDelete: string;
};
async function createRabbitMqUser({ axiosInstance, createUser, virtualHost }: TCreateRabbitMQUser): Promise<void> {
try {
// Create user
const userUrl = `/users/${createUser.username}`;
const userData = {
password: createUser.password,
tags: createUser.tags.join(",")
};
await axiosInstance.put(userUrl, userData);
// Set permissions for the virtual host
if (virtualHost) {
const permissionData = {
configure: virtualHost.permissions.configure,
write: virtualHost.permissions.write,
read: virtualHost.permissions.read
};
await axiosInstance.put(
`/permissions/${encodeURIComponent(virtualHost.name)}/${createUser.username}`,
permissionData
);
}
} catch (error) {
logger.error(error, "Error creating RabbitMQ user");
throw error;
}
}
async function deleteRabbitMqUser({ axiosInstance, usernameToDelete }: TDeleteRabbitMqUser) {
await axiosInstance.delete(`users/${usernameToDelete}`);
return { username: usernameToDelete };
}
export const RabbitMqProvider = (): 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 DynamicSecretRabbitMqSchema.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 DynamicSecretRabbitMqSchema>) => {
const axiosInstance = axios.create({
baseURL: `${removeTrailingSlash(providerInputs.host)}:${providerInputs.port}/api`,
auth: {
username: providerInputs.username,
password: providerInputs.password
},
headers: {
"Content-Type": "application/json"
},
...(providerInputs.ca && {
httpsAgent: new https.Agent({ ca: providerInputs.ca, rejectUnauthorized: false })
})
});
return axiosInstance;
};
const validateConnection = async (inputs: unknown) => {
const providerInputs = await validateProviderInputs(inputs);
const connection = await getClient(providerInputs);
const infoResponse = await connection.get("/whoami").then(() => true);
return infoResponse;
};
const create = async (inputs: unknown) => {
const providerInputs = await validateProviderInputs(inputs);
const connection = await getClient(providerInputs);
const username = generateUsername();
const password = generatePassword();
await createRabbitMqUser({
axiosInstance: connection,
virtualHost: providerInputs.virtualHost,
createUser: {
password,
username,
tags: [...(providerInputs.tags ?? []), "infisical-user"]
}
});
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 deleteRabbitMqUser({ axiosInstance: connection, usernameToDelete: entityId });
return { entityId };
};
const renew = async (inputs: unknown, entityId: string) => {
// Do nothing
return { entityId };
};
return {
validateProviderInputs,
validateConnection,
create,
revoke,
renew
};
};

@ -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
};
};

@ -41,10 +41,9 @@ export const userGroupMembershipDALFactory = (db: TDbClient) => {
};
// special query
const findUserGroupMembershipsInProject = async (usernames: string[], projectId: string) => {
const findUserGroupMembershipsInProject = async (usernames: string[], projectId: string, tx?: Knex) => {
try {
const usernameDocs: string[] = await db
.replicaNode()(TableName.UserGroupMembership)
const usernameDocs: string[] = await (tx || db.replicaNode())(TableName.UserGroupMembership)
.join(
TableName.GroupProjectMembership,
`${TableName.UserGroupMembership}.groupId`,

@ -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 = () => {};

@ -40,18 +40,24 @@ export const getDefaultOnPremFeatures = (): TFeatureSet => ({
secretRotation: true,
caCrl: false,
instanceUserManagement: false,
externalKms: false
externalKms: false,
rateLimits: {
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 }
@ -69,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
@ -80,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;
@ -91,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 };
};

@ -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 }

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

@ -126,7 +126,6 @@ const buildMemberPermission = () => {
can(OrgPermissionActions.Read, OrgPermissionSubjects.Workspace);
can(OrgPermissionActions.Create, OrgPermissionSubjects.Workspace);
can(OrgPermissionActions.Read, OrgPermissionSubjects.Member);
can(OrgPermissionActions.Create, OrgPermissionSubjects.Member);
can(OrgPermissionActions.Read, OrgPermissionSubjects.Groups);
can(OrgPermissionActions.Read, OrgPermissionSubjects.Role);
can(OrgPermissionActions.Read, OrgPermissionSubjects.Settings);

@ -66,6 +66,7 @@ export const permissionDALFactory = (db: TDbClient) => {
`${TableName.GroupProjectMembershipRole}.projectMembershipId`,
`${TableName.GroupProjectMembership}.id`
)
.leftJoin(
TableName.ProjectRoles,
`${TableName.GroupProjectMembershipRole}.customRoleId`,
@ -73,6 +74,12 @@ export const permissionDALFactory = (db: TDbClient) => {
)
.join(TableName.Project, `${TableName.GroupProjectMembership}.projectId`, `${TableName.Project}.id`)
.join(TableName.Organization, `${TableName.Project}.orgId`, `${TableName.Organization}.id`)
.leftJoin(
TableName.ProjectUserAdditionalPrivilege,
`${TableName.GroupProjectMembership}.projectId`,
`${TableName.Project}.id`
)
.select(selectAllTableCols(TableName.GroupProjectMembershipRole))
.select(
db.ref("id").withSchema(TableName.GroupProjectMembership).as("membershipId"),
@ -81,9 +88,30 @@ export const permissionDALFactory = (db: TDbClient) => {
db.ref("projectId").withSchema(TableName.GroupProjectMembership),
db.ref("authEnforced").withSchema(TableName.Organization).as("orgAuthEnforced"),
db.ref("orgId").withSchema(TableName.Project),
db.ref("slug").withSchema(TableName.ProjectRoles).as("customRoleSlug")
)
.select("permissions");
db.ref("slug").withSchema(TableName.ProjectRoles).as("customRoleSlug"),
db.ref("permissions").withSchema(TableName.ProjectRoles).as("permissions"),
// db.ref("permissions").withSchema(TableName.ProjectUserAdditionalPrivilege).as("apPermissions")
// Additional Privileges
db.ref("id").withSchema(TableName.ProjectUserAdditionalPrivilege).as("userApId"),
db.ref("permissions").withSchema(TableName.ProjectUserAdditionalPrivilege).as("userApPermissions"),
db.ref("temporaryMode").withSchema(TableName.ProjectUserAdditionalPrivilege).as("userApTemporaryMode"),
db.ref("isTemporary").withSchema(TableName.ProjectUserAdditionalPrivilege).as("userApIsTemporary"),
db.ref("temporaryRange").withSchema(TableName.ProjectUserAdditionalPrivilege).as("userApTemporaryRange"),
db.ref("projectId").withSchema(TableName.ProjectUserAdditionalPrivilege).as("userApProjectId"),
db.ref("userId").withSchema(TableName.ProjectUserAdditionalPrivilege).as("userApUserId"),
db
.ref("temporaryAccessStartTime")
.withSchema(TableName.ProjectUserAdditionalPrivilege)
.as("userApTemporaryAccessStartTime"),
db
.ref("temporaryAccessEndTime")
.withSchema(TableName.ProjectUserAdditionalPrivilege)
.as("userApTemporaryAccessEndTime")
);
// .select(`${TableName.ProjectRoles}.permissions`);
const docs = await db(TableName.ProjectMembership)
.join(
@ -98,12 +126,13 @@ export const permissionDALFactory = (db: TDbClient) => {
)
.leftJoin(
TableName.ProjectUserAdditionalPrivilege,
`${TableName.ProjectUserAdditionalPrivilege}.projectMembershipId`,
`${TableName.ProjectMembership}.id`
`${TableName.ProjectUserAdditionalPrivilege}.projectId`,
`${TableName.ProjectMembership}.projectId`
)
.join(TableName.Project, `${TableName.ProjectMembership}.projectId`, `${TableName.Project}.id`)
.join(TableName.Organization, `${TableName.Project}.orgId`, `${TableName.Organization}.id`)
.where("userId", userId)
.where(`${TableName.ProjectMembership}.userId`, userId)
.where(`${TableName.ProjectMembership}.projectId`, projectId)
.select(selectAllTableCols(TableName.ProjectUserMembershipRole))
.select(
@ -120,6 +149,10 @@ export const permissionDALFactory = (db: TDbClient) => {
db.ref("temporaryMode").withSchema(TableName.ProjectUserAdditionalPrivilege).as("userApTemporaryMode"),
db.ref("isTemporary").withSchema(TableName.ProjectUserAdditionalPrivilege).as("userApIsTemporary"),
db.ref("temporaryRange").withSchema(TableName.ProjectUserAdditionalPrivilege).as("userApTemporaryRange"),
db.ref("projectId").withSchema(TableName.ProjectUserAdditionalPrivilege).as("userApProjectId"),
db.ref("userId").withSchema(TableName.ProjectUserAdditionalPrivilege).as("userApUserId"),
db
.ref("temporaryAccessStartTime")
.withSchema(TableName.ProjectUserAdditionalPrivilege)
@ -198,6 +231,31 @@ export const permissionDALFactory = (db: TDbClient) => {
permissions: z.unknown(),
customRoleSlug: z.string().optional().nullable()
}).parse(data)
},
{
key: "userApId",
label: "additionalPrivileges" as const,
mapper: ({
userApId,
userApProjectId,
userApUserId,
userApPermissions,
userApIsTemporary,
userApTemporaryMode,
userApTemporaryRange,
userApTemporaryAccessEndTime,
userApTemporaryAccessStartTime
}) => ({
id: userApId,
userId: userApUserId,
projectId: userApProjectId,
permissions: userApPermissions,
temporaryRange: userApTemporaryRange,
temporaryMode: userApTemporaryMode,
temporaryAccessEndTime: userApTemporaryAccessEndTime,
temporaryAccessStartTime: userApTemporaryAccessStartTime,
isTemporary: userApIsTemporary
})
}
]
})
@ -218,15 +276,24 @@ export const permissionDALFactory = (db: TDbClient) => {
!isTemporary || (isTemporary && temporaryAccessEndTime && new Date() < temporaryAccessEndTime)
) ?? [];
const activeAdditionalPrivileges = permission?.[0]?.additionalPrivileges?.filter(
({ isTemporary, temporaryAccessEndTime }) =>
!isTemporary || (isTemporary && temporaryAccessEndTime && new Date() < temporaryAccessEndTime)
);
const activeAdditionalPrivileges =
permission?.[0]?.additionalPrivileges?.filter(
({ isTemporary, temporaryAccessEndTime }) =>
!isTemporary || (isTemporary && temporaryAccessEndTime && new Date() < temporaryAccessEndTime)
) ?? [];
const activeGroupAdditionalPrivileges =
groupPermission?.[0]?.additionalPrivileges?.filter(
({ isTemporary, temporaryAccessEndTime, userId: apUserId, projectId: apProjectId }) =>
apProjectId === projectId &&
apUserId === userId &&
(!isTemporary || (isTemporary && temporaryAccessEndTime && new Date() < temporaryAccessEndTime))
) ?? [];
return {
...(permission[0] || groupPermission[0]),
roles: [...activeRoles, ...activeGroupRoles],
additionalPrivileges: activeAdditionalPrivileges
additionalPrivileges: [...activeAdditionalPrivileges, ...activeGroupAdditionalPrivileges]
};
} catch (error) {
throw new DatabaseError({ error, name: "GetProjectPermission" });

@ -1,6 +1,7 @@
import { AbilityBuilder, createMongoAbility, ForcedSubject, MongoAbility } from "@casl/ability";
import { conditionsMatcher } from "@app/lib/casl";
import { BadRequestError } from "@app/lib/errors";
export enum ProjectPermissionActions {
Read = "read",
@ -30,6 +31,9 @@ export enum ProjectPermissionSub {
Identity = "identity",
CertificateAuthorities = "certificate-authorities",
Certificates = "certificates",
CertificateTemplates = "certificate-templates",
PkiAlerts = "pki-alerts",
PkiCollections = "pki-collections",
Kms = "kms"
}
@ -63,108 +67,134 @@ export type ProjectPermissionSet =
| [ProjectPermissionActions, ProjectPermissionSub.Identity]
| [ProjectPermissionActions, ProjectPermissionSub.CertificateAuthorities]
| [ProjectPermissionActions, ProjectPermissionSub.Certificates]
| [ProjectPermissionActions, ProjectPermissionSub.CertificateTemplates]
| [ProjectPermissionActions, ProjectPermissionSub.PkiAlerts]
| [ProjectPermissionActions, ProjectPermissionSub.PkiCollections]
| [ProjectPermissionActions.Delete, ProjectPermissionSub.Project]
| [ProjectPermissionActions.Edit, ProjectPermissionSub.Project]
| [ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback]
| [ProjectPermissionActions.Create, ProjectPermissionSub.SecretRollback]
| [ProjectPermissionActions.Edit, ProjectPermissionSub.Kms];
export const fullProjectPermissionSet: [ProjectPermissionActions, ProjectPermissionSub][] = [
[ProjectPermissionActions.Read, ProjectPermissionSub.Secrets],
[ProjectPermissionActions.Create, ProjectPermissionSub.Secrets],
[ProjectPermissionActions.Edit, ProjectPermissionSub.Secrets],
[ProjectPermissionActions.Delete, ProjectPermissionSub.Secrets],
[ProjectPermissionActions.Read, ProjectPermissionSub.SecretApproval],
[ProjectPermissionActions.Create, ProjectPermissionSub.SecretApproval],
[ProjectPermissionActions.Edit, ProjectPermissionSub.SecretApproval],
[ProjectPermissionActions.Delete, ProjectPermissionSub.SecretApproval],
[ProjectPermissionActions.Read, ProjectPermissionSub.SecretRotation],
[ProjectPermissionActions.Create, ProjectPermissionSub.SecretRotation],
[ProjectPermissionActions.Edit, ProjectPermissionSub.SecretRotation],
[ProjectPermissionActions.Delete, ProjectPermissionSub.SecretRotation],
[ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback],
[ProjectPermissionActions.Create, ProjectPermissionSub.SecretRollback],
[ProjectPermissionActions.Read, ProjectPermissionSub.Member],
[ProjectPermissionActions.Create, ProjectPermissionSub.Member],
[ProjectPermissionActions.Edit, ProjectPermissionSub.Member],
[ProjectPermissionActions.Delete, ProjectPermissionSub.Member],
[ProjectPermissionActions.Read, ProjectPermissionSub.Groups],
[ProjectPermissionActions.Create, ProjectPermissionSub.Groups],
[ProjectPermissionActions.Edit, ProjectPermissionSub.Groups],
[ProjectPermissionActions.Delete, ProjectPermissionSub.Groups],
[ProjectPermissionActions.Read, ProjectPermissionSub.Role],
[ProjectPermissionActions.Create, ProjectPermissionSub.Role],
[ProjectPermissionActions.Edit, ProjectPermissionSub.Role],
[ProjectPermissionActions.Delete, ProjectPermissionSub.Role],
[ProjectPermissionActions.Read, ProjectPermissionSub.Integrations],
[ProjectPermissionActions.Create, ProjectPermissionSub.Integrations],
[ProjectPermissionActions.Edit, ProjectPermissionSub.Integrations],
[ProjectPermissionActions.Delete, ProjectPermissionSub.Integrations],
[ProjectPermissionActions.Read, ProjectPermissionSub.Webhooks],
[ProjectPermissionActions.Create, ProjectPermissionSub.Webhooks],
[ProjectPermissionActions.Edit, ProjectPermissionSub.Webhooks],
[ProjectPermissionActions.Delete, ProjectPermissionSub.Webhooks],
[ProjectPermissionActions.Read, ProjectPermissionSub.Identity],
[ProjectPermissionActions.Create, ProjectPermissionSub.Identity],
[ProjectPermissionActions.Edit, ProjectPermissionSub.Identity],
[ProjectPermissionActions.Delete, ProjectPermissionSub.Identity],
[ProjectPermissionActions.Read, ProjectPermissionSub.ServiceTokens],
[ProjectPermissionActions.Create, ProjectPermissionSub.ServiceTokens],
[ProjectPermissionActions.Edit, ProjectPermissionSub.ServiceTokens],
[ProjectPermissionActions.Delete, ProjectPermissionSub.ServiceTokens],
[ProjectPermissionActions.Read, ProjectPermissionSub.Settings],
[ProjectPermissionActions.Create, ProjectPermissionSub.Settings],
[ProjectPermissionActions.Edit, ProjectPermissionSub.Settings],
[ProjectPermissionActions.Delete, ProjectPermissionSub.Settings],
[ProjectPermissionActions.Read, ProjectPermissionSub.Environments],
[ProjectPermissionActions.Create, ProjectPermissionSub.Environments],
[ProjectPermissionActions.Edit, ProjectPermissionSub.Environments],
[ProjectPermissionActions.Delete, ProjectPermissionSub.Environments],
[ProjectPermissionActions.Read, ProjectPermissionSub.Tags],
[ProjectPermissionActions.Create, ProjectPermissionSub.Tags],
[ProjectPermissionActions.Edit, ProjectPermissionSub.Tags],
[ProjectPermissionActions.Delete, ProjectPermissionSub.Tags],
[ProjectPermissionActions.Read, ProjectPermissionSub.AuditLogs],
[ProjectPermissionActions.Create, ProjectPermissionSub.AuditLogs],
[ProjectPermissionActions.Edit, ProjectPermissionSub.AuditLogs],
[ProjectPermissionActions.Delete, ProjectPermissionSub.AuditLogs],
[ProjectPermissionActions.Read, ProjectPermissionSub.IpAllowList],
[ProjectPermissionActions.Create, ProjectPermissionSub.IpAllowList],
[ProjectPermissionActions.Edit, ProjectPermissionSub.IpAllowList],
[ProjectPermissionActions.Delete, ProjectPermissionSub.IpAllowList],
// double check if all CRUD are needed for CA and Certificates
[ProjectPermissionActions.Read, ProjectPermissionSub.CertificateAuthorities],
[ProjectPermissionActions.Create, ProjectPermissionSub.CertificateAuthorities],
[ProjectPermissionActions.Edit, ProjectPermissionSub.CertificateAuthorities],
[ProjectPermissionActions.Delete, ProjectPermissionSub.CertificateAuthorities],
[ProjectPermissionActions.Read, ProjectPermissionSub.Certificates],
[ProjectPermissionActions.Create, ProjectPermissionSub.Certificates],
[ProjectPermissionActions.Edit, ProjectPermissionSub.Certificates],
[ProjectPermissionActions.Delete, ProjectPermissionSub.Certificates],
[ProjectPermissionActions.Read, ProjectPermissionSub.CertificateTemplates],
[ProjectPermissionActions.Create, ProjectPermissionSub.CertificateTemplates],
[ProjectPermissionActions.Edit, ProjectPermissionSub.CertificateTemplates],
[ProjectPermissionActions.Delete, ProjectPermissionSub.CertificateTemplates],
[ProjectPermissionActions.Read, ProjectPermissionSub.PkiAlerts],
[ProjectPermissionActions.Create, ProjectPermissionSub.PkiAlerts],
[ProjectPermissionActions.Edit, ProjectPermissionSub.PkiAlerts],
[ProjectPermissionActions.Delete, ProjectPermissionSub.PkiAlerts],
[ProjectPermissionActions.Read, ProjectPermissionSub.PkiCollections],
[ProjectPermissionActions.Create, ProjectPermissionSub.PkiCollections],
[ProjectPermissionActions.Edit, ProjectPermissionSub.PkiCollections],
[ProjectPermissionActions.Delete, ProjectPermissionSub.PkiCollections],
[ProjectPermissionActions.Edit, ProjectPermissionSub.Project],
[ProjectPermissionActions.Delete, ProjectPermissionSub.Project],
[ProjectPermissionActions.Edit, ProjectPermissionSub.Kms]
];
const buildAdminPermissionRules = () => {
const { can, rules } = new AbilityBuilder<MongoAbility<ProjectPermissionSet>>(createMongoAbility);
can(ProjectPermissionActions.Read, ProjectPermissionSub.Secrets);
can(ProjectPermissionActions.Create, ProjectPermissionSub.Secrets);
can(ProjectPermissionActions.Edit, ProjectPermissionSub.Secrets);
can(ProjectPermissionActions.Delete, ProjectPermissionSub.Secrets);
can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretApproval);
can(ProjectPermissionActions.Create, ProjectPermissionSub.SecretApproval);
can(ProjectPermissionActions.Edit, ProjectPermissionSub.SecretApproval);
can(ProjectPermissionActions.Delete, ProjectPermissionSub.SecretApproval);
can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretRotation);
can(ProjectPermissionActions.Create, ProjectPermissionSub.SecretRotation);
can(ProjectPermissionActions.Edit, ProjectPermissionSub.SecretRotation);
can(ProjectPermissionActions.Delete, ProjectPermissionSub.SecretRotation);
can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback);
can(ProjectPermissionActions.Create, ProjectPermissionSub.SecretRollback);
can(ProjectPermissionActions.Read, ProjectPermissionSub.Member);
can(ProjectPermissionActions.Create, ProjectPermissionSub.Member);
can(ProjectPermissionActions.Edit, ProjectPermissionSub.Member);
can(ProjectPermissionActions.Delete, ProjectPermissionSub.Member);
can(ProjectPermissionActions.Read, ProjectPermissionSub.Groups);
can(ProjectPermissionActions.Create, ProjectPermissionSub.Groups);
can(ProjectPermissionActions.Edit, ProjectPermissionSub.Groups);
can(ProjectPermissionActions.Delete, ProjectPermissionSub.Groups);
can(ProjectPermissionActions.Read, ProjectPermissionSub.Role);
can(ProjectPermissionActions.Create, ProjectPermissionSub.Role);
can(ProjectPermissionActions.Edit, ProjectPermissionSub.Role);
can(ProjectPermissionActions.Delete, ProjectPermissionSub.Role);
can(ProjectPermissionActions.Read, ProjectPermissionSub.Integrations);
can(ProjectPermissionActions.Create, ProjectPermissionSub.Integrations);
can(ProjectPermissionActions.Edit, ProjectPermissionSub.Integrations);
can(ProjectPermissionActions.Delete, ProjectPermissionSub.Integrations);
can(ProjectPermissionActions.Read, ProjectPermissionSub.Webhooks);
can(ProjectPermissionActions.Create, ProjectPermissionSub.Webhooks);
can(ProjectPermissionActions.Edit, ProjectPermissionSub.Webhooks);
can(ProjectPermissionActions.Delete, ProjectPermissionSub.Webhooks);
can(ProjectPermissionActions.Read, ProjectPermissionSub.Identity);
can(ProjectPermissionActions.Create, ProjectPermissionSub.Identity);
can(ProjectPermissionActions.Edit, ProjectPermissionSub.Identity);
can(ProjectPermissionActions.Delete, ProjectPermissionSub.Identity);
can(ProjectPermissionActions.Read, ProjectPermissionSub.ServiceTokens);
can(ProjectPermissionActions.Create, ProjectPermissionSub.ServiceTokens);
can(ProjectPermissionActions.Edit, ProjectPermissionSub.ServiceTokens);
can(ProjectPermissionActions.Delete, ProjectPermissionSub.ServiceTokens);
can(ProjectPermissionActions.Read, ProjectPermissionSub.Settings);
can(ProjectPermissionActions.Create, ProjectPermissionSub.Settings);
can(ProjectPermissionActions.Edit, ProjectPermissionSub.Settings);
can(ProjectPermissionActions.Delete, ProjectPermissionSub.Settings);
can(ProjectPermissionActions.Read, ProjectPermissionSub.Environments);
can(ProjectPermissionActions.Create, ProjectPermissionSub.Environments);
can(ProjectPermissionActions.Edit, ProjectPermissionSub.Environments);
can(ProjectPermissionActions.Delete, ProjectPermissionSub.Environments);
can(ProjectPermissionActions.Read, ProjectPermissionSub.Tags);
can(ProjectPermissionActions.Create, ProjectPermissionSub.Tags);
can(ProjectPermissionActions.Edit, ProjectPermissionSub.Tags);
can(ProjectPermissionActions.Delete, ProjectPermissionSub.Tags);
can(ProjectPermissionActions.Read, ProjectPermissionSub.AuditLogs);
can(ProjectPermissionActions.Create, ProjectPermissionSub.AuditLogs);
can(ProjectPermissionActions.Edit, ProjectPermissionSub.AuditLogs);
can(ProjectPermissionActions.Delete, ProjectPermissionSub.AuditLogs);
can(ProjectPermissionActions.Read, ProjectPermissionSub.IpAllowList);
can(ProjectPermissionActions.Create, ProjectPermissionSub.IpAllowList);
can(ProjectPermissionActions.Edit, ProjectPermissionSub.IpAllowList);
can(ProjectPermissionActions.Delete, ProjectPermissionSub.IpAllowList);
// double check if all CRUD are needed for CA and Certificates
can(ProjectPermissionActions.Read, ProjectPermissionSub.CertificateAuthorities);
can(ProjectPermissionActions.Create, ProjectPermissionSub.CertificateAuthorities);
can(ProjectPermissionActions.Edit, ProjectPermissionSub.CertificateAuthorities);
can(ProjectPermissionActions.Delete, ProjectPermissionSub.CertificateAuthorities);
can(ProjectPermissionActions.Read, ProjectPermissionSub.Certificates);
can(ProjectPermissionActions.Create, ProjectPermissionSub.Certificates);
can(ProjectPermissionActions.Edit, ProjectPermissionSub.Certificates);
can(ProjectPermissionActions.Delete, ProjectPermissionSub.Certificates);
can(ProjectPermissionActions.Edit, ProjectPermissionSub.Project);
can(ProjectPermissionActions.Delete, ProjectPermissionSub.Project);
can(ProjectPermissionActions.Edit, ProjectPermissionSub.Kms);
// Admins get full access to everything
fullProjectPermissionSet.forEach((permission) => {
const [action, subject] = permission;
can(action, subject);
});
return rules;
};
@ -237,6 +267,11 @@ const buildMemberPermissionRules = () => {
can(ProjectPermissionActions.Edit, ProjectPermissionSub.Certificates);
can(ProjectPermissionActions.Delete, ProjectPermissionSub.Certificates);
can(ProjectPermissionActions.Read, ProjectPermissionSub.CertificateTemplates);
can(ProjectPermissionActions.Read, ProjectPermissionSub.PkiAlerts);
can(ProjectPermissionActions.Read, ProjectPermissionSub.PkiCollections);
return rules;
};
@ -346,4 +381,31 @@ export const isAtLeastAsPrivilegedWorkspace = (
return set1.size >= set2.size;
};
/*
* Case: The user requests to create a role with permissions that are not valid and not supposed to be used ever.
* If we don't check for this, we can run into issues where functions like the `isAtLeastAsPrivileged` will not work as expected, because we compare the size of each permission set.
* If the permission set contains invalid permissions, the size will be different, and result in incorrect results.
*/
export const validateProjectPermissions = (permissions: unknown) => {
const parsedPermissions =
typeof permissions === "string" ? (JSON.parse(permissions) as string[]) : (permissions as string[]);
const flattenedPermissions = [...parsedPermissions];
for (const perm of flattenedPermissions) {
const [action, subject] = perm;
if (
!fullProjectPermissionSet.find(
(currentPermission) => currentPermission[0] === action && currentPermission[1] === subject
)
) {
throw new BadRequestError({
message: `Permission action ${action} on subject ${subject} is not valid`,
name: "Create Role"
});
}
}
};
/* eslint-enable */

@ -18,7 +18,7 @@ import {
type TProjectUserAdditionalPrivilegeServiceFactoryDep = {
projectUserAdditionalPrivilegeDAL: TProjectUserAdditionalPrivilegeDALFactory;
projectMembershipDAL: Pick<TProjectMembershipDALFactory, "findById">;
projectMembershipDAL: Pick<TProjectMembershipDALFactory, "findById" | "findOne">;
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
};
@ -53,12 +53,17 @@ export const projectUserAdditionalPrivilegeServiceFactory = ({
);
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Member);
const existingSlug = await projectUserAdditionalPrivilegeDAL.findOne({ slug, projectMembershipId });
const existingSlug = await projectUserAdditionalPrivilegeDAL.findOne({
slug,
projectId: projectMembership.projectId,
userId: projectMembership.userId
});
if (existingSlug) throw new BadRequestError({ message: "Additional privilege of provided slug exist" });
if (!dto.isTemporary) {
const additionalPrivilege = await projectUserAdditionalPrivilegeDAL.create({
projectMembershipId,
userId: projectMembership.userId,
projectId: projectMembership.projectId,
slug,
permissions: customPermission
});
@ -67,7 +72,8 @@ export const projectUserAdditionalPrivilegeServiceFactory = ({
const relativeTempAllocatedTimeInMs = ms(dto.temporaryRange);
const additionalPrivilege = await projectUserAdditionalPrivilegeDAL.create({
projectMembershipId,
projectId: projectMembership.projectId,
userId: projectMembership.userId,
slug,
permissions: customPermission,
isTemporary: true,
@ -90,7 +96,11 @@ export const projectUserAdditionalPrivilegeServiceFactory = ({
const userPrivilege = await projectUserAdditionalPrivilegeDAL.findById(privilegeId);
if (!userPrivilege) throw new BadRequestError({ message: "User additional privilege not found" });
const projectMembership = await projectMembershipDAL.findById(userPrivilege.projectMembershipId);
const projectMembership = await projectMembershipDAL.findOne({
userId: userPrivilege.userId,
projectId: userPrivilege.projectId
});
if (!projectMembership) throw new BadRequestError({ message: "Project membership not found" });
const { permission } = await permissionService.getProjectPermission(
@ -105,7 +115,8 @@ export const projectUserAdditionalPrivilegeServiceFactory = ({
if (dto?.slug) {
const existingSlug = await projectUserAdditionalPrivilegeDAL.findOne({
slug: dto.slug,
projectMembershipId: projectMembership.id
userId: projectMembership.id,
projectId: projectMembership.projectId
});
if (existingSlug && existingSlug.id !== userPrivilege.id)
throw new BadRequestError({ message: "Additional privilege of provided slug exist" });
@ -138,7 +149,10 @@ export const projectUserAdditionalPrivilegeServiceFactory = ({
const userPrivilege = await projectUserAdditionalPrivilegeDAL.findById(privilegeId);
if (!userPrivilege) throw new BadRequestError({ message: "User additional privilege not found" });
const projectMembership = await projectMembershipDAL.findById(userPrivilege.projectMembershipId);
const projectMembership = await projectMembershipDAL.findOne({
userId: userPrivilege.userId,
projectId: userPrivilege.projectId
});
if (!projectMembership) throw new BadRequestError({ message: "Project membership not found" });
const { permission } = await permissionService.getProjectPermission(
@ -164,7 +178,10 @@ export const projectUserAdditionalPrivilegeServiceFactory = ({
const userPrivilege = await projectUserAdditionalPrivilegeDAL.findById(privilegeId);
if (!userPrivilege) throw new BadRequestError({ message: "User additional privilege not found" });
const projectMembership = await projectMembershipDAL.findById(userPrivilege.projectMembershipId);
const projectMembership = await projectMembershipDAL.findOne({
userId: userPrivilege.userId,
projectId: userPrivilege.projectId
});
if (!projectMembership) throw new BadRequestError({ message: "Project membership not found" });
const { permission } = await permissionService.getProjectPermission(
@ -198,7 +215,10 @@ export const projectUserAdditionalPrivilegeServiceFactory = ({
);
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Member);
const userPrivileges = await projectUserAdditionalPrivilegeDAL.find({ projectMembershipId });
const userPrivileges = await projectUserAdditionalPrivilegeDAL.find({
userId: projectMembership.userId,
projectId: projectMembership.projectId
});
return userPrivileges;
};

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

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

@ -44,19 +44,18 @@ export const buildScimUser = ({
email,
firstName,
lastName,
groups = [],
active
active,
createdAt,
updatedAt
}: {
orgMembershipId: string;
username: string;
email?: string | null;
firstName: string;
lastName: string;
groups?: {
value: string;
display: string;
}[];
firstName: string | null | undefined;
lastName: string | null | undefined;
active: boolean;
createdAt: Date;
updatedAt: Date;
}): TScimUser => {
const scimUser = {
schemas: ["urn:ietf:params:scim:schemas:core:2.0:User"],
@ -64,9 +63,9 @@ export const buildScimUser = ({
userName: username,
displayName: `${firstName} ${lastName}`,
name: {
givenName: firstName,
givenName: firstName || "",
middleName: null,
familyName: lastName
familyName: lastName || ""
},
emails: email
? [
@ -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
}
};

@ -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";
@ -31,14 +31,8 @@ import { UserAliasType } from "@app/services/user-alias/user-alias-types";
import { TLicenseServiceFactory } from "../license/license-service";
import { OrgPermissionActions, OrgPermissionSubjects } from "../permission/org-permission";
import { TPermissionServiceFactory } from "../permission/permission-service";
import {
buildScimGroup,
buildScimGroupList,
buildScimUser,
buildScimUserList,
extractScimValueFromPath,
parseScimFilter
} from "./scim-fns";
import { TProjectUserAdditionalPrivilegeDALFactory } from "../project-user-additional-privilege/project-user-additional-privilege-dal";
import { buildScimGroup, buildScimGroupList, buildScimUser, buildScimUserList, parseScimFilter } from "./scim-fns";
import {
TCreateScimGroupDTO,
TCreateScimTokenDTO,
@ -63,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">;
@ -93,6 +93,7 @@ type TScimServiceFactoryDep = {
licenseService: Pick<TLicenseServiceFactory, "getPlan" | "updateSubscriptionOrgMemberCount">;
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">;
smtpService: Pick<TSmtpService, "sendMail">;
projectUserAdditionalPrivilegeDAL: Pick<TProjectUserAdditionalPrivilegeDALFactory, "delete">;
};
export type TScimServiceFactory = ReturnType<typeof scimServiceFactory>;
@ -112,6 +113,7 @@ export const scimServiceFactory = ({
projectKeyDAL,
projectBotDAL,
permissionService,
projectUserAdditionalPrivilegeDAL,
smtpService
}: TScimServiceFactoryDep) => {
const createScimToken = async ({
@ -190,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)
@ -204,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({
@ -255,22 +259,15 @@ export const scimServiceFactory = ({
status: 403
});
const groupMembershipsInOrg = await userGroupMembershipDAL.findGroupMembershipsByUserIdInOrg(
membership.userId,
orgId
);
return buildScimUser({
orgMembershipId: membership.id,
username: membership.externalId ?? membership.username,
email: membership.email ?? "",
firstName: membership.firstName as string,
lastName: membership.lastName as string,
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
});
};
@ -319,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
},
@ -346,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,
@ -424,13 +425,16 @@ export const scimServiceFactory = ({
return buildScimUser({
orgMembershipId: createdOrgMembership.id,
username: externalId,
firstName: createdUser.firstName as string,
lastName: createdUser.lastName as string,
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({
@ -456,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 as string,
lastName: membership.lastName as string,
active
lastName: membership.lastName,
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,
@ -511,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 as string,
lastName: membership.lastName as string,
firstName: membership.firstName,
lastName: membership.lastName,
active,
groups: groupMembershipsInOrg.map((group) => ({
value: group.groupId,
display: group.groupName
}))
createdAt: membership.createdAt,
updatedAt: membership.updatedAt
});
};
@ -558,6 +598,7 @@ export const scimServiceFactory = ({
orgId: membership.orgId,
orgDAL,
projectMembershipDAL,
projectUserAdditionalPrivilegeDAL,
projectKeyDAL,
userAliasDAL,
licenseService
@ -566,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({
@ -599,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);
@ -608,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);
}
@ -692,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
});
};
@ -735,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(
{
@ -778,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({
@ -867,7 +935,7 @@ export const scimServiceFactory = ({
status: 403
});
let group = await groupDAL.findOne({
const group = await groupDAL.findOne({
id: groupId,
orgId
});
@ -879,73 +947,28 @@ export const scimServiceFactory = ({
});
}
for await (const operation of operations) {
switch (operation.op) {
case "replace": {
group = await groupDAL.updateById(group.id, {
name: operation.value.displayName
});
break;
}
case "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");
}
break;
}
case "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
});
break;
}
default: {
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) => {
@ -1021,8 +1044,8 @@ export const scimServiceFactory = ({
createScimGroup,
getScimGroup,
deleteScimGroup,
updateScimGroupNamePut,
updateScimGroupNamePatch,
replaceScimGroup,
updateScimGroup,
fnValidateScimToken
};
};

@ -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,29 +106,7 @@ export type TUpdateScimGroupNamePutDTO = {
export type TUpdateScimGroupNamePatchDTO = {
groupId: string;
orgId: string;
operations: (TRemoveOp | TReplaceOp | TAddOp)[];
};
type TReplaceOp = {
op: "replace";
value: {
id: string;
displayName: string;
};
};
type TRemoveOp = {
op: "remove";
path: string;
};
type TAddOp = {
op: "add";
path: string;
value: {
value: string;
display?: string;
}[];
operations: ScimPatchOperation[];
};
export type TDeleteScimGroupDTO = {
@ -158,13 +135,10 @@ export type TScimUser = {
type: string;
}[];
active: boolean;
groups: {
value: string;
display: string;
}[];
meta: {
resourceType: string;
location: null;
created: Date;
lastModified: Date;
};
};
@ -174,10 +148,11 @@ export type TScimGroup = {
displayName: string;
members: {
value: string;
display: string;
display?: string;
}[];
meta: {
resourceType: string;
location: null;
created: Date;
lastModified: Date;
};
};

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