Compare commits

..

270 Commits

Author SHA1 Message Date
Sheen Capadngan
e28471a9f4 misc: add onUpdate trigger to oidc config 2024-06-27 19:55:01 +08:00
Maidul Islam
7cdc47cd3a Update build-staging-and-deploy-aws.yml 2024-06-26 18:55:13 -04:00
Maidul Islam
d666d60f9f Merge pull request #2030 from Infisical/doc/added-guide-for-configuring-certs
doc: added guide for configuring certs
2024-06-26 15:30:15 -04:00
Maidul Islam
491c4259ca small rephrase for gitlab cert docs 2024-06-26 15:29:44 -04:00
Sheen Capadngan
cff20eb621 doc: added guide for configuring certs 2024-06-27 03:00:15 +08:00
Sheen Capadngan
84d8879177 Merge pull request #2029 from Infisical/create-pull-request/patch-1719422383
GH Action: rename new migration file timestamp
2024-06-27 01:20:12 +08:00
Sheen Capadngan
aa4f2adbb6 Merge pull request #2028 from Infisical/create-pull-request/patch-1719422278
GH Action: rename new migration file timestamp
2024-06-27 01:19:50 +08:00
github-actions
86ed3ef6d6 chore: renamed new migration files to latest timestamp (gh-action) 2024-06-26 17:19:42 +00:00
Sheen Capadngan
a5bb80adc4 Merge pull request #2020 from Infisical/feat/allow-audit-log-retention-to-be-configurable
feat: enabled customization of project audit logs retention period
2024-06-27 01:19:20 +08:00
github-actions
0e87dd3996 chore: renamed new migration files to latest timestamp (gh-action) 2024-06-26 17:17:57 +00:00
Sheen Capadngan
e1801e9eb4 Merge pull request #2025 from Infisical/feat/added-support-for-configurable-ldap-user-identifier
misc: add support for configuring unique attribute property for ldap users
2024-06-27 01:16:51 +08:00
Sheen Capadngan
f4a33caba6 misc: upgraded doc description of new field 2024-06-27 00:38:17 +08:00
Sheen Capadngan
e0a6f09b5e misc: removed max in schema for api layer 2024-06-26 23:17:31 +08:00
Sheen Capadngan
1e701687ae misc: adjusted other hook usage to incorporate new properties 2024-06-26 19:04:11 +08:00
Sheen Capadngan
15758b91f8 doc: updated ldap documentation 2024-06-26 18:54:34 +08:00
Sheen Capadngan
2d3a4a7559 feat: added support for configurable ldap user identifier 2024-06-26 18:36:28 +08:00
Sheen Capadngan
a1d01d5cbd misc: display retention settings only for self-hosted/dedicated 2024-06-26 12:44:56 +08:00
Sheen Capadngan
2e3aedc62b Merge pull request #2018 from Infisical/feat/add-initial-sync-behavior-azure
feat: added initial sync behavior for azure key integration
2024-06-26 11:43:54 +08:00
Maidul Islam
e0a5b1444a add step to install docker 2024-06-25 18:17:58 -04:00
Maidul Islam
b50833bded Merge pull request #2023 from Infisical/revert-1995-identity-based-pricing
Revert "Add support for Identity-Based Pricing"
2024-06-25 18:04:14 -04:00
Maidul Islam
e0c774c045 Revert "Add support for Identity-Based Pricing" 2024-06-25 18:03:07 -04:00
BlackMagiq
514df55d67 Merge pull request #1995 from Infisical/identity-based-pricing
Add support for Identity-Based Pricing
2024-06-25 14:50:18 -07:00
Daniel Hougaard
311b378f3b Merge pull request #1383 from Grraahaam/feat/cli-secret-plain-output
feat(cli): plain secret value output
2024-06-25 23:47:10 +02:00
Tuan Dang
b01b4323ca Fix merge conflicts 2024-06-25 14:46:58 -07:00
Maidul Islam
285a01af51 Merge pull request #2010 from Infisical/misc/add-documentation-for-bitbucket-cli-integration
doc: added bitbucket integration with cli
2024-06-25 16:33:07 -04:00
Maidul Islam
f7e658e62b rename bit bucket options 2024-06-25 16:31:56 -04:00
Maidul Islam
a8aef2934a Merge pull request #2021 from Infisical/feat/add-option-to-share-secrets-directly
feat: added option to share secret directly from main page
2024-06-25 15:55:04 -04:00
Maidul Islam
cc30476f79 Merge pull request #2022 from Infisical/misc/add-prompt-for-deleting-aws-sm-integration
misc: added proper prompt for aws secret manager integration deletion
2024-06-25 15:47:55 -04:00
Sheen Capadngan
5139bf2385 misc: added delete prompt for aws secret manager integ deletion 2024-06-26 01:21:14 +08:00
Akhil Mohan
a016d0d33f Merge pull request #1999 from akhilmhdh/feat/ui-permission-check-broken
Terraform identity management apis
2024-06-25 22:46:53 +05:30
Sheen Capadngan
663be06d30 feat: added share secret to main page side nav 2024-06-26 00:22:47 +08:00
Sheen Capadngan
fa392382da feat: added option to share secret directly from main page 2024-06-25 23:41:17 +08:00
Sheen Capadngan
d34b2669c5 misc: finalized success message 2024-06-25 21:32:24 +08:00
Sheen Capadngan
11ea5990c9 feat: enabled customization of project audit logs retention period 2024-06-25 21:14:43 +08:00
Sheen Capadngan
9a66514178 Merge pull request #2007 from Infisical/feat/project-setting-for-rebuilding-index
feat: added project setting for rebuilding secret indices
2024-06-25 15:25:36 +08:00
Sheen Capadngan
d4f9faf24d feat: added initial sync behavior for azure key integration 2024-06-25 13:59:20 +08:00
Vlad Matsiiako
a3c8d06845 Update overview.mdx 2024-06-24 20:25:53 -07:00
Vlad Matsiiako
71b7be4057 Merge pull request #2017 from handotdev/patch-2
Update overview.mdx
2024-06-24 20:25:02 -07:00
Han Wang
5079a5889a Update overview.mdx 2024-06-24 17:37:35 -07:00
BlackMagiq
232b375f46 Merge pull request #2015 from Infisical/create-pull-request/patch-1719267521
GH Action: rename new migration file timestamp
2024-06-24 17:07:41 -07:00
github-actions
d2acedf79e chore: renamed new migration files to latest timestamp (gh-action) 2024-06-24 22:18:39 +00:00
BlackMagiq
9d846319b0 Merge pull request #2014 from Infisical/cert-san
Add Certificate Support for Alt Names (SANs)
2024-06-24 15:18:17 -07:00
Maidul Islam
d69267a3ca remove console.log 2024-06-24 17:27:02 -04:00
Tuan Dang
051eee8701 Update altName example in docs 2024-06-24 14:14:51 -07:00
Tuan Dang
b5aa650899 Add cert support for alt names 2024-06-24 14:07:15 -07:00
Daniel Hougaard
376e185e2b Merge pull request #2006 from Infisical/daniel/expand-single-secret-ref
feat(api): Expand single secret references
2024-06-24 20:39:54 +02:00
Daniel Hougaard
a15a0a257c Update identity-router.ts 2024-06-24 20:38:11 +02:00
Maidul Islam
6facce220c update select default org login 2024-06-24 14:06:31 -04:00
Maidul Islam
620a423cee update org selection error message 2024-06-24 13:43:56 -04:00
Maidul Islam
361496c644 Merge pull request #2012 from Infisical/create-pull-request/patch-1719249628
GH Action: rename new migration file timestamp
2024-06-24 13:20:49 -04:00
github-actions
e03f77d9cf chore: renamed new migration files to latest timestamp (gh-action) 2024-06-24 17:20:27 +00:00
Maidul Islam
60cb420242 Merge pull request #2000 from Infisical/daniel/default-org
Feat: Default organization slug for LDAP/SAML
2024-06-24 13:20:02 -04:00
Maidul Islam
1b8a77f507 Merge pull request #2002 from Infisical/patch-ldap
Patch LDAP undefined userId, email confirmation code sending
2024-06-24 13:19:48 -04:00
Daniel Hougaard
5a957514df Feat: Clear select option 2024-06-24 19:12:38 +02:00
Daniel Hougaard
a6865585f3 Fix: Failing to create admin config on first run 2024-06-24 19:11:58 +02:00
Daniel Hougaard
1aaca12781 Update super-admin-dal.ts 2024-06-24 19:11:58 +02:00
Daniel Hougaard
7ab5c02000 Requested changes 2024-06-24 19:11:58 +02:00
Daniel Hougaard
c735beea32 Fix: Requested changes 2024-06-24 19:11:58 +02:00
Daniel Hougaard
2d98560255 Updated "defaultOrgId" and "defaultOrgSlug" to "defaultAuthOrgId" and "defaultAuthOrgSlug" 2024-06-24 19:10:22 +02:00
Daniel Hougaard
91bdd7ea6a Fix: UI descriptions 2024-06-24 19:09:48 +02:00
Daniel Hougaard
b0f3476e4a Fix: Completely hide org slug input field when org slug is passed or default slug is provided 2024-06-24 19:09:03 +02:00
Daniel Hougaard
14751df9de Feat: Default SAML/LDAP organization slug 2024-06-24 19:09:03 +02:00
Daniel Hougaard
e1a4185f76 Hide org slug input when default slug is set 2024-06-24 19:08:19 +02:00
Daniel Hougaard
4905ad1f48 Feat: Default SAML/LDAP organization slug 2024-06-24 19:08:19 +02:00
Daniel Hougaard
56bc25025a Update Login.utils.tsx 2024-06-24 19:08:19 +02:00
Daniel Hougaard
45da563465 Convert navigate function to hook 2024-06-24 19:08:19 +02:00
Daniel Hougaard
1930d40be8 Feat: Default SAML/LDAP organization slug 2024-06-24 19:05:46 +02:00
Daniel Hougaard
30b8d59796 Feat: Default SAML/LDAP organization slug 2024-06-24 19:05:46 +02:00
Daniel Hougaard
aa6cca738e Update index.ts 2024-06-24 19:05:46 +02:00
Daniel Hougaard
04dee70a55 Type changes 2024-06-24 19:05:46 +02:00
Daniel Hougaard
dfb53dd333 Helper omit function 2024-06-24 19:05:20 +02:00
Daniel Hougaard
ab19e7df6d Feat: Default SAML/LDAP organization slug 2024-06-24 19:05:20 +02:00
Sheen Capadngan
f9a1accf84 Merge pull request #2011 from Infisical/create-pull-request/patch-1719245983
GH Action: rename new migration file timestamp
2024-06-25 00:24:08 +08:00
github-actions
ca86f3d2b6 chore: renamed new migration files to latest timestamp (gh-action) 2024-06-24 16:19:41 +00:00
Sheen Capadngan
de466b4b86 Merge pull request #1989 from Infisical/feature/oidc
feat: oidc
2024-06-25 00:19:15 +08:00
Sheen Capadngan
745f1c4e12 misc: only display when user is admin 2024-06-24 23:24:07 +08:00
Vlad Matsiiako
106dc261de Merge pull request #2008 from handotdev/patch-1
Update style.css in docs
2024-06-24 08:20:30 -07:00
Vlad Matsiiako
548a0aed2a Merge pull request #2009 from Infisical/daniel/sdk-docs-typo
fix(docs): duplicate faq item
2024-06-24 07:28:52 -07:00
Sheen Capadngan
6029eaa9df misc: modified step title 2024-06-24 20:17:31 +08:00
Sheen Capadngan
8703314c0c doc: added bitbucket integration with cli 2024-06-24 20:11:53 +08:00
Daniel Hougaard
b7b606ab9a Update overview.mdx 2024-06-24 14:01:24 +02:00
Han Wang
00617ea7e8 Update style.css 2024-06-24 00:56:39 -07:00
Vlad Matsiiako
6d9330e870 Update machine-identities.mdx 2024-06-23 14:27:11 -07:00
Vlad Matsiiako
d026a9b988 Update mint.json 2024-06-23 13:23:10 -07:00
Vlad Matsiiako
c2c693d295 Update mint.json 2024-06-23 13:22:30 -07:00
Vlad Matsiiako
c9c77f6c58 Update saml docs 2024-06-22 19:02:43 -07:00
Vlad Matsiiako
36a34b0f58 Update sdk docs 2024-06-22 18:38:37 -07:00
Vlad Matsiiako
45c153e592 Update terraform-cloud.mdx 2024-06-22 18:31:12 -07:00
Vlad Matsiiako
eeaabe44ec Update LDAP docs 2024-06-22 16:44:42 -07:00
=
084fc7c99e feat: resolved gcp auth revoke error 2024-06-23 01:25:27 +05:30
=
b6cc17d62a feat: updated var names and permission, rate limit changes based on comments 2024-06-23 01:21:26 +05:30
Sheen Capadngan
bd0d0bd333 feat: added project setting for rebuilding secret indices 2024-06-22 22:42:36 +08:00
Maidul Islam
4b37c0f1c4 Merge pull request #2004 from Infisical/daniel/show-imported-overwritten-values-overview
feat(platform): Show imported/overwritten values in secret overview
2024-06-21 17:13:02 -04:00
Daniel Hougaard
c426ba517a Feat: Expand single secret references 2024-06-21 23:12:38 +02:00
Sheen Capadngan
973403c7f9 Merge branch 'feature/oidc' of https://github.com/Infisical/infisical into feature/oidc 2024-06-21 22:23:23 +08:00
Sheen Capadngan
52fcf53d0e misc: moved authenticate to preValidation 2024-06-21 22:22:34 +08:00
Maidul Islam
cbef9ea514 Merge pull request #2003 from Infisical/sharing-ui-update
updated secret sharing design
2024-06-21 10:03:10 -04:00
Daniel Hougaard
d0f8394f50 Fix: Added explicit SecretType type 2024-06-21 15:23:50 +02:00
Daniel Hougaard
9c06cab99d Fix: Added explicit SecretType type 2024-06-21 15:23:31 +02:00
Daniel Hougaard
c43a18904d Feat: Show overwritten value in secret overview and allow for edits 2024-06-21 15:23:09 +02:00
Daniel Hougaard
dc0fe6920c Feat: Show overwritten value in secret overview and allow for edits 2024-06-21 15:22:48 +02:00
Daniel Hougaard
077cbc97d5 Update encryptSecrets.ts 2024-06-21 15:21:52 +02:00
Daniel Hougaard
f3da676b88 Update checkOverrides.ts 2024-06-21 15:21:44 +02:00
Daniel Hougaard
988c612048 Update DropZone.tsx 2024-06-21 15:20:48 +02:00
Daniel Hougaard
7cf7eb5acb Fix: Added explicit SecretType type 2024-06-21 15:20:37 +02:00
Daniel Hougaard
a2fd071b62 Update SecretOverviewPage.tsx 2024-06-21 14:31:45 +02:00
Daniel Hougaard
0d7a07dea3 Update SecretEditRow.tsx 2024-06-21 14:31:41 +02:00
Daniel Hougaard
f676b44335 Feat: Show imported values in overview 2024-06-21 14:31:36 +02:00
Daniel Hougaard
00d83f9136 Update SecretInput.tsx 2024-06-21 14:31:29 +02:00
Daniel Hougaard
eca6871cbc Feat: Show imported values in overview 2024-06-21 14:31:22 +02:00
Vladyslav Matsiiako
97cff783cf updated secret sharing design 2024-06-20 22:30:00 -07:00
Maidul Islam
3767ec9521 Update build-staging-and-deploy-aws.yml 2024-06-21 00:21:07 -04:00
Tuan Dang
91634fbe76 Patch LDAP 2024-06-20 17:49:09 -07:00
Tuan Dang
f31340cf53 Minor adjustments to oidc docs 2024-06-20 16:34:21 -07:00
Maidul Islam
908358b841 Update build-staging-and-deploy-aws.yml 2024-06-20 19:32:30 -04:00
Maidul Islam
b2a88a4384 Update build-staging-and-deploy-aws.yml 2024-06-20 16:01:55 -04:00
Maidul Islam
ab73e77499 Update build-staging-and-deploy-aws.yml 2024-06-20 15:54:06 -04:00
Maidul Islam
095a049661 Update build-staging-and-deploy-aws.yml 2024-06-20 15:48:54 -04:00
Maidul Islam
3a51155d23 Merge pull request #2001 from akhilmhdh/fix/handover-enc-v1
feat: resolved generate srp failing for user enc v1 users
2024-06-20 14:05:52 -04:00
=
c5f361a3e5 feat: updated fail message over srp key generation as per review 2024-06-20 22:30:45 +05:30
=
5ace8ed073 feat: resolved generate srp failing for user enc v1 users 2024-06-20 22:12:44 +05:30
Sheen Capadngan
193d6dad54 misc: removed read sso from org member 2024-06-21 00:39:58 +08:00
Sheen Capadngan
0f36fc46b3 docs: added docs for general oidc configuration 2024-06-21 00:37:37 +08:00
=
4072a40fe9 feat: completed all revoke for other identity auth 2024-06-20 21:18:45 +05:30
=
0dc132dda3 feat: added universal auth endpoints for revoke and client secret endpoint to fetch details 2024-06-20 21:18:45 +05:30
=
605ccb13e9 feat: added endpoints and docs for identity get by id and list operation 2024-06-20 21:18:45 +05:30
Sheen Capadngan
4a1a399fd8 docs: added documentation for auth0 oidc configuration 2024-06-20 22:56:53 +08:00
Sheen Capadngan
d19e2f64f0 misc: added oidc to user alias type 2024-06-20 21:14:47 +08:00
Sheen Capadngan
1e0f54d9a4 doc: added mentions of oidc 2024-06-20 21:14:09 +08:00
Sheen Capadngan
8d55c2802e misc: added redirect after user creation 2024-06-20 20:55:26 +08:00
Sheen Capadngan
e9639df8ce docs: added keycloak-oidc documentation 2024-06-20 20:25:57 +08:00
Sheen Capadngan
e0f5ecbe7b misc: added oidc to text label 2024-06-20 15:40:40 +08:00
Grraahaam
2160c66e20 doc(cli): improve --plain example + add --silent notice 2024-06-20 09:15:19 +02:00
Grraahaam
1c5c7c75c4 Merge remote-tracking branch 'origin/main' into feat/cli-secret-plain-output 2024-06-20 08:50:11 +02:00
Sheen Capadngan
3e230555fb misc: added oifc checks to signup 2024-06-20 13:59:50 +08:00
Maidul Islam
31e27ad1d7 update docs to include TELEMETRY_ENABLED 2024-06-19 21:53:26 -04:00
Tuan Dang
24c75c6325 Merge remote-tracking branch 'origin' into identity-based-pricing 2024-06-19 14:18:13 -07:00
Tuan Dang
0a22a2a9ef Readjustments 2024-06-19 14:16:32 -07:00
Tuan Dang
d0f1cad98c Add support for identity-based pricing 2024-06-19 13:59:47 -07:00
Daniel Hougaard
4962a63888 Merge pull request #1981 from Infisical/daniel/unify-cli-auth-methods
feat(cli): Unify CLI auth methods
2024-06-19 21:57:49 +02:00
Sheen Capadngan
ad92565783 misc: grammar update 2024-06-20 03:30:03 +08:00
Sheen Capadngan
6c98c96a15 misc: added comment for samesite lax 2024-06-20 03:29:20 +08:00
Sheen Capadngan
f0a70d8769 misc: added samesite lax 2024-06-20 03:19:10 +08:00
Daniel Hougaard
9e9de9f527 Merge pull request #1993 from Infisical/daniel/k8-managed-secret-recreation
feat(k8-operator): reconcile on managed secret deletion
2024-06-19 19:48:42 +02:00
Daniel Hougaard
6af4a06c02 Merge pull request #1983 from Infisical/daniel/login-all-auth-methods
feat(cli): Support for all authentication methods in `infisical login` command
2024-06-19 19:44:42 +02:00
Daniel Hougaard
fe6dc248b6 Update login.mdx 2024-06-19 19:40:27 +02:00
Sheen Capadngan
d64e2fa243 misc: added client id and secret focus toggle 2024-06-20 01:37:52 +08:00
Daniel Hougaard
7d380f9b43 fix: documentation improvements 2024-06-19 19:34:12 +02:00
Tuan Dang
76c8410081 Update mail attribute on ldap login check 2024-06-19 10:33:11 -07:00
Tuan Dang
afee158b95 Start adding identity based pricing logic 2024-06-19 10:31:58 -07:00
Daniel Hougaard
6df90fa825 Docs: Improve and update infisical login cmd docs 2024-06-19 19:24:11 +02:00
Daniel Hougaard
c042bafba3 Add flags 2024-06-19 19:24:11 +02:00
Daniel Hougaard
8067df821e Update login.go 2024-06-19 19:24:11 +02:00
Daniel Hougaard
1906896e56 Update constants.go 2024-06-19 19:24:11 +02:00
Daniel Hougaard
a8ccfd9c92 Feat: Login support for all auth methods 2024-06-19 19:24:11 +02:00
Daniel Hougaard
32609b95a0 Update constants.go 2024-06-19 19:24:11 +02:00
Daniel Hougaard
08d3436217 Update login.go 2024-06-19 19:24:11 +02:00
Daniel Hougaard
2ae45dc1cc Feat: Login support for all auth methods 2024-06-19 19:24:11 +02:00
Daniel Hougaard
44a898fb15 Update auth.go 2024-06-19 19:24:11 +02:00
Daniel Hougaard
4d194052b5 Feat: Login support for all auth methods 2024-06-19 19:24:11 +02:00
Daniel Hougaard
1d622bb121 Update agent.go 2024-06-19 19:24:11 +02:00
Sheen Capadngan
ecca6f4db5 Merge remote-tracking branch 'origin/main' into feature/oidc 2024-06-20 01:21:46 +08:00
Sheen Capadngan
b198f97930 misc: added oidc create validation in route 2024-06-20 00:28:14 +08:00
Sheen Capadngan
63a9e46936 misc: removed unnecessary zod assertions 2024-06-20 00:20:00 +08:00
Sheen Capadngan
7c067551a4 misc: added frontend validation for oidc form 2024-06-20 00:15:18 +08:00
Maidul Islam
5c149c6ac6 Merge pull request #1994 from Infisical/misc/added-special-handling-of-non-root-folders
misc: added special pruning of non-root folders
2024-06-19 12:14:11 -04:00
Maidul Islam
c19f8839ff Merge pull request #1944 from akhilmhdh/feat/srp-handover
Removing Master password for Oauth/SSO/LDAP users.
2024-06-19 12:09:42 -04:00
Sheen Capadngan
1193ddbed1 misc: added rate limit for oidc login endpoint 2024-06-19 23:02:49 +08:00
Vlad Matsiiako
c6c71a04e8 Update changelog 2024-06-19 07:53:00 -07:00
Sheen Capadngan
6457c34712 misc: addressed eslint issue regarding configurationType 2024-06-19 22:41:33 +08:00
Sheen Capadngan
6a83b58de4 misc: added support for dynamic discovery of OIDC configuration 2024-06-19 22:33:50 +08:00
Daniel Hougaard
d47c586a52 Helm 2024-06-19 15:26:06 +02:00
Maidul Islam
88156c8cd8 small k8s bug patch 2024-06-19 09:12:46 -04:00
Daniel Hougaard
27d5d90d02 Update infisicalsecret_controller.go 2024-06-19 15:00:51 +02:00
Sheen Capadngan
0100ddfb99 misc: addressed review comments 2024-06-19 19:35:02 +08:00
Sheen Capadngan
2bc6db1c47 misc: readded cookie nginx config for dev 2024-06-19 14:19:27 +08:00
Sheen Capadngan
92f2f16656 misc: added option for trusting OIDC emails by default 2024-06-19 13:46:17 +08:00
Sheen Capadngan
07ca1ed424 misc: added special pruning of non-root folders 2024-06-19 12:41:47 +08:00
Tuan Dang
18c5dd3cbd Update README, docs for pki 2024-06-18 13:19:02 -07:00
Daniel Hougaard
467e3aab56 Update infisicalsecret_controller.go 2024-06-18 20:50:14 +02:00
Daniel Hougaard
577b432861 feat(k8-operator): reconcile when managed secret is deleted 2024-06-18 20:45:17 +02:00
Daniel Hougaard
dda6b1d233 Update infisicalsecret_controller.go 2024-06-18 20:15:22 +02:00
Maidul Islam
e83f31249a Merge pull request #1978 from Infisical/daniel/cli-auth-methods
feat(agent): Authentication methods
2024-06-18 14:14:22 -04:00
Sheen Capadngan
18e69578f0 feat: added support for limiting email domains 2024-06-19 01:29:26 +08:00
Sheen Capadngan
0685a5ea8b Merge remote-tracking branch 'origin/main' into feature/oidc 2024-06-18 23:54:06 +08:00
Daniel Hougaard
3142d36ea1 Merge pull request #1988 from Infisical/daniel/ingrations-improvements
Fix: Silent integration errors
2024-06-18 15:14:23 +02:00
Sheen Capadngan
bdc7c018eb misc: added comment regarding session and redis usage 2024-06-18 20:36:09 +08:00
Daniel Hougaard
9506b60d02 Feat: Silent integration errors 2024-06-18 14:11:19 +02:00
Daniel Hougaard
ed25b82113 Update infisical-agent.mdx 2024-06-18 14:06:37 +02:00
Daniel Hougaard
83bd97fc70 Update infisical-agent.mdx 2024-06-18 14:04:42 +02:00
Daniel Hougaard
1d5115972b fix: agent docs improvements 2024-06-18 14:03:32 +02:00
Daniel Hougaard
d26521be0b Update helper.go 2024-06-18 14:03:16 +02:00
Daniel Hougaard
473f8137fd Improved flag descriptions 2024-06-18 13:33:41 +02:00
Sheen Capadngan
bcd65333c0 misc: added handling of inactive and undefined oidc config 2024-06-18 19:17:54 +08:00
Daniel Hougaard
719d0ea30f Optional response 2024-06-18 12:33:36 +02:00
Sheen Capadngan
371b96a13a misc: removed cookie path proxy for dev envs 2024-06-18 15:36:49 +08:00
Sheen Capadngan
c5c00b520c misc: added session regenerate for fresh state 2024-06-18 15:35:18 +08:00
Sheen Capadngan
8de4443be1 feat: added support for login via cli 2024-06-18 15:26:08 +08:00
Sheen Capadngan
96ad3b0264 misc: used redis for oic session managemen 2024-06-18 14:20:04 +08:00
Maidul Islam
aaef339e21 Revert "temp disable cors"
This reverts commit c8677ac548.
2024-06-17 20:33:09 -04:00
Maidul Islam
e3beeb68eb Merge pull request #1991 from Infisical/daniel/leave-project
Feat: Leave Project
2024-06-17 20:31:21 -04:00
Maidul Islam
d0c76ae4b4 Merge pull request #1979 from Infisical/analytics-update
remove k8s events from posthog
2024-06-17 20:14:28 -04:00
Daniel Hougaard
a5cf6f40c7 Update integration-sync-secret.ts 2024-06-17 22:44:35 +02:00
Daniel Hougaard
f121f8e828 Invalidate instead of hard reload 2024-06-17 22:35:35 +02:00
Daniel Hougaard
54c8da8ab6 Update DeleteProjectSection.tsx 2024-06-17 22:23:36 +02:00
Daniel Hougaard
6e0dfc72e4 Added leave project support 2024-06-17 22:09:31 +02:00
Daniel Hougaard
b226fdac9d Feat: Leave Project
This can be re-used for leaving organizations with minor tweaks
2024-06-17 22:09:14 +02:00
Daniel Hougaard
3c36d5dbd2 Create index.tsx 2024-06-17 22:08:40 +02:00
Daniel Hougaard
a5f895ad91 Update project-membership-types.ts 2024-06-17 22:08:30 +02:00
Daniel Hougaard
9f66b9bb4d Leave project service 2024-06-17 22:08:20 +02:00
Daniel Hougaard
80e55a9341 Leave project mutation 2024-06-17 22:08:08 +02:00
Daniel Hougaard
5142d6f3c1 Feat: Leave Project 2024-06-17 22:07:50 +02:00
Maidul Islam
c8677ac548 temp disable cors 2024-06-17 16:03:12 -04:00
Sheen Capadngan
df51d05c46 feat: integrated oidc with sso login 2024-06-18 02:10:08 +08:00
Sheen Capadngan
4f2f7b2f70 misc: moved oidc endpoints to /sso 2024-06-18 01:21:42 +08:00
Sheen Capadngan
d79ffbe37e misc: added license checks for oidc sso 2024-06-18 01:11:57 +08:00
Sheen Capadngan
2c237ee277 feat: moved oidc to ee directory 2024-06-18 00:53:56 +08:00
Sheen Capadngan
56cc248425 feature: finalized oidc core service methods 2024-06-17 23:28:27 +08:00
Sheen Capadngan
61fcb2b605 feat: finished oidc form functions 2024-06-17 22:37:23 +08:00
Akhil Mohan
992cc03eca Merge pull request #1987 from akhilmhdh/feat/ui-permission-check-broken
New API endpoints for Tag update, get by id and get by slug
2024-06-17 19:13:45 +05:30
=
f0e7c459e2 feat: switched back to prod openapi 2024-06-17 18:28:45 +05:30
Maidul Islam
29d0694a16 Merge pull request #1986 from Infisical/daniel/fix-null-name-fields
Fix: `null` name fields on signup
2024-06-17 08:49:34 -04:00
Sheen Capadngan
66e5edcfc0 feat: oidc poc 2024-06-17 20:32:47 +08:00
Daniel Hougaard
f13930bc6b Fix: Silent integration errors 2024-06-17 13:14:46 +02:00
Daniel Hougaard
0d5514834d Fix: Redundancies 2024-06-17 13:14:28 +02:00
=
b495156444 feat: added docs for new tag api operations 2024-06-17 15:16:48 +05:30
=
65a2b0116b feat: added update, get by id and get by slug as tag api methods 2024-06-17 15:13:11 +05:30
Daniel Hougaard
8ef2501407 Fix: null null firstName and lastName allowed during signup 2024-06-17 10:58:05 +02:00
Vlad Matsiiako
21c6160c84 Update overview.mdx 2024-06-16 21:33:47 -07:00
Vlad Matsiiako
8a2268956a Merge pull request #1984 from Infisical/daniel/go-sdk-docs-fixes
docs(sdks): Updated Go SDK docs
2024-06-16 07:35:25 -07:00
Daniel Hougaard
2675aa6969 Update agent.go 2024-06-16 07:45:47 +02:00
Daniel Hougaard
6bad13738f Fix: Abstraction of getting env variable or file content 2024-06-16 07:39:57 +02:00
Daniel Hougaard
dbae6968c9 Update constants.go 2024-06-16 07:39:33 +02:00
Daniel Hougaard
e019f3811b Helpers 2024-06-16 07:39:27 +02:00
Daniel Hougaard
24935f4e07 Remove redundant auth schema set (defaults to Bearer) 2024-06-15 08:59:00 +02:00
Daniel Hougaard
1835777832 Move validation 2024-06-15 08:56:55 +02:00
Daniel Hougaard
cb237831c7 Remove log 2024-06-15 08:56:48 +02:00
Daniel Hougaard
49d2ea6f2e Feat: Unify CLI auth methods 2024-06-15 08:48:14 +02:00
Daniel Hougaard
3b2a2d1a73 Feat: Unify CLI auth methods 2024-06-15 08:48:11 +02:00
Daniel Hougaard
f490fb6616 Update cli.go 2024-06-15 08:48:05 +02:00
Daniel Hougaard
c4f9a3b31e Feat: Unify CLI auth methods 2024-06-15 08:48:03 +02:00
Daniel Hougaard
afcf15df55 Set secrets raw support 2024-06-15 08:47:48 +02:00
Daniel Hougaard
bf8aee25fe Feat: Unify CLI auth methods 2024-06-15 08:45:25 +02:00
Daniel Hougaard
ebdfe31c17 Raw secrets operations models 2024-06-15 08:44:55 +02:00
Daniel Hougaard
e65ce932dd feat: create/update raw secrets 2024-06-15 08:44:42 +02:00
Daniel Hougaard
c119f506fd docs 2024-06-15 02:35:04 +02:00
Daniel Hougaard
93638baba7 Update agent.go 2024-06-15 02:34:21 +02:00
Vladyslav Matsiiako
bad97774c4 remove k8s events from posthog 2024-06-14 16:30:12 -07:00
Daniel Hougaard
68f5be2ff1 Fix: File-based credentials 2024-06-14 23:55:25 +02:00
Daniel Hougaard
0b54099789 Feat(agent): Multiple auth methods 2024-06-14 23:09:31 +02:00
Daniel Hougaard
9b2a2eda0c Feat(agent): Multiple auth methods 2024-06-14 23:09:21 +02:00
Daniel Hougaard
ccef9646c6 Update helper.go 2024-06-14 07:20:53 +02:00
Daniel Hougaard
458639e93d Create auth.go 2024-06-14 07:20:51 +02:00
Daniel Hougaard
35998e98cf Update token.go 2024-06-14 07:20:49 +02:00
Daniel Hougaard
e19b67f9a2 Feat: Auth methods (draft 1) 2024-06-14 07:20:42 +02:00
Daniel Hougaard
f41ec46a35 Fix: Properly rename function to CallMachineIdentityRefreshAccessToken 2024-06-14 07:20:17 +02:00
Daniel Hougaard
33aa9ea1a7 Install Go SDK & go mod tidy 2024-06-14 07:19:51 +02:00
=
21bd468307 feat: change field password to hashedPassword 2024-06-12 23:04:10 +05:30
=
e95109c446 feat: updated cli to include password on login 2024-06-12 22:43:59 +05:30
=
93d5180dfc feat: just forward for ldap users 2024-06-12 19:25:06 +05:30
=
a9bec84d27 feat: applied hashed password change in change-password route 2024-06-12 19:25:06 +05:30
Maidul Islam
e3f87382a3 rephrase comment and error message 2024-06-12 19:25:06 +05:30
=
736f067178 feat: srp handover for admin and minor bug fix in mfa 2024-06-12 19:25:06 +05:30
=
f3ea7b3dfd feat: updated ui for srp handover 2024-06-12 19:25:06 +05:30
=
777dfd5f58 feat: api changes for srp handover 2024-06-12 19:25:05 +05:30
Grraahaam
f9a9599659 doc(cli): improve --plain example 2024-05-24 17:29:44 +02:00
Grraahaam
637b0b955f fix(cli): add backward compatibility for --raw-value 2024-05-24 17:28:52 +02:00
Grraahaam
092665737f Merge remote-tracking branch 'origin/main' into feat/cli-secret-plain-output 2024-05-24 17:13:57 +02:00
Grraahaam
f83c2215a5 doc(cli): --plain 2024-02-08 17:44:55 +01:00
Grraahaam
0f41590d6a feat(cli): --plain, --expand, --include-imports in 'secrets get' subcommand 2024-02-08 17:32:31 +01:00
270 changed files with 8853 additions and 1632 deletions

View File

@@ -50,6 +50,13 @@ jobs:
environment:
name: Gamma
steps:
- uses: twingate/github-action@v1
with:
# The Twingate Service Key used to connect Twingate to the proper service
# Learn more about [Twingate Services](https://docs.twingate.com/docs/services)
#
# Required
service-key: ${{ secrets.TWINGATE_SERVICE_KEY }}
- name: Checkout code
uses: actions/checkout@v2
- name: Setup Node.js environment
@@ -74,21 +81,21 @@ jobs:
uses: pr-mpt/actions-commit-hash@v2
- name: Download task definition
run: |
aws ecs describe-task-definition --task-definition infisical-core-platform --query taskDefinition > task-definition.json
aws ecs describe-task-definition --task-definition infisical-core-gamma-stage --query taskDefinition > task-definition.json
- name: Render Amazon ECS task definition
id: render-web-container
uses: aws-actions/amazon-ecs-render-task-definition@v1
with:
task-definition: task-definition.json
container-name: infisical-core-platform
container-name: infisical-core
image: infisical/staging_infisical:${{ steps.commit.outputs.short }}
environment-variables: "LOG_LEVEL=info"
- name: Deploy to Amazon ECS service
uses: aws-actions/amazon-ecs-deploy-task-definition@v1
with:
task-definition: ${{ steps.render-web-container.outputs.task-definition }}
service: infisical-core-platform
cluster: infisical-core-platform
service: infisical-core-gamma-stage
cluster: infisical-gamma-stage
wait-for-service-stability: true
production-postgres-deployment:

View File

@@ -48,25 +48,26 @@
## Introduction
**[Infisical](https://infisical.com)** is the open source secret management platform that teams use to centralize their secrets like API keys, database credentials, and configurations.
**[Infisical](https://infisical.com)** is the open source secret management platform that teams use to centralize their application configuration and secrets like API keys and database credentials as well as manage their internal PKI.
We're on a mission to make secret management more accessible to everyone, not just security teams, and that means redesigning the entire developer experience from ground up.
We're on a mission to make security tooling more accessible to everyone, not just security teams, and that means redesigning the entire developer experience from ground up.
## Features
- **[User-friendly dashboard](https://infisical.com/docs/documentation/platform/project)** to manage secrets across projects and environments (e.g. development, production, etc.).
- **[Client SDKs](https://infisical.com/docs/sdks/overview)** to fetch secrets for your apps and infrastructure on demand.
- **[Infisical CLI](https://infisical.com/docs/cli/overview)** to fetch and inject secrets into any framework in local development and CI/CD.
- **[Infisical API](https://infisical.com/docs/api-reference/overview/introduction)** to perform CRUD operation on secrets, users, projects, and any other resource in Infisical.
- **[Native integrations](https://infisical.com/docs/integrations/overview)** with platforms like [GitHub](https://infisical.com/docs/integrations/cicd/githubactions), [Vercel](https://infisical.com/docs/integrations/cloud/vercel), [AWS](https://infisical.com/docs/integrations/cloud/aws-secret-manager), and tools like [Terraform](https://infisical.com/docs/integrations/frameworks/terraform), [Ansible](https://infisical.com/docs/integrations/platforms/ansible), and more.
- **[User-friendly dashboard](https://infisical.com/docs/documentation/platform/project)** to manage secrets across projects and environments (e.g. development, production, etc.).
- **[Client SDKs](https://infisical.com/docs/sdks/overview)** to fetch secrets for your apps and infrastructure on demand.
- **[Infisical CLI](https://infisical.com/docs/cli/overview)** to fetch and inject secrets into any framework in local development and CI/CD.
- **[Infisical API](https://infisical.com/docs/api-reference/overview/introduction)** to perform CRUD operation on secrets, users, projects, and any other resource in Infisical.
- **[Native integrations](https://infisical.com/docs/integrations/overview)** with platforms like [GitHub](https://infisical.com/docs/integrations/cicd/githubactions), [Vercel](https://infisical.com/docs/integrations/cloud/vercel), [AWS](https://infisical.com/docs/integrations/cloud/aws-secret-manager), and tools like [Terraform](https://infisical.com/docs/integrations/frameworks/terraform), [Ansible](https://infisical.com/docs/integrations/platforms/ansible), and more.
- **[Infisical Kubernetes operator](https://infisical.com/docs/documentation/getting-started/kubernetes)** to managed secrets in k8s, automatically reload deployments, and more.
- **[Infisical Agent](https://infisical.com/docs/infisical-agent/overview)** to inject secrets into your applications without modifying any code logic.
- **[Infisical Agent](https://infisical.com/docs/infisical-agent/overview)** to inject secrets into your applications without modifying any code logic.
- **[Self-hosting and on-prem](https://infisical.com/docs/self-hosting/overview)** to get complete control over your data.
- **[Secret versioning](https://infisical.com/docs/documentation/platform/secret-versioning)** and **[Point-in-Time Recovery](https://infisical.com/docs/documentation/platform/pit-recovery)** to version every secret and project state.
- **[Audit logs](https://infisical.com/docs/documentation/platform/audit-logs)** to record every action taken in a project.
- **[Role-based Access Controls](https://infisical.com/docs/documentation/platform/role-based-access-controls)** to create permission sets on any resource in Infisica and assign those to user or machine identities.
- **[Secret versioning](https://infisical.com/docs/documentation/platform/secret-versioning)** and **[Point-in-Time Recovery](https://infisical.com/docs/documentation/platform/pit-recovery)** to version every secret and project state.
- **[Audit logs](https://infisical.com/docs/documentation/platform/audit-logs)** to record every action taken in a project.
- **[Role-based Access Controls](https://infisical.com/docs/documentation/platform/role-based-access-controls)** to create permission sets on any resource in Infisica and assign those to user or machine identities.
- **[Simple on-premise deployments](https://infisical.com/docs/self-hosting/overview)** to AWS, Digital Ocean, and more.
- **[Secret Scanning and Leak Prevention](https://infisical.com/docs/cli/scanning-overview)** to prevent secrets from leaking to git.
- **[Internal PKI](https://infisical.com/docs/documentation/platform/pki/private-ca)** to create Private CA hierarchies and start issuing and managing X.509 digital certificates.
- **[Secret Scanning and Leak Prevention](https://infisical.com/docs/cli/scanning-overview)** to prevent secrets from leaking to git.
And much more.
@@ -74,9 +75,9 @@ And much more.
Check out the [Quickstart Guides](https://infisical.com/docs/getting-started/introduction)
| Use Infisical Cloud | Deploy Infisical on premise |
| ------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| The fastest and most reliable way to <br> get started with Infisical is signing up <br> for free to [Infisical Cloud](https://app.infisical.com/login). | <br> View all [deployment options](https://infisical.com/docs/self-hosting/overview) |
| Use Infisical Cloud | Deploy Infisical on premise |
| ------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------ |
| The fastest and most reliable way to <br> get started with Infisical is signing up <br> for free to [Infisical Cloud](https://app.infisical.com/login). | <br> View all [deployment options](https://infisical.com/docs/self-hosting/overview) |
### Run Infisical locally

View File

@@ -38,6 +38,7 @@
"bcrypt": "^5.1.1",
"bullmq": "^5.4.2",
"cassandra-driver": "^4.7.2",
"connect-redis": "^7.1.1",
"cron": "^3.1.7",
"dotenv": "^16.4.1",
"fastify": "^4.26.0",
@@ -57,6 +58,7 @@
"mysql2": "^3.9.8",
"nanoid": "^5.0.4",
"nodemailer": "^6.9.9",
"openid-client": "^5.6.5",
"ora": "^7.0.1",
"oracledb": "^6.4.0",
"passport-github": "^1.1.0",
@@ -6459,12 +6461,12 @@
}
},
"node_modules/braces": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz",
"integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==",
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
"integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
"dev": true,
"dependencies": {
"fill-range": "^7.0.1"
"fill-range": "^7.1.1"
},
"engines": {
"node": ">=8"
@@ -6790,6 +6792,17 @@
"integrity": "sha512-JsPKdmh8ZkmnHxDk55FZ1TqVLvEQTvoByJZRN9jzI0UjxK/QgAmsphz7PGtqgPieQZ/CQcHWXCR7ATDNhGe+YA==",
"dev": true
},
"node_modules/connect-redis": {
"version": "7.1.1",
"resolved": "https://registry.npmjs.org/connect-redis/-/connect-redis-7.1.1.tgz",
"integrity": "sha512-M+z7alnCJiuzKa8/1qAYdGUXHYfDnLolOGAUjOioB07pP39qxjG+X9ibsud7qUBc4jMV5Mcy3ugGv8eFcgamJQ==",
"engines": {
"node": ">=16"
},
"peerDependencies": {
"express-session": ">=1"
}
},
"node_modules/console-control-strings": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz",
@@ -7896,6 +7909,55 @@
"node": ">= 0.10.0"
}
},
"node_modules/express-session": {
"version": "1.18.0",
"resolved": "https://registry.npmjs.org/express-session/-/express-session-1.18.0.tgz",
"integrity": "sha512-m93QLWr0ju+rOwApSsyso838LQwgfs44QtOP/WBiwtAgPIo/SAh1a5c6nn2BR6mFNZehTpqKDESzP+fRHVbxwQ==",
"peer": true,
"dependencies": {
"cookie": "0.6.0",
"cookie-signature": "1.0.7",
"debug": "2.6.9",
"depd": "~2.0.0",
"on-headers": "~1.0.2",
"parseurl": "~1.3.3",
"safe-buffer": "5.2.1",
"uid-safe": "~2.1.5"
},
"engines": {
"node": ">= 0.8.0"
}
},
"node_modules/express-session/node_modules/cookie": {
"version": "0.6.0",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz",
"integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==",
"peer": true,
"engines": {
"node": ">= 0.6"
}
},
"node_modules/express-session/node_modules/cookie-signature": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz",
"integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==",
"peer": true
},
"node_modules/express-session/node_modules/debug": {
"version": "2.6.9",
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
"integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
"peer": true,
"dependencies": {
"ms": "2.0.0"
}
},
"node_modules/express-session/node_modules/ms": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
"peer": true
},
"node_modules/express/node_modules/cookie": {
"version": "0.6.0",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz",
@@ -8115,9 +8177,9 @@
}
},
"node_modules/fill-range": {
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
"integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==",
"version": "7.1.1",
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
"integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
"dev": true,
"dependencies": {
"to-regex-range": "^5.0.1"
@@ -9603,6 +9665,14 @@
"node": ">= 0.6.0"
}
},
"node_modules/jose": {
"version": "4.15.5",
"resolved": "https://registry.npmjs.org/jose/-/jose-4.15.5.tgz",
"integrity": "sha512-jc7BFxgKPKi94uOvEmzlSWFFe2+vASyXaKUpdQKatWAESU2MWjDfFf0fdfc83CDKcA5QecabZeNLyfhe3yKNkg==",
"funding": {
"url": "https://github.com/sponsors/panva"
}
},
"node_modules/joycon": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/joycon/-/joycon-3.1.1.tgz",
@@ -10728,6 +10798,14 @@
"node": ">=0.10.0"
}
},
"node_modules/object-hash": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/object-hash/-/object-hash-2.2.0.tgz",
"integrity": "sha512-gScRMn0bS5fH+IuwyIFgnh9zBdo4DV+6GhygmWM9HyNJSgS0hScp1f5vjtm7oIIOiT9trXrShAkLFSc2IqKNgw==",
"engines": {
"node": ">= 6"
}
},
"node_modules/object-inspect": {
"version": "1.13.1",
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz",
@@ -10851,6 +10929,14 @@
"@octokit/core": ">=5"
}
},
"node_modules/oidc-token-hash": {
"version": "5.0.3",
"resolved": "https://registry.npmjs.org/oidc-token-hash/-/oidc-token-hash-5.0.3.tgz",
"integrity": "sha512-IF4PcGgzAr6XXSff26Sk/+P4KZFJVuHAJZj3wgO3vX2bMdNVp/QXTP3P7CEm9V1IdG8lDLY3HhiqpsE/nOwpPw==",
"engines": {
"node": "^10.13.0 || >=12.0.0"
}
},
"node_modules/on-exit-leak-free": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/on-exit-leak-free/-/on-exit-leak-free-2.1.2.tgz",
@@ -10870,6 +10956,15 @@
"node": ">= 0.8"
}
},
"node_modules/on-headers": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz",
"integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==",
"peer": true,
"engines": {
"node": ">= 0.8"
}
},
"node_modules/once": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
@@ -10897,6 +10992,20 @@
"resolved": "https://registry.npmjs.org/openapi-types/-/openapi-types-12.1.3.tgz",
"integrity": "sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw=="
},
"node_modules/openid-client": {
"version": "5.6.5",
"resolved": "https://registry.npmjs.org/openid-client/-/openid-client-5.6.5.tgz",
"integrity": "sha512-5P4qO9nGJzB5PI0LFlhj4Dzg3m4odt0qsJTfyEtZyOlkgpILwEioOhVVJOrS1iVH494S4Ee5OCjjg6Bf5WOj3w==",
"dependencies": {
"jose": "^4.15.5",
"lru-cache": "^6.0.0",
"object-hash": "^2.2.0",
"oidc-token-hash": "^5.0.3"
},
"funding": {
"url": "https://github.com/sponsors/panva"
}
},
"node_modules/optionator": {
"version": "0.9.3",
"resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz",
@@ -11948,6 +12057,15 @@
"resolved": "https://registry.npmjs.org/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz",
"integrity": "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg=="
},
"node_modules/random-bytes": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/random-bytes/-/random-bytes-1.0.0.tgz",
"integrity": "sha512-iv7LhNVO047HzYR3InF6pUcUsPQiHTM1Qal51DcGSuZFBil1aBBWG5eHPNek7bvILMaYJ/8RU1e8w1AMdHmLQQ==",
"peer": true,
"engines": {
"node": ">= 0.8"
}
},
"node_modules/randombytes": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz",
@@ -14027,6 +14145,18 @@
"node": ">=0.8.0"
}
},
"node_modules/uid-safe": {
"version": "2.1.5",
"resolved": "https://registry.npmjs.org/uid-safe/-/uid-safe-2.1.5.tgz",
"integrity": "sha512-KPHm4VL5dDXKz01UuEd88Df+KzynaohSL9fBh096KWAxSKZQDI2uBrVqtvRM4rwrIrRRKsdLNML/lnaaVSRioA==",
"peer": true,
"dependencies": {
"random-bytes": "~1.0.0"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/uid2": {
"version": "0.0.4",
"resolved": "https://registry.npmjs.org/uid2/-/uid2-0.0.4.tgz",

View File

@@ -99,6 +99,7 @@
"bcrypt": "^5.1.1",
"bullmq": "^5.4.2",
"cassandra-driver": "^4.7.2",
"connect-redis": "^7.1.1",
"cron": "^3.1.7",
"dotenv": "^16.4.1",
"fastify": "^4.26.0",
@@ -118,6 +119,7 @@
"mysql2": "^3.9.8",
"nanoid": "^5.0.4",
"nodemailer": "^6.9.9",
"openid-client": "^5.6.5",
"ora": "^7.0.1",
"oracledb": "^6.4.0",
"passport-github": "^1.1.0",

View File

@@ -13,6 +13,7 @@ import { TGroupServiceFactory } from "@app/ee/services/group/group-service";
import { TIdentityProjectAdditionalPrivilegeServiceFactory } from "@app/ee/services/identity-project-additional-privilege/identity-project-additional-privilege-service";
import { TLdapConfigServiceFactory } from "@app/ee/services/ldap-config/ldap-config-service";
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
import { TOidcConfigServiceFactory } from "@app/ee/services/oidc/oidc-config-service";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { TProjectUserAdditionalPrivilegeServiceFactory } from "@app/ee/services/project-user-additional-privilege/project-user-additional-privilege-service";
import { TRateLimitServiceFactory } from "@app/ee/services/rate-limit/rate-limit-service";
@@ -102,6 +103,7 @@ declare module "fastify" {
permission: TPermissionServiceFactory;
org: TOrgServiceFactory;
orgRole: TOrgRoleServiceFactory;
oidc: TOidcConfigServiceFactory;
superAdmin: TSuperAdminServiceFactory;
user: TUserServiceFactory;
group: TGroupServiceFactory;

View File

@@ -134,6 +134,9 @@ import {
TLdapGroupMaps,
TLdapGroupMapsInsert,
TLdapGroupMapsUpdate,
TOidcConfigs,
TOidcConfigsInsert,
TOidcConfigsUpdate,
TOrganizations,
TOrganizationsInsert,
TOrganizationsUpdate,
@@ -549,6 +552,7 @@ declare module "knex/types/tables" {
TDynamicSecretLeasesUpdate
>;
[TableName.SamlConfig]: Knex.CompositeTableType<TSamlConfigs, TSamlConfigsInsert, TSamlConfigsUpdate>;
[TableName.OidcConfig]: Knex.CompositeTableType<TOidcConfigs, TOidcConfigsInsert, TOidcConfigsUpdate>;
[TableName.LdapConfig]: Knex.CompositeTableType<TLdapConfigs, TLdapConfigsInsert, TLdapConfigsUpdate>;
[TableName.LdapGroupMap]: Knex.CompositeTableType<TLdapGroupMaps, TLdapGroupMapsInsert, TLdapGroupMapsUpdate>;
[TableName.OrgBot]: Knex.CompositeTableType<TOrgBots, TOrgBotsInsert, TOrgBotsUpdate>;

View File

@@ -0,0 +1,61 @@
import { Knex } from "knex";
import { TableName } from "../schemas";
export async function up(knex: Knex): Promise<void> {
const doesPasswordFieldExist = await knex.schema.hasColumn(TableName.UserEncryptionKey, "hashedPassword");
const doesPrivateKeyFieldExist = await knex.schema.hasColumn(
TableName.UserEncryptionKey,
"serverEncryptedPrivateKey"
);
const doesPrivateKeyIVFieldExist = await knex.schema.hasColumn(
TableName.UserEncryptionKey,
"serverEncryptedPrivateKeyIV"
);
const doesPrivateKeyTagFieldExist = await knex.schema.hasColumn(
TableName.UserEncryptionKey,
"serverEncryptedPrivateKeyTag"
);
const doesPrivateKeyEncodingFieldExist = await knex.schema.hasColumn(
TableName.UserEncryptionKey,
"serverEncryptedPrivateKeyEncoding"
);
if (await knex.schema.hasTable(TableName.UserEncryptionKey)) {
await knex.schema.alterTable(TableName.UserEncryptionKey, (t) => {
if (!doesPasswordFieldExist) t.string("hashedPassword");
if (!doesPrivateKeyFieldExist) t.text("serverEncryptedPrivateKey");
if (!doesPrivateKeyIVFieldExist) t.text("serverEncryptedPrivateKeyIV");
if (!doesPrivateKeyTagFieldExist) t.text("serverEncryptedPrivateKeyTag");
if (!doesPrivateKeyEncodingFieldExist) t.text("serverEncryptedPrivateKeyEncoding");
});
}
}
export async function down(knex: Knex): Promise<void> {
const doesPasswordFieldExist = await knex.schema.hasColumn(TableName.UserEncryptionKey, "hashedPassword");
const doesPrivateKeyFieldExist = await knex.schema.hasColumn(
TableName.UserEncryptionKey,
"serverEncryptedPrivateKey"
);
const doesPrivateKeyIVFieldExist = await knex.schema.hasColumn(
TableName.UserEncryptionKey,
"serverEncryptedPrivateKeyIV"
);
const doesPrivateKeyTagFieldExist = await knex.schema.hasColumn(
TableName.UserEncryptionKey,
"serverEncryptedPrivateKeyTag"
);
const doesPrivateKeyEncodingFieldExist = await knex.schema.hasColumn(
TableName.UserEncryptionKey,
"serverEncryptedPrivateKeyEncoding"
);
if (await knex.schema.hasTable(TableName.UserEncryptionKey)) {
await knex.schema.alterTable(TableName.UserEncryptionKey, (t) => {
if (doesPasswordFieldExist) t.dropColumn("hashedPassword");
if (doesPrivateKeyFieldExist) t.dropColumn("serverEncryptedPrivateKey");
if (doesPrivateKeyIVFieldExist) t.dropColumn("serverEncryptedPrivateKeyIV");
if (doesPrivateKeyTagFieldExist) t.dropColumn("serverEncryptedPrivateKeyTag");
if (doesPrivateKeyEncodingFieldExist) t.dropColumn("serverEncryptedPrivateKeyEncoding");
});
}
}

View File

@@ -0,0 +1,49 @@
import { Knex } from "knex";
import { TableName } from "../schemas";
export async function up(knex: Knex): Promise<void> {
if (!(await knex.schema.hasTable(TableName.OidcConfig))) {
await knex.schema.createTable(TableName.OidcConfig, (tb) => {
tb.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
tb.string("discoveryURL");
tb.string("issuer");
tb.string("authorizationEndpoint");
tb.string("jwksUri");
tb.string("tokenEndpoint");
tb.string("userinfoEndpoint");
tb.text("encryptedClientId").notNullable();
tb.string("configurationType").notNullable();
tb.string("clientIdIV").notNullable();
tb.string("clientIdTag").notNullable();
tb.text("encryptedClientSecret").notNullable();
tb.string("clientSecretIV").notNullable();
tb.string("clientSecretTag").notNullable();
tb.string("allowedEmailDomains").nullable();
tb.boolean("isActive").notNullable();
tb.timestamps(true, true, true);
tb.uuid("orgId").notNullable().unique();
tb.foreign("orgId").references("id").inTable(TableName.Organization);
});
}
if (await knex.schema.hasTable(TableName.SuperAdmin)) {
if (!(await knex.schema.hasColumn(TableName.SuperAdmin, "trustOidcEmails"))) {
await knex.schema.alterTable(TableName.SuperAdmin, (tb) => {
tb.boolean("trustOidcEmails").defaultTo(false);
});
}
}
}
export async function down(knex: Knex): Promise<void> {
await knex.schema.dropTableIfExists(TableName.OidcConfig);
if (await knex.schema.hasTable(TableName.SuperAdmin)) {
if (await knex.schema.hasColumn(TableName.SuperAdmin, "trustOidcEmails")) {
await knex.schema.alterTable(TableName.SuperAdmin, (t) => {
t.dropColumn("trustOidcEmails");
});
}
}
}

View File

@@ -0,0 +1,27 @@
import { Knex } from "knex";
import { TableName } from "../schemas";
const DEFAULT_AUTH_ORG_ID_FIELD = "defaultAuthOrgId";
export async function up(knex: Knex): Promise<void> {
const hasDefaultOrgColumn = await knex.schema.hasColumn(TableName.SuperAdmin, DEFAULT_AUTH_ORG_ID_FIELD);
await knex.schema.alterTable(TableName.SuperAdmin, (t) => {
if (!hasDefaultOrgColumn) {
t.uuid(DEFAULT_AUTH_ORG_ID_FIELD).nullable();
t.foreign(DEFAULT_AUTH_ORG_ID_FIELD).references("id").inTable(TableName.Organization).onDelete("SET NULL");
}
});
}
export async function down(knex: Knex): Promise<void> {
const hasDefaultOrgColumn = await knex.schema.hasColumn(TableName.SuperAdmin, DEFAULT_AUTH_ORG_ID_FIELD);
await knex.schema.alterTable(TableName.SuperAdmin, (t) => {
if (hasDefaultOrgColumn) {
t.dropForeign([DEFAULT_AUTH_ORG_ID_FIELD]);
t.dropColumn(DEFAULT_AUTH_ORG_ID_FIELD);
}
});
}

View File

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

View File

@@ -0,0 +1,19 @@
import { Knex } from "knex";
import { TableName } from "../schemas";
export async function up(knex: Knex): Promise<void> {
if (!(await knex.schema.hasColumn(TableName.LdapConfig, "uniqueUserAttribute"))) {
await knex.schema.alterTable(TableName.LdapConfig, (tb) => {
tb.string("uniqueUserAttribute").notNullable().defaultTo("");
});
}
}
export async function down(knex: Knex): Promise<void> {
if (await knex.schema.hasColumn(TableName.LdapConfig, "uniqueUserAttribute")) {
await knex.schema.alterTable(TableName.LdapConfig, (t) => {
t.dropColumn("uniqueUserAttribute");
});
}
}

View File

@@ -0,0 +1,19 @@
import { Knex } from "knex";
import { TableName } from "../schemas";
export async function up(knex: Knex): Promise<void> {
if (!(await knex.schema.hasColumn(TableName.Project, "auditLogsRetentionDays"))) {
await knex.schema.alterTable(TableName.Project, (tb) => {
tb.integer("auditLogsRetentionDays").nullable();
});
}
}
export async function down(knex: Knex): Promise<void> {
if (await knex.schema.hasColumn(TableName.Project, "auditLogsRetentionDays")) {
await knex.schema.alterTable(TableName.Project, (t) => {
t.dropColumn("auditLogsRetentionDays");
});
}
}

View File

@@ -0,0 +1,12 @@
import { Knex } from "knex";
import { TableName } from "../schemas";
import { createOnUpdateTrigger, dropOnUpdateTrigger } from "../utils";
export async function up(knex: Knex): Promise<void> {
await createOnUpdateTrigger(knex, TableName.OidcConfig);
}
export async function down(knex: Knex): Promise<void> {
await dropOnUpdateTrigger(knex, TableName.OidcConfig);
}

View File

@@ -19,7 +19,8 @@ export const CertificatesSchema = z.object({
notBefore: z.date(),
notAfter: z.date(),
revokedAt: z.date().nullable().optional(),
revocationReason: z.number().nullable().optional()
revocationReason: z.number().nullable().optional(),
altNames: z.string().default("").nullable().optional()
});
export type TCertificates = z.infer<typeof CertificatesSchema>;

View File

@@ -43,6 +43,7 @@ export * from "./kms-root-config";
export * from "./ldap-configs";
export * from "./ldap-group-maps";
export * from "./models";
export * from "./oidc-configs";
export * from "./org-bots";
export * from "./org-memberships";
export * from "./org-roles";

View File

@@ -26,7 +26,8 @@ export const LdapConfigsSchema = z.object({
updatedAt: z.date(),
groupSearchBase: z.string().default(""),
groupSearchFilter: z.string().default(""),
searchFilter: z.string().default("")
searchFilter: z.string().default(""),
uniqueUserAttribute: z.string().default("")
});
export type TLdapConfigs = z.infer<typeof LdapConfigsSchema>;

View File

@@ -78,6 +78,7 @@ export enum TableName {
SecretRotationOutput = "secret_rotation_outputs",
SamlConfig = "saml_configs",
LdapConfig = "ldap_configs",
OidcConfig = "oidc_configs",
LdapGroupMap = "ldap_group_maps",
AuditLog = "audit_logs",
AuditLogStream = "audit_log_streams",

View File

@@ -0,0 +1,34 @@
// 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 OidcConfigsSchema = z.object({
id: z.string().uuid(),
discoveryURL: z.string().nullable().optional(),
issuer: z.string().nullable().optional(),
authorizationEndpoint: z.string().nullable().optional(),
jwksUri: z.string().nullable().optional(),
tokenEndpoint: z.string().nullable().optional(),
userinfoEndpoint: z.string().nullable().optional(),
encryptedClientId: z.string(),
configurationType: z.string(),
clientIdIV: z.string(),
clientIdTag: z.string(),
encryptedClientSecret: z.string(),
clientSecretIV: z.string(),
clientSecretTag: z.string(),
allowedEmailDomains: z.string().nullable().optional(),
isActive: z.boolean(),
createdAt: z.date(),
updatedAt: z.date(),
orgId: z.string().uuid()
});
export type TOidcConfigs = z.infer<typeof OidcConfigsSchema>;
export type TOidcConfigsInsert = Omit<z.input<typeof OidcConfigsSchema>, TImmutableDBKeys>;
export type TOidcConfigsUpdate = Partial<Omit<z.input<typeof OidcConfigsSchema>, TImmutableDBKeys>>;

View File

@@ -17,8 +17,9 @@ export const ProjectsSchema = z.object({
updatedAt: z.date(),
version: z.number().default(1),
upgradeStatus: z.string().nullable().optional(),
pitVersionLimit: z.number().default(10),
kmsCertificateKeyId: z.string().uuid().nullable().optional(),
pitVersionLimit: z.number().default(10)
auditLogsRetentionDays: z.number().nullable().optional()
});
export type TProjects = z.infer<typeof ProjectsSchema>;

View File

@@ -16,7 +16,9 @@ export const SuperAdminSchema = z.object({
allowedSignUpDomain: z.string().nullable().optional(),
instanceId: z.string().uuid().default("00000000-0000-0000-0000-000000000000"),
trustSamlEmails: z.boolean().default(false).nullable().optional(),
trustLdapEmails: z.boolean().default(false).nullable().optional()
trustLdapEmails: z.boolean().default(false).nullable().optional(),
trustOidcEmails: z.boolean().default(false).nullable().optional(),
defaultAuthOrgId: z.string().uuid().nullable().optional()
});
export type TSuperAdmin = z.infer<typeof SuperAdminSchema>;

View File

@@ -21,7 +21,12 @@ export const UserEncryptionKeysSchema = z.object({
tag: z.string(),
salt: z.string(),
verifier: z.string(),
userId: z.string().uuid()
userId: z.string().uuid(),
hashedPassword: z.string().nullable().optional(),
serverEncryptedPrivateKey: z.string().nullable().optional(),
serverEncryptedPrivateKeyIV: z.string().nullable().optional(),
serverEncryptedPrivateKeyTag: z.string().nullable().optional(),
serverEncryptedPrivateKeyEncoding: z.string().nullable().optional()
});
export type TUserEncryptionKeys = z.infer<typeof UserEncryptionKeysSchema>;

View File

@@ -8,6 +8,7 @@ import { registerGroupRouter } from "./group-router";
import { registerIdentityProjectAdditionalPrivilegeRouter } from "./identity-project-additional-privilege-router";
import { registerLdapRouter } from "./ldap-router";
import { registerLicenseRouter } from "./license-router";
import { registerOidcRouter } from "./oidc-router";
import { registerOrgRoleRouter } from "./org-role-router";
import { registerProjectRoleRouter } from "./project-role-router";
import { registerProjectRouter } from "./project-router";
@@ -64,7 +65,14 @@ export const registerV1EERoutes = async (server: FastifyZodProvider) => {
{ prefix: "/pki" }
);
await server.register(registerSamlRouter, { prefix: "/sso" });
await server.register(
async (ssoRouter) => {
await ssoRouter.register(registerSamlRouter);
await ssoRouter.register(registerOidcRouter, { prefix: "/oidc" });
},
{ prefix: "/sso" }
);
await server.register(registerScimRouter, { prefix: "/scim" });
await server.register(registerLdapRouter, { prefix: "/ldap" });
await server.register(registerSecretScanningRouter, { prefix: "/secret-scanning" });

View File

@@ -53,7 +53,7 @@ export const registerLdapRouter = async (server: FastifyZodProvider) => {
// eslint-disable-next-line
async (req: IncomingMessage, user, cb) => {
try {
if (!user.email) throw new BadRequestError({ message: "Invalid request. Missing email." });
if (!user.mail) throw new BadRequestError({ message: "Invalid request. Missing mail attribute on user." });
const ldapConfig = (req as unknown as FastifyRequest).ldapConfig as TLDAPConfig;
let groups: { dn: string; cn: string }[] | undefined;
@@ -70,10 +70,13 @@ export const registerLdapRouter = async (server: FastifyZodProvider) => {
groups = await searchGroups(ldapConfig, groupSearchFilter, ldapConfig.groupSearchBase);
}
const externalId = ldapConfig.uniqueUserAttribute ? user[ldapConfig.uniqueUserAttribute] : user.uidNumber;
const username = ldapConfig.uniqueUserAttribute ? externalId : user.uid;
const { isUserCompleted, providerAuthToken } = await server.services.ldap.ldapLogin({
externalId,
username,
ldapConfigId: ldapConfig.id,
externalId: user.uidNumber,
username: user.uid,
firstName: user.givenName ?? user.cn ?? "",
lastName: user.sn ?? "",
email: user.mail,
@@ -138,6 +141,7 @@ export const registerLdapRouter = async (server: FastifyZodProvider) => {
url: z.string(),
bindDN: z.string(),
bindPass: z.string(),
uniqueUserAttribute: z.string(),
searchBase: z.string(),
searchFilter: z.string(),
groupSearchBase: z.string(),
@@ -172,6 +176,7 @@ export const registerLdapRouter = async (server: FastifyZodProvider) => {
url: z.string().trim(),
bindDN: z.string().trim(),
bindPass: z.string().trim(),
uniqueUserAttribute: z.string().trim().default("uidNumber"),
searchBase: z.string().trim(),
searchFilter: z.string().trim().default("(uid={{username}})"),
groupSearchBase: z.string().trim(),
@@ -213,6 +218,7 @@ export const registerLdapRouter = async (server: FastifyZodProvider) => {
url: z.string().trim(),
bindDN: z.string().trim(),
bindPass: z.string().trim(),
uniqueUserAttribute: z.string().trim(),
searchBase: z.string().trim(),
searchFilter: z.string().trim(),
groupSearchBase: z.string().trim(),

View File

@@ -0,0 +1,355 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable @typescript-eslint/no-unsafe-return */
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
/* eslint-disable @typescript-eslint/no-unsafe-call */
/* eslint-disable @typescript-eslint/no-unsafe-argument */
// All the any rules are disabled because passport typesense with fastify is really poor
import { Authenticator, Strategy } from "@fastify/passport";
import fastifySession from "@fastify/session";
import RedisStore from "connect-redis";
import { Redis } from "ioredis";
import { z } from "zod";
import { OidcConfigsSchema } from "@app/db/schemas/oidc-configs";
import { OIDCConfigurationType } from "@app/ee/services/oidc/oidc-config-types";
import { getConfig } from "@app/lib/config/env";
import { authRateLimit, readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type";
export const registerOidcRouter = async (server: FastifyZodProvider) => {
const appCfg = getConfig();
const redis = new Redis(appCfg.REDIS_URL);
const passport = new Authenticator({ key: "oidc", userProperty: "passportUser" });
/*
- OIDC protocol cannot work without sessions: https://github.com/panva/node-openid-client/issues/190
- Current redis usage is not ideal and will eventually have to be refactored to use a better structure
- Fastify session <> Redis structure is based on the ff: https://github.com/fastify/session/blob/master/examples/redis.js
*/
const redisStore = new RedisStore({
client: redis,
prefix: "oidc-session:",
ttl: 600 // 10 minutes
});
await server.register(fastifySession, {
secret: appCfg.COOKIE_SECRET_SIGN_KEY,
store: redisStore,
cookie: {
secure: appCfg.HTTPS_ENABLED,
sameSite: "lax" // we want cookies to be sent to Infisical in redirects originating from IDP server
}
});
await server.register(passport.initialize());
await server.register(passport.secureSession());
// redirect to IDP for login
server.route({
url: "/login",
method: "GET",
config: {
rateLimit: authRateLimit
},
schema: {
querystring: z.object({
orgSlug: z.string().trim(),
callbackPort: z.string().trim().optional()
})
},
preValidation: [
async (req, res) => {
const { orgSlug, callbackPort } = req.query;
// ensure fresh session state per login attempt
await req.session.regenerate();
req.session.set<any>("oidcOrgSlug", orgSlug);
if (callbackPort) {
req.session.set<any>("callbackPort", callbackPort);
}
const oidcStrategy = await server.services.oidc.getOrgAuthStrategy(orgSlug, callbackPort);
return (
passport.authenticate(oidcStrategy as Strategy, {
scope: "profile email openid"
}) as any
)(req, res);
}
],
handler: () => {}
});
// callback route after login from IDP
server.route({
url: "/callback",
method: "GET",
preValidation: [
async (req, res) => {
const oidcOrgSlug = req.session.get<any>("oidcOrgSlug");
const callbackPort = req.session.get<any>("callbackPort");
const oidcStrategy = await server.services.oidc.getOrgAuthStrategy(oidcOrgSlug, callbackPort);
return (
passport.authenticate(oidcStrategy as Strategy, {
failureRedirect: "/api/v1/sso/oidc/login/error",
session: false,
failureMessage: true
}) as any
)(req, res);
}
],
handler: async (req, res) => {
await req.session.destroy();
if (req.passportUser.isUserCompleted) {
return res.redirect(
`${appCfg.SITE_URL}/login/sso?token=${encodeURIComponent(req.passportUser.providerAuthToken)}`
);
}
// signup
return res.redirect(
`${appCfg.SITE_URL}/signup/sso?token=${encodeURIComponent(req.passportUser.providerAuthToken)}`
);
}
});
server.route({
url: "/login/error",
method: "GET",
handler: async (req, res) => {
await req.session.destroy();
return res.status(500).send({
error: "Authentication error",
details: req.query
});
}
});
server.route({
url: "/config",
method: "GET",
config: {
rateLimit: readLimit
},
onRequest: verifyAuth([AuthMode.JWT]),
schema: {
querystring: z.object({
orgSlug: z.string().trim()
}),
response: {
200: OidcConfigsSchema.pick({
id: true,
issuer: true,
authorizationEndpoint: true,
jwksUri: true,
tokenEndpoint: true,
userinfoEndpoint: true,
configurationType: true,
discoveryURL: true,
isActive: true,
orgId: true,
allowedEmailDomains: true
}).extend({
clientId: z.string(),
clientSecret: z.string()
})
}
},
handler: async (req) => {
const { orgSlug } = req.query;
const oidc = await server.services.oidc.getOidc({
orgSlug,
type: "external",
actor: req.permission.type,
actorId: req.permission.id,
actorOrgId: req.permission.orgId,
actorAuthMethod: req.permission.authMethod
});
return oidc;
}
});
server.route({
method: "PATCH",
url: "/config",
config: {
rateLimit: writeLimit
},
onRequest: verifyAuth([AuthMode.JWT]),
schema: {
body: z
.object({
allowedEmailDomains: z
.string()
.trim()
.optional()
.default("")
.transform((data) => {
if (data === "") return "";
// Trim each ID and join with ', ' to ensure formatting
return data
.split(",")
.map((id) => id.trim())
.join(", ");
}),
discoveryURL: z.string().trim(),
configurationType: z.nativeEnum(OIDCConfigurationType),
issuer: z.string().trim(),
authorizationEndpoint: z.string().trim(),
jwksUri: z.string().trim(),
tokenEndpoint: z.string().trim(),
userinfoEndpoint: z.string().trim(),
clientId: z.string().trim(),
clientSecret: z.string().trim(),
isActive: z.boolean()
})
.partial()
.merge(z.object({ orgSlug: z.string() })),
response: {
200: OidcConfigsSchema.pick({
id: true,
issuer: true,
authorizationEndpoint: true,
configurationType: true,
discoveryURL: true,
jwksUri: true,
tokenEndpoint: true,
userinfoEndpoint: true,
orgId: true,
allowedEmailDomains: true,
isActive: true
})
}
},
handler: async (req) => {
const oidc = await server.services.oidc.updateOidcCfg({
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
...req.body
});
return oidc;
}
});
server.route({
method: "POST",
url: "/config",
config: {
rateLimit: writeLimit
},
onRequest: verifyAuth([AuthMode.JWT]),
schema: {
body: z
.object({
allowedEmailDomains: z
.string()
.trim()
.optional()
.default("")
.transform((data) => {
if (data === "") return "";
// Trim each ID and join with ', ' to ensure formatting
return data
.split(",")
.map((id) => id.trim())
.join(", ");
}),
configurationType: z.nativeEnum(OIDCConfigurationType),
issuer: z.string().trim().optional().default(""),
discoveryURL: z.string().trim().optional().default(""),
authorizationEndpoint: z.string().trim().optional().default(""),
jwksUri: z.string().trim().optional().default(""),
tokenEndpoint: z.string().trim().optional().default(""),
userinfoEndpoint: z.string().trim().optional().default(""),
clientId: z.string().trim(),
clientSecret: z.string().trim(),
isActive: z.boolean(),
orgSlug: z.string().trim()
})
.superRefine((data, ctx) => {
if (data.configurationType === OIDCConfigurationType.CUSTOM) {
if (!data.issuer) {
ctx.addIssue({
path: ["issuer"],
message: "Issuer is required",
code: z.ZodIssueCode.custom
});
}
if (!data.authorizationEndpoint) {
ctx.addIssue({
path: ["authorizationEndpoint"],
message: "Authorization endpoint is required",
code: z.ZodIssueCode.custom
});
}
if (!data.jwksUri) {
ctx.addIssue({
path: ["jwksUri"],
message: "JWKS URI is required",
code: z.ZodIssueCode.custom
});
}
if (!data.tokenEndpoint) {
ctx.addIssue({
path: ["tokenEndpoint"],
message: "Token endpoint is required",
code: z.ZodIssueCode.custom
});
}
if (!data.userinfoEndpoint) {
ctx.addIssue({
path: ["userinfoEndpoint"],
message: "Userinfo endpoint is required",
code: z.ZodIssueCode.custom
});
}
} else {
// eslint-disable-next-line no-lonely-if
if (!data.discoveryURL) {
ctx.addIssue({
path: ["discoveryURL"],
message: "Discovery URL is required",
code: z.ZodIssueCode.custom
});
}
}
}),
response: {
200: OidcConfigsSchema.pick({
id: true,
issuer: true,
authorizationEndpoint: true,
configurationType: true,
discoveryURL: true,
jwksUri: true,
tokenEndpoint: true,
userinfoEndpoint: true,
orgId: true,
isActive: true,
allowedEmailDomains: true
})
}
},
handler: async (req) => {
const oidc = await server.services.oidc.createOidcCfg({
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
...req.body
});
return oidc;
}
});
};

View File

@@ -45,18 +45,29 @@ export const auditLogQueueServiceFactory = ({
const { actor, event, ipAddress, projectId, userAgent, userAgentType } = job.data;
let { orgId } = job.data;
const MS_IN_DAY = 24 * 60 * 60 * 1000;
let project;
if (!orgId) {
// it will never be undefined for both org and project id
// TODO(akhilmhdh): use caching here in dal to avoid db calls
const project = await projectDAL.findById(projectId as string);
project = await projectDAL.findById(projectId as string);
orgId = project.orgId;
}
const plan = await licenseService.getPlan(orgId);
const ttl = plan.auditLogsRetentionDays * MS_IN_DAY;
// skip inserting if audit log retention is 0 meaning its not supported
if (ttl === 0) return;
if (plan.auditLogsRetentionDays === 0) {
// skip inserting if audit log retention is 0 meaning its not supported
return;
}
// For project actions, set TTL to project-level audit log retention config
// This condition ensures that the plan's audit log retention days cannot be bypassed
const ttlInDays =
project?.auditLogsRetentionDays && project.auditLogsRetentionDays < plan.auditLogsRetentionDays
? project.auditLogsRetentionDays
: plan.auditLogsRetentionDays;
const ttl = ttlInDays * MS_IN_DAY;
const auditLog = await auditLogDAL.create({
actor: actor.type,

View File

@@ -65,25 +65,31 @@ export enum EventType {
ADD_IDENTITY_UNIVERSAL_AUTH = "add-identity-universal-auth",
UPDATE_IDENTITY_UNIVERSAL_AUTH = "update-identity-universal-auth",
GET_IDENTITY_UNIVERSAL_AUTH = "get-identity-universal-auth",
REVOKE_IDENTITY_UNIVERSAL_AUTH = "revoke-identity-universal-auth",
LOGIN_IDENTITY_KUBERNETES_AUTH = "login-identity-kubernetes-auth",
ADD_IDENTITY_KUBERNETES_AUTH = "add-identity-kubernetes-auth",
UPDATE_IDENTITY_KUBENETES_AUTH = "update-identity-kubernetes-auth",
GET_IDENTITY_KUBERNETES_AUTH = "get-identity-kubernetes-auth",
REVOKE_IDENTITY_KUBERNETES_AUTH = "revoke-identity-kubernetes-auth",
CREATE_IDENTITY_UNIVERSAL_AUTH_CLIENT_SECRET = "create-identity-universal-auth-client-secret",
REVOKE_IDENTITY_UNIVERSAL_AUTH_CLIENT_SECRET = "revoke-identity-universal-auth-client-secret",
GET_IDENTITY_UNIVERSAL_AUTH_CLIENT_SECRETS = "get-identity-universal-auth-client-secret",
GET_IDENTITY_UNIVERSAL_AUTH_CLIENT_SECRET_BY_ID = "get-identity-universal-auth-client-secret-by-id",
LOGIN_IDENTITY_GCP_AUTH = "login-identity-gcp-auth",
ADD_IDENTITY_GCP_AUTH = "add-identity-gcp-auth",
UPDATE_IDENTITY_GCP_AUTH = "update-identity-gcp-auth",
REVOKE_IDENTITY_GCP_AUTH = "revoke-identity-gcp-auth",
GET_IDENTITY_GCP_AUTH = "get-identity-gcp-auth",
LOGIN_IDENTITY_AWS_AUTH = "login-identity-aws-auth",
ADD_IDENTITY_AWS_AUTH = "add-identity-aws-auth",
UPDATE_IDENTITY_AWS_AUTH = "update-identity-aws-auth",
REVOKE_IDENTITY_AWS_AUTH = "revoke-identity-aws-auth",
GET_IDENTITY_AWS_AUTH = "get-identity-aws-auth",
LOGIN_IDENTITY_AZURE_AUTH = "login-identity-azure-auth",
ADD_IDENTITY_AZURE_AUTH = "add-identity-azure-auth",
UPDATE_IDENTITY_AZURE_AUTH = "update-identity-azure-auth",
GET_IDENTITY_AZURE_AUTH = "get-identity-azure-auth",
REVOKE_IDENTITY_AZURE_AUTH = "revoke-identity-azure-auth",
CREATE_ENVIRONMENT = "create-environment",
UPDATE_ENVIRONMENT = "update-environment",
DELETE_ENVIRONMENT = "delete-environment",
@@ -434,6 +440,13 @@ interface GetIdentityUniversalAuthEvent {
};
}
interface DeleteIdentityUniversalAuthEvent {
type: EventType.REVOKE_IDENTITY_UNIVERSAL_AUTH;
metadata: {
identityId: string;
};
}
interface LoginIdentityKubernetesAuthEvent {
type: EventType.LOGIN_IDENTITY_KUBERNETES_AUTH;
metadata: {
@@ -457,6 +470,13 @@ interface AddIdentityKubernetesAuthEvent {
};
}
interface DeleteIdentityKubernetesAuthEvent {
type: EventType.REVOKE_IDENTITY_KUBERNETES_AUTH;
metadata: {
identityId: string;
};
}
interface UpdateIdentityKubernetesAuthEvent {
type: EventType.UPDATE_IDENTITY_KUBENETES_AUTH;
metadata: {
@@ -493,6 +513,14 @@ interface GetIdentityUniversalAuthClientSecretsEvent {
};
}
interface GetIdentityUniversalAuthClientSecretByIdEvent {
type: EventType.GET_IDENTITY_UNIVERSAL_AUTH_CLIENT_SECRET_BY_ID;
metadata: {
identityId: string;
clientSecretId: string;
};
}
interface RevokeIdentityUniversalAuthClientSecretEvent {
type: EventType.REVOKE_IDENTITY_UNIVERSAL_AUTH_CLIENT_SECRET;
metadata: {
@@ -525,6 +553,13 @@ interface AddIdentityGcpAuthEvent {
};
}
interface DeleteIdentityGcpAuthEvent {
type: EventType.REVOKE_IDENTITY_GCP_AUTH;
metadata: {
identityId: string;
};
}
interface UpdateIdentityGcpAuthEvent {
type: EventType.UPDATE_IDENTITY_GCP_AUTH;
metadata: {
@@ -570,6 +605,13 @@ interface AddIdentityAwsAuthEvent {
};
}
interface DeleteIdentityAwsAuthEvent {
type: EventType.REVOKE_IDENTITY_AWS_AUTH;
metadata: {
identityId: string;
};
}
interface UpdateIdentityAwsAuthEvent {
type: EventType.UPDATE_IDENTITY_AWS_AUTH;
metadata: {
@@ -613,6 +655,13 @@ interface AddIdentityAzureAuthEvent {
};
}
interface DeleteIdentityAzureAuthEvent {
type: EventType.REVOKE_IDENTITY_AZURE_AUTH;
metadata: {
identityId: string;
};
}
interface UpdateIdentityAzureAuthEvent {
type: EventType.UPDATE_IDENTITY_AZURE_AUTH;
metadata: {
@@ -1003,24 +1052,30 @@ export type Event =
| LoginIdentityUniversalAuthEvent
| AddIdentityUniversalAuthEvent
| UpdateIdentityUniversalAuthEvent
| DeleteIdentityUniversalAuthEvent
| GetIdentityUniversalAuthEvent
| LoginIdentityKubernetesAuthEvent
| DeleteIdentityKubernetesAuthEvent
| AddIdentityKubernetesAuthEvent
| UpdateIdentityKubernetesAuthEvent
| GetIdentityKubernetesAuthEvent
| CreateIdentityUniversalAuthClientSecretEvent
| GetIdentityUniversalAuthClientSecretsEvent
| GetIdentityUniversalAuthClientSecretByIdEvent
| RevokeIdentityUniversalAuthClientSecretEvent
| LoginIdentityGcpAuthEvent
| AddIdentityGcpAuthEvent
| DeleteIdentityGcpAuthEvent
| UpdateIdentityGcpAuthEvent
| GetIdentityGcpAuthEvent
| LoginIdentityAwsAuthEvent
| AddIdentityAwsAuthEvent
| UpdateIdentityAwsAuthEvent
| GetIdentityAwsAuthEvent
| DeleteIdentityAwsAuthEvent
| LoginIdentityAzureAuthEvent
| AddIdentityAzureAuthEvent
| DeleteIdentityAzureAuthEvent
| UpdateIdentityAzureAuthEvent
| GetIdentityAzureAuthEvent
| CreateEnvironmentEvent

View File

@@ -23,6 +23,8 @@ import {
} from "@app/lib/crypto/encryption";
import { BadRequestError } from "@app/lib/errors";
import { AuthMethod, AuthTokenType } from "@app/services/auth/auth-type";
import { TAuthTokenServiceFactory } from "@app/services/auth-token/auth-token-service";
import { TokenType } from "@app/services/auth-token/auth-token-types";
import { TGroupProjectDALFactory } from "@app/services/group-project/group-project-dal";
import { TOrgBotDALFactory } from "@app/services/org/org-bot-dal";
import { TOrgDALFactory } from "@app/services/org/org-dal";
@@ -30,6 +32,7 @@ import { TOrgMembershipDALFactory } from "@app/services/org-membership/org-membe
import { TProjectDALFactory } from "@app/services/project/project-dal";
import { TProjectBotDALFactory } from "@app/services/project-bot/project-bot-dal";
import { TProjectKeyDALFactory } from "@app/services/project-key/project-key-dal";
import { SmtpTemplates, TSmtpService } from "@app/services/smtp/smtp-service";
import { getServerCfg } from "@app/services/super-admin/super-admin-service";
import { TUserDALFactory } from "@app/services/user/user-dal";
import { normalizeUsername } from "@app/services/user/user-fns";
@@ -73,11 +76,19 @@ type TLdapConfigServiceFactoryDep = {
>;
userDAL: Pick<
TUserDALFactory,
"create" | "findOne" | "transaction" | "updateById" | "findUserEncKeyByUserIdsBatch" | "find"
| "create"
| "findOne"
| "transaction"
| "updateById"
| "findUserEncKeyByUserIdsBatch"
| "find"
| "findUserEncKeyByUserId"
>;
userAliasDAL: Pick<TUserAliasDALFactory, "create" | "findOne">;
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">;
licenseService: Pick<TLicenseServiceFactory, "getPlan" | "updateSubscriptionOrgMemberCount">;
tokenService: Pick<TAuthTokenServiceFactory, "createTokenForUser">;
smtpService: Pick<TSmtpService, "sendMail">;
};
export type TLdapConfigServiceFactory = ReturnType<typeof ldapConfigServiceFactory>;
@@ -97,7 +108,9 @@ export const ldapConfigServiceFactory = ({
userDAL,
userAliasDAL,
permissionService,
licenseService
licenseService,
tokenService,
smtpService
}: TLdapConfigServiceFactoryDep) => {
const createLdapCfg = async ({
actor,
@@ -109,6 +122,7 @@ export const ldapConfigServiceFactory = ({
url,
bindDN,
bindPass,
uniqueUserAttribute,
searchBase,
searchFilter,
groupSearchBase,
@@ -187,6 +201,7 @@ export const ldapConfigServiceFactory = ({
encryptedBindPass,
bindPassIV,
bindPassTag,
uniqueUserAttribute,
searchBase,
searchFilter,
groupSearchBase,
@@ -209,6 +224,7 @@ export const ldapConfigServiceFactory = ({
url,
bindDN,
bindPass,
uniqueUserAttribute,
searchBase,
searchFilter,
groupSearchBase,
@@ -231,7 +247,8 @@ export const ldapConfigServiceFactory = ({
searchBase,
searchFilter,
groupSearchBase,
groupSearchFilter
groupSearchFilter,
uniqueUserAttribute
};
const orgBot = await orgBotDAL.findOne({ orgId });
@@ -332,6 +349,7 @@ export const ldapConfigServiceFactory = ({
url: ldapConfig.url,
bindDN,
bindPass,
uniqueUserAttribute: ldapConfig.uniqueUserAttribute,
searchBase: ldapConfig.searchBase,
searchFilter: ldapConfig.searchFilter,
groupSearchBase: ldapConfig.groupSearchBase,
@@ -368,6 +386,7 @@ export const ldapConfigServiceFactory = ({
url: ldapConfig.url,
bindDN: ldapConfig.bindDN,
bindCredentials: ldapConfig.bindPass,
uniqueUserAttribute: ldapConfig.uniqueUserAttribute,
searchBase: ldapConfig.searchBase,
searchFilter: ldapConfig.searchFilter || "(uid={{username}})",
// searchAttributes: ["uid", "uidNumber", "givenName", "sn", "mail"],
@@ -488,7 +507,7 @@ export const ldapConfigServiceFactory = ({
if (!orgMembership) {
await orgMembershipDAL.create(
{
userId: userAlias.userId,
userId: newUser.id,
inviteEmail: email,
orgId,
role: OrgMembershipRole.Member,
@@ -592,12 +611,14 @@ export const ldapConfigServiceFactory = ({
});
const isUserCompleted = Boolean(user.isAccepted);
const userEnc = await userDAL.findUserEncKeyByUserId(user.id);
const providerAuthToken = jwt.sign(
{
authTokenType: AuthTokenType.PROVIDER_TOKEN,
userId: user.id,
username: user.username,
hasExchangedPrivateKey: Boolean(userEnc?.serverEncryptedPrivateKey),
...(user.email && { email: user.email, isEmailVerified: user.isEmailVerified }),
firstName,
lastName,
@@ -619,6 +640,22 @@ export const ldapConfigServiceFactory = ({
}
);
if (user.email && !user.isEmailVerified) {
const token = await tokenService.createTokenForUser({
type: TokenType.TOKEN_EMAIL_VERIFICATION,
userId: user.id
});
await smtpService.sendMail({
template: SmtpTemplates.EmailVerification,
subjectLine: "Infisical confirmation code",
recipients: [user.email],
substitutions: {
code: token
}
});
}
return { isUserCompleted, providerAuthToken };
};

View File

@@ -7,6 +7,7 @@ export type TLDAPConfig = {
url: string;
bindDN: string;
bindPass: string;
uniqueUserAttribute: string;
searchBase: string;
groupSearchBase: string;
groupSearchFilter: string;
@@ -19,6 +20,7 @@ export type TCreateLdapCfgDTO = {
url: string;
bindDN: string;
bindPass: string;
uniqueUserAttribute: string;
searchBase: string;
searchFilter: string;
groupSearchBase: string;
@@ -33,6 +35,7 @@ export type TUpdateLdapCfgDTO = {
url: string;
bindDN: string;
bindPass: string;
uniqueUserAttribute: string;
searchBase: string;
searchFilter: string;
groupSearchBase: string;

View File

@@ -27,6 +27,7 @@ export const getDefaultOnPremFeatures = (): TFeatureSet => ({
auditLogStreams: false,
auditLogStreamLimit: 3,
samlSSO: false,
oidcSSO: false,
scim: false,
ldap: false,
groups: false,

View File

@@ -44,6 +44,7 @@ export type TFeatureSet = {
auditLogStreams: false;
auditLogStreamLimit: 3;
samlSSO: false;
oidcSSO: false;
scim: false;
ldap: false;
groups: false;

View File

@@ -0,0 +1,11 @@
import { TDbClient } from "@app/db";
import { TableName } from "@app/db/schemas";
import { ormify } from "@app/lib/knex";
export type TOidcConfigDALFactory = ReturnType<typeof oidcConfigDALFactory>;
export const oidcConfigDALFactory = (db: TDbClient) => {
const oidcCfgOrm = ormify(db, TableName.OidcConfig);
return { ...oidcCfgOrm };
};

View File

@@ -0,0 +1,637 @@
/* eslint-disable @typescript-eslint/no-unsafe-call */
import { ForbiddenError } from "@casl/ability";
import jwt from "jsonwebtoken";
import { Issuer, Issuer as OpenIdIssuer, Strategy as OpenIdStrategy, TokenSet } from "openid-client";
import { OrgMembershipRole, OrgMembershipStatus, SecretKeyEncoding, TableName, TUsers } from "@app/db/schemas";
import { TOidcConfigsUpdate } from "@app/db/schemas/oidc-configs";
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { getConfig } from "@app/lib/config/env";
import {
decryptSymmetric,
encryptSymmetric,
generateAsymmetricKeyPair,
generateSymmetricKey,
infisicalSymmetricDecrypt,
infisicalSymmetricEncypt
} from "@app/lib/crypto/encryption";
import { BadRequestError } from "@app/lib/errors";
import { AuthMethod, AuthTokenType } from "@app/services/auth/auth-type";
import { TAuthTokenServiceFactory } from "@app/services/auth-token/auth-token-service";
import { TokenType } from "@app/services/auth-token/auth-token-types";
import { TOrgBotDALFactory } from "@app/services/org/org-bot-dal";
import { TOrgDALFactory } from "@app/services/org/org-dal";
import { TOrgMembershipDALFactory } from "@app/services/org-membership/org-membership-dal";
import { SmtpTemplates, TSmtpService } from "@app/services/smtp/smtp-service";
import { getServerCfg } from "@app/services/super-admin/super-admin-service";
import { TUserDALFactory } from "@app/services/user/user-dal";
import { normalizeUsername } from "@app/services/user/user-fns";
import { TUserAliasDALFactory } from "@app/services/user-alias/user-alias-dal";
import { UserAliasType } from "@app/services/user-alias/user-alias-types";
import { TOidcConfigDALFactory } from "./oidc-config-dal";
import {
OIDCConfigurationType,
TCreateOidcCfgDTO,
TGetOidcCfgDTO,
TOidcLoginDTO,
TUpdateOidcCfgDTO
} from "./oidc-config-types";
type TOidcConfigServiceFactoryDep = {
userDAL: Pick<
TUserDALFactory,
"create" | "findOne" | "transaction" | "updateById" | "findById" | "findUserEncKeyByUserId"
>;
userAliasDAL: Pick<TUserAliasDALFactory, "create" | "findOne">;
orgDAL: Pick<
TOrgDALFactory,
"createMembership" | "updateMembershipById" | "findMembership" | "findOrgById" | "findOne" | "updateById"
>;
orgMembershipDAL: Pick<TOrgMembershipDALFactory, "create">;
orgBotDAL: Pick<TOrgBotDALFactory, "findOne" | "create" | "transaction">;
licenseService: Pick<TLicenseServiceFactory, "getPlan" | "updateSubscriptionOrgMemberCount">;
tokenService: Pick<TAuthTokenServiceFactory, "createTokenForUser">;
smtpService: Pick<TSmtpService, "sendMail">;
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">;
oidcConfigDAL: Pick<TOidcConfigDALFactory, "findOne" | "update" | "create">;
};
export type TOidcConfigServiceFactory = ReturnType<typeof oidcConfigServiceFactory>;
export const oidcConfigServiceFactory = ({
orgDAL,
orgMembershipDAL,
userDAL,
userAliasDAL,
licenseService,
permissionService,
tokenService,
orgBotDAL,
smtpService,
oidcConfigDAL
}: TOidcConfigServiceFactoryDep) => {
const getOidc = async (dto: TGetOidcCfgDTO) => {
const org = await orgDAL.findOne({ slug: dto.orgSlug });
if (!org) {
throw new BadRequestError({
message: "Organization not found",
name: "OrgNotFound"
});
}
if (dto.type === "external") {
const { permission } = await permissionService.getOrgPermission(
dto.actor,
dto.actorId,
org.id,
dto.actorAuthMethod,
dto.actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Sso);
}
const oidcCfg = await oidcConfigDAL.findOne({
orgId: org.id
});
if (!oidcCfg) {
throw new BadRequestError({
message: "Failed to find organization OIDC configuration"
});
}
// decrypt and return cfg
const orgBot = await orgBotDAL.findOne({ orgId: oidcCfg.orgId });
if (!orgBot) {
throw new BadRequestError({ message: "Org bot not found", name: "OrgBotNotFound" });
}
const key = infisicalSymmetricDecrypt({
ciphertext: orgBot.encryptedSymmetricKey,
iv: orgBot.symmetricKeyIV,
tag: orgBot.symmetricKeyTag,
keyEncoding: orgBot.symmetricKeyKeyEncoding as SecretKeyEncoding
});
const { encryptedClientId, clientIdIV, clientIdTag, encryptedClientSecret, clientSecretIV, clientSecretTag } =
oidcCfg;
let clientId = "";
if (encryptedClientId && clientIdIV && clientIdTag) {
clientId = decryptSymmetric({
ciphertext: encryptedClientId,
key,
tag: clientIdTag,
iv: clientIdIV
});
}
let clientSecret = "";
if (encryptedClientSecret && clientSecretIV && clientSecretTag) {
clientSecret = decryptSymmetric({
key,
tag: clientSecretTag,
iv: clientSecretIV,
ciphertext: encryptedClientSecret
});
}
return {
id: oidcCfg.id,
issuer: oidcCfg.issuer,
authorizationEndpoint: oidcCfg.authorizationEndpoint,
configurationType: oidcCfg.configurationType,
discoveryURL: oidcCfg.discoveryURL,
jwksUri: oidcCfg.jwksUri,
tokenEndpoint: oidcCfg.tokenEndpoint,
userinfoEndpoint: oidcCfg.userinfoEndpoint,
orgId: oidcCfg.orgId,
isActive: oidcCfg.isActive,
allowedEmailDomains: oidcCfg.allowedEmailDomains,
clientId,
clientSecret
};
};
const oidcLogin = async ({ externalId, email, firstName, lastName, orgId, callbackPort }: TOidcLoginDTO) => {
const serverCfg = await getServerCfg();
const appCfg = getConfig();
const userAlias = await userAliasDAL.findOne({
externalId,
orgId,
aliasType: UserAliasType.OIDC
});
const organization = await orgDAL.findOrgById(orgId);
if (!organization) throw new BadRequestError({ message: "Org not found" });
let user: TUsers;
if (userAlias) {
user = await userDAL.transaction(async (tx) => {
const foundUser = await userDAL.findById(userAlias.userId, tx);
const [orgMembership] = await orgDAL.findMembership(
{
[`${TableName.OrgMembership}.userId` as "userId"]: foundUser.id,
[`${TableName.OrgMembership}.orgId` as "id"]: orgId
},
{ tx }
);
if (!orgMembership) {
await orgMembershipDAL.create(
{
userId: userAlias.userId,
inviteEmail: email,
orgId,
role: OrgMembershipRole.Member,
status: foundUser.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
},
tx
);
// Only update the membership to Accepted if the user account is already completed.
} else if (orgMembership.status === OrgMembershipStatus.Invited && foundUser.isAccepted) {
await orgDAL.updateMembershipById(
orgMembership.id,
{
status: OrgMembershipStatus.Accepted
},
tx
);
}
return foundUser;
});
} else {
user = await userDAL.transaction(async (tx) => {
let newUser: TUsers | undefined;
if (serverCfg.trustOidcEmails) {
newUser = await userDAL.findOne(
{
email,
isEmailVerified: true
},
tx
);
}
if (!newUser) {
const uniqueUsername = await normalizeUsername(externalId, userDAL);
newUser = await userDAL.create(
{
email,
firstName,
isEmailVerified: serverCfg.trustOidcEmails,
username: serverCfg.trustOidcEmails ? email : uniqueUsername,
lastName,
authMethods: [],
isGhost: false
},
tx
);
}
await userAliasDAL.create(
{
userId: newUser.id,
aliasType: UserAliasType.OIDC,
externalId,
emails: email ? [email] : [],
orgId
},
tx
);
const [orgMembership] = await orgDAL.findMembership(
{
[`${TableName.OrgMembership}.userId` as "userId"]: newUser.id,
[`${TableName.OrgMembership}.orgId` as "id"]: orgId
},
{ tx }
);
if (!orgMembership) {
await orgMembershipDAL.create(
{
userId: newUser.id,
inviteEmail: email,
orgId,
role: OrgMembershipRole.Member,
status: newUser.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
},
tx
);
// Only update the membership to Accepted if the user account is already completed.
} else if (orgMembership.status === OrgMembershipStatus.Invited && newUser.isAccepted) {
await orgDAL.updateMembershipById(
orgMembership.id,
{
status: OrgMembershipStatus.Accepted
},
tx
);
}
return newUser;
});
}
await licenseService.updateSubscriptionOrgMemberCount(organization.id);
const userEnc = await userDAL.findUserEncKeyByUserId(user.id);
const isUserCompleted = Boolean(user.isAccepted);
const providerAuthToken = jwt.sign(
{
authTokenType: AuthTokenType.PROVIDER_TOKEN,
userId: user.id,
username: user.username,
...(user.email && { email: user.email, isEmailVerified: user.isEmailVerified }),
firstName,
lastName,
organizationName: organization.name,
organizationId: organization.id,
organizationSlug: organization.slug,
hasExchangedPrivateKey: Boolean(userEnc?.serverEncryptedPrivateKey),
authMethod: AuthMethod.OIDC,
authType: UserAliasType.OIDC,
isUserCompleted,
...(callbackPort && { callbackPort })
},
appCfg.AUTH_SECRET,
{
expiresIn: appCfg.JWT_PROVIDER_AUTH_LIFETIME
}
);
if (user.email && !user.isEmailVerified) {
const token = await tokenService.createTokenForUser({
type: TokenType.TOKEN_EMAIL_VERIFICATION,
userId: user.id
});
await smtpService.sendMail({
template: SmtpTemplates.EmailVerification,
subjectLine: "Infisical confirmation code",
recipients: [user.email],
substitutions: {
code: token
}
});
}
return { isUserCompleted, providerAuthToken };
};
const updateOidcCfg = async ({
orgSlug,
allowedEmailDomains,
configurationType,
discoveryURL,
actor,
actorOrgId,
actorAuthMethod,
actorId,
issuer,
isActive,
authorizationEndpoint,
jwksUri,
tokenEndpoint,
userinfoEndpoint,
clientId,
clientSecret
}: TUpdateOidcCfgDTO) => {
const org = await orgDAL.findOne({
slug: orgSlug
});
if (!org) {
throw new BadRequestError({
message: "Organization not found"
});
}
const plan = await licenseService.getPlan(org.id);
if (!plan.oidcSSO)
throw new BadRequestError({
message:
"Failed to update OIDC SSO configuration due to plan restriction. Upgrade plan to update SSO configuration."
});
const { permission } = await permissionService.getOrgPermission(
actor,
actorId,
org.id,
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Sso);
const orgBot = await orgBotDAL.findOne({ orgId: org.id });
if (!orgBot) throw new BadRequestError({ message: "Org bot not found", name: "OrgBotNotFound" });
const key = infisicalSymmetricDecrypt({
ciphertext: orgBot.encryptedSymmetricKey,
iv: orgBot.symmetricKeyIV,
tag: orgBot.symmetricKeyTag,
keyEncoding: orgBot.symmetricKeyKeyEncoding as SecretKeyEncoding
});
const updateQuery: TOidcConfigsUpdate = {
allowedEmailDomains,
configurationType,
discoveryURL,
issuer,
authorizationEndpoint,
tokenEndpoint,
userinfoEndpoint,
jwksUri,
isActive
};
if (clientId !== undefined) {
const { ciphertext: encryptedClientId, iv: clientIdIV, tag: clientIdTag } = encryptSymmetric(clientId, key);
updateQuery.encryptedClientId = encryptedClientId;
updateQuery.clientIdIV = clientIdIV;
updateQuery.clientIdTag = clientIdTag;
}
if (clientSecret !== undefined) {
const {
ciphertext: encryptedClientSecret,
iv: clientSecretIV,
tag: clientSecretTag
} = encryptSymmetric(clientSecret, key);
updateQuery.encryptedClientSecret = encryptedClientSecret;
updateQuery.clientSecretIV = clientSecretIV;
updateQuery.clientSecretTag = clientSecretTag;
}
const [ssoConfig] = await oidcConfigDAL.update({ orgId: org.id }, updateQuery);
return ssoConfig;
};
const createOidcCfg = async ({
orgSlug,
allowedEmailDomains,
configurationType,
discoveryURL,
actor,
actorOrgId,
actorAuthMethod,
actorId,
issuer,
isActive,
authorizationEndpoint,
jwksUri,
tokenEndpoint,
userinfoEndpoint,
clientId,
clientSecret
}: TCreateOidcCfgDTO) => {
const org = await orgDAL.findOne({
slug: orgSlug
});
if (!org) {
throw new BadRequestError({
message: "Organization not found"
});
}
const plan = await licenseService.getPlan(org.id);
if (!plan.oidcSSO)
throw new BadRequestError({
message:
"Failed to create OIDC SSO configuration due to plan restriction. Upgrade plan to update SSO configuration."
});
const { permission } = await permissionService.getOrgPermission(
actor,
actorId,
org.id,
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Sso);
const orgBot = await orgBotDAL.transaction(async (tx) => {
const doc = await orgBotDAL.findOne({ orgId: org.id }, tx);
if (doc) return doc;
const { privateKey, publicKey } = generateAsymmetricKeyPair();
const key = generateSymmetricKey();
const {
ciphertext: encryptedPrivateKey,
iv: privateKeyIV,
tag: privateKeyTag,
encoding: privateKeyKeyEncoding,
algorithm: privateKeyAlgorithm
} = infisicalSymmetricEncypt(privateKey);
const {
ciphertext: encryptedSymmetricKey,
iv: symmetricKeyIV,
tag: symmetricKeyTag,
encoding: symmetricKeyKeyEncoding,
algorithm: symmetricKeyAlgorithm
} = infisicalSymmetricEncypt(key);
return orgBotDAL.create(
{
name: "Infisical org bot",
publicKey,
privateKeyIV,
encryptedPrivateKey,
symmetricKeyIV,
symmetricKeyTag,
encryptedSymmetricKey,
symmetricKeyAlgorithm,
orgId: org.id,
privateKeyTag,
privateKeyAlgorithm,
privateKeyKeyEncoding,
symmetricKeyKeyEncoding
},
tx
);
});
const key = infisicalSymmetricDecrypt({
ciphertext: orgBot.encryptedSymmetricKey,
iv: orgBot.symmetricKeyIV,
tag: orgBot.symmetricKeyTag,
keyEncoding: orgBot.symmetricKeyKeyEncoding as SecretKeyEncoding
});
const { ciphertext: encryptedClientId, iv: clientIdIV, tag: clientIdTag } = encryptSymmetric(clientId, key);
const {
ciphertext: encryptedClientSecret,
iv: clientSecretIV,
tag: clientSecretTag
} = encryptSymmetric(clientSecret, key);
const oidcCfg = await oidcConfigDAL.create({
issuer,
isActive,
configurationType,
discoveryURL,
authorizationEndpoint,
allowedEmailDomains,
jwksUri,
tokenEndpoint,
userinfoEndpoint,
orgId: org.id,
encryptedClientId,
clientIdIV,
clientIdTag,
encryptedClientSecret,
clientSecretIV,
clientSecretTag
});
return oidcCfg;
};
const getOrgAuthStrategy = async (orgSlug: string, callbackPort?: string) => {
const appCfg = getConfig();
const org = await orgDAL.findOne({
slug: orgSlug
});
if (!org) {
throw new BadRequestError({
message: "Organization not found."
});
}
const oidcCfg = await getOidc({
type: "internal",
orgSlug
});
if (!oidcCfg || !oidcCfg.isActive) {
throw new BadRequestError({
message: "Failed to authenticate with OIDC SSO"
});
}
let issuer: Issuer;
if (oidcCfg.configurationType === OIDCConfigurationType.DISCOVERY_URL) {
if (!oidcCfg.discoveryURL) {
throw new BadRequestError({
message: "OIDC not configured correctly"
});
}
issuer = await Issuer.discover(oidcCfg.discoveryURL);
} else {
if (
!oidcCfg.issuer ||
!oidcCfg.authorizationEndpoint ||
!oidcCfg.jwksUri ||
!oidcCfg.tokenEndpoint ||
!oidcCfg.userinfoEndpoint
) {
throw new BadRequestError({
message: "OIDC not configured correctly"
});
}
issuer = new OpenIdIssuer({
issuer: oidcCfg.issuer,
authorization_endpoint: oidcCfg.authorizationEndpoint,
jwks_uri: oidcCfg.jwksUri,
token_endpoint: oidcCfg.tokenEndpoint,
userinfo_endpoint: oidcCfg.userinfoEndpoint
});
}
const client = new issuer.Client({
client_id: oidcCfg.clientId,
client_secret: oidcCfg.clientSecret,
redirect_uris: [`${appCfg.SITE_URL}/api/v1/sso/oidc/callback`]
});
const strategy = new OpenIdStrategy(
{
client,
passReqToCallback: true
},
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(_req: any, tokenSet: TokenSet, cb: any) => {
const claims = tokenSet.claims();
if (!claims.email || !claims.given_name) {
throw new BadRequestError({
message: "Invalid request. Missing email or first name"
});
}
if (oidcCfg.allowedEmailDomains) {
const allowedDomains = oidcCfg.allowedEmailDomains.split(", ");
if (!allowedDomains.includes(claims.email.split("@")[1])) {
throw new BadRequestError({
message: "Email not allowed."
});
}
}
oidcLogin({
email: claims.email,
externalId: claims.sub,
firstName: claims.given_name ?? "",
lastName: claims.family_name ?? "",
orgId: org.id,
callbackPort
})
.then(({ isUserCompleted, providerAuthToken }) => {
cb(null, { isUserCompleted, providerAuthToken });
})
.catch((error) => {
cb(error);
});
}
);
return strategy;
};
return { oidcLogin, getOrgAuthStrategy, getOidc, updateOidcCfg, createOidcCfg };
};

View File

@@ -0,0 +1,56 @@
import { TGenericPermission } from "@app/lib/types";
export enum OIDCConfigurationType {
CUSTOM = "custom",
DISCOVERY_URL = "discoveryURL"
}
export type TOidcLoginDTO = {
externalId: string;
email: string;
firstName: string;
lastName?: string;
orgId: string;
callbackPort?: string;
};
export type TGetOidcCfgDTO =
| ({
type: "external";
orgSlug: string;
} & TGenericPermission)
| {
type: "internal";
orgSlug: string;
};
export type TCreateOidcCfgDTO = {
issuer?: string;
authorizationEndpoint?: string;
discoveryURL?: string;
configurationType: OIDCConfigurationType;
allowedEmailDomains?: string;
jwksUri?: string;
tokenEndpoint?: string;
userinfoEndpoint?: string;
clientId: string;
clientSecret: string;
isActive: boolean;
orgSlug: string;
} & TGenericPermission;
export type TUpdateOidcCfgDTO = Partial<{
issuer: string;
authorizationEndpoint: string;
allowedEmailDomains: string;
discoveryURL: string;
jwksUri: string;
configurationType: OIDCConfigurationType;
tokenEndpoint: string;
userinfoEndpoint: string;
clientId: string;
clientSecret: string;
isActive: boolean;
orgSlug: string;
}> &
TGenericPermission;

View File

@@ -116,7 +116,6 @@ const buildMemberPermission = () => {
can(OrgPermissionActions.Read, OrgPermissionSubjects.Role);
can(OrgPermissionActions.Read, OrgPermissionSubjects.Settings);
can(OrgPermissionActions.Read, OrgPermissionSubjects.Billing);
can(OrgPermissionActions.Read, OrgPermissionSubjects.Sso);
can(OrgPermissionActions.Read, OrgPermissionSubjects.IncidentAccount);
can(OrgPermissionActions.Read, OrgPermissionSubjects.SecretScanning);

View File

@@ -41,7 +41,10 @@ import { TCreateSamlCfgDTO, TGetSamlCfgDTO, TSamlLoginDTO, TUpdateSamlCfgDTO } f
type TSamlConfigServiceFactoryDep = {
samlConfigDAL: Pick<TSamlConfigDALFactory, "create" | "findOne" | "update" | "findById">;
userDAL: Pick<TUserDALFactory, "create" | "findOne" | "transaction" | "updateById" | "findById">;
userDAL: Pick<
TUserDALFactory,
"create" | "findOne" | "transaction" | "updateById" | "findById" | "findUserEncKeyByUserId"
>;
userAliasDAL: Pick<TUserAliasDALFactory, "create" | "findOne">;
orgDAL: Pick<
TOrgDALFactory,
@@ -452,6 +455,7 @@ export const samlConfigServiceFactory = ({
await licenseService.updateSubscriptionOrgMemberCount(organization.id);
const isUserCompleted = Boolean(user.isAccepted);
const userEnc = await userDAL.findUserEncKeyByUserId(user.id);
const providerAuthToken = jwt.sign(
{
authTokenType: AuthTokenType.PROVIDER_TOKEN,
@@ -464,6 +468,7 @@ export const samlConfigServiceFactory = ({
organizationId: organization.id,
organizationSlug: organization.slug,
authMethod: authProvider,
hasExchangedPrivateKey: Boolean(userEnc?.serverEncryptedPrivateKey),
authType: UserAliasType.SAML,
isUserCompleted,
...(relayState

View File

@@ -331,8 +331,8 @@ export const snapshotDALFactory = (db: TDbClient) => {
* Prunes excess snapshots from the database to ensure only a specified number of recent snapshots are retained for each folder.
*
* This function operates in three main steps:
* 1. Pruning snapshots from root/non-versioned folders.
* 2. Pruning snapshots from versioned folders.
* 1. Pruning snapshots from current folders.
* 2. Pruning snapshots from non-current folders (versioned ones).
* 3. Removing orphaned snapshots that do not belong to any existing folder or folder version.
*
* The function processes snapshots in batches, determined by the `PRUNE_FOLDER_BATCH_SIZE` constant,
@@ -350,7 +350,7 @@ export const snapshotDALFactory = (db: TDbClient) => {
try {
let uuidOffset = "00000000-0000-0000-0000-000000000000";
// cleanup snapshots from root/non-versioned folders
// cleanup snapshots from current folders
// eslint-disable-next-line no-constant-condition, no-unreachable-loop
while (true) {
const folderBatch = await db(TableName.SecretFolder)
@@ -382,12 +382,11 @@ export const snapshotDALFactory = (db: TDbClient) => {
.join(TableName.Environment, `${TableName.Environment}.id`, `${TableName.SecretFolder}.envId`)
.join(TableName.Project, `${TableName.Project}.id`, `${TableName.Environment}.projectId`)
.join("snapshot_cte", "snapshot_cte.id", `${TableName.Snapshot}.id`)
.whereNull(`${TableName.SecretFolder}.parentId`)
.whereRaw(`snapshot_cte.row_num > ${TableName.Project}."pitVersionLimit"`)
.delete();
} catch (err) {
logger.error(
`Failed to prune snapshots from root/non-versioned folders in range ${batchEntries[0]}:${
`Failed to prune snapshots from current folders in range ${batchEntries[0]}:${
batchEntries[batchEntries.length - 1]
}`
);
@@ -399,7 +398,7 @@ export const snapshotDALFactory = (db: TDbClient) => {
}
}
// cleanup snapshots from versioned folders
// cleanup snapshots from non-current folders
uuidOffset = "00000000-0000-0000-0000-000000000000";
// eslint-disable-next-line no-constant-condition
while (true) {
@@ -440,7 +439,7 @@ export const snapshotDALFactory = (db: TDbClient) => {
.delete();
} catch (err) {
logger.error(
`Failed to prune snapshots from versioned folders in range ${batchEntries[0]}:${
`Failed to prune snapshots from non-current folders in range ${batchEntries[0]}:${
batchEntries[batchEntries.length - 1]
}`
);

View File

@@ -42,6 +42,13 @@ export const IDENTITIES = {
},
DELETE: {
identityId: "The ID of the identity to delete."
},
GET_BY_ID: {
identityId: "The ID of the identity to get details.",
orgId: "The ID of the org of the identity"
},
LIST: {
orgId: "The ID of the organization to list identities."
}
} as const;
@@ -65,6 +72,9 @@ export const UNIVERSAL_AUTH = {
RETRIEVE: {
identityId: "The ID of the identity to retrieve."
},
REVOKE: {
identityId: "The ID of the identity to revoke."
},
UPDATE: {
identityId: "The ID of the identity to update.",
clientSecretTrustedIps: "The new list of IPs or CIDR ranges that the Client Secret can be used from.",
@@ -83,6 +93,10 @@ export const UNIVERSAL_AUTH = {
LIST_CLIENT_SECRETS: {
identityId: "The ID of the identity to list client secrets for."
},
GET_CLIENT_SECRET: {
identityId: "The ID of the identity to get the client secret from.",
clientSecretId: "The ID of the client secret to get details."
},
REVOKE_CLIENT_SECRET: {
identityId: "The ID of the identity to revoke the client secret from.",
clientSecretId: "The ID of the client secret to revoke."
@@ -104,6 +118,27 @@ export const AWS_AUTH = {
iamRequestBody:
"The base64-encoded body of the signed request. Most likely, the base64-encoding of Action=GetCallerIdentity&Version=2011-06-15.",
iamRequestHeaders: "The base64-encoded headers of the sts:GetCallerIdentity signed request."
},
REVOKE: {
identityId: "The ID of the identity to revoke."
}
} as const;
export const AZURE_AUTH = {
REVOKE: {
identityId: "The ID of the identity to revoke."
}
} as const;
export const GCP_AUTH = {
REVOKE: {
identityId: "The ID of the identity to revoke."
}
} as const;
export const KUBERNETES_AUTH = {
REVOKE: {
identityId: "The ID of the identity to revoke."
}
} as const;
@@ -347,6 +382,7 @@ export const RAW_SECRETS = {
tagIds: "The ID of the tags to be attached to the created secret."
},
GET: {
expand: "Whether or not to expand secret references",
secretName: "The name of the secret to get.",
workspaceId: "The ID of the project to get the secret from.",
workspaceSlug: "The slug of the project to get the secret from.",
@@ -508,12 +544,27 @@ export const SECRET_TAGS = {
LIST: {
projectId: "The ID of the project to list tags from."
},
GET_TAG_BY_ID: {
projectId: "The ID of the project to get tags from.",
tagId: "The ID of the tag to get details"
},
GET_TAG_BY_SLUG: {
projectId: "The ID of the project to get tags from.",
tagSlug: "The slug of the tag to get details"
},
CREATE: {
projectId: "The ID of the project to create the tag in.",
name: "The name of the tag to create.",
slug: "The slug of the tag to create.",
color: "The color of the tag to create."
},
UPDATE: {
projectId: "The ID of the project to update the tag in.",
tagId: "The ID of the tag to get details",
name: "The name of the tag to update.",
slug: "The slug of the tag to update.",
color: "The color of the tag to update."
},
DELETE: {
tagId: "The ID of the tag to delete.",
projectId: "The ID of the project to delete the tag from."
@@ -789,6 +840,8 @@ export const CERTIFICATE_AUTHORITIES = {
caId: "The ID of the CA to issue the certificate from",
friendlyName: "A friendly name for the certificate",
commonName: "The common name (CN) for the certificate",
altNames:
"A comma-delimited list of Subject Alternative Names (SANs) for the certificate; these can be host names or email addresses.",
ttl: "The time to live for the certificate such as 1m, 1h, 1d, 1y, ...",
notBefore: "The date and time when the certificate becomes valid in YYYY-MM-DDTHH:mm:ss.sssZ format",
notAfter: "The date and time when the certificate expires in YYYY-MM-DDTHH:mm:ss.sssZ format",

View File

@@ -29,7 +29,7 @@ const envSchema = z
DB_USER: zpStr(z.string().describe("Postgres database username").optional()),
DB_PASSWORD: zpStr(z.string().describe("Postgres database password").optional()),
DB_NAME: zpStr(z.string().describe("Postgres database name").optional()),
BCRYPT_SALT_ROUND: z.number().default(12),
NODE_ENV: z.enum(["development", "test", "production"]).default("production"),
SALT_ROUNDS: z.coerce.number().default(10),
INITIAL_ORGANIZATION_NAME: zpStr(z.string().optional()),

View File

@@ -6,7 +6,7 @@ import tweetnacl from "tweetnacl-util";
import { TUserEncryptionKeys } from "@app/db/schemas";
import { decryptSymmetric, encryptAsymmetric, encryptSymmetric } from "./encryption";
import { decryptSymmetric128BitHexKeyUTF8, encryptAsymmetric, encryptSymmetric } from "./encryption";
export const generateSrpServerKey = async (salt: string, verifier: string) => {
// eslint-disable-next-line new-cap
@@ -97,30 +97,55 @@ export const generateUserSrpKeys = async (email: string, password: string) => {
};
};
export const getUserPrivateKey = async (password: string, user: TUserEncryptionKeys) => {
const derivedKey = await argon2.hash(password, {
salt: Buffer.from(user.salt),
memoryCost: 65536,
timeCost: 3,
parallelism: 1,
hashLength: 32,
type: argon2.argon2id,
raw: true
});
if (!derivedKey) throw new Error("Failed to derive key from password");
const key = decryptSymmetric({
ciphertext: user.protectedKey!,
iv: user.protectedKeyIV!,
tag: user.protectedKeyTag!,
key: derivedKey.toString("base64")
});
const privateKey = decryptSymmetric({
ciphertext: user.encryptedPrivateKey,
iv: user.iv,
tag: user.tag,
key
});
return privateKey;
export const getUserPrivateKey = async (
password: string,
user: Pick<
TUserEncryptionKeys,
| "protectedKeyTag"
| "protectedKey"
| "protectedKeyIV"
| "encryptedPrivateKey"
| "iv"
| "salt"
| "tag"
| "encryptionVersion"
>
) => {
if (user.encryptionVersion === 1) {
return decryptSymmetric128BitHexKeyUTF8({
ciphertext: user.encryptedPrivateKey,
iv: user.iv,
tag: user.tag,
key: password.slice(0, 32).padStart(32 + (password.slice(0, 32).length - new Blob([password]).size), "0")
});
}
if (user.encryptionVersion === 2 && user.protectedKey && user.protectedKeyIV && user.protectedKeyTag) {
const derivedKey = await argon2.hash(password, {
salt: Buffer.from(user.salt),
memoryCost: 65536,
timeCost: 3,
parallelism: 1,
hashLength: 32,
type: argon2.argon2id,
raw: true
});
if (!derivedKey) throw new Error("Failed to derive key from password");
const key = decryptSymmetric128BitHexKeyUTF8({
ciphertext: user.protectedKey,
iv: user.protectedKeyIV,
tag: user.protectedKeyTag,
key: derivedKey
});
const privateKey = decryptSymmetric128BitHexKeyUTF8({
ciphertext: user.encryptedPrivateKey,
iv: user.iv,
tag: user.tag,
key: Buffer.from(key, "hex")
});
return privateKey;
}
throw new Error(`GetUserPrivateKey: Encryption version not found`);
};
export const buildUserProjectKey = async (privateKey: string, publickey: string) => {

View File

@@ -59,6 +59,18 @@ export class BadRequestError extends Error {
}
}
export class NotFoundError extends Error {
name: string;
error: unknown;
constructor({ name, error, message }: { message?: string; name?: string; error?: unknown }) {
super(message ?? "The requested entity is not found");
this.name = name || "NotFound";
this.error = error;
}
}
export class DisableRotationErrors extends Error {
name: string;

View File

@@ -6,6 +6,7 @@ import {
BadRequestError,
DatabaseError,
InternalServerError,
NotFoundError,
ScimRequestError,
UnauthorizedError
} from "@app/lib/errors";
@@ -15,6 +16,8 @@ export const fastifyErrHandler = fastifyPlugin(async (server: FastifyZodProvider
req.log.error(error);
if (error instanceof BadRequestError) {
void res.status(400).send({ statusCode: 400, message: error.message, error: error.name });
} else if (error instanceof NotFoundError) {
void res.status(404).send({ statusCode: 404, message: error.message, error: error.name });
} else if (error instanceof UnauthorizedError) {
void res.status(403).send({ statusCode: 403, message: error.message, error: error.name });
} else if (error instanceof DatabaseError || error instanceof InternalServerError) {

View File

@@ -32,6 +32,8 @@ import { ldapConfigServiceFactory } from "@app/ee/services/ldap-config/ldap-conf
import { ldapGroupMapDALFactory } from "@app/ee/services/ldap-config/ldap-group-map-dal";
import { licenseDALFactory } from "@app/ee/services/license/license-dal";
import { licenseServiceFactory } from "@app/ee/services/license/license-service";
import { oidcConfigDALFactory } from "@app/ee/services/oidc/oidc-config-dal";
import { oidcConfigServiceFactory } from "@app/ee/services/oidc/oidc-config-service";
import { permissionDALFactory } from "@app/ee/services/permission/permission-dal";
import { permissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { projectUserAdditionalPrivilegeDALFactory } from "@app/ee/services/project-user-additional-privilege/project-user-additional-privilege-dal";
@@ -250,6 +252,7 @@ export const registerRoutes = async (
const ldapConfigDAL = ldapConfigDALFactory(db);
const ldapGroupMapDAL = ldapGroupMapDALFactory(db);
const oidcConfigDAL = oidcConfigDALFactory(db);
const accessApprovalPolicyDAL = accessApprovalPolicyDALFactory(db);
const accessApprovalRequestDAL = accessApprovalRequestDALFactory(db);
const accessApprovalPolicyApproverDAL = accessApprovalPolicyApproverDALFactory(db);
@@ -392,7 +395,9 @@ export const registerRoutes = async (
userDAL,
userAliasDAL,
permissionService,
licenseService
licenseService,
tokenService,
smtpService
});
const telemetryService = telemetryServiceFactory({
@@ -903,6 +908,19 @@ export const registerRoutes = async (
secretSharingDAL
});
const oidcService = oidcConfigServiceFactory({
orgDAL,
orgMembershipDAL,
userDAL,
userAliasDAL,
licenseService,
tokenService,
smtpService,
orgBotDAL,
permissionService,
oidcConfigDAL
});
await superAdminService.initServerCfg();
//
// setup the communication with license key server
@@ -923,6 +941,7 @@ export const registerRoutes = async (
permission: permissionService,
org: orgService,
orgRole: orgRoleService,
oidc: oidcService,
apiKey: apiKeyService,
authToken: tokenService,
superAdmin: superAdminService,

View File

@@ -22,6 +22,7 @@ export const registerAdminRouter = async (server: FastifyZodProvider) => {
200: z.object({
config: SuperAdminSchema.omit({ createdAt: true, updatedAt: true }).extend({
isMigrationModeOn: z.boolean(),
defaultAuthOrgSlug: z.string().nullable(),
isSecretScanningDisabled: z.boolean()
})
})
@@ -51,11 +52,15 @@ export const registerAdminRouter = async (server: FastifyZodProvider) => {
allowSignUp: z.boolean().optional(),
allowedSignUpDomain: z.string().optional().nullable(),
trustSamlEmails: z.boolean().optional(),
trustLdapEmails: z.boolean().optional()
trustLdapEmails: z.boolean().optional(),
trustOidcEmails: z.boolean().optional(),
defaultAuthOrgId: z.string().optional().nullable()
}),
response: {
200: z.object({
config: SuperAdminSchema
config: SuperAdminSchema.extend({
defaultAuthOrgSlug: z.string().nullable()
})
})
}
},
@@ -79,6 +84,7 @@ export const registerAdminRouter = async (server: FastifyZodProvider) => {
schema: {
body: z.object({
email: z.string().email().trim(),
password: z.string().trim(),
firstName: z.string().trim(),
lastName: z.string().trim().optional(),
protectedKey: z.string().trim(),

View File

@@ -9,7 +9,10 @@ import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type";
import { CertKeyAlgorithm } from "@app/services/certificate/certificate-types";
import { CaStatus, CaType } from "@app/services/certificate-authority/certificate-authority-types";
import { validateCaDateField } from "@app/services/certificate-authority/certificate-authority-validators";
import {
validateAltNamesField,
validateCaDateField
} from "@app/services/certificate-authority/certificate-authority-validators";
export const registerCaRouter = async (server: FastifyZodProvider) => {
server.route({
@@ -452,6 +455,7 @@ export const registerCaRouter = async (server: FastifyZodProvider) => {
.object({
friendlyName: z.string().optional().describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.friendlyName),
commonName: z.string().trim().min(1).describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.commonName),
altNames: validateAltNamesField.describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.altNames),
ttl: z
.string()
.refine((val) => ms(val) > 0, "TTL must be a positive number")

View File

@@ -266,4 +266,51 @@ export const registerIdentityAwsAuthRouter = async (server: FastifyZodProvider)
return { identityAwsAuth };
}
});
server.route({
method: "DELETE",
url: "/aws-auth/identities/:identityId",
config: {
rateLimit: writeLimit
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: {
description: "Delete AWS Auth configuration on identity",
security: [
{
bearerAuth: []
}
],
params: z.object({
identityId: z.string().describe(AWS_AUTH.REVOKE.identityId)
}),
response: {
200: z.object({
identityAwsAuth: IdentityAwsAuthsSchema
})
}
},
handler: async (req) => {
const identityAwsAuth = await server.services.identityAwsAuth.revokeIdentityAwsAuth({
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
identityId: req.params.identityId
});
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
orgId: identityAwsAuth.orgId,
event: {
type: EventType.REVOKE_IDENTITY_AWS_AUTH,
metadata: {
identityId: identityAwsAuth.identityId
}
}
});
return { identityAwsAuth };
}
});
};

View File

@@ -2,6 +2,7 @@ import { z } from "zod";
import { IdentityAzureAuthsSchema } from "@app/db/schemas";
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
import { AZURE_AUTH } from "@app/lib/api-docs";
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";
@@ -259,4 +260,51 @@ export const registerIdentityAzureAuthRouter = async (server: FastifyZodProvider
return { identityAzureAuth };
}
});
server.route({
method: "DELETE",
url: "/azure-auth/identities/:identityId",
config: {
rateLimit: writeLimit
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: {
description: "Delete Azure Auth configuration on identity",
security: [
{
bearerAuth: []
}
],
params: z.object({
identityId: z.string().describe(AZURE_AUTH.REVOKE.identityId)
}),
response: {
200: z.object({
identityAzureAuth: IdentityAzureAuthsSchema
})
}
},
handler: async (req) => {
const identityAzureAuth = await server.services.identityAzureAuth.revokeIdentityAzureAuth({
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
identityId: req.params.identityId
});
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
orgId: identityAzureAuth.orgId,
event: {
type: EventType.REVOKE_IDENTITY_AZURE_AUTH,
metadata: {
identityId: identityAzureAuth.identityId
}
}
});
return { identityAzureAuth };
}
});
};

View File

@@ -2,6 +2,7 @@ import { z } from "zod";
import { IdentityGcpAuthsSchema } from "@app/db/schemas";
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
import { GCP_AUTH } from "@app/lib/api-docs";
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";
@@ -265,4 +266,51 @@ export const registerIdentityGcpAuthRouter = async (server: FastifyZodProvider)
return { identityGcpAuth };
}
});
server.route({
method: "DELETE",
url: "/gcp-auth/identities/:identityId",
config: {
rateLimit: writeLimit
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: {
description: "Delete GCP Auth configuration on identity",
security: [
{
bearerAuth: []
}
],
params: z.object({
identityId: z.string().describe(GCP_AUTH.REVOKE.identityId)
}),
response: {
200: z.object({
identityGcpAuth: IdentityGcpAuthsSchema
})
}
},
handler: async (req) => {
const identityGcpAuth = await server.services.identityGcpAuth.revokeIdentityGcpAuth({
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
identityId: req.params.identityId
});
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
orgId: identityGcpAuth.orgId,
event: {
type: EventType.REVOKE_IDENTITY_GCP_AUTH,
metadata: {
identityId: identityGcpAuth.identityId
}
}
});
return { identityGcpAuth };
}
});
};

View File

@@ -2,6 +2,7 @@ import { z } from "zod";
import { IdentityKubernetesAuthsSchema } from "@app/db/schemas";
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
import { KUBERNETES_AUTH } from "@app/lib/api-docs";
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";
@@ -198,7 +199,7 @@ export const registerIdentityKubernetesRouter = async (server: FastifyZodProvide
}),
response: {
200: z.object({
identityKubernetesAuth: IdentityKubernetesAuthsSchema
identityKubernetesAuth: IdentityKubernetesAuthResponseSchema
})
}
},
@@ -280,4 +281,54 @@ export const registerIdentityKubernetesRouter = async (server: FastifyZodProvide
return { identityKubernetesAuth: IdentityKubernetesAuthResponseSchema.parse(identityKubernetesAuth) };
}
});
server.route({
method: "DELETE",
url: "/kubernetes-auth/identities/:identityId",
config: {
rateLimit: writeLimit
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: {
description: "Delete Kubernetes Auth configuration on identity",
security: [
{
bearerAuth: []
}
],
params: z.object({
identityId: z.string().describe(KUBERNETES_AUTH.REVOKE.identityId)
}),
response: {
200: z.object({
identityKubernetesAuth: IdentityKubernetesAuthResponseSchema.omit({
caCert: true,
tokenReviewerJwt: true
})
})
}
},
handler: async (req) => {
const identityKubernetesAuth = await server.services.identityKubernetesAuth.revokeIdentityKubernetesAuth({
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
identityId: req.params.identityId
});
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
orgId: identityKubernetesAuth.orgId,
event: {
type: EventType.REVOKE_IDENTITY_KUBERNETES_AUTH,
metadata: {
identityId: identityKubernetesAuth.identityId
}
}
});
return { identityKubernetesAuth };
}
});
};

View File

@@ -1,9 +1,9 @@
import { z } from "zod";
import { IdentitiesSchema, OrgMembershipRole } from "@app/db/schemas";
import { IdentitiesSchema, IdentityOrgMembershipsSchema, OrgMembershipRole, OrgRolesSchema } from "@app/db/schemas";
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
import { IDENTITIES } from "@app/lib/api-docs";
import { creationLimit, writeLimit } from "@app/server/config/rateLimiter";
import { creationLimit, readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { getTelemetryDistinctId } from "@app/server/lib/telemetry";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type";
@@ -170,4 +170,94 @@ export const registerIdentityRouter = async (server: FastifyZodProvider) => {
return { identity };
}
});
server.route({
method: "GET",
url: "/:identityId",
config: {
rateLimit: readLimit
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: {
description: "Get an identity by id",
security: [
{
bearerAuth: []
}
],
params: z.object({
identityId: z.string().describe(IDENTITIES.GET_BY_ID.identityId)
}),
response: {
200: z.object({
identity: IdentityOrgMembershipsSchema.extend({
customRole: OrgRolesSchema.pick({
id: true,
name: true,
slug: true,
permissions: true,
description: true
}).optional(),
identity: IdentitiesSchema.pick({ name: true, id: true, authMethod: true })
})
})
}
},
handler: async (req) => {
const identity = await server.services.identity.getIdentityById({
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
id: req.params.identityId
});
return { identity };
}
});
server.route({
method: "GET",
url: "/",
config: {
rateLimit: writeLimit
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: {
description: "List identities",
security: [
{
bearerAuth: []
}
],
querystring: z.object({
orgId: z.string().describe(IDENTITIES.LIST.orgId)
}),
response: {
200: z.object({
identities: IdentityOrgMembershipsSchema.extend({
customRole: OrgRolesSchema.pick({
id: true,
name: true,
slug: true,
permissions: true,
description: true
}).optional(),
identity: IdentitiesSchema.pick({ name: true, id: true, authMethod: true })
}).array()
})
}
},
handler: async (req) => {
const identities = await server.services.identity.listOrgIdentities({
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
orgId: req.query.orgId
});
return { identities };
}
});
};

View File

@@ -134,7 +134,7 @@ export const registerIdentityUaRouter = async (server: FastifyZodProvider) => {
}
},
handler: async (req) => {
const identityUniversalAuth = await server.services.identityUa.attachUa({
const identityUniversalAuth = await server.services.identityUa.attachUniversalAuth({
actor: req.permission.type,
actorId: req.permission.id,
actorOrgId: req.permission.orgId,
@@ -219,7 +219,7 @@ export const registerIdentityUaRouter = async (server: FastifyZodProvider) => {
}
},
handler: async (req) => {
const identityUniversalAuth = await server.services.identityUa.updateUa({
const identityUniversalAuth = await server.services.identityUa.updateUniversalAuth({
actor: req.permission.type,
actorId: req.permission.id,
actorOrgId: req.permission.orgId,
@@ -272,7 +272,7 @@ export const registerIdentityUaRouter = async (server: FastifyZodProvider) => {
}
},
handler: async (req) => {
const identityUniversalAuth = await server.services.identityUa.getIdentityUa({
const identityUniversalAuth = await server.services.identityUa.getIdentityUniversalAuth({
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
@@ -295,6 +295,53 @@ export const registerIdentityUaRouter = async (server: FastifyZodProvider) => {
}
});
server.route({
method: "DELETE",
url: "/universal-auth/identities/:identityId",
config: {
rateLimit: writeLimit
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: {
description: "Delete Universal Auth configuration on identity",
security: [
{
bearerAuth: []
}
],
params: z.object({
identityId: z.string().describe(UNIVERSAL_AUTH.REVOKE.identityId)
}),
response: {
200: z.object({
identityUniversalAuth: IdentityUniversalAuthsSchema
})
}
},
handler: async (req) => {
const identityUniversalAuth = await server.services.identityUa.revokeIdentityUniversalAuth({
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
identityId: req.params.identityId
});
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
orgId: identityUniversalAuth.orgId,
event: {
type: EventType.REVOKE_IDENTITY_UNIVERSAL_AUTH,
metadata: {
identityId: identityUniversalAuth.identityId
}
}
});
return { identityUniversalAuth };
}
});
server.route({
method: "POST",
url: "/universal-auth/identities/:identityId/client-secrets",
@@ -325,14 +372,15 @@ export const registerIdentityUaRouter = async (server: FastifyZodProvider) => {
}
},
handler: async (req) => {
const { clientSecret, clientSecretData, orgId } = await server.services.identityUa.createUaClientSecret({
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
identityId: req.params.identityId,
...req.body
});
const { clientSecret, clientSecretData, orgId } =
await server.services.identityUa.createUniversalAuthClientSecret({
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
identityId: req.params.identityId,
...req.body
});
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
@@ -374,13 +422,15 @@ export const registerIdentityUaRouter = async (server: FastifyZodProvider) => {
}
},
handler: async (req) => {
const { clientSecrets: clientSecretData, orgId } = await server.services.identityUa.getUaClientSecrets({
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
identityId: req.params.identityId
});
const { clientSecrets: clientSecretData, orgId } = await server.services.identityUa.getUniversalAuthClientSecrets(
{
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
identityId: req.params.identityId
}
);
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
@@ -396,6 +446,56 @@ export const registerIdentityUaRouter = async (server: FastifyZodProvider) => {
}
});
server.route({
method: "GET",
url: "/universal-auth/identities/:identityId/client-secrets/:clientSecretId",
config: {
rateLimit: readLimit
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: {
description: "Get Universal Auth Client Secret for identity",
security: [
{
bearerAuth: []
}
],
params: z.object({
identityId: z.string().describe(UNIVERSAL_AUTH.GET_CLIENT_SECRET.identityId),
clientSecretId: z.string().describe(UNIVERSAL_AUTH.GET_CLIENT_SECRET.clientSecretId)
}),
response: {
200: z.object({
clientSecretData: sanitizedClientSecretSchema
})
}
},
handler: async (req) => {
const clientSecretData = await server.services.identityUa.getUniversalAuthClientSecretById({
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
identityId: req.params.identityId,
clientSecretId: req.params.clientSecretId
});
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
orgId: clientSecretData.orgId,
event: {
type: EventType.REVOKE_IDENTITY_UNIVERSAL_AUTH_CLIENT_SECRET,
metadata: {
identityId: clientSecretData.identityId,
clientSecretId: clientSecretData.id
}
}
});
return { clientSecretData };
}
});
server.route({
method: "POST",
url: "/universal-auth/identities/:identityId/client-secrets/:clientSecretId/revoke",
@@ -421,7 +521,7 @@ export const registerIdentityUaRouter = async (server: FastifyZodProvider) => {
}
},
handler: async (req) => {
const clientSecretData = await server.services.identityUa.revokeUaClientSecret({
const clientSecretData = await server.services.identityUa.revokeUniversalAuthClientSecret({
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,

View File

@@ -9,7 +9,7 @@ import { registerIdentityAzureAuthRouter } from "./identity-azure-auth-router";
import { registerIdentityGcpAuthRouter } from "./identity-gcp-auth-router";
import { registerIdentityKubernetesRouter } from "./identity-kubernetes-auth-router";
import { registerIdentityRouter } from "./identity-router";
import { registerIdentityUaRouter } from "./identity-ua";
import { registerIdentityUaRouter } from "./identity-universal-auth-router";
import { registerIntegrationAuthRouter } from "./integration-auth-router";
import { registerIntegrationRouter } from "./integration-router";
import { registerInviteOrgRouter } from "./invite-org-router";

View File

@@ -51,7 +51,8 @@ export const registerPasswordRouter = async (server: FastifyZodProvider) => {
encryptedPrivateKeyIV: z.string().trim(),
encryptedPrivateKeyTag: z.string().trim(),
salt: z.string().trim(),
verifier: z.string().trim()
verifier: z.string().trim(),
password: z.string().trim()
}),
response: {
200: z.object({

View File

@@ -309,4 +309,32 @@ export const registerProjectMembershipRouter = async (server: FastifyZodProvider
return { membership };
}
});
server.route({
method: "DELETE",
url: "/:workspaceId/leave",
config: {
rateLimit: writeLimit
},
schema: {
params: z.object({
workspaceId: z.string().trim()
}),
response: {
200: z.object({
membership: ProjectMembershipsSchema
})
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
const membership = await server.services.projectMembership.leaveProject({
actorId: req.permission.id,
actor: req.permission.type,
projectId: req.params.workspaceId
});
return { membership };
}
});
};

View File

@@ -372,6 +372,44 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
}
});
server.route({
method: "PUT",
url: "/:workspaceSlug/audit-logs-retention",
config: {
rateLimit: writeLimit
},
schema: {
params: z.object({
workspaceSlug: z.string().trim()
}),
body: z.object({
auditLogsRetentionDays: z.number().min(0)
}),
response: {
200: z.object({
message: z.string(),
workspace: ProjectsSchema
})
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
const workspace = await server.services.project.updateAuditLogsRetention({
actorId: req.permission.id,
actor: req.permission.type,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
workspaceSlug: req.params.workspaceSlug,
auditLogsRetentionDays: req.body.auditLogsRetentionDays
});
return {
message: "Successfully updated project's audit logs retention period",
workspace
};
}
});
server.route({
method: "GET",
url: "/:workspaceId/integrations",

View File

@@ -36,6 +36,67 @@ export const registerSecretTagRouter = async (server: FastifyZodProvider) => {
}
});
server.route({
method: "GET",
url: "/:projectId/tags/:tagId",
config: {
rateLimit: readLimit
},
schema: {
params: z.object({
projectId: z.string().trim().describe(SECRET_TAGS.GET_TAG_BY_ID.projectId),
tagId: z.string().trim().describe(SECRET_TAGS.GET_TAG_BY_ID.tagId)
}),
response: {
200: z.object({
workspaceTag: SecretTagsSchema
})
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const workspaceTag = await server.services.secretTag.getTagById({
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
id: req.params.tagId
});
return { workspaceTag };
}
});
server.route({
method: "GET",
url: "/:projectId/tags/slug/:tagSlug",
config: {
rateLimit: readLimit
},
schema: {
params: z.object({
projectId: z.string().trim().describe(SECRET_TAGS.GET_TAG_BY_SLUG.projectId),
tagSlug: z.string().trim().describe(SECRET_TAGS.GET_TAG_BY_SLUG.tagSlug)
}),
response: {
200: z.object({
workspaceTag: SecretTagsSchema
})
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const workspaceTag = await server.services.secretTag.getTagBySlug({
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
slug: req.params.tagSlug,
projectId: req.params.projectId
});
return { workspaceTag };
}
});
server.route({
method: "POST",
url: "/:projectId/tags",
@@ -71,6 +132,42 @@ export const registerSecretTagRouter = async (server: FastifyZodProvider) => {
}
});
server.route({
method: "PATCH",
url: "/:projectId/tags/:tagId",
config: {
rateLimit: writeLimit
},
schema: {
params: z.object({
projectId: z.string().trim().describe(SECRET_TAGS.UPDATE.projectId),
tagId: z.string().trim().describe(SECRET_TAGS.UPDATE.tagId)
}),
body: z.object({
name: z.string().trim().describe(SECRET_TAGS.UPDATE.name),
slug: z.string().trim().describe(SECRET_TAGS.UPDATE.slug),
color: z.string().trim().describe(SECRET_TAGS.UPDATE.color)
}),
response: {
200: z.object({
workspaceTag: SecretTagsSchema
})
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const workspaceTag = await server.services.secretTag.updateTag({
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
...req.body,
id: req.params.tagId
});
return { workspaceTag };
}
});
server.route({
method: "DELETE",
url: "/:projectId/tags/:tagId",

View File

@@ -259,4 +259,50 @@ export const registerSsoRouter = async (server: FastifyZodProvider) => {
);
}
});
server.route({
url: "/token-exchange",
method: "POST",
schema: {
body: z.object({
providerAuthToken: z.string(),
email: z.string()
})
},
handler: async (req, res) => {
const userAgent = req.headers["user-agent"];
if (!userAgent) throw new Error("user agent header is required");
const data = await server.services.login.oauth2TokenExchange({
email: req.body.email,
ip: req.realIp,
userAgent,
providerAuthToken: req.body.providerAuthToken
});
if (data.isMfaEnabled) {
return { mfaEnabled: true, token: data.token } as const; // for discriminated union
}
void res.setCookie("jid", data.token.refresh, {
httpOnly: true,
path: "/",
sameSite: "strict",
secure: appCfg.HTTPS_ENABLED
});
return {
mfaEnabled: false,
encryptionVersion: data.user.encryptionVersion,
token: data.token.access,
publicKey: data.user.publicKey,
encryptedPrivateKey: data.user.encryptedPrivateKey,
iv: data.user.iv,
tag: data.user.tag,
protectedKey: data.user.protectedKey || null,
protectedKeyIV: data.user.protectedKeyIV || null,
protectedKeyTag: data.user.protectedKeyTag || null
} as const;
}
});
};

View File

@@ -19,7 +19,23 @@ export const registerUserRouter = async (server: FastifyZodProvider) => {
schema: {
response: {
200: z.object({
user: UsersSchema.merge(UserEncryptionKeysSchema.omit({ verifier: true }))
user: UsersSchema.merge(
UserEncryptionKeysSchema.pick({
clientPublicKey: true,
serverPrivateKey: true,
encryptionVersion: true,
protectedKey: true,
protectedKeyIV: true,
protectedKeyTag: true,
publicKey: true,
encryptedPrivateKey: true,
iv: true,
tag: true,
salt: true,
verifier: true,
userId: true
})
)
})
}
},
@@ -30,6 +46,26 @@ export const registerUserRouter = async (server: FastifyZodProvider) => {
}
});
server.route({
method: "GET",
url: "/private-key",
config: {
rateLimit: readLimit
},
schema: {
response: {
200: z.object({
privateKey: z.string()
})
}
},
onRequest: verifyAuth([AuthMode.JWT], { requireOrg: false }),
handler: async (req) => {
const privateKey = await server.services.user.getUserPrivateKey(req.permission.id);
return { privateKey };
}
});
server.route({
method: "GET",
url: "/:userId/unlock",

View File

@@ -255,7 +255,23 @@ export const registerUserRouter = async (server: FastifyZodProvider) => {
description: "Retrieve the current user on the request",
response: {
200: z.object({
user: UsersSchema.merge(UserEncryptionKeysSchema.omit({ verifier: true }))
user: UsersSchema.merge(
UserEncryptionKeysSchema.pick({
clientPublicKey: true,
serverPrivateKey: true,
encryptionVersion: true,
protectedKey: true,
protectedKeyIV: true,
protectedKeyTag: true,
publicKey: true,
encryptedPrivateKey: true,
iv: true,
tag: true,
salt: true,
verifier: true,
userId: true
})
)
})
}
},

View File

@@ -81,7 +81,8 @@ export const registerLoginRouter = async (server: FastifyZodProvider) => {
email: z.string().trim(),
providerAuthToken: z.string().trim().optional(),
clientProof: z.string().trim(),
captchaToken: z.string().trim().optional()
captchaToken: z.string().trim().optional(),
password: z.string().optional()
}),
response: {
200: z.discriminatedUnion("mfaEnabled", [
@@ -112,7 +113,8 @@ export const registerLoginRouter = async (server: FastifyZodProvider) => {
ip: req.realIp,
userAgent,
providerAuthToken: req.body.providerAuthToken,
clientProof: req.body.clientProof
clientProof: req.body.clientProof,
password: req.body.password
});
if (data.isMfaEnabled) {

View File

@@ -8,7 +8,7 @@ import {
SecretType,
ServiceTokenScopes
} from "@app/db/schemas";
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
import { EventType, UserAgentType } from "@app/ee/services/audit-log/audit-log-types";
import { RAW_SECRETS, SECRETS } from "@app/lib/api-docs";
import { BadRequestError } from "@app/lib/errors";
import { removeTrailingSlash } from "@app/lib/fn";
@@ -259,18 +259,20 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
}
});
await server.services.telemetry.sendPostHogEvents({
event: PostHogEventTypes.SecretPulled,
distinctId: getTelemetryDistinctId(req),
properties: {
numberOfSecrets: secrets.length,
workspaceId,
environment,
secretPath: req.query.secretPath,
channel: getUserAgentType(req.headers["user-agent"]),
...req.auditLogInfo
}
});
if (getUserAgentType(req.headers["user-agent"]) !== UserAgentType.K8_OPERATOR) {
await server.services.telemetry.sendPostHogEvents({
event: PostHogEventTypes.SecretPulled,
distinctId: getTelemetryDistinctId(req),
properties: {
numberOfSecrets: secrets.length,
workspaceId,
environment,
secretPath: req.query.secretPath,
channel: getUserAgentType(req.headers["user-agent"]),
...req.auditLogInfo
}
});
}
return { secrets, imports };
}
});
@@ -298,6 +300,11 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
secretPath: z.string().trim().default("/").transform(removeTrailingSlash).describe(RAW_SECRETS.GET.secretPath),
version: z.coerce.number().optional().describe(RAW_SECRETS.GET.version),
type: z.nativeEnum(SecretType).default(SecretType.Shared).describe(RAW_SECRETS.GET.type),
expandSecretReferences: z
.enum(["true", "false"])
.default("false")
.transform((value) => value === "true")
.describe(RAW_SECRETS.GET.expand),
include_imports: z
.enum(["true", "false"])
.default("false")
@@ -342,6 +349,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
actor: req.permission.type,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
expandSecretReferences: req.query.expandSecretReferences,
environment,
projectId: workspaceId,
projectSlug: workspaceSlug,
@@ -367,18 +375,20 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
}
});
await server.services.telemetry.sendPostHogEvents({
event: PostHogEventTypes.SecretPulled,
distinctId: getTelemetryDistinctId(req),
properties: {
numberOfSecrets: 1,
workspaceId: secret.workspace,
environment,
secretPath: req.query.secretPath,
channel: getUserAgentType(req.headers["user-agent"]),
...req.auditLogInfo
}
});
if (getUserAgentType(req.headers["user-agent"]) !== UserAgentType.K8_OPERATOR) {
await server.services.telemetry.sendPostHogEvents({
event: PostHogEventTypes.SecretPulled,
distinctId: getTelemetryDistinctId(req),
properties: {
numberOfSecrets: 1,
workspaceId: secret.workspace,
environment,
secretPath: req.query.secretPath,
channel: getUserAgentType(req.headers["user-agent"]),
...req.auditLogInfo
}
});
}
return { secret };
}
});
@@ -723,24 +733,22 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
});
// TODO: Move to telemetry plugin
let shouldRecordK8Event = false;
if (req.headers["user-agent"] === "k8-operatoer") {
const randomNumber = Math.random();
if (randomNumber > 0.95) {
shouldRecordK8Event = true;
}
}
// let shouldRecordK8Event = false;
// if (req.headers["user-agent"] === "k8-operatoer") {
// const randomNumber = Math.random();
// if (randomNumber > 0.95) {
// shouldRecordK8Event = true;
// }
// }
const shouldCapture =
req.query.workspaceId !== "650e71fbae3e6c8572f436d4" &&
(req.headers["user-agent"] !== "k8-operator" || shouldRecordK8Event);
const approximateNumberTotalSecrets = secrets.length * 20;
req.query.workspaceId !== "650e71fbae3e6c8572f436d4" && req.headers["user-agent"] !== "k8-operator";
if (shouldCapture) {
await server.services.telemetry.sendPostHogEvents({
event: PostHogEventTypes.SecretPulled,
distinctId: getTelemetryDistinctId(req),
properties: {
numberOfSecrets: shouldRecordK8Event ? approximateNumberTotalSecrets : secrets.length,
numberOfSecrets: secrets.length,
workspaceId: req.query.workspaceId,
environment: req.query.environment,
secretPath: req.query.secretPath,
@@ -817,18 +825,20 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
}
});
await server.services.telemetry.sendPostHogEvents({
event: PostHogEventTypes.SecretPulled,
distinctId: getTelemetryDistinctId(req),
properties: {
numberOfSecrets: 1,
workspaceId: req.query.workspaceId,
environment: req.query.environment,
secretPath: req.query.secretPath,
channel: getUserAgentType(req.headers["user-agent"]),
...req.auditLogInfo
}
});
if (getUserAgentType(req.headers["user-agent"]) !== UserAgentType.K8_OPERATOR) {
await server.services.telemetry.sendPostHogEvents({
event: PostHogEventTypes.SecretPulled,
distinctId: getTelemetryDistinctId(req),
properties: {
numberOfSecrets: 1,
workspaceId: req.query.workspaceId,
environment: req.query.environment,
secretPath: req.query.secretPath,
channel: getUserAgentType(req.headers["user-agent"]),
...req.auditLogInfo
}
});
}
return { secret };
}
});

View File

@@ -102,7 +102,8 @@ export const registerSignupRouter = async (server: FastifyZodProvider) => {
verifier: z.string().trim(),
organizationName: z.string().trim().min(1),
providerAuthToken: z.string().trim().optional().nullish(),
attributionSource: z.string().trim().optional()
attributionSource: z.string().trim().optional(),
password: z.string()
}),
response: {
200: z.object({
@@ -167,6 +168,7 @@ export const registerSignupRouter = async (server: FastifyZodProvider) => {
schema: {
body: z.object({
email: z.string().email().trim(),
password: z.string(),
firstName: z.string().trim(),
lastName: z.string().trim().optional(),
protectedKey: z.string().trim(),

View File

@@ -15,10 +15,10 @@ export const validateProviderAuthToken = (providerToken: string, username?: stri
if (decodedToken.username !== username) throw new Error("Invalid auth credentials");
if (decodedToken.organizationId) {
return { orgId: decodedToken.organizationId, authMethod: decodedToken.authMethod };
return { orgId: decodedToken.organizationId, authMethod: decodedToken.authMethod, userName: decodedToken.username };
}
return { authMethod: decodedToken.authMethod, orgId: null };
return { authMethod: decodedToken.authMethod, orgId: null, userName: decodedToken.username };
};
export const validateSignUpAuthorization = (token: string, userId: string, validate = true) => {

View File

@@ -1,3 +1,4 @@
import bcrypt from "bcrypt";
import jwt from "jsonwebtoken";
import { TUsers, UserDeviceSchema } from "@app/db/schemas";
@@ -5,7 +6,10 @@ import { isAuthMethodSaml } from "@app/ee/services/permission/permission-fns";
import { getConfig } from "@app/lib/config/env";
import { request } from "@app/lib/config/request";
import { generateSrpServerKey, srpCheckClientProof } from "@app/lib/crypto";
import { infisicalSymmetricEncypt } from "@app/lib/crypto/encryption";
import { getUserPrivateKey } from "@app/lib/crypto/srp";
import { BadRequestError, DatabaseError, UnauthorizedError } from "@app/lib/errors";
import { logger } from "@app/lib/logger";
import { getServerCfg } from "@app/services/super-admin/super-admin-service";
import { TTokenDALFactory } from "../auth-token/auth-token-dal";
@@ -19,6 +23,7 @@ import {
TLoginClientProofDTO,
TLoginGenServerPublicKeyDTO,
TOauthLoginDTO,
TOauthTokenExchangeDTO,
TVerifyMfaTokenDTO
} from "./auth-login-type";
import { AuthMethod, AuthModeJwtTokenPayload, AuthModeMfaJwtTokenPayload, AuthTokenType } from "./auth-type";
@@ -101,7 +106,7 @@ export const authLoginServiceFactory = ({
user: TUsers;
ip: string;
userAgent: string;
organizationId: string | undefined;
organizationId?: string;
authMethod: AuthMethod;
}) => {
const cfg = getConfig();
@@ -178,7 +183,8 @@ export const authLoginServiceFactory = ({
ip,
userAgent,
providerAuthToken,
captchaToken
captchaToken,
password
}: TLoginClientProofDTO) => {
const appCfg = getConfig();
@@ -196,7 +202,10 @@ export const authLoginServiceFactory = ({
const decodedProviderToken = validateProviderAuthToken(providerAuthToken, email);
authMethod = decodedProviderToken.authMethod;
if ((isAuthMethodSaml(authMethod) || authMethod === AuthMethod.LDAP) && decodedProviderToken.orgId) {
if (
(isAuthMethodSaml(authMethod) || [AuthMethod.LDAP, AuthMethod.OIDC].includes(authMethod)) &&
decodedProviderToken.orgId
) {
organizationId = decodedProviderToken.orgId;
}
}
@@ -248,14 +257,35 @@ export const authLoginServiceFactory = ({
throw new Error("Failed to authenticate. Try again?");
}
await userDAL.updateUserEncryptionByUserId(userEnc.userId, {
serverPrivateKey: null,
clientPublicKey: null
});
await userDAL.updateById(userEnc.userId, {
consecutiveFailedPasswordAttempts: 0
});
// from password decrypt the private key
if (password) {
const privateKey = await getUserPrivateKey(password, userEnc).catch((err) => {
logger.error(
err,
`loginExchangeClientProof: private key generation failed for [userId=${user.id}] and [email=${user.email}] `
);
return "";
});
const hashedPassword = await bcrypt.hash(password, cfg.BCRYPT_SALT_ROUND);
const { iv, tag, ciphertext, encoding } = infisicalSymmetricEncypt(privateKey);
await userDAL.updateUserEncryptionByUserId(userEnc.userId, {
serverPrivateKey: null,
clientPublicKey: null,
hashedPassword,
serverEncryptedPrivateKey: ciphertext,
serverEncryptedPrivateKeyIV: iv,
serverEncryptedPrivateKeyTag: tag,
serverEncryptedPrivateKeyEncoding: encoding
});
} else {
await userDAL.updateUserEncryptionByUserId(userEnc.userId, {
serverPrivateKey: null,
clientPublicKey: null
});
}
// send multi factor auth token if they it enabled
if (userEnc.isMfaEnabled && userEnc.email) {
@@ -324,9 +354,12 @@ export const authLoginServiceFactory = ({
// Check if the user actually has access to the specified organization.
const userOrgs = await orgDAL.findAllOrgsByUserId(user.id);
const hasOrganizationMembership = userOrgs.some((org) => org.id === organizationId);
const selectedOrg = await orgDAL.findById(organizationId);
if (!hasOrganizationMembership) {
throw new UnauthorizedError({ message: "User does not have access to the organization" });
throw new UnauthorizedError({
message: `User does not have access to the organization named ${selectedOrg?.name}`
});
}
await tokenDAL.incrementTokenSessionVersion(user.id, decodedToken.tokenVersionId);
@@ -499,8 +532,14 @@ export const authLoginServiceFactory = ({
authMethods: [authMethod],
isGhost: false
});
} else {
const isLinkingRequired = !user?.authMethods?.includes(authMethod);
if (isLinkingRequired) {
user = await userDAL.updateById(user.id, { authMethods: [...(user.authMethods || []), authMethod] });
}
}
const isLinkingRequired = !user?.authMethods?.includes(authMethod);
const userEnc = await userDAL.findUserEncKeyByUserId(user.id);
const isUserCompleted = user.isAccepted;
const providerAuthToken = jwt.sign(
{
@@ -511,9 +550,9 @@ export const authLoginServiceFactory = ({
isEmailVerified: user.isEmailVerified,
firstName: user.firstName,
lastName: user.lastName,
hasExchangedPrivateKey: Boolean(userEnc?.serverEncryptedPrivateKey),
authMethod,
isUserCompleted,
isLinkingRequired,
...(callbackPort
? {
callbackPort
@@ -525,10 +564,72 @@ export const authLoginServiceFactory = ({
expiresIn: appCfg.JWT_PROVIDER_AUTH_LIFETIME
}
);
return { isUserCompleted, providerAuthToken };
};
/**
* Handles OAuth2 token exchange for user login with private key handoff.
*
* The process involves exchanging a provider's authorization token for an Infisical access token.
* The provider token is returned to the client, who then sends it back to obtain the Infisical access token.
*
* This approach is used instead of directly sending the access token for the following reasons:
* 1. To facilitate easier logic changes from SRP OAuth to simple OAuth.
* 2. To avoid attaching the access token to the URL, which could be logged. The provider token has a very short lifespan, reducing security risks.
*/
const oauth2TokenExchange = async ({ userAgent, ip, providerAuthToken, email }: TOauthTokenExchangeDTO) => {
const decodedProviderToken = validateProviderAuthToken(providerAuthToken, email);
const appCfg = getConfig();
const { authMethod, userName } = decodedProviderToken;
if (!userName) throw new BadRequestError({ message: "Missing user name" });
const organizationId =
(isAuthMethodSaml(authMethod) || [AuthMethod.LDAP, AuthMethod.OIDC].includes(authMethod)) &&
decodedProviderToken.orgId
? decodedProviderToken.orgId
: undefined;
const userEnc = await userDAL.findUserEncKeyByUsername({
username: email
});
if (!userEnc) throw new BadRequestError({ message: "Invalid token" });
if (!userEnc.serverEncryptedPrivateKey)
throw new BadRequestError({ message: "Key handoff incomplete. Please try logging in again." });
// send multi factor auth token if they it enabled
if (userEnc.isMfaEnabled && userEnc.email) {
enforceUserLockStatus(Boolean(userEnc.isLocked), userEnc.temporaryLockDateEnd);
const mfaToken = jwt.sign(
{
authMethod,
authTokenType: AuthTokenType.MFA_TOKEN,
userId: userEnc.userId
},
appCfg.AUTH_SECRET,
{
expiresIn: appCfg.JWT_MFA_LIFETIME
}
);
await sendUserMfaCode({
userId: userEnc.userId,
email: userEnc.email
});
return { isMfaEnabled: true, token: mfaToken } as const;
}
const token = await generateUserTokens({
user: { ...userEnc, id: userEnc.userId },
ip,
userAgent,
authMethod,
organizationId
});
return { token, isMfaEnabled: false, user: userEnc } as const;
};
/*
* logout user by incrementing the version by 1 meaning any old session will become invalid
* as there number is behind
@@ -542,6 +643,7 @@ export const authLoginServiceFactory = ({
loginExchangeClientProof,
logout,
oauth2Login,
oauth2TokenExchange,
resendMfaToken,
verifyMfaToken,
selectOrganization,

View File

@@ -13,6 +13,7 @@ export type TLoginClientProofDTO = {
ip: string;
userAgent: string;
captchaToken?: string;
password?: string;
};
export type TVerifyMfaTokenDTO = {
@@ -31,3 +32,10 @@ export type TOauthLoginDTO = {
authMethod: AuthMethod;
callbackPort?: string;
};
export type TOauthTokenExchangeDTO = {
providerAuthToken: string;
ip: string;
userAgent: string;
email: string;
};

View File

@@ -1,3 +1,4 @@
import bcrypt from "bcrypt";
import jwt from "jsonwebtoken";
import { SecretEncryptionAlgo, SecretKeyEncoding } from "@app/db/schemas";
@@ -57,7 +58,8 @@ export const authPaswordServiceFactory = ({
encryptedPrivateKeyTag,
salt,
verifier,
tokenVersionId
tokenVersionId,
password
}: TChangePasswordDTO) => {
const userEnc = await userDAL.findUserEncKeyByUserId(userId);
if (!userEnc) throw new Error("Failed to find user");
@@ -76,6 +78,8 @@ export const authPaswordServiceFactory = ({
);
if (!isValidClientProof) throw new Error("Failed to authenticate. Try again?");
const appCfg = getConfig();
const hashedPassword = await bcrypt.hash(password, appCfg.BCRYPT_SALT_ROUND);
await userDAL.updateUserEncryptionByUserId(userId, {
encryptionVersion: 2,
protectedKey,
@@ -87,7 +91,8 @@ export const authPaswordServiceFactory = ({
salt,
verifier,
serverPrivateKey: null,
clientPublicKey: null
clientPublicKey: null,
hashedPassword
});
if (tokenVersionId) {

View File

@@ -10,6 +10,7 @@ export type TChangePasswordDTO = {
salt: string;
verifier: string;
tokenVersionId?: string;
password: string;
};
export type TResetPasswordViaBackupKeyDTO = {

View File

@@ -1,3 +1,4 @@
import bcrypt from "bcrypt";
import jwt from "jsonwebtoken";
import { OrgMembershipStatus, TableName } from "@app/db/schemas";
@@ -6,6 +7,8 @@ import { TUserGroupMembershipDALFactory } from "@app/ee/services/group/user-grou
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
import { isAuthMethodSaml } from "@app/ee/services/permission/permission-fns";
import { getConfig } from "@app/lib/config/env";
import { infisicalSymmetricEncypt } from "@app/lib/crypto/encryption";
import { getUserPrivateKey } from "@app/lib/crypto/srp";
import { BadRequestError } from "@app/lib/errors";
import { isDisposableEmail } from "@app/lib/validator";
import { TGroupProjectDALFactory } from "@app/services/group-project/group-project-dal";
@@ -119,6 +122,7 @@ export const authSignupServiceFactory = ({
const completeEmailAccountSignup = async ({
email,
password,
firstName,
lastName,
providerAuthToken,
@@ -137,6 +141,7 @@ export const authSignupServiceFactory = ({
userAgent,
authorization
}: TCompleteAccountSignupDTO) => {
const appCfg = getConfig();
const user = await userDAL.findOne({ username: email });
if (!user || (user && user.isAccepted)) {
throw new Error("Failed to complete account for complete user");
@@ -152,6 +157,18 @@ export const authSignupServiceFactory = ({
validateSignUpAuthorization(authorization, user.id);
}
const hashedPassword = await bcrypt.hash(password, appCfg.BCRYPT_SALT_ROUND);
const privateKey = await getUserPrivateKey(password, {
salt,
protectedKey,
protectedKeyIV,
protectedKeyTag,
encryptedPrivateKey,
iv: encryptedPrivateKeyIV,
tag: encryptedPrivateKeyTag,
encryptionVersion: 2
});
const { tag, encoding, ciphertext, iv } = infisicalSymmetricEncypt(privateKey);
const updateduser = await authDAL.transaction(async (tx) => {
const us = await userDAL.updateById(user.id, { firstName, lastName, isAccepted: true }, tx);
if (!us) throw new Error("User not found");
@@ -166,12 +183,20 @@ export const authSignupServiceFactory = ({
protectedKeyTag,
encryptedPrivateKey,
iv: encryptedPrivateKeyIV,
tag: encryptedPrivateKeyTag
tag: encryptedPrivateKeyTag,
hashedPassword,
serverEncryptedPrivateKeyEncoding: encoding,
serverEncryptedPrivateKeyTag: tag,
serverEncryptedPrivateKeyIV: iv,
serverEncryptedPrivateKey: ciphertext
},
tx
);
// If it's SAML Auth and the organization ID is present, we should check if the user has a pending invite for this org, and accept it
if ((isAuthMethodSaml(authMethod) || authMethod === AuthMethod.LDAP) && organizationId) {
if (
(isAuthMethodSaml(authMethod) || [AuthMethod.LDAP, AuthMethod.OIDC].includes(authMethod as AuthMethod)) &&
organizationId
) {
const [pendingOrgMembership] = await orgDAL.findMembership({
[`${TableName.OrgMembership}.userId` as "userId"]: user.id,
status: OrgMembershipStatus.Invited,
@@ -227,7 +252,6 @@ export const authSignupServiceFactory = ({
userId: updateduser.info.id
});
if (!tokenSession) throw new Error("Failed to create token");
const appCfg = getConfig();
const accessToken = jwt.sign(
{
@@ -265,6 +289,7 @@ export const authSignupServiceFactory = ({
ip,
salt,
email,
password,
verifier,
firstName,
publicKey,
@@ -295,6 +320,19 @@ export const authSignupServiceFactory = ({
name: "complete account invite"
});
const appCfg = getConfig();
const hashedPassword = await bcrypt.hash(password, appCfg.BCRYPT_SALT_ROUND);
const privateKey = await getUserPrivateKey(password, {
salt,
protectedKey,
protectedKeyIV,
protectedKeyTag,
encryptedPrivateKey,
iv: encryptedPrivateKeyIV,
tag: encryptedPrivateKeyTag,
encryptionVersion: 2
});
const { tag, encoding, ciphertext, iv } = infisicalSymmetricEncypt(privateKey);
const updateduser = await authDAL.transaction(async (tx) => {
const us = await userDAL.updateById(user.id, { firstName, lastName, isAccepted: true }, tx);
if (!us) throw new Error("User not found");
@@ -310,7 +348,12 @@ export const authSignupServiceFactory = ({
protectedKeyTag,
encryptedPrivateKey,
iv: encryptedPrivateKeyIV,
tag: encryptedPrivateKeyTag
tag: encryptedPrivateKeyTag,
hashedPassword,
serverEncryptedPrivateKeyEncoding: encoding,
serverEncryptedPrivateKeyTag: tag,
serverEncryptedPrivateKeyIV: iv,
serverEncryptedPrivateKey: ciphertext
},
tx
);
@@ -343,7 +386,6 @@ export const authSignupServiceFactory = ({
userId: updateduser.info.id
});
if (!tokenSession) throw new Error("Failed to create token");
const appCfg = getConfig();
const accessToken = jwt.sign(
{

View File

@@ -1,5 +1,6 @@
export type TCompleteAccountSignupDTO = {
email: string;
password: string;
firstName: string;
lastName?: string;
protectedKey: string;
@@ -21,6 +22,7 @@ export type TCompleteAccountSignupDTO = {
export type TCompleteAccountInviteDTO = {
email: string;
password: string;
firstName: string;
lastName?: string;
protectedKey: string;

View File

@@ -8,7 +8,8 @@ export enum AuthMethod {
JUMPCLOUD_SAML = "jumpcloud-saml",
GOOGLE_SAML = "google-saml",
KEYCLOAK_SAML = "keycloak-saml",
LDAP = "ldap"
LDAP = "ldap",
OIDC = "oidc"
}
export enum AuthTokenType {

View File

@@ -3,6 +3,7 @@ import { ForbiddenError } from "@casl/ability";
import * as x509 from "@peculiar/x509";
import crypto, { KeyObject } from "crypto";
import ms from "ms";
import { z } from "zod";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
@@ -38,6 +39,7 @@ import {
TSignIntermediateDTO,
TUpdateCaDTO
} from "./certificate-authority-types";
import { hostnameRegex } from "./certificate-authority-validators";
type TCertificateAuthorityServiceFactoryDep = {
certificateAuthorityDAL: Pick<
@@ -653,6 +655,7 @@ export const certificateAuthorityServiceFactory = ({
caId,
friendlyName,
commonName,
altNames,
ttl,
notBefore,
notAfter,
@@ -738,6 +741,45 @@ export const certificateAuthorityServiceFactory = ({
kmsService
});
const extensions: x509.Extension[] = [
new x509.KeyUsagesExtension(x509.KeyUsageFlags.digitalSignature | x509.KeyUsageFlags.keyEncipherment, true),
new x509.BasicConstraintsExtension(false),
await x509.AuthorityKeyIdentifierExtension.create(caCertObj, false),
await x509.SubjectKeyIdentifierExtension.create(csrObj.publicKey)
];
if (altNames) {
const altNamesArray: {
type: "email" | "dns";
value: string;
}[] = altNames
.split(",")
.map((name) => name.trim())
.map((altName) => {
// check if the altName is a valid email
if (z.string().email().safeParse(altName).success) {
return {
type: "email",
value: altName
};
}
// check if the altName is a valid hostname
if (hostnameRegex.test(altName)) {
return {
type: "dns",
value: altName
};
}
// If altName is neither a valid email nor a valid hostname, throw an error or handle it accordingly
throw new Error(`Invalid altName: ${altName}`);
});
const altNamesExtension = new x509.SubjectAlternativeNameExtension(altNamesArray, false);
extensions.push(altNamesExtension);
}
const serialNumber = crypto.randomBytes(32).toString("hex");
const leafCert = await x509.X509CertificateGenerator.create({
serialNumber,
@@ -748,12 +790,7 @@ export const certificateAuthorityServiceFactory = ({
signingKey: caPrivateKey,
publicKey: csrObj.publicKey,
signingAlgorithm: alg,
extensions: [
new x509.KeyUsagesExtension(x509.KeyUsageFlags.digitalSignature | x509.KeyUsageFlags.keyEncipherment, true),
new x509.BasicConstraintsExtension(false),
await x509.AuthorityKeyIdentifierExtension.create(caCertObj, false),
await x509.SubjectKeyIdentifierExtension.create(csrObj.publicKey)
]
extensions
});
const skLeafObj = KeyObject.from(leafKeys.privateKey);
@@ -771,6 +808,7 @@ export const certificateAuthorityServiceFactory = ({
status: CertStatus.ACTIVE,
friendlyName: friendlyName || commonName,
commonName,
altNames,
serialNumber,
notBefore: notBeforeDate,
notAfter: notAfterDate

View File

@@ -75,6 +75,7 @@ export type TIssueCertFromCaDTO = {
caId: string;
friendlyName?: string;
commonName: string;
altNames: string;
ttl: string;
notBefore?: string;
notAfter?: string;

View File

@@ -6,3 +6,29 @@ const isValidDate = (dateString: string) => {
};
export const validateCaDateField = z.string().trim().refine(isValidDate, { message: "Invalid date format" });
export const hostnameRegex = /^(?!:\/\/)([a-zA-Z0-9-_]{1,63}\.?)+(?!:\/\/)([a-zA-Z]{2,63})$/;
export const validateAltNamesField = z
.string()
.trim()
.default("")
.transform((data) => {
if (data === "") return "";
// Trim each alt name and join with ', ' to ensure formatting
return data
.split(",")
.map((id) => id.trim())
.join(", ");
})
.refine(
(data) => {
if (data === "") return true;
// Split and validate each alt name
return data.split(", ").every((name) => {
return hostnameRegex.test(name) || z.string().email().safeParse(name).success;
});
},
{
message: "Each alt name must be a valid hostname or email address"
}
);

View File

@@ -7,11 +7,12 @@ import { IdentityAuthMethod } from "@app/db/schemas";
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { isAtLeastAsPrivileged } from "@app/lib/casl";
import { getConfig } from "@app/lib/config/env";
import { BadRequestError, UnauthorizedError } from "@app/lib/errors";
import { BadRequestError, ForbiddenRequestError, UnauthorizedError } from "@app/lib/errors";
import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip";
import { AuthTokenType } from "../auth/auth-type";
import { ActorType, AuthTokenType } from "../auth/auth-type";
import { TIdentityDALFactory } from "../identity/identity-dal";
import { TIdentityOrgDALFactory } from "../identity/identity-org-dal";
import { TIdentityAccessTokenDALFactory } from "../identity-access-token/identity-access-token-dal";
@@ -24,12 +25,13 @@ import {
TGetAwsAuthDTO,
TGetCallerIdentityResponse,
TLoginAwsAuthDTO,
TRevokeAwsAuthDTO,
TUpdateAwsAuthDTO
} from "./identity-aws-auth-types";
type TIdentityAwsAuthServiceFactoryDep = {
identityAccessTokenDAL: Pick<TIdentityAccessTokenDALFactory, "create">;
identityAwsAuthDAL: Pick<TIdentityAwsAuthDALFactory, "findOne" | "transaction" | "create" | "updateById">;
identityAwsAuthDAL: Pick<TIdentityAwsAuthDALFactory, "findOne" | "transaction" | "create" | "updateById" | "delete">;
identityOrgMembershipDAL: Pick<TIdentityOrgDALFactory, "findOne">;
identityDAL: Pick<TIdentityDALFactory, "updateById">;
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
@@ -301,10 +303,54 @@ export const identityAwsAuthServiceFactory = ({
return { ...awsIdentityAuth, orgId: identityMembershipOrg.orgId };
};
const revokeIdentityAwsAuth = async ({
identityId,
actorId,
actor,
actorAuthMethod,
actorOrgId
}: TRevokeAwsAuthDTO) => {
const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId });
if (!identityMembershipOrg) throw new BadRequestError({ message: "Failed to find identity" });
if (identityMembershipOrg.identity?.authMethod !== IdentityAuthMethod.AWS_AUTH)
throw new BadRequestError({
message: "The identity does not have aws auth"
});
const { permission } = await permissionService.getOrgPermission(
actor,
actorId,
identityMembershipOrg.orgId,
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Identity);
const { permission: rolePermission } = await permissionService.getOrgPermission(
ActorType.IDENTITY,
identityMembershipOrg.identityId,
identityMembershipOrg.orgId,
actorAuthMethod,
actorOrgId
);
const hasPriviledge = isAtLeastAsPrivileged(permission, rolePermission);
if (!hasPriviledge)
throw new ForbiddenRequestError({
message: "Failed to revoke aws auth of identity with more privileged role"
});
const revokedIdentityAwsAuth = await identityAwsAuthDAL.transaction(async (tx) => {
const deletedAwsAuth = await identityAwsAuthDAL.delete({ identityId }, tx);
await identityDAL.updateById(identityId, { authMethod: null }, tx);
return { ...deletedAwsAuth?.[0], orgId: identityMembershipOrg.orgId };
});
return revokedIdentityAwsAuth;
};
return {
login,
attachAwsAuth,
updateAwsAuth,
getAwsAuth
getAwsAuth,
revokeIdentityAwsAuth
};
};

View File

@@ -52,3 +52,7 @@ export type TGetCallerIdentityResponse = {
ResponseMetadata: { RequestId: string };
};
};
export type TRevokeAwsAuthDTO = {
identityId: string;
} & Omit<TProjectPermission, "projectId">;

View File

@@ -5,11 +5,12 @@ import { IdentityAuthMethod } from "@app/db/schemas";
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { isAtLeastAsPrivileged } from "@app/lib/casl";
import { getConfig } from "@app/lib/config/env";
import { BadRequestError, UnauthorizedError } from "@app/lib/errors";
import { BadRequestError, ForbiddenRequestError, UnauthorizedError } from "@app/lib/errors";
import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip";
import { AuthTokenType } from "../auth/auth-type";
import { ActorType, AuthTokenType } from "../auth/auth-type";
import { TIdentityDALFactory } from "../identity/identity-dal";
import { TIdentityOrgDALFactory } from "../identity/identity-org-dal";
import { TIdentityAccessTokenDALFactory } from "../identity-access-token/identity-access-token-dal";
@@ -20,11 +21,15 @@ import {
TAttachAzureAuthDTO,
TGetAzureAuthDTO,
TLoginAzureAuthDTO,
TRevokeAzureAuthDTO,
TUpdateAzureAuthDTO
} from "./identity-azure-auth-types";
type TIdentityAzureAuthServiceFactoryDep = {
identityAzureAuthDAL: Pick<TIdentityAzureAuthDALFactory, "findOne" | "transaction" | "create" | "updateById">;
identityAzureAuthDAL: Pick<
TIdentityAzureAuthDALFactory,
"findOne" | "transaction" | "create" | "updateById" | "delete"
>;
identityOrgMembershipDAL: Pick<TIdentityOrgDALFactory, "findOne">;
identityAccessTokenDAL: Pick<TIdentityAccessTokenDALFactory, "create">;
identityDAL: Pick<TIdentityDALFactory, "updateById">;
@@ -277,10 +282,54 @@ export const identityAzureAuthServiceFactory = ({
return { ...identityAzureAuth, orgId: identityMembershipOrg.orgId };
};
const revokeIdentityAzureAuth = async ({
identityId,
actorId,
actor,
actorAuthMethod,
actorOrgId
}: TRevokeAzureAuthDTO) => {
const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId });
if (!identityMembershipOrg) throw new BadRequestError({ message: "Failed to find identity" });
if (identityMembershipOrg.identity?.authMethod !== IdentityAuthMethod.AZURE_AUTH)
throw new BadRequestError({
message: "The identity does not have azure auth"
});
const { permission } = await permissionService.getOrgPermission(
actor,
actorId,
identityMembershipOrg.orgId,
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Identity);
const { permission: rolePermission } = await permissionService.getOrgPermission(
ActorType.IDENTITY,
identityMembershipOrg.identityId,
identityMembershipOrg.orgId,
actorAuthMethod,
actorOrgId
);
const hasPriviledge = isAtLeastAsPrivileged(permission, rolePermission);
if (!hasPriviledge)
throw new ForbiddenRequestError({
message: "Failed to revoke azure auth of identity with more privileged role"
});
const revokedIdentityAzureAuth = await identityAzureAuthDAL.transaction(async (tx) => {
const deletedAzureAuth = await identityAzureAuthDAL.delete({ identityId }, tx);
await identityDAL.updateById(identityId, { authMethod: null }, tx);
return { ...deletedAzureAuth?.[0], orgId: identityMembershipOrg.orgId };
});
return revokedIdentityAzureAuth;
};
return {
login,
attachAzureAuth,
updateAzureAuth,
getAzureAuth
getAzureAuth,
revokeIdentityAzureAuth
};
};

View File

@@ -118,3 +118,7 @@ export type TDecodedAzureAuthJwt = {
[key: string]: string;
};
};
export type TRevokeAzureAuthDTO = {
identityId: string;
} & Omit<TProjectPermission, "projectId">;

View File

@@ -5,11 +5,12 @@ import { IdentityAuthMethod } from "@app/db/schemas";
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { isAtLeastAsPrivileged } from "@app/lib/casl";
import { getConfig } from "@app/lib/config/env";
import { BadRequestError, UnauthorizedError } from "@app/lib/errors";
import { BadRequestError, ForbiddenRequestError, UnauthorizedError } from "@app/lib/errors";
import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip";
import { AuthTokenType } from "../auth/auth-type";
import { ActorType, AuthTokenType } from "../auth/auth-type";
import { TIdentityDALFactory } from "../identity/identity-dal";
import { TIdentityOrgDALFactory } from "../identity/identity-org-dal";
import { TIdentityAccessTokenDALFactory } from "../identity-access-token/identity-access-token-dal";
@@ -21,11 +22,12 @@ import {
TGcpIdentityDetails,
TGetGcpAuthDTO,
TLoginGcpAuthDTO,
TRevokeGcpAuthDTO,
TUpdateGcpAuthDTO
} from "./identity-gcp-auth-types";
type TIdentityGcpAuthServiceFactoryDep = {
identityGcpAuthDAL: Pick<TIdentityGcpAuthDALFactory, "findOne" | "transaction" | "create" | "updateById">;
identityGcpAuthDAL: Pick<TIdentityGcpAuthDALFactory, "findOne" | "transaction" | "create" | "updateById" | "delete">;
identityOrgMembershipDAL: Pick<TIdentityOrgDALFactory, "findOne">;
identityAccessTokenDAL: Pick<TIdentityAccessTokenDALFactory, "create">;
identityDAL: Pick<TIdentityDALFactory, "updateById">;
@@ -315,10 +317,54 @@ export const identityGcpAuthServiceFactory = ({
return { ...identityGcpAuth, orgId: identityMembershipOrg.orgId };
};
const revokeIdentityGcpAuth = async ({
identityId,
actorId,
actor,
actorAuthMethod,
actorOrgId
}: TRevokeGcpAuthDTO) => {
const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId });
if (!identityMembershipOrg) throw new BadRequestError({ message: "Failed to find identity" });
if (identityMembershipOrg.identity?.authMethod !== IdentityAuthMethod.GCP_AUTH)
throw new BadRequestError({
message: "The identity does not have gcp auth"
});
const { permission } = await permissionService.getOrgPermission(
actor,
actorId,
identityMembershipOrg.orgId,
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Identity);
const { permission: rolePermission } = await permissionService.getOrgPermission(
ActorType.IDENTITY,
identityMembershipOrg.identityId,
identityMembershipOrg.orgId,
actorAuthMethod,
actorOrgId
);
const hasPriviledge = isAtLeastAsPrivileged(permission, rolePermission);
if (!hasPriviledge)
throw new ForbiddenRequestError({
message: "Failed to revoke gcp auth of identity with more privileged role"
});
const revokedIdentityGcpAuth = await identityGcpAuthDAL.transaction(async (tx) => {
const deletedGcpAuth = await identityGcpAuthDAL.delete({ identityId }, tx);
await identityDAL.updateById(identityId, { authMethod: null }, tx);
return { ...deletedGcpAuth?.[0], orgId: identityMembershipOrg.orgId };
});
return revokedIdentityGcpAuth;
};
return {
login,
attachGcpAuth,
updateGcpAuth,
getGcpAuth
getGcpAuth,
revokeIdentityGcpAuth
};
};

View File

@@ -76,3 +76,7 @@ export type TDecodedGcpIamAuthJwt = {
[key: string]: string;
};
};
export type TRevokeGcpAuthDTO = {
identityId: string;
} & Omit<TProjectPermission, "projectId">;

View File

@@ -7,6 +7,7 @@ import { IdentityAuthMethod, SecretKeyEncoding, TIdentityKubernetesAuthsUpdate }
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { isAtLeastAsPrivileged } from "@app/lib/casl";
import { getConfig } from "@app/lib/config/env";
import {
decryptSymmetric,
@@ -16,11 +17,11 @@ import {
infisicalSymmetricDecrypt,
infisicalSymmetricEncypt
} from "@app/lib/crypto/encryption";
import { BadRequestError, UnauthorizedError } from "@app/lib/errors";
import { BadRequestError, ForbiddenRequestError, UnauthorizedError } from "@app/lib/errors";
import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip";
import { TOrgBotDALFactory } from "@app/services/org/org-bot-dal";
import { AuthTokenType } from "../auth/auth-type";
import { ActorType, AuthTokenType } from "../auth/auth-type";
import { TIdentityDALFactory } from "../identity/identity-dal";
import { TIdentityOrgDALFactory } from "../identity/identity-org-dal";
import { TIdentityAccessTokenDALFactory } from "../identity-access-token/identity-access-token-dal";
@@ -32,13 +33,14 @@ import {
TCreateTokenReviewResponse,
TGetKubernetesAuthDTO,
TLoginKubernetesAuthDTO,
TRevokeKubernetesAuthDTO,
TUpdateKubernetesAuthDTO
} from "./identity-kubernetes-auth-types";
type TIdentityKubernetesAuthServiceFactoryDep = {
identityKubernetesAuthDAL: Pick<
TIdentityKubernetesAuthDALFactory,
"create" | "findOne" | "transaction" | "updateById"
"create" | "findOne" | "transaction" | "updateById" | "delete"
>;
identityAccessTokenDAL: Pick<TIdentityAccessTokenDALFactory, "create">;
identityOrgMembershipDAL: Pick<TIdentityOrgDALFactory, "findOne" | "findById">;
@@ -442,7 +444,34 @@ export const identityKubernetesAuthServiceFactory = ({
const updatedKubernetesAuth = await identityKubernetesAuthDAL.updateById(identityKubernetesAuth.id, updateQuery);
return { ...updatedKubernetesAuth, orgId: identityMembershipOrg.orgId };
const updatedCACert =
updatedKubernetesAuth.encryptedCaCert && updatedKubernetesAuth.caCertIV && updatedKubernetesAuth.caCertTag
? decryptSymmetric({
ciphertext: updatedKubernetesAuth.encryptedCaCert,
iv: updatedKubernetesAuth.caCertIV,
tag: updatedKubernetesAuth.caCertTag,
key
})
: "";
const updatedTokenReviewerJwt =
updatedKubernetesAuth.encryptedTokenReviewerJwt &&
updatedKubernetesAuth.tokenReviewerJwtIV &&
updatedKubernetesAuth.tokenReviewerJwtTag
? decryptSymmetric({
ciphertext: updatedKubernetesAuth.encryptedTokenReviewerJwt,
iv: updatedKubernetesAuth.tokenReviewerJwtIV,
tag: updatedKubernetesAuth.tokenReviewerJwtTag,
key
})
: "";
return {
...updatedKubernetesAuth,
orgId: identityMembershipOrg.orgId,
caCert: updatedCACert,
tokenReviewerJwt: updatedTokenReviewerJwt
};
};
const getKubernetesAuth = async ({
@@ -506,10 +535,54 @@ export const identityKubernetesAuthServiceFactory = ({
return { ...identityKubernetesAuth, caCert, tokenReviewerJwt, orgId: identityMembershipOrg.orgId };
};
const revokeIdentityKubernetesAuth = async ({
identityId,
actorId,
actor,
actorAuthMethod,
actorOrgId
}: TRevokeKubernetesAuthDTO) => {
const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId });
if (!identityMembershipOrg) throw new BadRequestError({ message: "Failed to find identity" });
if (identityMembershipOrg.identity?.authMethod !== IdentityAuthMethod.KUBERNETES_AUTH)
throw new BadRequestError({
message: "The identity does not have kubenetes auth"
});
const { permission } = await permissionService.getOrgPermission(
actor,
actorId,
identityMembershipOrg.orgId,
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Identity);
const { permission: rolePermission } = await permissionService.getOrgPermission(
ActorType.IDENTITY,
identityMembershipOrg.identityId,
identityMembershipOrg.orgId,
actorAuthMethod,
actorOrgId
);
const hasPriviledge = isAtLeastAsPrivileged(permission, rolePermission);
if (!hasPriviledge)
throw new ForbiddenRequestError({
message: "Failed to revoke kubenetes auth of identity with more privileged role"
});
const revokedIdentityKubernetesAuth = await identityKubernetesAuthDAL.transaction(async (tx) => {
const deletedKubernetesAuth = await identityKubernetesAuthDAL.delete({ identityId }, tx);
await identityDAL.updateById(identityId, { authMethod: null }, tx);
return { ...deletedKubernetesAuth?.[0], orgId: identityMembershipOrg.orgId };
});
return revokedIdentityKubernetesAuth;
};
return {
login,
attachKubernetesAuth,
updateKubernetesAuth,
getKubernetesAuth
getKubernetesAuth,
revokeIdentityKubernetesAuth
};
};

View File

@@ -59,3 +59,7 @@ export type TCreateTokenReviewResponse = {
};
status: TCreateTokenReviewSuccessResponse | TCreateTokenReviewErrorResponse;
};
export type TRevokeKubernetesAuthDTO = {
identityId: string;
} & Omit<TProjectPermission, "projectId">;

View File

@@ -25,7 +25,9 @@ import {
TCreateUaClientSecretDTO,
TGetUaClientSecretsDTO,
TGetUaDTO,
TGetUniversalAuthClientSecretByIdDTO,
TRevokeUaClientSecretDTO,
TRevokeUaDTO,
TUpdateUaDTO
} from "./identity-ua-types";
@@ -136,7 +138,7 @@ export const identityUaServiceFactory = ({
return { accessToken, identityUa, validClientSecretInfo, identityAccessToken, identityMembershipOrg };
};
const attachUa = async ({
const attachUniversalAuth = async ({
accessTokenMaxTTL,
identityId,
accessTokenNumUsesLimit,
@@ -227,7 +229,7 @@ export const identityUaServiceFactory = ({
return { ...identityUa, orgId: identityMembershipOrg.orgId };
};
const updateUa = async ({
const updateUniversalAuth = async ({
accessTokenMaxTTL,
identityId,
accessTokenNumUsesLimit,
@@ -312,7 +314,7 @@ export const identityUaServiceFactory = ({
return { ...updatedUaAuth, orgId: identityMembershipOrg.orgId };
};
const getIdentityUa = async ({ identityId, actorId, actor, actorAuthMethod, actorOrgId }: TGetUaDTO) => {
const getIdentityUniversalAuth = async ({ identityId, actorId, actor, actorAuthMethod, actorOrgId }: TGetUaDTO) => {
const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId });
if (!identityMembershipOrg) throw new BadRequestError({ message: "Failed to find identity" });
if (identityMembershipOrg.identity?.authMethod !== IdentityAuthMethod.Univeral)
@@ -333,7 +335,50 @@ export const identityUaServiceFactory = ({
return { ...uaIdentityAuth, orgId: identityMembershipOrg.orgId };
};
const createUaClientSecret = async ({
const revokeIdentityUniversalAuth = async ({
identityId,
actorId,
actor,
actorAuthMethod,
actorOrgId
}: TRevokeUaDTO) => {
const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId });
if (!identityMembershipOrg) throw new BadRequestError({ message: "Failed to find identity" });
if (identityMembershipOrg.identity?.authMethod !== IdentityAuthMethod.Univeral)
throw new BadRequestError({
message: "The identity does not have universal auth"
});
const { permission } = await permissionService.getOrgPermission(
actor,
actorId,
identityMembershipOrg.orgId,
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Identity);
const { permission: rolePermission } = await permissionService.getOrgPermission(
ActorType.IDENTITY,
identityMembershipOrg.identityId,
identityMembershipOrg.orgId,
actorAuthMethod,
actorOrgId
);
const hasPriviledge = isAtLeastAsPrivileged(permission, rolePermission);
if (!hasPriviledge)
throw new ForbiddenRequestError({
message: "Failed to revoke universal auth of identity with more privileged role"
});
const revokedIdentityUniversalAuth = await identityUaDAL.transaction(async (tx) => {
const deletedUniversalAuth = await identityUaDAL.delete({ identityId }, tx);
await identityDAL.updateById(identityId, { authMethod: null }, tx);
return { ...deletedUniversalAuth?.[0], orgId: identityMembershipOrg.orgId };
});
return revokedIdentityUniversalAuth;
};
const createUniversalAuthClientSecret = async ({
actor,
actorId,
actorOrgId,
@@ -396,7 +441,7 @@ export const identityUaServiceFactory = ({
};
};
const getUaClientSecrets = async ({
const getUniversalAuthClientSecrets = async ({
actor,
actorId,
actorOrgId,
@@ -442,7 +487,47 @@ export const identityUaServiceFactory = ({
return { clientSecrets, orgId: identityMembershipOrg.orgId };
};
const revokeUaClientSecret = async ({
const getUniversalAuthClientSecretById = async ({
identityId,
actorId,
actor,
actorOrgId,
actorAuthMethod,
clientSecretId
}: TGetUniversalAuthClientSecretByIdDTO) => {
const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId });
if (!identityMembershipOrg) throw new BadRequestError({ message: "Failed to find identity" });
if (identityMembershipOrg.identity?.authMethod !== IdentityAuthMethod.Univeral)
throw new BadRequestError({
message: "The identity does not have universal auth"
});
const { permission } = await permissionService.getOrgPermission(
actor,
actorId,
identityMembershipOrg.orgId,
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Identity);
const { permission: rolePermission } = await permissionService.getOrgPermission(
ActorType.IDENTITY,
identityMembershipOrg.identityId,
identityMembershipOrg.orgId,
actorAuthMethod,
actorOrgId
);
const hasPriviledge = isAtLeastAsPrivileged(permission, rolePermission);
if (!hasPriviledge)
throw new ForbiddenRequestError({
message: "Failed to read identity client secret of project with more privileged role"
});
const clientSecret = await identityUaClientSecretDAL.findById(clientSecretId);
return { ...clientSecret, identityId, orgId: identityMembershipOrg.orgId };
};
const revokeUniversalAuthClientSecret = async ({
identityId,
actorId,
actor,
@@ -475,7 +560,7 @@ export const identityUaServiceFactory = ({
const hasPriviledge = isAtLeastAsPrivileged(permission, rolePermission);
if (!hasPriviledge)
throw new ForbiddenRequestError({
message: "Failed to add identity to project with more privileged role"
message: "Failed to revoke identity client secret with more privileged role"
});
const clientSecret = await identityUaClientSecretDAL.updateById(clientSecretId, {
@@ -486,11 +571,13 @@ export const identityUaServiceFactory = ({
return {
login,
attachUa,
updateUa,
getIdentityUa,
createUaClientSecret,
getUaClientSecrets,
revokeUaClientSecret
attachUniversalAuth,
updateUniversalAuth,
getIdentityUniversalAuth,
revokeIdentityUniversalAuth,
createUniversalAuthClientSecret,
getUniversalAuthClientSecrets,
revokeUniversalAuthClientSecret,
getUniversalAuthClientSecretById
};
};

View File

@@ -22,6 +22,10 @@ export type TGetUaDTO = {
identityId: string;
} & Omit<TProjectPermission, "projectId">;
export type TRevokeUaDTO = {
identityId: string;
} & Omit<TProjectPermission, "projectId">;
export type TCreateUaClientSecretDTO = {
identityId: string;
description: string;
@@ -37,3 +41,8 @@ export type TRevokeUaClientSecretDTO = {
identityId: string;
clientSecretId: string;
} & Omit<TProjectPermission, "projectId">;
export type TGetUniversalAuthClientSecretByIdDTO = {
identityId: string;
clientSecretId: string;
} & Omit<TProjectPermission, "projectId">;

View File

@@ -27,10 +27,10 @@ export const identityOrgDALFactory = (db: TDbClient) => {
}
};
const findByOrgId = async (orgId: string, tx?: Knex) => {
const find = async (filter: Partial<TIdentityOrgMemberships>, tx?: Knex) => {
try {
const docs = await (tx || db)(TableName.IdentityOrgMembership)
.where(`${TableName.IdentityOrgMembership}.orgId`, orgId)
.where(filter)
.join(TableName.Identity, `${TableName.IdentityOrgMembership}.identityId`, `${TableName.Identity}.id`)
.leftJoin(TableName.OrgRoles, `${TableName.IdentityOrgMembership}.roleId`, `${TableName.OrgRoles}.id`)
.select(selectAllTableCols(TableName.IdentityOrgMembership))
@@ -79,5 +79,5 @@ export const identityOrgDALFactory = (db: TDbClient) => {
}
};
return { ...identityOrgOrm, findOne, findByOrgId };
return { ...identityOrgOrm, find, findOne };
};

View File

@@ -1,6 +1,6 @@
import { ForbiddenError } from "@casl/ability";
import { OrgMembershipRole, TOrgRoles } from "@app/db/schemas";
import { OrgMembershipRole, TableName, TOrgRoles } from "@app/db/schemas";
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { isAtLeastAsPrivileged } from "@app/lib/casl";
@@ -10,7 +10,7 @@ import { TOrgPermission } from "@app/lib/types";
import { ActorType } from "../auth/auth-type";
import { TIdentityDALFactory } from "./identity-dal";
import { TIdentityOrgDALFactory } from "./identity-org-dal";
import { TCreateIdentityDTO, TDeleteIdentityDTO, TUpdateIdentityDTO } from "./identity-types";
import { TCreateIdentityDTO, TDeleteIdentityDTO, TGetIdentityByIdDTO, TUpdateIdentityDTO } from "./identity-types";
type TIdentityServiceFactoryDep = {
identityDAL: TIdentityDALFactory;
@@ -126,6 +126,24 @@ export const identityServiceFactory = ({
return { ...identity, orgId: identityOrgMembership.orgId };
};
const getIdentityById = async ({ id, actor, actorId, actorOrgId, actorAuthMethod }: TGetIdentityByIdDTO) => {
const doc = await identityOrgMembershipDAL.find({
[`${TableName.IdentityOrgMembership}.identityId` as "identityId"]: id
});
const identity = doc[0];
if (!identity) throw new BadRequestError({ message: `Failed to find identity with id ${id}` });
const { permission } = await permissionService.getOrgPermission(
actor,
actorId,
identity.orgId,
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Identity);
return identity;
};
const deleteIdentity = async ({ actorId, actor, actorOrgId, actorAuthMethod, id }: TDeleteIdentityDTO) => {
const identityOrgMembership = await identityOrgMembershipDAL.findOne({ identityId: id });
if (!identityOrgMembership) throw new BadRequestError({ message: `Failed to find identity with id ${id}` });
@@ -157,7 +175,9 @@ export const identityServiceFactory = ({
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Identity);
const identityMemberships = await identityOrgMembershipDAL.findByOrgId(orgId);
const identityMemberships = await identityOrgMembershipDAL.find({
[`${TableName.IdentityOrgMembership}.orgId` as "orgId"]: orgId
});
return identityMemberships;
};
@@ -165,6 +185,7 @@ export const identityServiceFactory = ({
createIdentity,
updateIdentity,
deleteIdentity,
listOrgIdentities
listOrgIdentities,
getIdentityById
};
};

View File

@@ -16,6 +16,10 @@ export type TDeleteIdentityDTO = {
id: string;
} & Omit<TOrgPermission, "orgId">;
export type TGetIdentityByIdDTO = {
id: string;
} & Omit<TOrgPermission, "orgId">;
export interface TIdentityTrustedIp {
ipAddress: string;
type: IPType;

View File

@@ -18,7 +18,7 @@ import {
UpdateSecretCommand
} from "@aws-sdk/client-secrets-manager";
import { Octokit } from "@octokit/rest";
import AWS from "aws-sdk";
import AWS, { AWSError } from "aws-sdk";
import { AxiosError } from "axios";
import sodium from "libsodium-wrappers";
import isEqual from "lodash.isequal";
@@ -257,14 +257,27 @@ const syncSecretsGCPSecretManager = async ({
const syncSecretsAzureKeyVault = async ({
integration,
secrets,
accessToken
accessToken,
createManySecretsRawFn,
updateManySecretsRawFn
}: {
integration: TIntegrations;
integration: TIntegrations & {
projectId: string;
environment: {
id: string;
name: string;
slug: string;
};
secretPath: string;
};
secrets: Record<string, { value: string; comment?: string }>;
accessToken: string;
createManySecretsRawFn: (params: TCreateManySecretsRawFn) => Promise<Array<TSecrets & { _id: string }>>;
updateManySecretsRawFn: (params: TUpdateManySecretsRawFn) => Promise<Array<TSecrets & { _id: string }>>;
}) => {
interface GetAzureKeyVaultSecret {
id: string; // secret URI
value: string;
attributes: {
enabled: true;
created: number;
@@ -361,6 +374,83 @@ const syncSecretsAzureKeyVault = async ({
}
});
const secretsToAdd: { [key: string]: string } = {};
const secretsToUpdate: { [key: string]: string } = {};
const secretKeysToRemoveFromDelete = new Set<string>();
const metadata = IntegrationMetadataSchema.parse(integration.metadata);
if (!integration.lastUsed) {
Object.keys(res).forEach((key) => {
// first time using integration
const underscoredKey = key.replace(/-/g, "_");
// -> apply initial sync behavior
switch (metadata.initialSyncBehavior) {
case IntegrationInitialSyncBehavior.PREFER_TARGET: {
if (!(underscoredKey in secrets)) {
secretsToAdd[underscoredKey] = res[key].value;
setSecrets.push({
key,
value: res[key].value
});
} else if (secrets[underscoredKey]?.value !== res[key].value) {
secretsToUpdate[underscoredKey] = res[key].value;
const toEditSecretIndex = setSecrets.findIndex((secret) => secret.key === key);
if (toEditSecretIndex >= 0) {
setSecrets[toEditSecretIndex].value = res[key].value;
}
}
secretKeysToRemoveFromDelete.add(key);
break;
}
case IntegrationInitialSyncBehavior.PREFER_SOURCE: {
if (!(underscoredKey in secrets)) {
secretsToAdd[underscoredKey] = res[key].value;
setSecrets.push({
key,
value: res[key].value
});
}
secretKeysToRemoveFromDelete.add(key);
break;
}
default:
break;
}
});
}
if (Object.keys(secretsToUpdate).length) {
await updateManySecretsRawFn({
projectId: integration.projectId,
environment: integration.environment.slug,
path: integration.secretPath,
secrets: Object.keys(secretsToUpdate).map((key) => ({
secretName: key,
secretValue: secretsToUpdate[key],
type: SecretType.Shared,
secretComment: ""
}))
});
}
if (Object.keys(secretsToAdd).length) {
await createManySecretsRawFn({
projectId: integration.projectId,
environment: integration.environment.slug,
path: integration.secretPath,
secrets: Object.keys(secretsToAdd).map((key) => ({
secretName: key,
secretValue: secretsToAdd[key],
type: SecretType.Shared,
secretComment: ""
}))
});
}
const setSecretAzureKeyVault = async ({
key,
value,
@@ -428,7 +518,7 @@ const syncSecretsAzureKeyVault = async ({
});
}
for await (const deleteSecret of deleteSecrets) {
for await (const deleteSecret of deleteSecrets.filter((secret) => !secretKeysToRemoveFromDelete.has(secret.key))) {
const { key } = deleteSecret;
await request.delete(`${integration.app}/secrets/${key}?api-version=7.3`, {
headers: {
@@ -452,7 +542,11 @@ const syncSecretsAWSParameterStore = async ({
accessId: string | null;
accessToken: string;
}) => {
if (!accessId) return;
let response: { isSynced: boolean; syncMessage: string } | null = null;
if (!accessId) {
throw new Error("AWS access ID is required");
}
const config = new AWS.Config({
region: integration.region as string,
@@ -557,6 +651,11 @@ const syncSecretsAWSParameterStore = async ({
`AWS Parameter Store Error [integration=${integration.id}]: double check AWS account permissions (refer to the Infisical docs)`
);
}
response = {
isSynced: false,
syncMessage: (err as AWSError)?.message || "Error syncing with AWS Parameter Store"
};
}
}
}
@@ -585,6 +684,8 @@ const syncSecretsAWSParameterStore = async ({
}
}
}
return response;
};
/**
@@ -603,7 +704,9 @@ const syncSecretsAWSSecretManager = async ({
}) => {
const metadata = z.record(z.any()).parse(integration.metadata || {});
if (!accessId) return;
if (!accessId) {
throw new Error("AWS access ID is required");
}
const secretsManager = new SecretsManagerClient({
region: integration.region as string,
@@ -722,7 +825,7 @@ const syncSecretsAWSSecretManager = async ({
}
}
} catch (err) {
// case when AWS manager can't find the specified secret
// case 1: when AWS manager can't find the specified secret
if (err instanceof ResourceNotFoundException && secretsManager) {
await secretsManager.send(
new CreateSecretCommand({
@@ -734,6 +837,9 @@ const syncSecretsAWSSecretManager = async ({
: []
})
);
// case 2: something unexpected went wrong, so we'll throw the error to reflect the error in the integration sync status
} else {
throw err;
}
}
};
@@ -753,14 +859,12 @@ const syncSecretsAWSSecretManager = async ({
const syncSecretsHeroku = async ({
createManySecretsRawFn,
updateManySecretsRawFn,
integrationDAL,
integration,
secrets,
accessToken
}: {
createManySecretsRawFn: (params: TCreateManySecretsRawFn) => Promise<Array<TSecrets & { _id: string }>>;
updateManySecretsRawFn: (params: TUpdateManySecretsRawFn) => Promise<Array<TSecrets & { _id: string }>>;
integrationDAL: Pick<TIntegrationDALFactory, "updateById">;
integration: TIntegrations & {
projectId: string;
environment: {
@@ -862,10 +966,6 @@ const syncSecretsHeroku = async ({
}
}
);
await integrationDAL.updateById(integration.id, {
lastUsed: new Date()
});
};
/**
@@ -2656,7 +2756,9 @@ const syncSecretsHashiCorpVault = async ({
accessId: string | null;
accessToken: string;
}) => {
if (!accessId) return;
if (!accessId) {
throw new Error("Access ID is required");
}
interface LoginAppRoleRes {
auth: {
@@ -3486,6 +3588,8 @@ export const syncIntegrationSecrets = async ({
accessToken: string;
appendices?: { prefix: string; suffix: string };
}) => {
let response: { isSynced: boolean; syncMessage: string } | null = null;
switch (integration.integration) {
case Integrations.GCP_SECRET_MANAGER:
await syncSecretsGCPSecretManager({
@@ -3498,11 +3602,13 @@ export const syncIntegrationSecrets = async ({
await syncSecretsAzureKeyVault({
integration,
secrets,
accessToken
accessToken,
createManySecretsRawFn,
updateManySecretsRawFn
});
break;
case Integrations.AWS_PARAMETER_STORE:
await syncSecretsAWSParameterStore({
response = await syncSecretsAWSParameterStore({
integration,
secrets,
accessId,
@@ -3521,7 +3627,6 @@ export const syncIntegrationSecrets = async ({
await syncSecretsHeroku({
createManySecretsRawFn,
updateManySecretsRawFn,
integrationDAL,
integration,
secrets,
accessToken
@@ -3727,4 +3832,6 @@ export const syncIntegrationSecrets = async ({
default:
throw new BadRequestError({ message: "Invalid integration" });
}
return response;
};

View File

@@ -36,6 +36,7 @@ import {
TDeleteProjectMembershipsDTO,
TGetProjectMembershipByUsernameDTO,
TGetProjectMembershipDTO,
TLeaveProjectDTO,
TUpdateProjectMembershipDTO
} from "./project-membership-types";
import { TProjectUserMembershipRoleDALFactory } from "./project-user-membership-role-dal";
@@ -531,6 +532,53 @@ export const projectMembershipServiceFactory = ({
return memberships;
};
const leaveProject = async ({ projectId, actorId, actor }: TLeaveProjectDTO) => {
if (actor !== ActorType.USER) {
throw new BadRequestError({ message: "Only users can leave projects" });
}
const project = await projectDAL.findById(projectId);
if (!project) throw new BadRequestError({ message: "Project not found" });
if (project.version !== ProjectVersion.V2) {
throw new BadRequestError({
message: "Please ask your project administrator to upgrade the project before leaving."
});
}
const projectMembers = await projectMembershipDAL.findAllProjectMembers(projectId);
if (!projectMembers?.length) {
throw new BadRequestError({ message: "Failed to find project members" });
}
if (projectMembers.length < 2) {
throw new BadRequestError({ message: "You cannot leave the project as you are the only member" });
}
const adminMembers = projectMembers.filter(
(member) => member.roles.map((r) => r.role).includes("admin") && member.userId !== actorId
);
if (!adminMembers.length) {
throw new BadRequestError({
message: "You cannot leave the project as you are the only admin. Promote another user to admin before leaving."
});
}
const deletedMembership = (
await projectMembershipDAL.delete({
projectId: project.id,
userId: actorId
})
)?.[0];
if (!deletedMembership) {
throw new BadRequestError({ message: "Failed to leave project" });
}
return deletedMembership;
};
return {
getProjectMemberships,
getProjectMembershipByUsername,
@@ -538,6 +586,7 @@ export const projectMembershipServiceFactory = ({
addUsersToProjectNonE2EE,
deleteProjectMemberships,
deleteProjectMembership, // TODO: Remove this
addUsersToProject
addUsersToProject,
leaveProject
};
};

View File

@@ -1,6 +1,7 @@
import { TProjectPermission } from "@app/lib/types";
export type TGetProjectMembershipDTO = TProjectPermission;
export type TLeaveProjectDTO = Omit<TProjectPermission, "actorOrgId" | "actorAuthMethod">;
export enum ProjectUserMembershipTemporaryMode {
Relative = "relative"
}

View File

@@ -11,7 +11,7 @@ import { isAtLeastAsPrivileged } from "@app/lib/casl";
import { getConfig } from "@app/lib/config/env";
import { createSecretBlindIndex } from "@app/lib/crypto";
import { infisicalSymmetricEncypt } from "@app/lib/crypto/encryption";
import { BadRequestError, ForbiddenRequestError } from "@app/lib/errors";
import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
import { alphaNumericNanoId } from "@app/lib/nanoid";
import { TProjectPermission } from "@app/lib/types";
@@ -41,6 +41,7 @@ import {
TListProjectCasDTO,
TListProjectCertsDTO,
TToggleProjectAutoCapitalizationDTO,
TUpdateAuditLogsRetentionDTO,
TUpdateProjectDTO,
TUpdateProjectNameDTO,
TUpdateProjectVersionLimitDTO,
@@ -446,6 +447,43 @@ export const projectServiceFactory = ({
return projectDAL.updateById(project.id, { pitVersionLimit });
};
const updateAuditLogsRetention = async ({
actor,
actorId,
actorOrgId,
actorAuthMethod,
auditLogsRetentionDays,
workspaceSlug
}: TUpdateAuditLogsRetentionDTO) => {
const project = await projectDAL.findProjectBySlug(workspaceSlug, actorOrgId);
if (!project) {
throw new NotFoundError({
message: "Project not found."
});
}
const { hasRole } = await permissionService.getProjectPermission(
actor,
actorId,
project.id,
actorAuthMethod,
actorOrgId
);
if (!hasRole(ProjectMembershipRole.Admin)) {
throw new BadRequestError({ message: "Only admins are allowed to take this action" });
}
const plan = await licenseService.getPlan(project.orgId);
if (!plan.auditLogs || auditLogsRetentionDays > plan.auditLogsRetentionDays) {
throw new BadRequestError({
message: "Failed to update audit logs retention due to plan limit reached. Upgrade plan to increase."
});
}
return projectDAL.updateById(project.id, { auditLogsRetentionDays });
};
const updateName = async ({
projectId,
actor,
@@ -621,6 +659,7 @@ export const projectServiceFactory = ({
upgradeProject,
listProjectCas,
listProjectCertificates,
updateVersionLimit
updateVersionLimit,
updateAuditLogsRetention
};
};

View File

@@ -49,6 +49,11 @@ export type TUpdateProjectVersionLimitDTO = {
workspaceSlug: string;
} & Omit<TProjectPermission, "projectId">;
export type TUpdateAuditLogsRetentionDTO = {
auditLogsRetentionDays: number;
workspaceSlug: string;
} & Omit<TProjectPermission, "projectId">;
export type TUpdateProjectNameDTO = {
name: string;
} & TProjectPermission;

View File

@@ -30,7 +30,6 @@ export const secretBlindIndexDALFactory = (db: TDbClient) => {
.leftJoin(TableName.SecretFolder, `${TableName.SecretFolder}.id`, `${TableName.Secret}.folderId`)
.leftJoin(TableName.Environment, `${TableName.Environment}.id`, `${TableName.SecretFolder}.envId`)
.where({ projectId })
.whereNull("secretBlindIndex")
.select(selectAllTableCols(TableName.Secret))
.select(
db.ref("slug").withSchema(TableName.Environment).as("environment"),
@@ -49,7 +48,6 @@ export const secretBlindIndexDALFactory = (db: TDbClient) => {
.leftJoin(TableName.Environment, `${TableName.Environment}.id`, `${TableName.SecretFolder}.envId`)
.where({ projectId })
.whereIn(`${TableName.Secret}.id`, secretIds)
.whereNull("secretBlindIndex")
.select(selectAllTableCols(TableName.Secret))
.select(
db.ref("slug").withSchema(TableName.Environment).as("environment"),

View File

@@ -2,10 +2,17 @@ import { ForbiddenError } from "@casl/ability";
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 { TSecretTagDALFactory } from "./secret-tag-dal";
import { TCreateTagDTO, TDeleteTagDTO, TListProjectTagsDTO } from "./secret-tag-types";
import {
TCreateTagDTO,
TDeleteTagDTO,
TGetTagByIdDTO,
TGetTagBySlugDTO,
TListProjectTagsDTO,
TUpdateTagDTO
} from "./secret-tag-types";
type TSecretTagServiceFactoryDep = {
secretTagDAL: TSecretTagDALFactory;
@@ -48,6 +55,28 @@ export const secretTagServiceFactory = ({ secretTagDAL, permissionService }: TSe
return newTag;
};
const updateTag = async ({ actorId, actor, actorOrgId, actorAuthMethod, id, name, color, slug }: TUpdateTagDTO) => {
const tag = await secretTagDAL.findById(id);
if (!tag) throw new BadRequestError({ message: "Tag doesn't exist" });
if (slug) {
const existingTag = await secretTagDAL.findOne({ slug, projectId: tag.projectId });
if (existingTag && existingTag.id !== tag.id) throw new BadRequestError({ message: "Tag already exist" });
}
const { permission } = await permissionService.getProjectPermission(
actor,
actorId,
tag.projectId,
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Tags);
const updatedTag = await secretTagDAL.updateById(tag.id, { name, color, slug });
return updatedTag;
};
const deleteTag = async ({ actorId, actor, actorOrgId, actorAuthMethod, id }: TDeleteTagDTO) => {
const tag = await secretTagDAL.findById(id);
if (!tag) throw new BadRequestError({ message: "Tag doesn't exist" });
@@ -65,6 +94,38 @@ export const secretTagServiceFactory = ({ secretTagDAL, permissionService }: TSe
return deletedTag;
};
const getTagById = async ({ actorId, actor, actorOrgId, actorAuthMethod, id }: TGetTagByIdDTO) => {
const tag = await secretTagDAL.findById(id);
if (!tag) throw new NotFoundError({ message: "Tag doesn't exist" });
const { permission } = await permissionService.getProjectPermission(
actor,
actorId,
tag.projectId,
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Tags);
return tag;
};
const getTagBySlug = async ({ actorId, actor, actorOrgId, actorAuthMethod, slug, projectId }: TGetTagBySlugDTO) => {
const tag = await secretTagDAL.findOne({ projectId, slug });
if (!tag) throw new NotFoundError({ message: "Tag doesn't exist" });
const { permission } = await permissionService.getProjectPermission(
actor,
actorId,
tag.projectId,
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Tags);
return tag;
};
const getProjectTags = async ({ actor, actorId, actorOrgId, actorAuthMethod, projectId }: TListProjectTagsDTO) => {
const { permission } = await permissionService.getProjectPermission(
actor,
@@ -79,5 +140,5 @@ export const secretTagServiceFactory = ({ secretTagDAL, permissionService }: TSe
return tags;
};
return { createTag, deleteTag, getProjectTags };
return { createTag, deleteTag, getProjectTags, getTagById, getTagBySlug, updateTag };
};

View File

@@ -6,6 +6,21 @@ export type TCreateTagDTO = {
slug: string;
} & TProjectPermission;
export type TUpdateTagDTO = {
id: string;
name?: string;
slug?: string;
color?: string;
} & Omit<TProjectPermission, "projectId">;
export type TGetTagByIdDTO = {
id: string;
} & Omit<TProjectPermission, "projectId">;
export type TGetTagBySlugDTO = {
slug: string;
} & TProjectPermission;
export type TDeleteTagDTO = {
id: string;
} & Omit<TProjectPermission, "projectId">;

View File

@@ -421,94 +421,88 @@ export const secretQueueFactory = ({
const folder = await folderDAL.findBySecretPath(projectId, environment, secretPath);
if (!folder) {
logger.error(new Error("Secret path not found"));
return;
throw new Error("Secret path not found");
}
// start syncing all linked imports also
if (depth < MAX_SYNC_SECRET_DEPTH) {
// find all imports made with the given environment and secret path
const linkSourceDto = {
projectId,
importEnv: folder.environment.id,
importPath: secretPath,
isReplication: false
};
const imports = await secretImportDAL.find(linkSourceDto);
// find all imports made with the given environment and secret path
const linkSourceDto = {
projectId,
importEnv: folder.environment.id,
importPath: secretPath,
isReplication: false
};
const imports = await secretImportDAL.find(linkSourceDto);
if (imports.length) {
// keep calling sync secret for all the imports made
const importedFolderIds = unique(imports, (i) => i.folderId).map(({ folderId }) => folderId);
const importedFolders = await folderDAL.findSecretPathByFolderIds(projectId, importedFolderIds);
const foldersGroupedById = groupBy(importedFolders.filter(Boolean), (i) => i?.id as string);
logger.info(
`getIntegrationSecrets: Syncing secret due to link change [jobId=${job.id}] [projectId=${job.data.projectId}] [environment=${job.data.environment}] [secretPath=${job.data.secretPath}] [depth=${depth}]`
);
await Promise.all(
imports
.filter(({ folderId }) => Boolean(foldersGroupedById[folderId][0]?.path as string))
// filter out already synced ones
.filter(
({ folderId }) =>
!deDupeQueue[
uniqueSecretQueueKey(
foldersGroupedById[folderId][0]?.environmentSlug as string,
foldersGroupedById[folderId][0]?.path as string
)
]
)
.map(({ folderId }) =>
syncSecrets({
projectId,
secretPath: foldersGroupedById[folderId][0]?.path as string,
environmentSlug: foldersGroupedById[folderId][0]?.environmentSlug as string,
_deDupeQueue: deDupeQueue,
_depth: depth + 1,
excludeReplication: true
})
)
);
}
const secretReferences = await secretDAL.findReferencedSecretReferences(
projectId,
folder.environment.slug,
secretPath
if (imports.length) {
// keep calling sync secret for all the imports made
const importedFolderIds = unique(imports, (i) => i.folderId).map(({ folderId }) => folderId);
const importedFolders = await folderDAL.findSecretPathByFolderIds(projectId, importedFolderIds);
const foldersGroupedById = groupBy(importedFolders.filter(Boolean), (i) => i?.id as string);
logger.info(
`getIntegrationSecrets: Syncing secret due to link change [jobId=${job.id}] [projectId=${job.data.projectId}] [environment=${job.data.environment}] [secretPath=${job.data.secretPath}] [depth=${depth}]`
);
await Promise.all(
imports
.filter(({ folderId }) => Boolean(foldersGroupedById[folderId][0]?.path as string))
// filter out already synced ones
.filter(
({ folderId }) =>
!deDupeQueue[
uniqueSecretQueueKey(
foldersGroupedById[folderId][0]?.environmentSlug as string,
foldersGroupedById[folderId][0]?.path as string
)
]
)
.map(({ folderId }) =>
syncSecrets({
projectId,
secretPath: foldersGroupedById[folderId][0]?.path as string,
environmentSlug: foldersGroupedById[folderId][0]?.environmentSlug as string,
_deDupeQueue: deDupeQueue,
_depth: depth + 1,
excludeReplication: true
})
)
);
}
const secretReferences = await secretDAL.findReferencedSecretReferences(
projectId,
folder.environment.slug,
secretPath
);
if (secretReferences.length) {
const referencedFolderIds = unique(secretReferences, (i) => i.folderId).map(({ folderId }) => folderId);
const referencedFolders = await folderDAL.findSecretPathByFolderIds(projectId, referencedFolderIds);
const referencedFoldersGroupedById = groupBy(referencedFolders.filter(Boolean), (i) => i?.id as string);
logger.info(
`getIntegrationSecrets: Syncing secret due to reference change [jobId=${job.id}] [projectId=${job.data.projectId}] [environment=${job.data.environment}] [secretPath=${job.data.secretPath}] [depth=${depth}]`
);
await Promise.all(
secretReferences
.filter(({ folderId }) => Boolean(referencedFoldersGroupedById[folderId][0]?.path))
// filter out already synced ones
.filter(
({ folderId }) =>
!deDupeQueue[
uniqueSecretQueueKey(
referencedFoldersGroupedById[folderId][0]?.environmentSlug as string,
referencedFoldersGroupedById[folderId][0]?.path as string
)
]
)
.map(({ folderId }) =>
syncSecrets({
projectId,
secretPath: referencedFoldersGroupedById[folderId][0]?.path as string,
environmentSlug: referencedFoldersGroupedById[folderId][0]?.environmentSlug as string,
_deDupeQueue: deDupeQueue,
_depth: depth + 1,
excludeReplication: true
})
)
);
if (secretReferences.length) {
const referencedFolderIds = unique(secretReferences, (i) => i.folderId).map(({ folderId }) => folderId);
const referencedFolders = await folderDAL.findSecretPathByFolderIds(projectId, referencedFolderIds);
const referencedFoldersGroupedById = groupBy(referencedFolders.filter(Boolean), (i) => i?.id as string);
logger.info(
`getIntegrationSecrets: Syncing secret due to reference change [jobId=${job.id}] [projectId=${job.data.projectId}] [environment=${job.data.environment}] [secretPath=${job.data.secretPath}] [depth=${depth}]`
);
await Promise.all(
secretReferences
.filter(({ folderId }) => Boolean(referencedFoldersGroupedById[folderId][0]?.path))
// filter out already synced ones
.filter(
({ folderId }) =>
!deDupeQueue[
uniqueSecretQueueKey(
referencedFoldersGroupedById[folderId][0]?.environmentSlug as string,
referencedFoldersGroupedById[folderId][0]?.path as string
)
]
)
.map(({ folderId }) =>
syncSecrets({
projectId,
secretPath: referencedFoldersGroupedById[folderId][0]?.path as string,
environmentSlug: referencedFoldersGroupedById[folderId][0]?.environmentSlug as string,
_deDupeQueue: deDupeQueue,
_depth: depth + 1,
excludeReplication: true
})
)
);
}
} else {
logger.info(`getIntegrationSecrets: Secret depth exceeded for [projectId=${projectId}] [folderId=${folder.id}]`);
}
const integrations = await integrationDAL.findByProjectIdV2(projectId, environment); // note: returns array of integrations + integration auths in this environment
@@ -550,7 +544,7 @@ export const secretQueueFactory = ({
}
try {
await syncIntegrationSecrets({
const response = await syncIntegrationSecrets({
createManySecretsRawFn,
updateManySecretsRawFn,
integrationDAL,
@@ -568,13 +562,15 @@ export const secretQueueFactory = ({
await integrationDAL.updateById(integration.id, {
lastSyncJobId: job.id,
lastUsed: new Date(),
syncMessage: "",
isSynced: true
syncMessage: response?.syncMessage ?? "",
isSynced: response?.isSynced ?? true
});
} catch (err: unknown) {
} catch (err) {
logger.info("Secret integration sync error: %o", err);
const message =
err instanceof AxiosError ? JSON.stringify((err as AxiosError)?.response?.data) : (err as Error)?.message;
(err instanceof AxiosError ? JSON.stringify(err?.response?.data) : (err as Error)?.message) ||
"Unknown error occurred.";
await integrationDAL.updateById(integration.id, {
lastSyncJobId: job.id,

View File

@@ -1078,6 +1078,7 @@ export const secretServiceFactory = ({
actor,
environment,
projectId: workspaceId,
expandSecretReferences,
projectSlug,
actorId,
actorOrgId,
@@ -1091,7 +1092,7 @@ export const secretServiceFactory = ({
const botKey = await projectBotService.getBotKey(projectId);
if (!botKey) throw new BadRequestError({ message: "Project bot not found", name: "bot_not_found_error" });
const secret = await getSecretByName({
const encryptedSecret = await getSecretByName({
actorId,
projectId,
actorAuthMethod,
@@ -1105,7 +1106,46 @@ export const secretServiceFactory = ({
version
});
return decryptSecretRaw(secret, botKey);
const decryptedSecret = decryptSecretRaw(encryptedSecret, botKey);
if (expandSecretReferences) {
const expandSecrets = interpolateSecrets({
folderDAL,
projectId,
secretDAL,
secretEncKey: botKey
});
const expandSingleSecret = async (secret: {
secretKey: string;
secretValue: string;
secretComment?: string;
secretPath: string;
skipMultilineEncoding: boolean | null | undefined;
}) => {
const secretRecord: Record<
string,
{ value: string; comment?: string; skipMultilineEncoding: boolean | null | undefined }
> = {
[secret.secretKey]: {
value: secret.secretValue,
comment: secret.secretComment,
skipMultilineEncoding: secret.skipMultilineEncoding
}
};
await expandSecrets(secretRecord);
// Update the secret with the expanded value
// eslint-disable-next-line no-param-reassign
secret.secretValue = secretRecord[secret.secretKey].value;
};
// Expand the secret
await expandSingleSecret(decryptedSecret);
}
return decryptedSecret;
};
const createSecretRaw = async ({

View File

@@ -151,6 +151,7 @@ export type TGetASecretRawDTO = {
secretName: string;
path: string;
environment: string;
expandSecretReferences?: boolean;
type: "shared" | "personal";
includeImports?: boolean;
version?: number;

View File

@@ -1,7 +1,57 @@
import { Knex } from "knex";
import { TDbClient } from "@app/db";
import { TableName } from "@app/db/schemas";
import { TableName, TSuperAdmin, TSuperAdminUpdate } from "@app/db/schemas";
import { DatabaseError } from "@app/lib/errors";
import { ormify } from "@app/lib/knex";
export type TSuperAdminDALFactory = ReturnType<typeof superAdminDALFactory>;
export const superAdminDALFactory = (db: TDbClient) => ormify(db, TableName.SuperAdmin, {});
export const superAdminDALFactory = (db: TDbClient) => {
const superAdminOrm = ormify(db, TableName.SuperAdmin);
const findById = async (id: string, tx?: Knex) => {
const config = await (tx || db)(TableName.SuperAdmin)
.where(`${TableName.SuperAdmin}.id`, id)
.leftJoin(TableName.Organization, `${TableName.SuperAdmin}.defaultAuthOrgId`, `${TableName.Organization}.id`)
.select(
db.ref("*").withSchema(TableName.SuperAdmin) as unknown as keyof TSuperAdmin,
db.ref("slug").withSchema(TableName.Organization).as("defaultAuthOrgSlug")
)
.first();
if (!config) {
return null;
}
return {
...config,
defaultAuthOrgSlug: config?.defaultAuthOrgSlug || null
} as TSuperAdmin & { defaultAuthOrgSlug: string | null };
};
const updateById = async (id: string, data: TSuperAdminUpdate, tx?: Knex) => {
const updatedConfig = await (superAdminOrm || tx).transaction(async (trx: Knex) => {
await superAdminOrm.updateById(id, data, trx);
const config = await findById(id, trx);
if (!config) {
throw new DatabaseError({
error: "Failed to find updated super admin config",
message: "Failed to update super admin config",
name: "UpdateById"
});
}
return config;
});
return updatedConfig;
};
return {
...superAdminOrm,
findById,
updateById
};
};

View File

@@ -1,6 +1,10 @@
import bcrypt from "bcrypt";
import { TSuperAdmin, TSuperAdminUpdate } from "@app/db/schemas";
import { TKeyStoreFactory } from "@app/keystore/keystore";
import { getConfig } from "@app/lib/config/env";
import { infisicalSymmetricEncypt } from "@app/lib/crypto/encryption";
import { getUserPrivateKey } from "@app/lib/crypto/srp";
import { BadRequestError } from "@app/lib/errors";
import { TAuthLoginFactory } from "../auth/auth-login-service";
@@ -21,7 +25,7 @@ type TSuperAdminServiceFactoryDep = {
export type TSuperAdminServiceFactory = ReturnType<typeof superAdminServiceFactory>;
// eslint-disable-next-line
export let getServerCfg: () => Promise<TSuperAdmin>;
export let getServerCfg: () => Promise<TSuperAdmin & { defaultAuthOrgSlug: string | null }>;
const ADMIN_CONFIG_KEY = "infisical-admin-cfg";
const ADMIN_CONFIG_KEY_EXP = 60; // 60s
@@ -38,16 +42,20 @@ export const superAdminServiceFactory = ({
// TODO(akhilmhdh): bad pattern time less change this later to me itself
getServerCfg = async () => {
const config = await keyStore.getItem(ADMIN_CONFIG_KEY);
// missing in keystore means fetch from db
if (!config) {
const serverCfg = await serverCfgDAL.findById(ADMIN_CONFIG_DB_UUID);
if (serverCfg) {
await keyStore.setItemWithExpiry(ADMIN_CONFIG_KEY, ADMIN_CONFIG_KEY_EXP, JSON.stringify(serverCfg)); // insert it back to keystore
if (!serverCfg) {
throw new BadRequestError({ name: "Admin config", message: "Admin config not found" });
}
await keyStore.setItemWithExpiry(ADMIN_CONFIG_KEY, ADMIN_CONFIG_KEY_EXP, JSON.stringify(serverCfg)); // insert it back to keystore
return serverCfg;
}
const keyStoreServerCfg = JSON.parse(config) as TSuperAdmin;
const keyStoreServerCfg = JSON.parse(config) as TSuperAdmin & { defaultAuthOrgSlug: string | null };
return {
...keyStoreServerCfg,
// this is to allow admin router to work
@@ -61,14 +69,21 @@ export const superAdminServiceFactory = ({
const serverCfg = await serverCfgDAL.findById(ADMIN_CONFIG_DB_UUID);
if (serverCfg) return;
// @ts-expect-error id is kept as fixed for idempotence and to avoid race condition
const newCfg = await serverCfgDAL.create({ initialized: false, allowSignUp: true, id: ADMIN_CONFIG_DB_UUID });
const newCfg = await serverCfgDAL.create({
// @ts-expect-error id is kept as fixed for idempotence and to avoid race condition
id: ADMIN_CONFIG_DB_UUID,
initialized: false,
allowSignUp: true,
defaultAuthOrgId: null
});
return newCfg;
};
const updateServerCfg = async (data: TSuperAdminUpdate) => {
const updatedServerCfg = await serverCfgDAL.updateById(ADMIN_CONFIG_DB_UUID, data);
await keyStore.setItemWithExpiry(ADMIN_CONFIG_KEY, ADMIN_CONFIG_KEY_EXP, JSON.stringify(updatedServerCfg));
return updatedServerCfg;
};
@@ -77,6 +92,7 @@ export const superAdminServiceFactory = ({
firstName,
salt,
email,
password,
verifier,
publicKey,
protectedKey,
@@ -92,6 +108,18 @@ export const superAdminServiceFactory = ({
const existingUser = await userDAL.findOne({ email });
if (existingUser) throw new BadRequestError({ name: "Admin sign up", message: "User already exist" });
const privateKey = await getUserPrivateKey(password, {
encryptionVersion: 2,
salt,
protectedKey,
protectedKeyIV,
protectedKeyTag,
encryptedPrivateKey,
iv: encryptedPrivateKeyIV,
tag: encryptedPrivateKeyTag
});
const hashedPassword = await bcrypt.hash(password, appCfg.BCRYPT_SALT_ROUND);
const { iv, tag, ciphertext, encoding } = infisicalSymmetricEncypt(privateKey);
const userInfo = await userDAL.transaction(async (tx) => {
const newUser = await userDAL.create(
{
@@ -119,7 +147,12 @@ export const superAdminServiceFactory = ({
iv: encryptedPrivateKeyIV,
tag: encryptedPrivateKeyTag,
verifier,
userId: newUser.id
userId: newUser.id,
hashedPassword,
serverEncryptedPrivateKey: ciphertext,
serverEncryptedPrivateKeyIV: iv,
serverEncryptedPrivateKeyTag: tag,
serverEncryptedPrivateKeyEncoding: encoding
},
tx
);

View File

@@ -1,5 +1,6 @@
export type TAdminSignUpDTO = {
email: string;
password: string;
publicKey: string;
salt: string;
lastName?: string;

View File

@@ -1,4 +1,5 @@
export enum UserAliasType {
LDAP = "ldap",
SAML = "saml"
SAML = "saml",
OIDC = "oidc"
}

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