mirror of
https://github.com/Infisical/infisical.git
synced 2025-03-22 14:05:22 +00:00
Compare commits
485 Commits
misc/moved
...
ca-renewal
Author | SHA1 | Date | |
---|---|---|---|
856c2423be | |||
f993e4aa5c | |||
bb6416acb7 | |||
5af53d3398 | |||
8da8c6a66c | |||
88a4390ea0 | |||
c70d0a577c | |||
587a4a1120 | |||
e6848828f2 | |||
c8b93e4467 | |||
0bca24bb00 | |||
c563ada50f | |||
26d1616e22 | |||
5fd071d1de | |||
a6ac78356b | |||
e4a2137991 | |||
9721d7a15e | |||
93db5c4555 | |||
ad4393fdef | |||
cd06e4e7f3 | |||
711a4179ce | |||
b4a2a477d3 | |||
8e53a1b171 | |||
71af463ad8 | |||
7abd18b11c | |||
1aee50a751 | |||
e9b37a1f98 | |||
43fded2350 | |||
7b6f4d810d | |||
b97bbe5beb | |||
cf5260b383 | |||
13e0dd8e0f | |||
7f9150e60e | |||
995f0360fb | |||
ecab69a7ab | |||
cca36ab106 | |||
76311a1b5f | |||
a0490d0fde | |||
78e41a51c0 | |||
8414f04e94 | |||
79e414ea9f | |||
83772c1770 | |||
09928efba3 | |||
48eb4e772f | |||
7467a05fc4 | |||
afba636850 | |||
96cc315762 | |||
e95d7e55c1 | |||
520c068ac4 | |||
cf330777ed | |||
c1eae42b26 | |||
9f0d7c6d11 | |||
683e3dd7be | |||
46ca3856b3 | |||
891cb06de0 | |||
aff7481fbc | |||
e7c1a4d4a0 | |||
27f9628dc5 | |||
1866ce4240 | |||
e6b6de5e8e | |||
02e8f20cbf | |||
9184ec0765 | |||
1d55c7bcb0 | |||
96cffd6196 | |||
5bb2866b28 | |||
7a7841e487 | |||
b0819ee592 | |||
b4689bed17 | |||
bfd24ea938 | |||
cea1a5e7ea | |||
8d32ca2fb6 | |||
d468067d43 | |||
3a640d6cf8 | |||
8fc85105a9 | |||
48bd354bae | |||
6e1dc7375c | |||
164627139e | |||
f7c962425c | |||
d92979d50e | |||
021dbf3558 | |||
29060ffc9e | |||
d9c7724857 | |||
9063787772 | |||
c821bc0e14 | |||
83eed831da | |||
5c8d6157d7 | |||
5d78b6941d | |||
1d09d4cdfd | |||
9877444117 | |||
6f2ae344a7 | |||
549d388f59 | |||
e2caa98c74 | |||
6bb41913bf | |||
844a4ebc02 | |||
b37f780c4c | |||
6e7997b1bd | |||
e210a6a24f | |||
b950bf0cf7 | |||
a53d0b2334 | |||
ab88e6c414 | |||
49eb6d6474 | |||
05d7e26f8b | |||
6a156371c0 | |||
8435b20178 | |||
7d7fcd0db6 | |||
b5182550da | |||
3e0ae5765f | |||
f7ef86eb11 | |||
acf9a488ac | |||
4a06e3e712 | |||
b7b0e60b1d | |||
d4747abba8 | |||
641860cdb8 | |||
36ac1f47ca | |||
643d13b0ec | |||
ef2816b2ee | |||
9e314d7a09 | |||
8eab27d752 | |||
b563c4030b | |||
761a0f121c | |||
70400ef369 | |||
9aecfe77ad | |||
cedeb1ce27 | |||
0e75a8f6d7 | |||
a5b030c4a7 | |||
4009580cf2 | |||
64869ea8e0 | |||
ffc1b1ec1c | |||
880a689376 | |||
3709f31b5a | |||
6b6fd9735c | |||
a57d1f1c9a | |||
6c06de6da4 | |||
0c9e979fb8 | |||
32fc254ae1 | |||
69d813887b | |||
80be054425 | |||
4d032cfbfa | |||
d41011e056 | |||
d918f3ecdf | |||
7e5c3e8163 | |||
cb347aa16a | |||
88a7cc3068 | |||
4ddfb05134 | |||
7bb0ec0111 | |||
31af4a4602 | |||
dd46a21035 | |||
26a5d74b14 | |||
7e9389cb26 | |||
eda57881ec | |||
5eafdba6c8 | |||
9c4bb79472 | |||
937b0c0a7c | |||
cb132f4c65 | |||
4caa77e28a | |||
547be80dcf | |||
2cbae96c9a | |||
553d51e5b3 | |||
16e0a441ae | |||
d6c0941fa9 | |||
7cbd254f06 | |||
4b83b92725 | |||
fe72f034c1 | |||
d5f4ce4376 | |||
6803553b21 | |||
1c8299054a | |||
85653a90d5 | |||
98b6373d6a | |||
1d97921c7c | |||
0d4164ea81 | |||
79bd8613d3 | |||
8deea21a83 | |||
3b3c2be933 | |||
c041e44399 | |||
c1aeb04174 | |||
3f3c0aab0f | |||
b740e8c900 | |||
4416b11094 | |||
d8169a866d | |||
7239158e7f | |||
879ef2c178 | |||
8777cfe680 | |||
2b630f75aa | |||
91cee20cc8 | |||
4249ec6030 | |||
e7a95e6af2 | |||
a9f04a3c1f | |||
3d380710ee | |||
2177ec6bcc | |||
fefe2d1de1 | |||
3f3e41282d | |||
c14f94177a | |||
ceb741955d | |||
f5bc4e1b5f | |||
06900b9c99 | |||
d71cb96adf | |||
61ebec25b3 | |||
57320c51fb | |||
4aa9cd0f72 | |||
ea39ef9269 | |||
15749a1f52 | |||
9e9aff129e | |||
4ac487c974 | |||
2e50072caa | |||
2bd170df7d | |||
938a7b7e72 | |||
af864b456b | |||
a30e3874cd | |||
de886f8dd0 | |||
b3db29ac37 | |||
070eb2aacd | |||
e619cfa313 | |||
c3038e3ca1 | |||
ce1db38afd | |||
0fa6b7a08a | |||
29c5bf5491 | |||
4d711ae149 | |||
ff0e7feeee | |||
9dd675ff98 | |||
8fd3e50d04 | |||
391ed0723e | |||
84af8e708e | |||
b39b5bd1a1 | |||
b3d9d91b52 | |||
5ad4061881 | |||
f29862eaf2 | |||
7cb174b644 | |||
bf00d16c80 | |||
e30a0fe8be | |||
6e6f0252ae | |||
2348df7a4d | |||
962cf67dfb | |||
32627c20c4 | |||
c50f8fd78c | |||
1cb4dc9e84 | |||
977ce09245 | |||
08d7dead8c | |||
a30e06e392 | |||
23f3f09cb6 | |||
5cd0f665fa | |||
443e76c1df | |||
4ea22b6761 | |||
ae7e0d0963 | |||
ed6c6d54c0 | |||
428ff5186f | |||
d07b0d20d6 | |||
8e373fe9bf | |||
28087cdcc4 | |||
dcef49950d | |||
1e5d567ef7 | |||
d09c320150 | |||
229599b8de | |||
02eea4d886 | |||
d12144a7e7 | |||
5fa69235d1 | |||
7dd9337b1c | |||
f9eaee4dbc | |||
cd3a64f3e7 | |||
121254f98d | |||
1591c1dbac | |||
3c59d288c4 | |||
632b775d7f | |||
d66da3d770 | |||
da43f405c4 | |||
d5c0abbc3b | |||
7a642e7634 | |||
de686acc23 | |||
b359f4278e | |||
29d76c1deb | |||
6ba1012f5b | |||
4abb3ef348 | |||
73e764474d | |||
7eb5689b4c | |||
5d945f432d | |||
1066710c4f | |||
b64d4e57c4 | |||
bd860e6c5a | |||
37137b8c68 | |||
8b10cf863d | |||
eb45bed7d9 | |||
1ee65205a0 | |||
f41272d4df | |||
8bf4df9f27 | |||
037a8f2ebb | |||
14bc436283 | |||
a108c7dde1 | |||
54ccd73d2a | |||
729ca7b6d6 | |||
754db67f11 | |||
f97756a07b | |||
22df51ab8e | |||
bff8f55ea2 | |||
2f17f5e7df | |||
72d2247bf2 | |||
4ecd4c0337 | |||
538613dd40 | |||
4c5c24f689 | |||
dead16a98a | |||
224368b172 | |||
3731459e99 | |||
dc055c11ab | |||
22878a035b | |||
2f2c9d4508 | |||
774017adbe | |||
f9d1d9c89f | |||
eb82fc0d9a | |||
e45585a909 | |||
6f0484f074 | |||
4ba529f22d | |||
5360fb033a | |||
27e14bcafe | |||
bc5003ae4c | |||
f544b39597 | |||
8381f52f1e | |||
aa96a833d7 | |||
53c64b759c | |||
74f2224c6b | |||
ecb5342a55 | |||
bcb657b81e | |||
ebe6b08cab | |||
43b14d0091 | |||
7127f6d1e1 | |||
20387cff35 | |||
997d7f22fc | |||
e1ecad2331 | |||
ce26a06129 | |||
7622cac07e | |||
a101602e0a | |||
ca63a7baa7 | |||
ff4f15c437 | |||
d6c2715852 | |||
fc386c0cbc | |||
263a88379f | |||
4b718b679a | |||
498b1109c9 | |||
b70bf4cadb | |||
d301f74feb | |||
454826fbb6 | |||
f464d7a096 | |||
cae9ace1ca | |||
8a5a295a01 | |||
95a4661787 | |||
7e9c846ba3 | |||
aed310b9ee | |||
c331af5345 | |||
d4dd684f32 | |||
1f6c33bdb8 | |||
a538e37a62 | |||
f3f87cfd84 | |||
2c57bd94fb | |||
869fcd6541 | |||
7b3e116bf8 | |||
0a95f6dc1d | |||
d19c856e9b | |||
ada0033bd0 | |||
6818c8730f | |||
8542ec8c3e | |||
c141b916d3 | |||
b09dddec1c | |||
1ae375188b | |||
22b954b657 | |||
9efeb8926f | |||
389bbfcade | |||
0b8427a004 | |||
8a470772e3 | |||
853f3c40bc | |||
fed44f328d | |||
a1d00f2c41 | |||
1d6d424c91 | |||
c39ea130b1 | |||
95a68f2c2d | |||
db7c0c45f6 | |||
82bca03162 | |||
043c04778f | |||
560cd81a1c | |||
df3a87fabf | |||
6eae98c1d4 | |||
6ceeccf583 | |||
9b0b14b847 | |||
78f4c0f002 | |||
6cff2f0437 | |||
6cefb180d6 | |||
59a44155c5 | |||
d0ad9c6b17 | |||
58a406b114 | |||
8a85695dc5 | |||
7ed8feee6f | |||
de67c0ad9f | |||
b8d11d31a6 | |||
d630ceaffe | |||
a89e60f296 | |||
a5d9abf1c8 | |||
d97dea2573 | |||
bc58f6b988 | |||
ed8e3f34fb | |||
91315c88c3 | |||
9267f881d6 | |||
c2ddb7e2fe | |||
5514508482 | |||
5921dcaa51 | |||
c90ecd336c | |||
d8b1da3ddd | |||
58e86382fe | |||
b2c62c4193 | |||
2080c4419e | |||
b582a4a06d | |||
a5c6a864de | |||
5082c1ba3b | |||
cceb08b1b5 | |||
4c34e58945 | |||
72de1901a1 | |||
65fefcdd87 | |||
8e753eda72 | |||
7137c94fa2 | |||
52ea7dfa61 | |||
093925ed0e | |||
356afd18c4 | |||
4491f2d8f1 | |||
4a401957c7 | |||
539785acae | |||
3c63346d3a | |||
0c673f6cca | |||
10f4cbf11f | |||
a6a8c32326 | |||
99a474dba7 | |||
ae2ecf1540 | |||
10214ea5dc | |||
918cd414a8 | |||
f9a125acee | |||
52415ea83e | |||
c5ca2b6796 | |||
ef5bcac925 | |||
6cbeb29b4e | |||
fbe344c0df | |||
5821f65a63 | |||
3af510d487 | |||
c15adc7df9 | |||
93af7573ac | |||
cddda1148e | |||
9c37eeeda6 | |||
eadf5bef77 | |||
5dff46ee3a | |||
8b202c2a79 | |||
4574519a76 | |||
82ee77bc05 | |||
9a861499df | |||
17c7207f9d | |||
d1f3c98f21 | |||
d248a6166c | |||
8fdd82a335 | |||
eac621db73 | |||
ab7983973e | |||
ff43773f37 | |||
68574be05b | |||
1d9966af76 | |||
4dddf764bd | |||
2d9435457d | |||
5d4c7c2cbf | |||
8b06215366 | |||
08f0bf9c67 | |||
654dd97793 | |||
2e7baf8c89 | |||
7ca7a95070 | |||
71c49c8b90 | |||
4fab746b95 | |||
179edd98bf | |||
dc05b34fb1 | |||
899757ab7c | |||
20f6dbfbd1 | |||
8ff524a037 | |||
3913e2f462 | |||
ebbccdb857 | |||
28723e9a4e | |||
079e005f49 | |||
d20ae39f32 | |||
05bf2e4696 | |||
a06dee66f8 | |||
8bc6edd165 | |||
2497aada8a | |||
5921f349a8 | |||
4927cc804a | |||
2153dd94eb | |||
08322f46f9 | |||
fc9326272a | |||
c90e423e4a |
.github/workflows
backend
e2e-test/routes/v3
package-lock.jsonpackage.jsonsrc
@types
db
migrations
20240708100026_external-kms.ts20240710045107_identity-oidc-auth.ts20240715113110_org-membership-active-status.ts20240717184929_add-enforcement-level-secrets-policies.ts20240717194958_add-enforcement-level-access-policies.ts20240718170955_add-access-secret-sharing.ts20240719182539_add-bypass-reason-secret-approval-requets.ts20240728010334_secret-sharing-name.ts20240730181830_add-org-kms-data-key.ts20240730181840_add-project-data-key.ts20240730181850_secret-v2.ts20240802181855_ca-cert-version.ts
utils
schemas
access-approval-policies.tscertificate-authorities.tscertificate-authority-certs.tscertificates.tsexternal-kms.tsidentity-oidc-auths.tsindex.tsintegration-auths.tsinternal-kms-key-version.tsinternal-kms.tskms-keys.tsmodels.tsorg-memberships.tsorganizations.tsprojects.tssecret-approval-policies.tssecret-approval-request-secret-tags-v2.tssecret-approval-requests-secrets-v2.tssecret-approval-requests.tssecret-references-v2.tssecret-rotation-output-v2.tssecret-sharing.tssecret-snapshot-secrets-v2.tssecret-v2-tag-junction.tssecret-version-v2-tag-junction.tssecret-versions-v2.tssecrets-v2.ts
seed-data.tsseeds
ee
routes/v1
access-approval-policy-router.tsaccess-approval-request-router.tsexternal-kms-router.tsindex.tsorg-role-router.tsproject-role-router.tsproject-router.tsscim-router.tssecret-approval-policy-router.tssecret-approval-request-router.tssecret-rotation-router.tssecret-version-router.tssnapshot-router.ts
services
access-approval-policy
access-approval-request
audit-log
certificate-authority-crl
dynamic-secret-lease
external-kms
group
ldap-config
license
oidc
permission
saml-config
scim
secret-approval-policy
secret-approval-request
secret-approval-request-dal.tssecret-approval-request-secret-dal.tssecret-approval-request-service.tssecret-approval-request-types.ts
secret-replication
secret-rotation
secret-snapshot
keystore
lib
api-docs
crypto
fn
knex
types
queue
server/routes
index.tssanitizedSchemas.ts
v1
certificate-authority-router.tsidentity-aws-iam-auth-router.tsidentity-azure-auth-router.tsidentity-gcp-auth-router.tsidentity-kubernetes-auth-router.tsidentity-oidc-auth-router.tsidentity-router.tsidentity-token-auth-router.tsidentity-universal-auth-router.tsindex.tsorg-admin-router.tsproject-env-router.tsproject-router.tssecret-folder-router.tssecret-import-router.tssecret-sharing-router.tswebhook-router.ts
v2
v3
services
auth-token
certificate-authority
certificate-authority-fns.tscertificate-authority-queue.tscertificate-authority-service.tscertificate-authority-types.ts
certificate
identity-access-token
identity-aws-auth
identity-azure-auth
identity-gcp-auth
identity-kubernetes-auth
identity-oidc-auth
identity-oidc-auth-dal.tsidentity-oidc-auth-service.tsidentity-oidc-auth-types.tsidentity-oidc-auth-validators.ts
identity-project
integration-auth
integration
kms
org-admin
org-membership
org
project-bot
project-env
project-membership
project-role
project
resource-cleanup
secret-folder
secret-import
secret-sharing
secret-tag
secret-v2-bridge
secret-v2-bridge-dal.tssecret-v2-bridge-fns.tssecret-v2-bridge-service.tssecret-v2-bridge-types.tssecret-version-dal.tssecret-version-tag-dal.ts
secret
smtp
smtp-service.ts
templates
webhook
cli
company
docs
api-reference/endpoints
aws-auth
azure-auth
certificate-authorities
certificates
folders
gcp-auth
kubernetes-auth
oidc-auth
token-auth
changelog
cli/commands
documentation
images
integrations/cloudflare
platform
access-controls
identities
kms
aws-hsm
create-key-store-cert.pngcreate-key-store-cluster.pngcreate-key-store-name.pngcreate-key-store-password.pngcreate-kms-key-1.pngcreate-kms-key-2.pngcreate-kms-select-hsm.png
aws
aws-kms-key-id.pngencryption-modal-provider-select.pngencryption-org-settings-add.pngencryption-org-settings.pngencryption-project-settings-select.pngencryption-project-settings.png
configure-kms-existing.pngconfigure-kms-new.pngpki
pr-workflows
secret-sharing
self-hosting/deployment-options/native
integrations
mint.jsonsdks
self-hosting
frontend
next.config.jspackage-lock.jsonpackage.jsonconst.tsusePopUp.tsx
src
components
utilities/secrets
v2
Checkbox
DeleteActionModal
EmptyState
FormControl
InfisicalSecretInput
Input
Pagination
UpgradeOverlay
UpgradeProjectAlert
index.tsxcontext
helpers
hooks
api
accessApproval
auditLogs
ca
identities
index.tsxkms
orgAdmin
policies
roles
secretApproval
secretApprovalRequest
secretImports
secretRotation
secretSharing
secretSnapshots
secrets
subscriptions
users
workspace
layouts/AppLayout
pages
cli-redirect.tsx
integrations
aws-parameter-store
aws-secret-manager
circleci
flyio
github
gitlab
hashicorp-vault
heroku
qovery
render
vercel
login
org/[id]
project/[id]
share-secret
shared/secret/[id]
views
IntegrationsPage
Login/components
Org
IdentityPage
IdentityPage.tsx
components
MembersPage
MembersPage.tsx
components
OrgGroupsTab/components/OrgGroupsSection
OrgIdentityTab/components/IdentitySection
IdentityAuthMethodModal.tsxIdentityAwsAuthForm.tsxIdentityAzureAuthForm.tsxIdentityGcpAuthForm.tsxIdentityKubernetesAuthForm.tsxIdentityOidcAuthForm.tsxIdentitySection.tsxIdentityTable.tsxIdentityTokenAuthForm.tsxIdentityUniversalAuthClientSecretModal.tsxIdentityUniversalAuthForm.tsx
OrgMembersTab
OrgRoleTabSection
RolePage
RolePage.tsx
components
OrgRoleModifySection.utils.tsRoleDetailsSection.tsxRoleModal.tsx
index.tsxRolePermissionsSection
OrgPermissionAdminConsoleRow.tsxOrgRoleWorkspaceRow.tsxRolePermissionRow.tsxRolePermissionsSection.tsxindex.tsx
index.tsxTypes
UserPage
OrgAdminPage
Project
AuditLogsPage/components
CaPage
CertificatesPage/components/CaTab/components
MembersPage
MembersPage.tsx
components
GroupsTab/components/GroupsSection
MemberListTab
MembersTab
MembersTab.tsx
components
index.tsxProjectRoleListTab
ProjectRoleListTab.tsx
index.tsxcomponents
RolePage
RolePage.tsx
components
RoleDetailsSection.tsxRoleModal.tsx
index.tsxRolePermissionsSection
ProjectRoleModifySection.utils.tsRolePermissionRow.tsxRolePermissionSecretFoldersRow.tsxRolePermissionSecretsRow.tsxRolePermissionsSection.tsxindex.tsx
index.tsxTypes
SecretApprovalPage
SecretApprovalPage.tsx
components
AccessApprovalPolicyList
AccessApprovalRequest
ApprovalPolicyList
SecretApprovalPolicyList
SecretApprovalRequest/components
SecretMainPage
SecretMainPage.tsx
components
ActionBar
CreateSecretForm
FolderListView
SecretDropzone
SecretImportListView
SecretListView
SnapshotView
SecretOverviewPage
SecretOverviewPage.tsx
components
CreateSecretForm
ProjectIndexSecretsSection
SecretOverviewTableRow
SecretV2MigrationSection
SelectionPanel
SecretRotationPage
Settings
OrgSettingsPage/components
OrgEncryptionTab
OrgTabGroup
ProjectSettingsPage
ShareSecretPage/components
AddShareSecretForm.tsxAddShareSecretModal.tsxShareSecretSection.tsxShareSecretsRow.tsxShareSecretsTable.tsxViewAndCopySharedSecret.tsx
ShareSecretPublicPage
ViewSecretPublicPage
helm-charts/secrets-operator
k8-operator
13
.github/workflows/build-binaries.yml
vendored
13
.github/workflows/build-binaries.yml
vendored
@ -14,7 +14,6 @@ defaults:
|
||||
|
||||
jobs:
|
||||
build-and-deploy:
|
||||
runs-on: ubuntu-20.04
|
||||
strategy:
|
||||
matrix:
|
||||
arch: [x64, arm64]
|
||||
@ -24,6 +23,7 @@ jobs:
|
||||
target: node20-linux
|
||||
- os: win
|
||||
target: node20-win
|
||||
runs-on: ${{ (matrix.arch == 'arm64' && matrix.os == 'linux') && 'ubuntu24-arm64' || 'ubuntu-latest' }}
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
@ -49,9 +49,9 @@ jobs:
|
||||
- name: Package into node binary
|
||||
run: |
|
||||
if [ "${{ matrix.os }}" != "linux" ]; then
|
||||
pkg --no-bytecode --public-packages "*" --public --compress Brotli --target ${{ matrix.target }}-${{ matrix.arch }} --output ./binary/infisical-core-${{ matrix.os }}-${{ matrix.arch }} .
|
||||
pkg --no-bytecode --public-packages "*" --public --target ${{ matrix.target }}-${{ matrix.arch }} --output ./binary/infisical-core-${{ matrix.os }}-${{ matrix.arch }} .
|
||||
else
|
||||
pkg --no-bytecode --public-packages "*" --public --compress Brotli --target ${{ matrix.target }}-${{ matrix.arch }} --output ./binary/infisical-core .
|
||||
pkg --no-bytecode --public-packages "*" --public --target ${{ matrix.target }}-${{ matrix.arch }} --output ./binary/infisical-core .
|
||||
fi
|
||||
|
||||
# Set up .deb package structure (Debian/Ubuntu only)
|
||||
@ -84,7 +84,12 @@ jobs:
|
||||
mv infisical-core.deb ./binary/infisical-core-${{matrix.arch}}.deb
|
||||
|
||||
- uses: actions/setup-python@v4
|
||||
- run: pip install --upgrade cloudsmith-cli
|
||||
with:
|
||||
python-version: "3.x" # Specify the Python version you need
|
||||
- name: Install Python dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install --upgrade cloudsmith-cli
|
||||
|
||||
# Publish .deb file to Cloudsmith (Debian/Ubuntu only)
|
||||
- name: Publish to Cloudsmith (Debian/Ubuntu)
|
||||
|
@ -22,14 +22,14 @@ jobs:
|
||||
# uncomment this when testing locally using nektos/act
|
||||
- uses: KengoTODA/actions-setup-docker-compose@v1
|
||||
if: ${{ env.ACT }}
|
||||
name: Install `docker-compose` for local simulations
|
||||
name: Install `docker compose` for local simulations
|
||||
with:
|
||||
version: "2.14.2"
|
||||
- name: 📦Build the latest image
|
||||
run: docker build --tag infisical-api .
|
||||
working-directory: backend
|
||||
- name: Start postgres and redis
|
||||
run: touch .env && docker-compose -f docker-compose.dev.yml up -d db redis
|
||||
run: touch .env && docker compose -f docker-compose.dev.yml up -d db redis
|
||||
- name: Start the server
|
||||
run: |
|
||||
echo "SECRET_SCANNING_GIT_APP_ID=793712" >> .env
|
||||
@ -72,6 +72,6 @@ jobs:
|
||||
run: oasdiff breaking https://app.infisical.com/api/docs/json http://localhost:4000/api/docs/json --fail-on ERR
|
||||
- name: cleanup
|
||||
run: |
|
||||
docker-compose -f "docker-compose.dev.yml" down
|
||||
docker compose -f "docker-compose.dev.yml" down
|
||||
docker stop infisical-api
|
||||
docker remove infisical-api
|
||||
|
6
.github/workflows/run-backend-tests.yml
vendored
6
.github/workflows/run-backend-tests.yml
vendored
@ -20,7 +20,7 @@ jobs:
|
||||
uses: actions/checkout@v3
|
||||
- uses: KengoTODA/actions-setup-docker-compose@v1
|
||||
if: ${{ env.ACT }}
|
||||
name: Install `docker-compose` for local simulations
|
||||
name: Install `docker compose` for local simulations
|
||||
with:
|
||||
version: "2.14.2"
|
||||
- name: 🔧 Setup Node 20
|
||||
@ -33,7 +33,7 @@ jobs:
|
||||
run: npm install
|
||||
working-directory: backend
|
||||
- name: Start postgres and redis
|
||||
run: touch .env && docker-compose -f docker-compose.dev.yml up -d db redis
|
||||
run: touch .env && docker compose -f docker-compose.dev.yml up -d db redis
|
||||
- name: Start integration test
|
||||
run: npm run test:e2e
|
||||
working-directory: backend
|
||||
@ -44,4 +44,4 @@ jobs:
|
||||
ENCRYPTION_KEY: 4bnfe4e407b8921c104518903515b218
|
||||
- name: cleanup
|
||||
run: |
|
||||
docker-compose -f "docker-compose.dev.yml" down
|
||||
docker compose -f "docker-compose.dev.yml" down
|
576
backend/e2e-test/routes/v3/secrets-v2.spec.ts
Normal file
576
backend/e2e-test/routes/v3/secrets-v2.spec.ts
Normal file
@ -0,0 +1,576 @@
|
||||
import { SecretType } from "@app/db/schemas";
|
||||
import { seedData1 } from "@app/db/seed-data";
|
||||
import { AuthMode } from "@app/services/auth/auth-type";
|
||||
|
||||
type TRawSecret = {
|
||||
secretKey: string;
|
||||
secretValue: string;
|
||||
secretComment?: string;
|
||||
version: number;
|
||||
};
|
||||
const createSecret = async (dto: { path: string; key: string; value: string; comment: string; type?: SecretType }) => {
|
||||
const createSecretReqBody = {
|
||||
workspaceId: seedData1.projectV3.id,
|
||||
environment: seedData1.environment.slug,
|
||||
type: dto.type || SecretType.Shared,
|
||||
secretPath: dto.path,
|
||||
secretKey: dto.key,
|
||||
secretValue: dto.value,
|
||||
secretComment: dto.comment
|
||||
};
|
||||
const createSecRes = await testServer.inject({
|
||||
method: "POST",
|
||||
url: `/api/v3/secrets/raw/${dto.key}`,
|
||||
headers: {
|
||||
authorization: `Bearer ${jwtAuthToken}`
|
||||
},
|
||||
body: createSecretReqBody
|
||||
});
|
||||
expect(createSecRes.statusCode).toBe(200);
|
||||
const createdSecretPayload = JSON.parse(createSecRes.payload);
|
||||
expect(createdSecretPayload).toHaveProperty("secret");
|
||||
return createdSecretPayload.secret as TRawSecret;
|
||||
};
|
||||
|
||||
const deleteSecret = async (dto: { path: string; key: string }) => {
|
||||
const deleteSecRes = await testServer.inject({
|
||||
method: "DELETE",
|
||||
url: `/api/v3/secrets/raw/${dto.key}`,
|
||||
headers: {
|
||||
authorization: `Bearer ${jwtAuthToken}`
|
||||
},
|
||||
body: {
|
||||
workspaceId: seedData1.projectV3.id,
|
||||
environment: seedData1.environment.slug,
|
||||
secretPath: dto.path
|
||||
}
|
||||
});
|
||||
expect(deleteSecRes.statusCode).toBe(200);
|
||||
const updatedSecretPayload = JSON.parse(deleteSecRes.payload);
|
||||
expect(updatedSecretPayload).toHaveProperty("secret");
|
||||
return updatedSecretPayload.secret as TRawSecret;
|
||||
};
|
||||
|
||||
describe.each([{ auth: AuthMode.JWT }, { auth: AuthMode.IDENTITY_ACCESS_TOKEN }])(
|
||||
"Secret V2 Architecture - $auth mode",
|
||||
async ({ auth }) => {
|
||||
let folderId = "";
|
||||
let authToken = "";
|
||||
const secretTestCases = [
|
||||
{
|
||||
path: "/",
|
||||
secret: {
|
||||
key: "SEC1",
|
||||
value: "something-secret",
|
||||
comment: "some comment"
|
||||
}
|
||||
},
|
||||
{
|
||||
path: "/nested1/nested2/folder",
|
||||
secret: {
|
||||
key: "NESTED-SEC1",
|
||||
value: "something-secret",
|
||||
comment: "some comment"
|
||||
}
|
||||
},
|
||||
{
|
||||
path: "/",
|
||||
secret: {
|
||||
key: "secret-key-2",
|
||||
value: `-----BEGIN PRIVATE KEY-----
|
||||
MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCa6eeFk+cMVqFn
|
||||
hoVQDYgn2Ptp5Azysr2UPq6P73pCL9BzUtOXKZROqDyGehzzfg3wE2KdYU1Jk5Uq
|
||||
fP0ZOWDIlM2SaVCSI3FW32o5+ZiggjpqcVdLFc/PS0S/ZdSmpPd8h11iO2brtIAI
|
||||
ugTW8fcKlGSNUwx9aFmE7A6JnTRliTxB1l6QaC+YAwTK39VgeVH2gDSWC407aS15
|
||||
QobAkaBKKmFkzB5D7i2ZJwt+uXJV/rbLmyDmtnw0lubciGn7NX9wbYef180fisqT
|
||||
aPNAz0nPKk0fFH2Wd5MZixNGbrrpDA+FCYvI5doThZyT2hpj08qWP07oXXCAqw46
|
||||
IEupNSILAgMBAAECggEBAIJb5KzeaiZS3B3O8G4OBQ5rJB3WfyLYUHnoSWLsBbie
|
||||
nc392/ovThLmtZAAQE6SO85Tsb93+t64Z2TKqv1H8G658UeMgfWIB78v4CcLJ2mi
|
||||
TN/3opqXrzjkQOTDHzBgT7al/mpETHZ6fOdbCemK0fVALGFUioUZg4M8VXtuI4Jw
|
||||
q28jAyoRKrCrzda4BeQ553NZ4G5RvwhX3O2I8B8upTbt5hLcisBKy8MPLYY5LUFj
|
||||
YKAP+raf6QLliP6KYHuVxUlgzxjLTxVG41etcyqqZF+foyiKBO3PU3n8oh++tgQP
|
||||
ExOxiR0JSkBG5b+oOBD0zxcvo3/SjBHn0dJOZCSU2SkCgYEAyCe676XnNyBZMRD7
|
||||
6trsaoiCWBpA6M8H44+x3w4cQFtqV38RyLy60D+iMKjIaLqeBbnay61VMzo24Bz3
|
||||
EuF2n4+9k/MetLJ0NCw8HmN5k0WSMD2BFsJWG8glVbzaqzehP4tIclwDTYc1jQVt
|
||||
IoV2/iL7HGT+x2daUwbU5kN5hK0CgYEAxiLB+fmjxJW7VY4SHDLqPdpIW0q/kv4K
|
||||
d/yZBrCX799vjmFb9vLh7PkQUfJhMJ/ttJOd7EtT3xh4mfkBeLfHwVU0d/ahbmSH
|
||||
UJu/E9ZGxAW3PP0kxHZtPrLKQwBnfq8AxBauIhR3rPSorQTIOKtwz1jMlHFSUpuL
|
||||
3KeK2YfDYJcCgYEAkQnJOlNcAuRb/WQzSHIvktssqK8NjiZHryy3Vc0hx7j2jES2
|
||||
HGI2dSVHYD9OSiXA0KFm3OTTsnViwm/60iGzFdjRJV6tR39xGUVcoyCuPnvRfUd0
|
||||
PYvBXgxgkYpyYlPDcwp5CvWGJy3tLi1acgOIwIuUr3S38sL//t4adGk8q1kCgYB8
|
||||
Jbs1Tl53BvrimKpwUNbE+sjrquJu0A7vL68SqgQJoQ7dP9PH4Ff/i+/V6PFM7mib
|
||||
BQOm02wyFbs7fvKVGVJoqWK+6CIucX732x7W5yRgHtS5ukQXdbzt1Ek3wkEW98Cb
|
||||
HTruz7RNAt/NyXlLSODeit1lBbx3Vk9EaxZtRsv88QKBgGn7JwXgez9NOyobsNIo
|
||||
QVO80rpUeenSjuFi+R0VmbLKe/wgAQbYJ0xTAsQ0btqViMzB27D6mJyC+KUIwWNX
|
||||
MN8a+m46v4kqvZkKL2c4gmDibyURNe/vCtCHFuanJS/1mo2tr4XDyEeiuK52eTd9
|
||||
omQDpP86RX/hIIQ+JyLSaWYa
|
||||
-----END PRIVATE KEY-----`,
|
||||
comment:
|
||||
"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation"
|
||||
}
|
||||
},
|
||||
{
|
||||
path: "/nested1/nested2/folder",
|
||||
secret: {
|
||||
key: "secret-key-3",
|
||||
value: `-----BEGIN PRIVATE KEY-----
|
||||
MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCa6eeFk+cMVqFn
|
||||
hoVQDYgn2Ptp5Azysr2UPq6P73pCL9BzUtOXKZROqDyGehzzfg3wE2KdYU1Jk5Uq
|
||||
fP0ZOWDIlM2SaVCSI3FW32o5+ZiggjpqcVdLFc/PS0S/ZdSmpPd8h11iO2brtIAI
|
||||
ugTW8fcKlGSNUwx9aFmE7A6JnTRliTxB1l6QaC+YAwTK39VgeVH2gDSWC407aS15
|
||||
QobAkaBKKmFkzB5D7i2ZJwt+uXJV/rbLmyDmtnw0lubciGn7NX9wbYef180fisqT
|
||||
aPNAz0nPKk0fFH2Wd5MZixNGbrrpDA+FCYvI5doThZyT2hpj08qWP07oXXCAqw46
|
||||
IEupNSILAgMBAAECggEBAIJb5KzeaiZS3B3O8G4OBQ5rJB3WfyLYUHnoSWLsBbie
|
||||
nc392/ovThLmtZAAQE6SO85Tsb93+t64Z2TKqv1H8G658UeMgfWIB78v4CcLJ2mi
|
||||
TN/3opqXrzjkQOTDHzBgT7al/mpETHZ6fOdbCemK0fVALGFUioUZg4M8VXtuI4Jw
|
||||
q28jAyoRKrCrzda4BeQ553NZ4G5RvwhX3O2I8B8upTbt5hLcisBKy8MPLYY5LUFj
|
||||
YKAP+raf6QLliP6KYHuVxUlgzxjLTxVG41etcyqqZF+foyiKBO3PU3n8oh++tgQP
|
||||
ExOxiR0JSkBG5b+oOBD0zxcvo3/SjBHn0dJOZCSU2SkCgYEAyCe676XnNyBZMRD7
|
||||
6trsaoiCWBpA6M8H44+x3w4cQFtqV38RyLy60D+iMKjIaLqeBbnay61VMzo24Bz3
|
||||
EuF2n4+9k/MetLJ0NCw8HmN5k0WSMD2BFsJWG8glVbzaqzehP4tIclwDTYc1jQVt
|
||||
IoV2/iL7HGT+x2daUwbU5kN5hK0CgYEAxiLB+fmjxJW7VY4SHDLqPdpIW0q/kv4K
|
||||
d/yZBrCX799vjmFb9vLh7PkQUfJhMJ/ttJOd7EtT3xh4mfkBeLfHwVU0d/ahbmSH
|
||||
UJu/E9ZGxAW3PP0kxHZtPrLKQwBnfq8AxBauIhR3rPSorQTIOKtwz1jMlHFSUpuL
|
||||
3KeK2YfDYJcCgYEAkQnJOlNcAuRb/WQzSHIvktssqK8NjiZHryy3Vc0hx7j2jES2
|
||||
HGI2dSVHYD9OSiXA0KFm3OTTsnViwm/60iGzFdjRJV6tR39xGUVcoyCuPnvRfUd0
|
||||
PYvBXgxgkYpyYlPDcwp5CvWGJy3tLi1acgOIwIuUr3S38sL//t4adGk8q1kCgYB8
|
||||
Jbs1Tl53BvrimKpwUNbE+sjrquJu0A7vL68SqgQJoQ7dP9PH4Ff/i+/V6PFM7mib
|
||||
BQOm02wyFbs7fvKVGVJoqWK+6CIucX732x7W5yRgHtS5ukQXdbzt1Ek3wkEW98Cb
|
||||
HTruz7RNAt/NyXlLSODeit1lBbx3Vk9EaxZtRsv88QKBgGn7JwXgez9NOyobsNIo
|
||||
QVO80rpUeenSjuFi+R0VmbLKe/wgAQbYJ0xTAsQ0btqViMzB27D6mJyC+KUIwWNX
|
||||
MN8a+m46v4kqvZkKL2c4gmDibyURNe/vCtCHFuanJS/1mo2tr4XDyEeiuK52eTd9
|
||||
omQDpP86RX/hIIQ+JyLSaWYa
|
||||
-----END PRIVATE KEY-----`,
|
||||
comment:
|
||||
"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation"
|
||||
}
|
||||
},
|
||||
{
|
||||
path: "/nested1/nested2/folder",
|
||||
secret: {
|
||||
key: "secret-key-3",
|
||||
value:
|
||||
"TG9yZW0gaXBzdW0gZG9sb3Igc2l0IGFtZXQsIGNvbnNlY3RldHVyIGFkaXBpc2NpbmcgZWxpdC4gU2VkIGRvIGVpdXNtb2QgdGVtcG9yIGluY2lkaWR1bnQgdXQgbGFib3JlIGV0IGRvbG9yZSBtYWduYSBhbGlxdWEuIFV0IGVuaW0gYWQgbWluaW0gdmVuaWFtLCBxdWlzIG5vc3RydWQgZXhlcmNpdGF0aW9uCg==",
|
||||
comment: ""
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
beforeAll(async () => {
|
||||
if (auth === AuthMode.JWT) {
|
||||
authToken = jwtAuthToken;
|
||||
} else if (auth === AuthMode.IDENTITY_ACCESS_TOKEN) {
|
||||
const identityLogin = await testServer.inject({
|
||||
method: "POST",
|
||||
url: "/api/v1/auth/universal-auth/login",
|
||||
body: {
|
||||
clientSecret: seedData1.machineIdentity.clientCredentials.secret,
|
||||
clientId: seedData1.machineIdentity.clientCredentials.id
|
||||
}
|
||||
});
|
||||
expect(identityLogin.statusCode).toBe(200);
|
||||
authToken = identityLogin.json().accessToken;
|
||||
}
|
||||
// create a deep folder
|
||||
const folderCreate = await testServer.inject({
|
||||
method: "POST",
|
||||
url: `/api/v1/folders`,
|
||||
headers: {
|
||||
authorization: `Bearer ${jwtAuthToken}`
|
||||
},
|
||||
body: {
|
||||
workspaceId: seedData1.projectV3.id,
|
||||
environment: seedData1.environment.slug,
|
||||
name: "folder",
|
||||
path: "/nested1/nested2"
|
||||
}
|
||||
});
|
||||
expect(folderCreate.statusCode).toBe(200);
|
||||
folderId = folderCreate.json().folder.id;
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
const deleteFolder = await testServer.inject({
|
||||
method: "DELETE",
|
||||
url: `/api/v1/folders/${folderId}`,
|
||||
headers: {
|
||||
authorization: `Bearer ${authToken}`
|
||||
},
|
||||
body: {
|
||||
workspaceId: seedData1.projectV3.id,
|
||||
environment: seedData1.environment.slug,
|
||||
path: "/nested1/nested2"
|
||||
}
|
||||
});
|
||||
expect(deleteFolder.statusCode).toBe(200);
|
||||
});
|
||||
|
||||
const getSecrets = async (environment: string, secretPath = "/") => {
|
||||
const res = await testServer.inject({
|
||||
method: "GET",
|
||||
url: `/api/v3/secrets/raw`,
|
||||
headers: {
|
||||
authorization: `Bearer ${authToken}`
|
||||
},
|
||||
query: {
|
||||
secretPath,
|
||||
environment,
|
||||
workspaceId: seedData1.projectV3.id
|
||||
}
|
||||
});
|
||||
const secrets: TRawSecret[] = JSON.parse(res.payload).secrets || [];
|
||||
return secrets;
|
||||
};
|
||||
|
||||
test.each(secretTestCases)("Create secret in path $path", async ({ secret, path }) => {
|
||||
const createdSecret = await createSecret({ path, ...secret });
|
||||
expect(createdSecret.secretKey).toEqual(secret.key);
|
||||
expect(createdSecret.secretValue).toEqual(secret.value);
|
||||
expect(createdSecret.secretComment || "").toEqual(secret.comment);
|
||||
expect(createdSecret.version).toEqual(1);
|
||||
|
||||
const secrets = await getSecrets(seedData1.environment.slug, path);
|
||||
expect(secrets).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
secretKey: secret.key,
|
||||
secretValue: secret.value,
|
||||
type: SecretType.Shared
|
||||
})
|
||||
])
|
||||
);
|
||||
await deleteSecret({ path, key: secret.key });
|
||||
});
|
||||
|
||||
test.each(secretTestCases)("Get secret by name in path $path", async ({ secret, path }) => {
|
||||
await createSecret({ path, ...secret });
|
||||
|
||||
const getSecByNameRes = await testServer.inject({
|
||||
method: "GET",
|
||||
url: `/api/v3/secrets/raw/${secret.key}`,
|
||||
headers: {
|
||||
authorization: `Bearer ${authToken}`
|
||||
},
|
||||
query: {
|
||||
secretPath: path,
|
||||
workspaceId: seedData1.projectV3.id,
|
||||
environment: seedData1.environment.slug
|
||||
}
|
||||
});
|
||||
expect(getSecByNameRes.statusCode).toBe(200);
|
||||
const getSecretByNamePayload = JSON.parse(getSecByNameRes.payload);
|
||||
expect(getSecretByNamePayload).toHaveProperty("secret");
|
||||
const decryptedSecret = getSecretByNamePayload.secret as TRawSecret;
|
||||
expect(decryptedSecret.secretKey).toEqual(secret.key);
|
||||
expect(decryptedSecret.secretValue).toEqual(secret.value);
|
||||
expect(decryptedSecret.secretComment || "").toEqual(secret.comment);
|
||||
|
||||
await deleteSecret({ path, key: secret.key });
|
||||
});
|
||||
|
||||
if (auth === AuthMode.JWT) {
|
||||
test.each(secretTestCases)(
|
||||
"Creating personal secret without shared throw error in path $path",
|
||||
async ({ secret }) => {
|
||||
const createSecretReqBody = {
|
||||
workspaceId: seedData1.projectV3.id,
|
||||
environment: seedData1.environment.slug,
|
||||
type: SecretType.Personal,
|
||||
secretKey: secret.key,
|
||||
secretValue: secret.value,
|
||||
secretComment: secret.comment
|
||||
};
|
||||
const createSecRes = await testServer.inject({
|
||||
method: "POST",
|
||||
url: `/api/v3/secrets/raw/SEC2`,
|
||||
headers: {
|
||||
authorization: `Bearer ${authToken}`
|
||||
},
|
||||
body: createSecretReqBody
|
||||
});
|
||||
const payload = JSON.parse(createSecRes.payload);
|
||||
expect(createSecRes.statusCode).toBe(400);
|
||||
expect(payload.error).toEqual("BadRequest");
|
||||
}
|
||||
);
|
||||
|
||||
test.each(secretTestCases)("Creating personal secret in path $path", async ({ secret, path }) => {
|
||||
await createSecret({ path, ...secret });
|
||||
|
||||
const createSecretReqBody = {
|
||||
workspaceId: seedData1.projectV3.id,
|
||||
environment: seedData1.environment.slug,
|
||||
type: SecretType.Personal,
|
||||
secretPath: path,
|
||||
secretKey: secret.key,
|
||||
secretValue: "personal-value",
|
||||
secretComment: secret.comment
|
||||
};
|
||||
const createSecRes = await testServer.inject({
|
||||
method: "POST",
|
||||
url: `/api/v3/secrets/raw/${secret.key}`,
|
||||
headers: {
|
||||
authorization: `Bearer ${authToken}`
|
||||
},
|
||||
body: createSecretReqBody
|
||||
});
|
||||
expect(createSecRes.statusCode).toBe(200);
|
||||
|
||||
// list secrets should contain personal one and shared one
|
||||
const secrets = await getSecrets(seedData1.environment.slug, path);
|
||||
expect(secrets).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
secretKey: secret.key,
|
||||
secretValue: secret.value,
|
||||
type: SecretType.Shared
|
||||
}),
|
||||
expect.objectContaining({
|
||||
secretKey: secret.key,
|
||||
secretValue: "personal-value",
|
||||
type: SecretType.Personal
|
||||
})
|
||||
])
|
||||
);
|
||||
|
||||
await deleteSecret({ path, key: secret.key });
|
||||
});
|
||||
|
||||
test.each(secretTestCases)(
|
||||
"Deleting personal one should not delete shared secret in path $path",
|
||||
async ({ secret, path }) => {
|
||||
await createSecret({ path, ...secret }); // shared one
|
||||
await createSecret({ path, ...secret, type: SecretType.Personal });
|
||||
|
||||
// shared secret deletion should delete personal ones also
|
||||
const secrets = await getSecrets(seedData1.environment.slug, path);
|
||||
expect(secrets).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
secretKey: secret.key,
|
||||
type: SecretType.Shared
|
||||
}),
|
||||
expect.not.objectContaining({
|
||||
secretKey: secret.key,
|
||||
type: SecretType.Personal
|
||||
})
|
||||
])
|
||||
);
|
||||
await deleteSecret({ path, key: secret.key });
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
test.each(secretTestCases)("Update secret in path $path", async ({ path, secret }) => {
|
||||
await createSecret({ path, ...secret });
|
||||
const updateSecretReqBody = {
|
||||
workspaceId: seedData1.projectV3.id,
|
||||
environment: seedData1.environment.slug,
|
||||
type: SecretType.Shared,
|
||||
secretPath: path,
|
||||
secretKey: secret.key,
|
||||
secretValue: "new-value",
|
||||
secretComment: secret.comment
|
||||
};
|
||||
const updateSecRes = await testServer.inject({
|
||||
method: "PATCH",
|
||||
url: `/api/v3/secrets/raw/${secret.key}`,
|
||||
headers: {
|
||||
authorization: `Bearer ${authToken}`
|
||||
},
|
||||
body: updateSecretReqBody
|
||||
});
|
||||
expect(updateSecRes.statusCode).toBe(200);
|
||||
const updatedSecretPayload = JSON.parse(updateSecRes.payload);
|
||||
expect(updatedSecretPayload).toHaveProperty("secret");
|
||||
const decryptedSecret = updatedSecretPayload.secret;
|
||||
expect(decryptedSecret.secretKey).toEqual(secret.key);
|
||||
expect(decryptedSecret.secretValue).toEqual("new-value");
|
||||
expect(decryptedSecret.secretComment || "").toEqual(secret.comment);
|
||||
|
||||
// list secret should have updated value
|
||||
const secrets = await getSecrets(seedData1.environment.slug, path);
|
||||
expect(secrets).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
secretKey: secret.key,
|
||||
secretValue: "new-value",
|
||||
type: SecretType.Shared
|
||||
})
|
||||
])
|
||||
);
|
||||
|
||||
await deleteSecret({ path, key: secret.key });
|
||||
});
|
||||
|
||||
test.each(secretTestCases)("Delete secret in path $path", async ({ secret, path }) => {
|
||||
await createSecret({ path, ...secret });
|
||||
const deletedSecret = await deleteSecret({ path, key: secret.key });
|
||||
expect(deletedSecret.secretKey).toEqual(secret.key);
|
||||
|
||||
// shared secret deletion should delete personal ones also
|
||||
const secrets = await getSecrets(seedData1.environment.slug, path);
|
||||
expect(secrets).toEqual(
|
||||
expect.not.arrayContaining([
|
||||
expect.objectContaining({
|
||||
secretKey: secret.key,
|
||||
type: SecretType.Shared
|
||||
}),
|
||||
expect.objectContaining({
|
||||
secretKey: secret.key,
|
||||
type: SecretType.Personal
|
||||
})
|
||||
])
|
||||
);
|
||||
});
|
||||
|
||||
test.each(secretTestCases)("Bulk create secrets in path $path", async ({ secret, path }) => {
|
||||
const createSharedSecRes = await testServer.inject({
|
||||
method: "POST",
|
||||
url: `/api/v3/secrets/batch/raw`,
|
||||
headers: {
|
||||
authorization: `Bearer ${authToken}`
|
||||
},
|
||||
body: {
|
||||
workspaceId: seedData1.projectV3.id,
|
||||
environment: seedData1.environment.slug,
|
||||
secretPath: path,
|
||||
secrets: Array.from(Array(5)).map((_e, i) => ({
|
||||
secretKey: `BULK-${secret.key}-${i + 1}`,
|
||||
secretValue: secret.value,
|
||||
secretComment: secret.comment
|
||||
}))
|
||||
}
|
||||
});
|
||||
expect(createSharedSecRes.statusCode).toBe(200);
|
||||
const createSharedSecPayload = JSON.parse(createSharedSecRes.payload);
|
||||
expect(createSharedSecPayload).toHaveProperty("secrets");
|
||||
|
||||
// bulk ones should exist
|
||||
const secrets = await getSecrets(seedData1.environment.slug, path);
|
||||
expect(secrets).toEqual(
|
||||
expect.arrayContaining(
|
||||
Array.from(Array(5)).map((_e, i) =>
|
||||
expect.objectContaining({
|
||||
secretKey: `BULK-${secret.key}-${i + 1}`,
|
||||
secretValue: secret.value,
|
||||
type: SecretType.Shared
|
||||
})
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
await Promise.all(
|
||||
Array.from(Array(5)).map((_e, i) => deleteSecret({ path, key: `BULK-${secret.key}-${i + 1}` }))
|
||||
);
|
||||
});
|
||||
|
||||
test.each(secretTestCases)("Bulk create fail on existing secret in path $path", async ({ secret, path }) => {
|
||||
await createSecret({ ...secret, key: `BULK-${secret.key}-1`, path });
|
||||
|
||||
const createSharedSecRes = await testServer.inject({
|
||||
method: "POST",
|
||||
url: `/api/v3/secrets/batch/raw`,
|
||||
headers: {
|
||||
authorization: `Bearer ${authToken}`
|
||||
},
|
||||
body: {
|
||||
workspaceId: seedData1.projectV3.id,
|
||||
environment: seedData1.environment.slug,
|
||||
secretPath: path,
|
||||
secrets: Array.from(Array(5)).map((_e, i) => ({
|
||||
secretKey: `BULK-${secret.key}-${i + 1}`,
|
||||
secretValue: secret.value,
|
||||
secretComment: secret.comment
|
||||
}))
|
||||
}
|
||||
});
|
||||
expect(createSharedSecRes.statusCode).toBe(400);
|
||||
|
||||
await deleteSecret({ path, key: `BULK-${secret.key}-1` });
|
||||
});
|
||||
|
||||
test.each(secretTestCases)("Bulk update secrets in path $path", async ({ secret, path }) => {
|
||||
await Promise.all(
|
||||
Array.from(Array(5)).map((_e, i) => createSecret({ ...secret, key: `BULK-${secret.key}-${i + 1}`, path }))
|
||||
);
|
||||
|
||||
const updateSharedSecRes = await testServer.inject({
|
||||
method: "PATCH",
|
||||
url: `/api/v3/secrets/batch/raw`,
|
||||
headers: {
|
||||
authorization: `Bearer ${authToken}`
|
||||
},
|
||||
body: {
|
||||
workspaceId: seedData1.projectV3.id,
|
||||
environment: seedData1.environment.slug,
|
||||
secretPath: path,
|
||||
secrets: Array.from(Array(5)).map((_e, i) => ({
|
||||
secretKey: `BULK-${secret.key}-${i + 1}`,
|
||||
secretValue: "update-value",
|
||||
secretComment: secret.comment
|
||||
}))
|
||||
}
|
||||
});
|
||||
expect(updateSharedSecRes.statusCode).toBe(200);
|
||||
const updateSharedSecPayload = JSON.parse(updateSharedSecRes.payload);
|
||||
expect(updateSharedSecPayload).toHaveProperty("secrets");
|
||||
|
||||
// bulk ones should exist
|
||||
const secrets = await getSecrets(seedData1.environment.slug, path);
|
||||
expect(secrets).toEqual(
|
||||
expect.arrayContaining(
|
||||
Array.from(Array(5)).map((_e, i) =>
|
||||
expect.objectContaining({
|
||||
secretKey: `BULK-${secret.key}-${i + 1}`,
|
||||
secretValue: "update-value",
|
||||
type: SecretType.Shared
|
||||
})
|
||||
)
|
||||
)
|
||||
);
|
||||
await Promise.all(
|
||||
Array.from(Array(5)).map((_e, i) => deleteSecret({ path, key: `BULK-${secret.key}-${i + 1}` }))
|
||||
);
|
||||
});
|
||||
|
||||
test.each(secretTestCases)("Bulk delete secrets in path $path", async ({ secret, path }) => {
|
||||
await Promise.all(
|
||||
Array.from(Array(5)).map((_e, i) => createSecret({ ...secret, key: `BULK-${secret.key}-${i + 1}`, path }))
|
||||
);
|
||||
|
||||
const deletedSharedSecRes = await testServer.inject({
|
||||
method: "DELETE",
|
||||
url: `/api/v3/secrets/batch/raw`,
|
||||
headers: {
|
||||
authorization: `Bearer ${authToken}`
|
||||
},
|
||||
body: {
|
||||
workspaceId: seedData1.projectV3.id,
|
||||
environment: seedData1.environment.slug,
|
||||
secretPath: path,
|
||||
secrets: Array.from(Array(5)).map((_e, i) => ({
|
||||
secretKey: `BULK-${secret.key}-${i + 1}`
|
||||
}))
|
||||
}
|
||||
});
|
||||
|
||||
expect(deletedSharedSecRes.statusCode).toBe(200);
|
||||
const deletedSecretPayload = JSON.parse(deletedSharedSecRes.payload);
|
||||
expect(deletedSecretPayload).toHaveProperty("secrets");
|
||||
|
||||
// bulk ones should exist
|
||||
const secrets = await getSecrets(seedData1.environment.slug, path);
|
||||
expect(secrets).toEqual(
|
||||
expect.not.arrayContaining(
|
||||
Array.from(Array(5)).map((_e, i) =>
|
||||
expect.objectContaining({
|
||||
secretKey: `BULK-${secret.value}-${i + 1}`,
|
||||
type: SecretType.Shared
|
||||
})
|
||||
)
|
||||
)
|
||||
);
|
||||
});
|
||||
}
|
||||
);
|
1119
backend/package-lock.json
generated
1119
backend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -40,8 +40,8 @@
|
||||
"type:check": "tsc --noEmit",
|
||||
"lint:fix": "eslint --fix --ext js,ts ./src",
|
||||
"lint": "eslint 'src/**/*.ts'",
|
||||
"test:e2e": "vitest run -c vitest.e2e.config.ts",
|
||||
"test:e2e-watch": "vitest -c vitest.e2e.config.ts",
|
||||
"test:e2e": "vitest run -c vitest.e2e.config.ts --bail=1",
|
||||
"test:e2e-watch": "vitest -c vitest.e2e.config.ts --bail=1",
|
||||
"test:e2e-coverage": "vitest run --coverage -c vitest.e2e.config.ts",
|
||||
"generate:component": "tsx ./scripts/create-backend-file.ts",
|
||||
"generate:schema": "tsx ./scripts/generate-schema-types.ts",
|
||||
@ -106,6 +106,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-iam": "^3.525.0",
|
||||
"@aws-sdk/client-kms": "^3.609.0",
|
||||
"@aws-sdk/client-secrets-manager": "^3.504.0",
|
||||
"@aws-sdk/client-sts": "^3.600.0",
|
||||
"@casl/ability": "^6.5.0",
|
||||
@ -125,8 +126,8 @@
|
||||
"@peculiar/asn1-schema": "^2.3.8",
|
||||
"@peculiar/x509": "^1.10.0",
|
||||
"@serdnam/pino-cloudwatch-transport": "^1.0.4",
|
||||
"@team-plain/typescript-sdk": "^4.6.1",
|
||||
"@sindresorhus/slugify": "1.1.0",
|
||||
"@team-plain/typescript-sdk": "^4.6.1",
|
||||
"@ucast/mongo2js": "^1.3.4",
|
||||
"ajv": "^8.12.0",
|
||||
"argon2": "^0.31.2",
|
||||
@ -148,6 +149,7 @@
|
||||
"jmespath": "^0.16.0",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"jsrp": "^0.2.4",
|
||||
"jwks-rsa": "^3.1.0",
|
||||
"knex": "^3.0.1",
|
||||
"ldapjs": "^3.0.7",
|
||||
"libsodium-wrappers": "^0.7.13",
|
||||
|
6
backend/src/@types/fastify.d.ts
vendored
6
backend/src/@types/fastify.d.ts
vendored
@ -9,6 +9,7 @@ import { TAuditLogStreamServiceFactory } from "@app/ee/services/audit-log-stream
|
||||
import { TCertificateAuthorityCrlServiceFactory } from "@app/ee/services/certificate-authority-crl/certificate-authority-crl-service";
|
||||
import { TDynamicSecretServiceFactory } from "@app/ee/services/dynamic-secret/dynamic-secret-service";
|
||||
import { TDynamicSecretLeaseServiceFactory } from "@app/ee/services/dynamic-secret-lease/dynamic-secret-lease-service";
|
||||
import { TExternalKmsServiceFactory } from "@app/ee/services/external-kms/external-kms-service";
|
||||
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";
|
||||
@ -41,6 +42,7 @@ import { TIdentityAwsAuthServiceFactory } from "@app/services/identity-aws-auth/
|
||||
import { TIdentityAzureAuthServiceFactory } from "@app/services/identity-azure-auth/identity-azure-auth-service";
|
||||
import { TIdentityGcpAuthServiceFactory } from "@app/services/identity-gcp-auth/identity-gcp-auth-service";
|
||||
import { TIdentityKubernetesAuthServiceFactory } from "@app/services/identity-kubernetes-auth/identity-kubernetes-auth-service";
|
||||
import { TIdentityOidcAuthServiceFactory } from "@app/services/identity-oidc-auth/identity-oidc-auth-service";
|
||||
import { TIdentityProjectServiceFactory } from "@app/services/identity-project/identity-project-service";
|
||||
import { TIdentityTokenAuthServiceFactory } from "@app/services/identity-token-auth/identity-token-auth-service";
|
||||
import { TIdentityUaServiceFactory } from "@app/services/identity-ua/identity-ua-service";
|
||||
@ -48,6 +50,7 @@ import { TIntegrationServiceFactory } from "@app/services/integration/integratio
|
||||
import { TIntegrationAuthServiceFactory } from "@app/services/integration-auth/integration-auth-service";
|
||||
import { TOrgRoleServiceFactory } from "@app/services/org/org-role-service";
|
||||
import { TOrgServiceFactory } from "@app/services/org/org-service";
|
||||
import { TOrgAdminServiceFactory } from "@app/services/org-admin/org-admin-service";
|
||||
import { TProjectServiceFactory } from "@app/services/project/project-service";
|
||||
import { TProjectBotServiceFactory } from "@app/services/project-bot/project-bot-service";
|
||||
import { TProjectEnvServiceFactory } from "@app/services/project-env/project-env-service";
|
||||
@ -135,6 +138,7 @@ declare module "fastify" {
|
||||
identityGcpAuth: TIdentityGcpAuthServiceFactory;
|
||||
identityAwsAuth: TIdentityAwsAuthServiceFactory;
|
||||
identityAzureAuth: TIdentityAzureAuthServiceFactory;
|
||||
identityOidcAuth: TIdentityOidcAuthServiceFactory;
|
||||
accessApprovalPolicy: TAccessApprovalPolicyServiceFactory;
|
||||
accessApprovalRequest: TAccessApprovalRequestServiceFactory;
|
||||
secretApprovalPolicy: TSecretApprovalPolicyServiceFactory;
|
||||
@ -161,6 +165,8 @@ declare module "fastify" {
|
||||
secretSharing: TSecretSharingServiceFactory;
|
||||
rateLimit: TRateLimitServiceFactory;
|
||||
userEngagement: TUserEngagementServiceFactory;
|
||||
externalKms: TExternalKmsServiceFactory;
|
||||
orgAdmin: TOrgAdminServiceFactory;
|
||||
};
|
||||
// this is exclusive use for middlewares in which we need to inject data
|
||||
// everywhere else access using service layer
|
||||
|
86
backend/src/@types/knex.d.ts
vendored
86
backend/src/@types/knex.d.ts
vendored
@ -59,6 +59,9 @@ import {
|
||||
TDynamicSecrets,
|
||||
TDynamicSecretsInsert,
|
||||
TDynamicSecretsUpdate,
|
||||
TExternalKms,
|
||||
TExternalKmsInsert,
|
||||
TExternalKmsUpdate,
|
||||
TGitAppInstallSessions,
|
||||
TGitAppInstallSessionsInsert,
|
||||
TGitAppInstallSessionsUpdate,
|
||||
@ -92,6 +95,9 @@ import {
|
||||
TIdentityKubernetesAuths,
|
||||
TIdentityKubernetesAuthsInsert,
|
||||
TIdentityKubernetesAuthsUpdate,
|
||||
TIdentityOidcAuths,
|
||||
TIdentityOidcAuthsInsert,
|
||||
TIdentityOidcAuthsUpdate,
|
||||
TIdentityOrgMemberships,
|
||||
TIdentityOrgMembershipsInsert,
|
||||
TIdentityOrgMembershipsUpdate,
|
||||
@ -122,6 +128,9 @@ import {
|
||||
TIntegrations,
|
||||
TIntegrationsInsert,
|
||||
TIntegrationsUpdate,
|
||||
TInternalKms,
|
||||
TInternalKmsInsert,
|
||||
TInternalKmsUpdate,
|
||||
TKmsKeys,
|
||||
TKmsKeysInsert,
|
||||
TKmsKeysUpdate,
|
||||
@ -195,6 +204,9 @@ import {
|
||||
TSecretApprovalRequestSecretTags,
|
||||
TSecretApprovalRequestSecretTagsInsert,
|
||||
TSecretApprovalRequestSecretTagsUpdate,
|
||||
TSecretApprovalRequestSecretTagsV2,
|
||||
TSecretApprovalRequestSecretTagsV2Insert,
|
||||
TSecretApprovalRequestSecretTagsV2Update,
|
||||
TSecretApprovalRequestsInsert,
|
||||
TSecretApprovalRequestsReviewers,
|
||||
TSecretApprovalRequestsReviewersInsert,
|
||||
@ -202,6 +214,9 @@ import {
|
||||
TSecretApprovalRequestsSecrets,
|
||||
TSecretApprovalRequestsSecretsInsert,
|
||||
TSecretApprovalRequestsSecretsUpdate,
|
||||
TSecretApprovalRequestsSecretsV2,
|
||||
TSecretApprovalRequestsSecretsV2Insert,
|
||||
TSecretApprovalRequestsSecretsV2Update,
|
||||
TSecretApprovalRequestsUpdate,
|
||||
TSecretBlindIndexes,
|
||||
TSecretBlindIndexesInsert,
|
||||
@ -218,9 +233,15 @@ import {
|
||||
TSecretReferences,
|
||||
TSecretReferencesInsert,
|
||||
TSecretReferencesUpdate,
|
||||
TSecretReferencesV2,
|
||||
TSecretReferencesV2Insert,
|
||||
TSecretReferencesV2Update,
|
||||
TSecretRotationOutputs,
|
||||
TSecretRotationOutputsInsert,
|
||||
TSecretRotationOutputsUpdate,
|
||||
TSecretRotationOutputV2,
|
||||
TSecretRotationOutputV2Insert,
|
||||
TSecretRotationOutputV2Update,
|
||||
TSecretRotations,
|
||||
TSecretRotationsInsert,
|
||||
TSecretRotationsUpdate,
|
||||
@ -239,6 +260,9 @@ import {
|
||||
TSecretSnapshotSecrets,
|
||||
TSecretSnapshotSecretsInsert,
|
||||
TSecretSnapshotSecretsUpdate,
|
||||
TSecretSnapshotSecretsV2,
|
||||
TSecretSnapshotSecretsV2Insert,
|
||||
TSecretSnapshotSecretsV2Update,
|
||||
TSecretSnapshotsInsert,
|
||||
TSecretSnapshotsUpdate,
|
||||
TSecretsUpdate,
|
||||
@ -254,6 +278,9 @@ import {
|
||||
TSecretVersionTagJunction,
|
||||
TSecretVersionTagJunctionInsert,
|
||||
TSecretVersionTagJunctionUpdate,
|
||||
TSecretVersionV2TagJunction,
|
||||
TSecretVersionV2TagJunctionInsert,
|
||||
TSecretVersionV2TagJunctionUpdate,
|
||||
TServiceTokens,
|
||||
TServiceTokensInsert,
|
||||
TServiceTokensUpdate,
|
||||
@ -282,6 +309,17 @@ import {
|
||||
TWebhooksInsert,
|
||||
TWebhooksUpdate
|
||||
} from "@app/db/schemas";
|
||||
import {
|
||||
TSecretV2TagJunction,
|
||||
TSecretV2TagJunctionInsert,
|
||||
TSecretV2TagJunctionUpdate
|
||||
} from "@app/db/schemas/secret-v2-tag-junction";
|
||||
import {
|
||||
TSecretVersionsV2,
|
||||
TSecretVersionsV2Insert,
|
||||
TSecretVersionsV2Update
|
||||
} from "@app/db/schemas/secret-versions-v2";
|
||||
import { TSecretsV2, TSecretsV2Insert, TSecretsV2Update } from "@app/db/schemas/secrets-v2";
|
||||
|
||||
declare module "knex" {
|
||||
namespace Knex {
|
||||
@ -483,6 +521,11 @@ declare module "knex/types/tables" {
|
||||
TIdentityAzureAuthsInsert,
|
||||
TIdentityAzureAuthsUpdate
|
||||
>;
|
||||
[TableName.IdentityOidcAuth]: KnexOriginal.CompositeTableType<
|
||||
TIdentityOidcAuths,
|
||||
TIdentityOidcAuthsInsert,
|
||||
TIdentityOidcAuthsUpdate
|
||||
>;
|
||||
[TableName.IdentityUaClientSecret]: KnexOriginal.CompositeTableType<
|
||||
TIdentityUaClientSecrets,
|
||||
TIdentityUaClientSecretsInsert,
|
||||
@ -631,7 +674,23 @@ declare module "knex/types/tables" {
|
||||
TSecretScanningGitRisksUpdate
|
||||
>;
|
||||
[TableName.TrustedIps]: KnexOriginal.CompositeTableType<TTrustedIps, TTrustedIpsInsert, TTrustedIpsUpdate>;
|
||||
[TableName.SecretV2]: KnexOriginal.CompositeTableType<TSecretsV2, TSecretsV2Insert, TSecretsV2Update>;
|
||||
[TableName.SecretVersionV2]: KnexOriginal.CompositeTableType<
|
||||
TSecretVersionsV2,
|
||||
TSecretVersionsV2Insert,
|
||||
TSecretVersionsV2Update
|
||||
>;
|
||||
[TableName.SecretReferenceV2]: KnexOriginal.CompositeTableType<
|
||||
TSecretReferencesV2,
|
||||
TSecretReferencesV2Insert,
|
||||
TSecretReferencesV2Update
|
||||
>;
|
||||
// Junction tables
|
||||
[TableName.SecretV2JnTag]: KnexOriginal.CompositeTableType<
|
||||
TSecretV2TagJunction,
|
||||
TSecretV2TagJunctionInsert,
|
||||
TSecretV2TagJunctionUpdate
|
||||
>;
|
||||
[TableName.JnSecretTag]: KnexOriginal.CompositeTableType<
|
||||
TSecretTagJunction,
|
||||
TSecretTagJunctionInsert,
|
||||
@ -642,12 +701,39 @@ declare module "knex/types/tables" {
|
||||
TSecretVersionTagJunctionInsert,
|
||||
TSecretVersionTagJunctionUpdate
|
||||
>;
|
||||
[TableName.SecretVersionV2Tag]: KnexOriginal.CompositeTableType<
|
||||
TSecretVersionV2TagJunction,
|
||||
TSecretVersionV2TagJunctionInsert,
|
||||
TSecretVersionV2TagJunctionUpdate
|
||||
>;
|
||||
[TableName.SnapshotSecretV2]: KnexOriginal.CompositeTableType<
|
||||
TSecretSnapshotSecretsV2,
|
||||
TSecretSnapshotSecretsV2Insert,
|
||||
TSecretSnapshotSecretsV2Update
|
||||
>;
|
||||
[TableName.SecretApprovalRequestSecretV2]: KnexOriginal.CompositeTableType<
|
||||
TSecretApprovalRequestsSecretsV2,
|
||||
TSecretApprovalRequestsSecretsV2Insert,
|
||||
TSecretApprovalRequestsSecretsV2Update
|
||||
>;
|
||||
[TableName.SecretApprovalRequestSecretTagV2]: KnexOriginal.CompositeTableType<
|
||||
TSecretApprovalRequestSecretTagsV2,
|
||||
TSecretApprovalRequestSecretTagsV2Insert,
|
||||
TSecretApprovalRequestSecretTagsV2Update
|
||||
>;
|
||||
[TableName.SecretRotationOutputV2]: KnexOriginal.CompositeTableType<
|
||||
TSecretRotationOutputV2,
|
||||
TSecretRotationOutputV2Insert,
|
||||
TSecretRotationOutputV2Update
|
||||
>;
|
||||
// KMS service
|
||||
[TableName.KmsServerRootConfig]: KnexOriginal.CompositeTableType<
|
||||
TKmsRootConfig,
|
||||
TKmsRootConfigInsert,
|
||||
TKmsRootConfigUpdate
|
||||
>;
|
||||
[TableName.InternalKms]: KnexOriginal.CompositeTableType<TInternalKms, TInternalKmsInsert, TInternalKmsUpdate>;
|
||||
[TableName.ExternalKms]: KnexOriginal.CompositeTableType<TExternalKms, TExternalKmsInsert, TExternalKmsUpdate>;
|
||||
[TableName.KmsKey]: KnexOriginal.CompositeTableType<TKmsKeys, TKmsKeysInsert, TKmsKeysUpdate>;
|
||||
[TableName.KmsKeyVersion]: KnexOriginal.CompositeTableType<
|
||||
TKmsKeyVersions,
|
||||
|
256
backend/src/db/migrations/20240708100026_external-kms.ts
Normal file
256
backend/src/db/migrations/20240708100026_external-kms.ts
Normal file
@ -0,0 +1,256 @@
|
||||
import slugify from "@sindresorhus/slugify";
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
||||
|
||||
import { TableName } from "../schemas";
|
||||
|
||||
const createInternalKmsTableAndBackfillData = async (knex: Knex) => {
|
||||
const doesOldKmsKeyTableExist = await knex.schema.hasTable(TableName.KmsKey);
|
||||
const doesInternalKmsTableExist = await knex.schema.hasTable(TableName.InternalKms);
|
||||
|
||||
// building the internal kms table by filling from old kms table
|
||||
if (doesOldKmsKeyTableExist && !doesInternalKmsTableExist) {
|
||||
await knex.schema.createTable(TableName.InternalKms, (tb) => {
|
||||
tb.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
|
||||
tb.binary("encryptedKey").notNullable();
|
||||
tb.string("encryptionAlgorithm").notNullable();
|
||||
tb.integer("version").defaultTo(1).notNullable();
|
||||
tb.uuid("kmsKeyId").unique().notNullable();
|
||||
tb.foreign("kmsKeyId").references("id").inTable(TableName.KmsKey).onDelete("CASCADE");
|
||||
});
|
||||
|
||||
// copy the old kms and backfill
|
||||
const oldKmsKey = await knex(TableName.KmsKey).select("version", "encryptedKey", "encryptionAlgorithm", "id");
|
||||
if (oldKmsKey.length) {
|
||||
await knex(TableName.InternalKms).insert(
|
||||
oldKmsKey.map((el) => ({
|
||||
encryptionAlgorithm: el.encryptionAlgorithm,
|
||||
encryptedKey: el.encryptedKey,
|
||||
kmsKeyId: el.id,
|
||||
version: el.version
|
||||
}))
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const renameKmsKeyVersionTableAsInternalKmsKeyVersion = async (knex: Knex) => {
|
||||
const doesOldKmsKeyVersionTableExist = await knex.schema.hasTable(TableName.KmsKeyVersion);
|
||||
const doesNewKmsKeyVersionTableExist = await knex.schema.hasTable(TableName.InternalKmsKeyVersion);
|
||||
|
||||
if (doesOldKmsKeyVersionTableExist && !doesNewKmsKeyVersionTableExist) {
|
||||
// because we haven't started using versioning for kms thus no data exist
|
||||
await knex.schema.renameTable(TableName.KmsKeyVersion, TableName.InternalKmsKeyVersion);
|
||||
const hasKmsKeyIdColumn = await knex.schema.hasColumn(TableName.InternalKmsKeyVersion, "kmsKeyId");
|
||||
const hasInternalKmsIdColumn = await knex.schema.hasColumn(TableName.InternalKmsKeyVersion, "internalKmsId");
|
||||
|
||||
await knex.schema.alterTable(TableName.InternalKmsKeyVersion, (tb) => {
|
||||
if (hasKmsKeyIdColumn) tb.dropColumn("kmsKeyId");
|
||||
if (!hasInternalKmsIdColumn) {
|
||||
tb.uuid("internalKmsId").notNullable();
|
||||
tb.foreign("internalKmsId").references("id").inTable(TableName.InternalKms).onDelete("CASCADE");
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const createExternalKmsKeyTable = async (knex: Knex) => {
|
||||
const doesExternalKmsServiceExist = await knex.schema.hasTable(TableName.ExternalKms);
|
||||
if (!doesExternalKmsServiceExist) {
|
||||
await knex.schema.createTable(TableName.ExternalKms, (tb) => {
|
||||
tb.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
|
||||
tb.string("provider").notNullable();
|
||||
tb.binary("encryptedProviderInputs").notNullable();
|
||||
tb.string("status");
|
||||
tb.string("statusDetails");
|
||||
tb.uuid("kmsKeyId").unique().notNullable();
|
||||
tb.foreign("kmsKeyId").references("id").inTable(TableName.KmsKey).onDelete("CASCADE");
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const removeNonRequiredFieldsFromKmsKeyTableAndBackfillRequiredData = async (knex: Knex) => {
|
||||
const doesOldKmsKeyTableExist = await knex.schema.hasTable(TableName.KmsKey);
|
||||
|
||||
// building the internal kms table by filling from old kms table
|
||||
if (doesOldKmsKeyTableExist) {
|
||||
const hasSlugColumn = await knex.schema.hasColumn(TableName.KmsKey, "slug");
|
||||
const hasEncryptedKeyColumn = await knex.schema.hasColumn(TableName.KmsKey, "encryptedKey");
|
||||
const hasEncryptionAlgorithmColumn = await knex.schema.hasColumn(TableName.KmsKey, "encryptionAlgorithm");
|
||||
const hasVersionColumn = await knex.schema.hasColumn(TableName.KmsKey, "version");
|
||||
const hasTimestamps = await knex.schema.hasColumn(TableName.KmsKey, "createdAt");
|
||||
const hasProjectId = await knex.schema.hasColumn(TableName.KmsKey, "projectId");
|
||||
const hasOrgId = await knex.schema.hasColumn(TableName.KmsKey, "orgId");
|
||||
|
||||
await knex.schema.alterTable(TableName.KmsKey, (tb) => {
|
||||
if (!hasSlugColumn) tb.string("slug", 32);
|
||||
if (hasEncryptedKeyColumn) tb.dropColumn("encryptedKey");
|
||||
if (hasEncryptionAlgorithmColumn) tb.dropColumn("encryptionAlgorithm");
|
||||
if (hasVersionColumn) tb.dropColumn("version");
|
||||
if (!hasTimestamps) tb.timestamps(true, true, true);
|
||||
});
|
||||
|
||||
// backfill all org id in kms key because its gonna be changed to non nullable
|
||||
if (hasProjectId && hasOrgId) {
|
||||
await knex(TableName.KmsKey)
|
||||
.whereNull("orgId")
|
||||
.update({
|
||||
// eslint-disable-next-line
|
||||
// @ts-ignore because generate schema happens after this
|
||||
orgId: knex(TableName.Project)
|
||||
.select("orgId")
|
||||
.where("id", knex.raw("??", [`${TableName.KmsKey}.projectId`]))
|
||||
});
|
||||
}
|
||||
|
||||
// backfill slugs in kms
|
||||
const missingSlugs = await knex(TableName.KmsKey).whereNull("slug").select("id");
|
||||
if (missingSlugs.length) {
|
||||
await knex(TableName.KmsKey)
|
||||
// eslint-disable-next-line
|
||||
// @ts-ignore because generate schema happens after this
|
||||
.insert(missingSlugs.map(({ id }) => ({ id, slug: slugify(alphaNumericNanoId(8).toLowerCase()) })))
|
||||
.onConflict("id")
|
||||
.merge();
|
||||
}
|
||||
|
||||
await knex.schema.alterTable(TableName.KmsKey, (tb) => {
|
||||
if (hasOrgId) tb.uuid("orgId").notNullable().alter();
|
||||
tb.string("slug", 32).notNullable().alter();
|
||||
if (hasProjectId) tb.dropColumn("projectId");
|
||||
if (hasOrgId) tb.unique(["orgId", "slug"]);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/*
|
||||
* The goal for this migration is split the existing kms key into three table
|
||||
* the kms-key table would be a container table that contains
|
||||
* the internal kms key table and external kms table
|
||||
*/
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
await createInternalKmsTableAndBackfillData(knex);
|
||||
await renameKmsKeyVersionTableAsInternalKmsKeyVersion(knex);
|
||||
await removeNonRequiredFieldsFromKmsKeyTableAndBackfillRequiredData(knex);
|
||||
await createExternalKmsKeyTable(knex);
|
||||
|
||||
const doesOrgKmsKeyExist = await knex.schema.hasColumn(TableName.Organization, "kmsDefaultKeyId");
|
||||
if (!doesOrgKmsKeyExist) {
|
||||
await knex.schema.alterTable(TableName.Organization, (tb) => {
|
||||
tb.uuid("kmsDefaultKeyId").nullable();
|
||||
tb.foreign("kmsDefaultKeyId").references("id").inTable(TableName.KmsKey);
|
||||
});
|
||||
}
|
||||
|
||||
const doesProjectKmsSecretManagerKeyExist = await knex.schema.hasColumn(TableName.Project, "kmsSecretManagerKeyId");
|
||||
if (!doesProjectKmsSecretManagerKeyExist) {
|
||||
await knex.schema.alterTable(TableName.Project, (tb) => {
|
||||
tb.uuid("kmsSecretManagerKeyId").nullable();
|
||||
tb.foreign("kmsSecretManagerKeyId").references("id").inTable(TableName.KmsKey);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const renameInternalKmsKeyVersionBackToKmsKeyVersion = async (knex: Knex) => {
|
||||
const doesInternalKmsKeyVersionTableExist = await knex.schema.hasTable(TableName.InternalKmsKeyVersion);
|
||||
const doesKmsKeyVersionTableExist = await knex.schema.hasTable(TableName.KmsKeyVersion);
|
||||
if (doesInternalKmsKeyVersionTableExist && !doesKmsKeyVersionTableExist) {
|
||||
// because we haven't started using versioning for kms thus no data exist
|
||||
await knex.schema.renameTable(TableName.InternalKmsKeyVersion, TableName.KmsKeyVersion);
|
||||
const hasInternalKmsIdColumn = await knex.schema.hasColumn(TableName.KmsKeyVersion, "internalKmsId");
|
||||
const hasKmsKeyIdColumn = await knex.schema.hasColumn(TableName.KmsKeyVersion, "kmsKeyId");
|
||||
|
||||
await knex.schema.alterTable(TableName.KmsKeyVersion, (tb) => {
|
||||
if (hasInternalKmsIdColumn) tb.dropColumn("internalKmsId");
|
||||
if (!hasKmsKeyIdColumn) {
|
||||
tb.uuid("kmsKeyId").notNullable();
|
||||
tb.foreign("kmsKeyId").references("id").inTable(TableName.KmsKey).onDelete("CASCADE");
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const bringBackKmsKeyFields = async (knex: Knex) => {
|
||||
const doesOldKmsKeyTableExist = await knex.schema.hasTable(TableName.KmsKey);
|
||||
const doesInternalKmsTableExist = await knex.schema.hasTable(TableName.InternalKms);
|
||||
if (doesOldKmsKeyTableExist && doesInternalKmsTableExist) {
|
||||
const hasSlug = await knex.schema.hasColumn(TableName.KmsKey, "slug");
|
||||
const hasEncryptedKeyColumn = await knex.schema.hasColumn(TableName.KmsKey, "encryptedKey");
|
||||
const hasEncryptionAlgorithmColumn = await knex.schema.hasColumn(TableName.KmsKey, "encryptionAlgorithm");
|
||||
const hasVersionColumn = await knex.schema.hasColumn(TableName.KmsKey, "version");
|
||||
const hasNullableOrgId = await knex.schema.hasColumn(TableName.KmsKey, "orgId");
|
||||
const hasProjectIdColumn = await knex.schema.hasColumn(TableName.KmsKey, "projectId");
|
||||
|
||||
await knex.schema.alterTable(TableName.KmsKey, (tb) => {
|
||||
if (!hasEncryptedKeyColumn) tb.binary("encryptedKey");
|
||||
if (!hasEncryptionAlgorithmColumn) tb.string("encryptionAlgorithm");
|
||||
if (!hasVersionColumn) tb.integer("version").defaultTo(1);
|
||||
if (hasNullableOrgId) tb.uuid("orgId").nullable().alter();
|
||||
if (!hasProjectIdColumn) {
|
||||
tb.string("projectId");
|
||||
tb.foreign("projectId").references("id").inTable(TableName.Project).onDelete("CASCADE");
|
||||
}
|
||||
if (hasSlug) tb.dropColumn("slug");
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const backfillKmsKeyFromInternalKmsTable = async (knex: Knex) => {
|
||||
const doesOldKmsKeyTableExist = await knex.schema.hasTable(TableName.KmsKey);
|
||||
const doesInternalKmsTableExist = await knex.schema.hasTable(TableName.InternalKms);
|
||||
if (doesInternalKmsTableExist && doesOldKmsKeyTableExist) {
|
||||
// backfill kms key with internal kms data
|
||||
await knex(TableName.KmsKey).update({
|
||||
// eslint-disable-next-line
|
||||
// @ts-ignore because generate schema happens after this
|
||||
encryptedKey: knex(TableName.InternalKms)
|
||||
.select("encryptedKey")
|
||||
.where("kmsKeyId", knex.raw("??", [`${TableName.KmsKey}.id`])),
|
||||
// eslint-disable-next-line
|
||||
// @ts-ignore because generate schema happens after this
|
||||
encryptionAlgorithm: knex(TableName.InternalKms)
|
||||
.select("encryptionAlgorithm")
|
||||
.where("kmsKeyId", knex.raw("??", [`${TableName.KmsKey}.id`])),
|
||||
// eslint-disable-next-line
|
||||
// @ts-ignore because generate schema happens after this
|
||||
projectId: knex(TableName.Project)
|
||||
.select("id")
|
||||
.where("kmsCertificateKeyId", knex.raw("??", [`${TableName.KmsKey}.id`]))
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
const doesOrgKmsKeyExist = await knex.schema.hasColumn(TableName.Organization, "kmsDefaultKeyId");
|
||||
if (doesOrgKmsKeyExist) {
|
||||
await knex.schema.alterTable(TableName.Organization, (tb) => {
|
||||
tb.dropColumn("kmsDefaultKeyId");
|
||||
});
|
||||
}
|
||||
|
||||
const doesProjectKmsSecretManagerKeyExist = await knex.schema.hasColumn(TableName.Project, "kmsSecretManagerKeyId");
|
||||
if (doesProjectKmsSecretManagerKeyExist) {
|
||||
await knex.schema.alterTable(TableName.Project, (tb) => {
|
||||
tb.dropColumn("kmsSecretManagerKeyId");
|
||||
});
|
||||
}
|
||||
|
||||
await renameInternalKmsKeyVersionBackToKmsKeyVersion(knex);
|
||||
await bringBackKmsKeyFields(knex);
|
||||
await backfillKmsKeyFromInternalKmsTable(knex);
|
||||
|
||||
const doesOldKmsKeyTableExist = await knex.schema.hasTable(TableName.KmsKey);
|
||||
if (doesOldKmsKeyTableExist) {
|
||||
await knex.schema.alterTable(TableName.KmsKey, (tb) => {
|
||||
tb.binary("encryptedKey").notNullable().alter();
|
||||
tb.string("encryptionAlgorithm").notNullable().alter();
|
||||
});
|
||||
}
|
||||
|
||||
const doesInternalKmsTableExist = await knex.schema.hasTable(TableName.InternalKms);
|
||||
if (doesInternalKmsTableExist) await knex.schema.dropTable(TableName.InternalKms);
|
||||
|
||||
const doesExternalKmsServiceExist = await knex.schema.hasTable(TableName.ExternalKms);
|
||||
if (doesExternalKmsServiceExist) await knex.schema.dropTable(TableName.ExternalKms);
|
||||
}
|
@ -0,0 +1,34 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TableName } from "../schemas";
|
||||
import { createOnUpdateTrigger, dropOnUpdateTrigger } from "../utils";
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
if (!(await knex.schema.hasTable(TableName.IdentityOidcAuth))) {
|
||||
await knex.schema.createTable(TableName.IdentityOidcAuth, (t) => {
|
||||
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
|
||||
t.bigInteger("accessTokenTTL").defaultTo(7200).notNullable();
|
||||
t.bigInteger("accessTokenMaxTTL").defaultTo(7200).notNullable();
|
||||
t.bigInteger("accessTokenNumUsesLimit").defaultTo(0).notNullable();
|
||||
t.jsonb("accessTokenTrustedIps").notNullable();
|
||||
t.uuid("identityId").notNullable().unique();
|
||||
t.foreign("identityId").references("id").inTable(TableName.Identity).onDelete("CASCADE");
|
||||
t.string("oidcDiscoveryUrl").notNullable();
|
||||
t.text("encryptedCaCert").notNullable();
|
||||
t.string("caCertIV").notNullable();
|
||||
t.string("caCertTag").notNullable();
|
||||
t.string("boundIssuer").notNullable();
|
||||
t.string("boundAudiences").notNullable();
|
||||
t.jsonb("boundClaims").notNullable();
|
||||
t.string("boundSubject");
|
||||
t.timestamps(true, true, true);
|
||||
});
|
||||
|
||||
await createOnUpdateTrigger(knex, TableName.IdentityOidcAuth);
|
||||
}
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
await knex.schema.dropTableIfExists(TableName.IdentityOidcAuth);
|
||||
await dropOnUpdateTrigger(knex, TableName.IdentityOidcAuth);
|
||||
}
|
@ -0,0 +1,25 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TableName } from "../schemas";
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
if (await knex.schema.hasTable(TableName.OrgMembership)) {
|
||||
const doesUserIdExist = await knex.schema.hasColumn(TableName.OrgMembership, "userId");
|
||||
const doesOrgIdExist = await knex.schema.hasColumn(TableName.OrgMembership, "orgId");
|
||||
await knex.schema.alterTable(TableName.OrgMembership, (t) => {
|
||||
t.boolean("isActive").notNullable().defaultTo(true);
|
||||
if (doesUserIdExist && doesOrgIdExist) t.index(["userId", "orgId"]);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
if (await knex.schema.hasTable(TableName.OrgMembership)) {
|
||||
const doesUserIdExist = await knex.schema.hasColumn(TableName.OrgMembership, "userId");
|
||||
const doesOrgIdExist = await knex.schema.hasColumn(TableName.OrgMembership, "orgId");
|
||||
await knex.schema.alterTable(TableName.OrgMembership, (t) => {
|
||||
t.dropColumn("isActive");
|
||||
if (doesUserIdExist && doesOrgIdExist) t.dropIndex(["userId", "orgId"]);
|
||||
});
|
||||
}
|
||||
}
|
@ -0,0 +1,23 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { EnforcementLevel } from "@app/lib/types";
|
||||
|
||||
import { TableName } from "../schemas";
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
const hasColumn = await knex.schema.hasColumn(TableName.SecretApprovalPolicy, "enforcementLevel");
|
||||
if (!hasColumn) {
|
||||
await knex.schema.table(TableName.SecretApprovalPolicy, (table) => {
|
||||
table.string("enforcementLevel", 10).notNullable().defaultTo(EnforcementLevel.Hard);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
const hasColumn = await knex.schema.hasColumn(TableName.SecretApprovalPolicy, "enforcementLevel");
|
||||
if (hasColumn) {
|
||||
await knex.schema.table(TableName.SecretApprovalPolicy, (table) => {
|
||||
table.dropColumn("enforcementLevel");
|
||||
});
|
||||
}
|
||||
}
|
@ -0,0 +1,23 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { EnforcementLevel } from "@app/lib/types";
|
||||
|
||||
import { TableName } from "../schemas";
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
const hasColumn = await knex.schema.hasColumn(TableName.AccessApprovalPolicy, "enforcementLevel");
|
||||
if (!hasColumn) {
|
||||
await knex.schema.table(TableName.AccessApprovalPolicy, (table) => {
|
||||
table.string("enforcementLevel", 10).notNullable().defaultTo(EnforcementLevel.Hard);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
const hasColumn = await knex.schema.hasColumn(TableName.AccessApprovalPolicy, "enforcementLevel");
|
||||
if (hasColumn) {
|
||||
await knex.schema.table(TableName.AccessApprovalPolicy, (table) => {
|
||||
table.dropColumn("enforcementLevel");
|
||||
});
|
||||
}
|
||||
}
|
@ -0,0 +1,23 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { SecretSharingAccessType } from "@app/lib/types";
|
||||
|
||||
import { TableName } from "../schemas";
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
const hasColumn = await knex.schema.hasColumn(TableName.SecretSharing, "accessType");
|
||||
if (!hasColumn) {
|
||||
await knex.schema.table(TableName.SecretSharing, (table) => {
|
||||
table.string("accessType").notNullable().defaultTo(SecretSharingAccessType.Anyone);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
const hasColumn = await knex.schema.hasColumn(TableName.SecretSharing, "accessType");
|
||||
if (hasColumn) {
|
||||
await knex.schema.table(TableName.SecretSharing, (table) => {
|
||||
table.dropColumn("accessType");
|
||||
});
|
||||
}
|
||||
}
|
21
backend/src/db/migrations/20240719182539_add-bypass-reason-secret-approval-requets.ts
Normal file
21
backend/src/db/migrations/20240719182539_add-bypass-reason-secret-approval-requets.ts
Normal file
@ -0,0 +1,21 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TableName } from "../schemas";
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
const hasColumn = await knex.schema.hasColumn(TableName.SecretApprovalRequest, "bypassReason");
|
||||
if (!hasColumn) {
|
||||
await knex.schema.table(TableName.SecretApprovalRequest, (table) => {
|
||||
table.string("bypassReason").nullable();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
const hasColumn = await knex.schema.hasColumn(TableName.SecretApprovalRequest, "bypassReason");
|
||||
if (hasColumn) {
|
||||
await knex.schema.table(TableName.SecretApprovalRequest, (table) => {
|
||||
table.dropColumn("bypassReason");
|
||||
});
|
||||
}
|
||||
}
|
@ -0,0 +1,39 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TableName } from "../schemas";
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
if (await knex.schema.hasTable(TableName.SecretSharing)) {
|
||||
const doesNameExist = await knex.schema.hasColumn(TableName.SecretSharing, "name");
|
||||
if (!doesNameExist) {
|
||||
await knex.schema.alterTable(TableName.SecretSharing, (t) => {
|
||||
t.string("name").nullable();
|
||||
});
|
||||
}
|
||||
|
||||
const doesLastViewedAtExist = await knex.schema.hasColumn(TableName.SecretSharing, "lastViewedAt");
|
||||
if (!doesLastViewedAtExist) {
|
||||
await knex.schema.alterTable(TableName.SecretSharing, (t) => {
|
||||
t.timestamp("lastViewedAt").nullable();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
if (await knex.schema.hasTable(TableName.SecretSharing)) {
|
||||
const doesNameExist = await knex.schema.hasColumn(TableName.SecretSharing, "name");
|
||||
if (doesNameExist) {
|
||||
await knex.schema.alterTable(TableName.SecretSharing, (t) => {
|
||||
t.dropColumn("name");
|
||||
});
|
||||
}
|
||||
|
||||
const doesLastViewedAtExist = await knex.schema.hasColumn(TableName.SecretSharing, "lastViewedAt");
|
||||
if (doesLastViewedAtExist) {
|
||||
await knex.schema.alterTable(TableName.SecretSharing, (t) => {
|
||||
t.dropColumn("lastViewedAt");
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,21 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TableName } from "../schemas";
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
const hasKmsDataKeyCol = await knex.schema.hasColumn(TableName.Organization, "kmsEncryptedDataKey");
|
||||
await knex.schema.alterTable(TableName.Organization, (tb) => {
|
||||
if (!hasKmsDataKeyCol) {
|
||||
tb.binary("kmsEncryptedDataKey");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
const hasKmsDataKeyCol = await knex.schema.hasColumn(TableName.Organization, "kmsEncryptedDataKey");
|
||||
await knex.schema.alterTable(TableName.Organization, (t) => {
|
||||
if (hasKmsDataKeyCol) {
|
||||
t.dropColumn("kmsEncryptedDataKey");
|
||||
}
|
||||
});
|
||||
}
|
@ -0,0 +1,29 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TableName } from "../schemas";
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
const hasKmsSecretManagerEncryptedDataKey = await knex.schema.hasColumn(
|
||||
TableName.Project,
|
||||
"kmsSecretManagerEncryptedDataKey"
|
||||
);
|
||||
|
||||
await knex.schema.alterTable(TableName.Project, (tb) => {
|
||||
if (!hasKmsSecretManagerEncryptedDataKey) {
|
||||
tb.binary("kmsSecretManagerEncryptedDataKey");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
const hasKmsSecretManagerEncryptedDataKey = await knex.schema.hasColumn(
|
||||
TableName.Project,
|
||||
"kmsSecretManagerEncryptedDataKey"
|
||||
);
|
||||
|
||||
await knex.schema.alterTable(TableName.Project, (t) => {
|
||||
if (hasKmsSecretManagerEncryptedDataKey) {
|
||||
t.dropColumn("kmsSecretManagerEncryptedDataKey");
|
||||
}
|
||||
});
|
||||
}
|
181
backend/src/db/migrations/20240730181850_secret-v2.ts
Normal file
181
backend/src/db/migrations/20240730181850_secret-v2.ts
Normal file
@ -0,0 +1,181 @@
|
||||
/* eslint-disable @typescript-eslint/ban-ts-comment */
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { SecretType, TableName } from "../schemas";
|
||||
import { createJunctionTable, createOnUpdateTrigger, dropOnUpdateTrigger } from "../utils";
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
const doesSecretV2TableExist = await knex.schema.hasTable(TableName.SecretV2);
|
||||
if (!doesSecretV2TableExist) {
|
||||
await knex.schema.createTable(TableName.SecretV2, (t) => {
|
||||
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
|
||||
t.integer("version").defaultTo(1).notNullable();
|
||||
t.string("type").notNullable().defaultTo(SecretType.Shared);
|
||||
t.string("key", 500).notNullable();
|
||||
t.binary("encryptedValue");
|
||||
t.binary("encryptedComment");
|
||||
t.string("reminderNote");
|
||||
t.integer("reminderRepeatDays");
|
||||
t.boolean("skipMultilineEncoding").defaultTo(false);
|
||||
t.jsonb("metadata");
|
||||
t.uuid("userId");
|
||||
t.foreign("userId").references("id").inTable(TableName.Users).onDelete("CASCADE");
|
||||
t.uuid("folderId").notNullable();
|
||||
t.foreign("folderId").references("id").inTable(TableName.SecretFolder).onDelete("CASCADE");
|
||||
t.timestamps(true, true, true);
|
||||
t.index(["folderId", "userId"]);
|
||||
});
|
||||
}
|
||||
await createOnUpdateTrigger(knex, TableName.SecretV2);
|
||||
|
||||
// many to many relation between tags
|
||||
await createJunctionTable(knex, TableName.SecretV2JnTag, TableName.SecretV2, TableName.SecretTag);
|
||||
|
||||
const doesSecretV2VersionTableExist = await knex.schema.hasTable(TableName.SecretVersionV2);
|
||||
if (!doesSecretV2VersionTableExist) {
|
||||
await knex.schema.createTable(TableName.SecretVersionV2, (t) => {
|
||||
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
|
||||
t.integer("version").defaultTo(1).notNullable();
|
||||
t.string("type").notNullable().defaultTo(SecretType.Shared);
|
||||
t.string("key", 500).notNullable();
|
||||
t.binary("encryptedValue");
|
||||
t.binary("encryptedComment");
|
||||
t.string("reminderNote");
|
||||
t.integer("reminderRepeatDays");
|
||||
t.boolean("skipMultilineEncoding").defaultTo(false);
|
||||
t.jsonb("metadata");
|
||||
// to avoid orphan rows
|
||||
t.uuid("envId");
|
||||
t.foreign("envId").references("id").inTable(TableName.Environment).onDelete("CASCADE");
|
||||
t.uuid("secretId").notNullable();
|
||||
t.uuid("folderId").notNullable();
|
||||
t.uuid("userId");
|
||||
t.foreign("userId").references("id").inTable(TableName.Users).onDelete("CASCADE");
|
||||
t.timestamps(true, true, true);
|
||||
});
|
||||
}
|
||||
await createOnUpdateTrigger(knex, TableName.SecretVersionV2);
|
||||
|
||||
if (!(await knex.schema.hasTable(TableName.SecretReferenceV2))) {
|
||||
await knex.schema.createTable(TableName.SecretReferenceV2, (t) => {
|
||||
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
|
||||
t.string("environment").notNullable();
|
||||
t.string("secretPath").notNullable();
|
||||
t.string("secretKey", 500).notNullable();
|
||||
t.uuid("secretId").notNullable();
|
||||
t.foreign("secretId").references("id").inTable(TableName.SecretV2).onDelete("CASCADE");
|
||||
});
|
||||
}
|
||||
|
||||
await createJunctionTable(knex, TableName.SecretVersionV2Tag, TableName.SecretVersionV2, TableName.SecretTag);
|
||||
|
||||
if (!(await knex.schema.hasTable(TableName.SecretApprovalRequestSecretV2))) {
|
||||
await knex.schema.createTable(TableName.SecretApprovalRequestSecretV2, (t) => {
|
||||
// everything related to secret
|
||||
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
|
||||
t.integer("version").defaultTo(1);
|
||||
t.string("key", 500).notNullable();
|
||||
t.binary("encryptedValue");
|
||||
t.binary("encryptedComment");
|
||||
t.string("reminderNote");
|
||||
t.integer("reminderRepeatDays");
|
||||
t.boolean("skipMultilineEncoding").defaultTo(false);
|
||||
t.jsonb("metadata");
|
||||
t.timestamps(true, true, true);
|
||||
// commit details
|
||||
t.uuid("requestId").notNullable();
|
||||
t.foreign("requestId").references("id").inTable(TableName.SecretApprovalRequest).onDelete("CASCADE");
|
||||
t.string("op").notNullable();
|
||||
t.uuid("secretId");
|
||||
t.foreign("secretId").references("id").inTable(TableName.SecretV2).onDelete("SET NULL");
|
||||
t.uuid("secretVersion");
|
||||
t.foreign("secretVersion").references("id").inTable(TableName.SecretVersionV2).onDelete("SET NULL");
|
||||
});
|
||||
}
|
||||
|
||||
if (!(await knex.schema.hasTable(TableName.SecretApprovalRequestSecretTagV2))) {
|
||||
await knex.schema.createTable(TableName.SecretApprovalRequestSecretTagV2, (t) => {
|
||||
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
|
||||
t.uuid("secretId").notNullable();
|
||||
t.foreign("secretId").references("id").inTable(TableName.SecretApprovalRequestSecretV2).onDelete("CASCADE");
|
||||
t.uuid("tagId").notNullable();
|
||||
t.foreign("tagId").references("id").inTable(TableName.SecretTag).onDelete("CASCADE");
|
||||
t.timestamps(true, true, true);
|
||||
});
|
||||
}
|
||||
|
||||
if (!(await knex.schema.hasTable(TableName.SnapshotSecretV2))) {
|
||||
await knex.schema.createTable(TableName.SnapshotSecretV2, (t) => {
|
||||
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
|
||||
t.uuid("envId").index().notNullable();
|
||||
t.foreign("envId").references("id").inTable(TableName.Environment).onDelete("CASCADE");
|
||||
// not a relation kept like that to keep it when rolled back
|
||||
t.uuid("secretVersionId").index().notNullable();
|
||||
t.foreign("secretVersionId").references("id").inTable(TableName.SecretVersionV2).onDelete("CASCADE");
|
||||
t.uuid("snapshotId").index().notNullable();
|
||||
t.foreign("snapshotId").references("id").inTable(TableName.Snapshot).onDelete("CASCADE");
|
||||
t.timestamps(true, true, true);
|
||||
});
|
||||
}
|
||||
|
||||
if (await knex.schema.hasTable(TableName.IntegrationAuth)) {
|
||||
const hasEncryptedAccess = await knex.schema.hasColumn(TableName.IntegrationAuth, "encryptedAccess");
|
||||
const hasEncryptedAccessId = await knex.schema.hasColumn(TableName.IntegrationAuth, "encryptedAccessId");
|
||||
const hasEncryptedRefresh = await knex.schema.hasColumn(TableName.IntegrationAuth, "encryptedRefresh");
|
||||
const hasEncryptedAwsIamAssumRole = await knex.schema.hasColumn(
|
||||
TableName.IntegrationAuth,
|
||||
"encryptedAwsAssumeIamRoleArn"
|
||||
);
|
||||
await knex.schema.alterTable(TableName.IntegrationAuth, (t) => {
|
||||
if (!hasEncryptedAccess) t.binary("encryptedAccess");
|
||||
if (!hasEncryptedAccessId) t.binary("encryptedAccessId");
|
||||
if (!hasEncryptedRefresh) t.binary("encryptedRefresh");
|
||||
if (!hasEncryptedAwsIamAssumRole) t.binary("encryptedAwsAssumeIamRoleArn");
|
||||
});
|
||||
}
|
||||
|
||||
if (!(await knex.schema.hasTable(TableName.SecretRotationOutputV2))) {
|
||||
await knex.schema.createTable(TableName.SecretRotationOutputV2, (t) => {
|
||||
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
|
||||
t.string("key").notNullable();
|
||||
t.uuid("secretId").notNullable();
|
||||
t.foreign("secretId").references("id").inTable(TableName.SecretV2).onDelete("CASCADE");
|
||||
t.uuid("rotationId").notNullable();
|
||||
t.foreign("rotationId").references("id").inTable(TableName.SecretRotation).onDelete("CASCADE");
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
await knex.schema.dropTableIfExists(TableName.SnapshotSecretV2);
|
||||
await knex.schema.dropTableIfExists(TableName.SecretApprovalRequestSecretTagV2);
|
||||
await knex.schema.dropTableIfExists(TableName.SecretApprovalRequestSecretV2);
|
||||
|
||||
await knex.schema.dropTableIfExists(TableName.SecretV2JnTag);
|
||||
await knex.schema.dropTableIfExists(TableName.SecretReferenceV2);
|
||||
|
||||
await knex.schema.dropTableIfExists(TableName.SecretRotationOutputV2);
|
||||
|
||||
await dropOnUpdateTrigger(knex, TableName.SecretVersionV2);
|
||||
await knex.schema.dropTableIfExists(TableName.SecretVersionV2Tag);
|
||||
await knex.schema.dropTableIfExists(TableName.SecretVersionV2);
|
||||
|
||||
await dropOnUpdateTrigger(knex, TableName.SecretV2);
|
||||
await knex.schema.dropTableIfExists(TableName.SecretV2);
|
||||
|
||||
if (await knex.schema.hasTable(TableName.IntegrationAuth)) {
|
||||
const hasEncryptedAccess = await knex.schema.hasColumn(TableName.IntegrationAuth, "encryptedAccess");
|
||||
const hasEncryptedAccessId = await knex.schema.hasColumn(TableName.IntegrationAuth, "encryptedAccessId");
|
||||
const hasEncryptedRefresh = await knex.schema.hasColumn(TableName.IntegrationAuth, "encryptedRefresh");
|
||||
const hasEncryptedAwsIamAssumRole = await knex.schema.hasColumn(
|
||||
TableName.IntegrationAuth,
|
||||
"encryptedAwsAssumeIamRoleArn"
|
||||
);
|
||||
await knex.schema.alterTable(TableName.IntegrationAuth, (t) => {
|
||||
if (hasEncryptedAccess) t.dropColumn("encryptedAccess");
|
||||
if (hasEncryptedAccessId) t.dropColumn("encryptedAccessId");
|
||||
if (hasEncryptedRefresh) t.dropColumn("encryptedRefresh");
|
||||
if (hasEncryptedAwsIamAssumRole) t.dropColumn("encryptedAwsAssumeIamRoleArn");
|
||||
});
|
||||
}
|
||||
}
|
117
backend/src/db/migrations/20240802181855_ca-cert-version.ts
Normal file
117
backend/src/db/migrations/20240802181855_ca-cert-version.ts
Normal file
@ -0,0 +1,117 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TableName } from "../schemas";
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
if (await knex.schema.hasTable(TableName.CertificateAuthority)) {
|
||||
const hasActiveCaCertIdColumn = await knex.schema.hasColumn(TableName.CertificateAuthority, "activeCaCertId");
|
||||
if (!hasActiveCaCertIdColumn) {
|
||||
await knex.schema.alterTable(TableName.CertificateAuthority, (t) => {
|
||||
t.uuid("activeCaCertId").nullable();
|
||||
t.foreign("activeCaCertId").references("id").inTable(TableName.CertificateAuthorityCert);
|
||||
});
|
||||
|
||||
await knex.raw(`
|
||||
UPDATE "${TableName.CertificateAuthority}" ca
|
||||
SET "activeCaCertId" = cac.id
|
||||
FROM "${TableName.CertificateAuthorityCert}" cac
|
||||
WHERE ca.id = cac."caId"
|
||||
`);
|
||||
}
|
||||
}
|
||||
|
||||
if (await knex.schema.hasTable(TableName.CertificateAuthorityCert)) {
|
||||
const hasVersionColumn = await knex.schema.hasColumn(TableName.CertificateAuthorityCert, "version");
|
||||
if (!hasVersionColumn) {
|
||||
await knex.schema.alterTable(TableName.CertificateAuthorityCert, (t) => {
|
||||
t.integer("version").nullable();
|
||||
t.dropUnique(["caId"]);
|
||||
});
|
||||
|
||||
await knex(TableName.CertificateAuthorityCert).update({ version: 1 }).whereNull("version");
|
||||
|
||||
await knex.schema.alterTable(TableName.CertificateAuthorityCert, (t) => {
|
||||
t.integer("version").notNullable().alter();
|
||||
});
|
||||
}
|
||||
|
||||
const hasCaSecretIdColumn = await knex.schema.hasColumn(TableName.CertificateAuthorityCert, "caSecretId");
|
||||
if (!hasCaSecretIdColumn) {
|
||||
await knex.schema.alterTable(TableName.CertificateAuthorityCert, (t) => {
|
||||
t.uuid("caSecretId").nullable();
|
||||
t.foreign("caSecretId").references("id").inTable(TableName.CertificateAuthoritySecret).onDelete("CASCADE");
|
||||
});
|
||||
|
||||
await knex.raw(`
|
||||
UPDATE "${TableName.CertificateAuthorityCert}" cert
|
||||
SET "caSecretId" = (
|
||||
SELECT sec.id
|
||||
FROM "${TableName.CertificateAuthoritySecret}" sec
|
||||
WHERE sec."caId" = cert."caId"
|
||||
)
|
||||
`);
|
||||
|
||||
await knex.schema.alterTable(TableName.CertificateAuthorityCert, (t) => {
|
||||
t.uuid("caSecretId").notNullable().alter();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (await knex.schema.hasTable(TableName.CertificateAuthoritySecret)) {
|
||||
await knex.schema.alterTable(TableName.CertificateAuthoritySecret, (t) => {
|
||||
t.dropUnique(["caId"]);
|
||||
});
|
||||
}
|
||||
|
||||
if (await knex.schema.hasTable(TableName.Certificate)) {
|
||||
await knex.schema.alterTable(TableName.Certificate, (t) => {
|
||||
t.uuid("caCertId").nullable();
|
||||
t.foreign("caCertId").references("id").inTable(TableName.CertificateAuthorityCert);
|
||||
});
|
||||
|
||||
await knex.raw(`
|
||||
UPDATE "${TableName.Certificate}" cert
|
||||
SET "caCertId" = (
|
||||
SELECT caCert.id
|
||||
FROM "${TableName.CertificateAuthorityCert}" caCert
|
||||
WHERE caCert."caId" = cert."caId"
|
||||
)
|
||||
`);
|
||||
|
||||
await knex.schema.alterTable(TableName.Certificate, (t) => {
|
||||
t.uuid("caCertId").notNullable().alter();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
if (await knex.schema.hasTable(TableName.CertificateAuthority)) {
|
||||
if (await knex.schema.hasColumn(TableName.CertificateAuthority, "activeCaCertId")) {
|
||||
await knex.schema.alterTable(TableName.CertificateAuthority, (t) => {
|
||||
t.dropColumn("activeCaCertId");
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (await knex.schema.hasTable(TableName.CertificateAuthorityCert)) {
|
||||
if (await knex.schema.hasColumn(TableName.CertificateAuthorityCert, "version")) {
|
||||
await knex.schema.alterTable(TableName.CertificateAuthorityCert, (t) => {
|
||||
t.dropColumn("version");
|
||||
});
|
||||
}
|
||||
|
||||
if (await knex.schema.hasColumn(TableName.CertificateAuthorityCert, "caSecretId")) {
|
||||
await knex.schema.alterTable(TableName.CertificateAuthorityCert, (t) => {
|
||||
t.dropColumn("caSecretId");
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (await knex.schema.hasTable(TableName.Certificate)) {
|
||||
if (await knex.schema.hasColumn(TableName.Certificate, "caCertId")) {
|
||||
await knex.schema.alterTable(TableName.Certificate, (t) => {
|
||||
t.dropColumn("caCertId");
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
105
backend/src/db/migrations/utils/kms.ts
Normal file
105
backend/src/db/migrations/utils/kms.ts
Normal file
@ -0,0 +1,105 @@
|
||||
import slugify from "@sindresorhus/slugify";
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TableName } from "@app/db/schemas";
|
||||
import { randomSecureBytes } from "@app/lib/crypto";
|
||||
import { symmetricCipherService, SymmetricEncryption } from "@app/lib/crypto/cipher";
|
||||
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
||||
|
||||
const getInstanceRootKey = async (knex: Knex) => {
|
||||
const encryptionKey = process.env.ENCRYPTION_KEY || process.env.ROOT_ENCRYPTION_KEY;
|
||||
// if root key its base64 encoded
|
||||
const isBase64 = !process.env.ENCRYPTION_KEY;
|
||||
if (!encryptionKey) throw new Error("ENCRYPTION_KEY variable needed for migration");
|
||||
const encryptionKeyBuffer = Buffer.from(encryptionKey, isBase64 ? "base64" : "utf8");
|
||||
|
||||
const KMS_ROOT_CONFIG_UUID = "00000000-0000-0000-0000-000000000000";
|
||||
const kmsRootConfig = await knex(TableName.KmsServerRootConfig).where({ id: KMS_ROOT_CONFIG_UUID }).first();
|
||||
const cipher = symmetricCipherService(SymmetricEncryption.AES_GCM_256);
|
||||
if (kmsRootConfig) {
|
||||
const decryptedRootKey = cipher.decrypt(kmsRootConfig.encryptedRootKey, encryptionKeyBuffer);
|
||||
// set the flag so that other instancen nodes can start
|
||||
return decryptedRootKey;
|
||||
}
|
||||
|
||||
const newRootKey = randomSecureBytes(32);
|
||||
const encryptedRootKey = cipher.encrypt(newRootKey, encryptionKeyBuffer);
|
||||
await knex(TableName.KmsServerRootConfig).insert({
|
||||
encryptedRootKey,
|
||||
// eslint-disable-next-line
|
||||
// @ts-ignore id is kept as fixed for idempotence and to avoid race condition
|
||||
id: KMS_ROOT_CONFIG_UUID
|
||||
});
|
||||
return encryptedRootKey;
|
||||
};
|
||||
|
||||
export const getSecretManagerDataKey = async (knex: Knex, projectId: string) => {
|
||||
const KMS_VERSION = "v01";
|
||||
const KMS_VERSION_BLOB_LENGTH = 3;
|
||||
const cipher = symmetricCipherService(SymmetricEncryption.AES_GCM_256);
|
||||
const project = await knex(TableName.Project).where({ id: projectId }).first();
|
||||
if (!project) throw new Error("Missing project id");
|
||||
|
||||
const ROOT_ENCRYPTION_KEY = await getInstanceRootKey(knex);
|
||||
|
||||
let secretManagerKmsKey;
|
||||
const projectSecretManagerKmsId = project?.kmsSecretManagerKeyId;
|
||||
if (projectSecretManagerKmsId) {
|
||||
const kmsDoc = await knex(TableName.KmsKey)
|
||||
.leftJoin(TableName.InternalKms, `${TableName.KmsKey}.id`, `${TableName.InternalKms}.kmsKeyId`)
|
||||
.where({ [`${TableName.KmsKey}.id` as "id"]: projectSecretManagerKmsId })
|
||||
.first();
|
||||
if (!kmsDoc) throw new Error("missing kms");
|
||||
secretManagerKmsKey = cipher.decrypt(kmsDoc.encryptedKey, ROOT_ENCRYPTION_KEY);
|
||||
} else {
|
||||
const [kmsDoc] = await knex(TableName.KmsKey)
|
||||
.insert({
|
||||
slug: slugify(alphaNumericNanoId(8).toLowerCase()),
|
||||
orgId: project.orgId,
|
||||
isReserved: false
|
||||
})
|
||||
.returning("*");
|
||||
|
||||
secretManagerKmsKey = randomSecureBytes(32);
|
||||
const encryptedKeyMaterial = cipher.encrypt(secretManagerKmsKey, ROOT_ENCRYPTION_KEY);
|
||||
await knex(TableName.InternalKms).insert({
|
||||
version: 1,
|
||||
encryptedKey: encryptedKeyMaterial,
|
||||
encryptionAlgorithm: SymmetricEncryption.AES_GCM_256,
|
||||
kmsKeyId: kmsDoc.id
|
||||
});
|
||||
}
|
||||
|
||||
const encryptedSecretManagerDataKey = project?.kmsSecretManagerEncryptedDataKey;
|
||||
let dataKey: Buffer;
|
||||
if (!encryptedSecretManagerDataKey) {
|
||||
dataKey = randomSecureBytes();
|
||||
// the below versioning we do it automatically in kms service
|
||||
const unversionedDataKey = cipher.encrypt(dataKey, secretManagerKmsKey);
|
||||
const versionBlob = Buffer.from(KMS_VERSION, "utf8"); // length is 3
|
||||
await knex(TableName.Project)
|
||||
.where({ id: projectId })
|
||||
.update({
|
||||
kmsSecretManagerEncryptedDataKey: Buffer.concat([unversionedDataKey, versionBlob])
|
||||
});
|
||||
} else {
|
||||
const cipherTextBlob = encryptedSecretManagerDataKey.subarray(0, -KMS_VERSION_BLOB_LENGTH);
|
||||
dataKey = cipher.decrypt(cipherTextBlob, secretManagerKmsKey);
|
||||
}
|
||||
|
||||
return {
|
||||
encryptor: ({ plainText }: { plainText: Buffer }) => {
|
||||
const encryptedPlainTextBlob = cipher.encrypt(plainText, dataKey);
|
||||
|
||||
// Buffer#1 encrypted text + Buffer#2 version number
|
||||
const versionBlob = Buffer.from(KMS_VERSION, "utf8"); // length is 3
|
||||
const cipherTextBlob = Buffer.concat([encryptedPlainTextBlob, versionBlob]);
|
||||
return { cipherTextBlob };
|
||||
},
|
||||
decryptor: ({ cipherTextBlob: versionedCipherTextBlob }: { cipherTextBlob: Buffer }) => {
|
||||
const cipherTextBlob = versionedCipherTextBlob.subarray(0, -KMS_VERSION_BLOB_LENGTH);
|
||||
const decryptedBlob = cipher.decrypt(cipherTextBlob, dataKey);
|
||||
return decryptedBlob;
|
||||
}
|
||||
};
|
||||
};
|
@ -14,7 +14,8 @@ export const AccessApprovalPoliciesSchema = z.object({
|
||||
secretPath: z.string().nullable().optional(),
|
||||
envId: z.string().uuid(),
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date()
|
||||
updatedAt: z.date(),
|
||||
enforcementLevel: z.string().default("hard")
|
||||
});
|
||||
|
||||
export type TAccessApprovalPolicies = z.infer<typeof AccessApprovalPoliciesSchema>;
|
||||
|
@ -27,7 +27,8 @@ export const CertificateAuthoritiesSchema = z.object({
|
||||
maxPathLength: z.number().nullable().optional(),
|
||||
keyAlgorithm: z.string(),
|
||||
notBefore: z.date().nullable().optional(),
|
||||
notAfter: z.date().nullable().optional()
|
||||
notAfter: z.date().nullable().optional(),
|
||||
activeCaCertId: z.string().uuid().nullable().optional()
|
||||
});
|
||||
|
||||
export type TCertificateAuthorities = z.infer<typeof CertificateAuthoritiesSchema>;
|
||||
|
@ -15,7 +15,9 @@ export const CertificateAuthorityCertsSchema = z.object({
|
||||
updatedAt: z.date(),
|
||||
caId: z.string().uuid(),
|
||||
encryptedCertificate: zodBuffer,
|
||||
encryptedCertificateChain: zodBuffer
|
||||
encryptedCertificateChain: zodBuffer,
|
||||
version: z.number(),
|
||||
caSecretId: z.string().uuid()
|
||||
});
|
||||
|
||||
export type TCertificateAuthorityCerts = z.infer<typeof CertificateAuthorityCertsSchema>;
|
||||
|
@ -20,7 +20,8 @@ export const CertificatesSchema = z.object({
|
||||
notAfter: z.date(),
|
||||
revokedAt: z.date().nullable().optional(),
|
||||
revocationReason: z.number().nullable().optional(),
|
||||
altNames: z.string().default("").nullable().optional()
|
||||
altNames: z.string().default("").nullable().optional(),
|
||||
caCertId: z.string().uuid()
|
||||
});
|
||||
|
||||
export type TCertificates = z.infer<typeof CertificatesSchema>;
|
||||
|
23
backend/src/db/schemas/external-kms.ts
Normal file
23
backend/src/db/schemas/external-kms.ts
Normal file
@ -0,0 +1,23 @@
|
||||
// Code generated by automation script, DO NOT EDIT.
|
||||
// Automated by pulling database and generating zod schema
|
||||
// To update. Just run npm run generate:schema
|
||||
// Written by akhilmhdh.
|
||||
|
||||
import { z } from "zod";
|
||||
|
||||
import { zodBuffer } from "@app/lib/zod";
|
||||
|
||||
import { TImmutableDBKeys } from "./models";
|
||||
|
||||
export const ExternalKmsSchema = z.object({
|
||||
id: z.string().uuid(),
|
||||
provider: z.string(),
|
||||
encryptedProviderInputs: zodBuffer,
|
||||
status: z.string().nullable().optional(),
|
||||
statusDetails: z.string().nullable().optional(),
|
||||
kmsKeyId: z.string().uuid()
|
||||
});
|
||||
|
||||
export type TExternalKms = z.infer<typeof ExternalKmsSchema>;
|
||||
export type TExternalKmsInsert = Omit<z.input<typeof ExternalKmsSchema>, TImmutableDBKeys>;
|
||||
export type TExternalKmsUpdate = Partial<Omit<z.input<typeof ExternalKmsSchema>, TImmutableDBKeys>>;
|
31
backend/src/db/schemas/identity-oidc-auths.ts
Normal file
31
backend/src/db/schemas/identity-oidc-auths.ts
Normal file
@ -0,0 +1,31 @@
|
||||
// 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 IdentityOidcAuthsSchema = z.object({
|
||||
id: z.string().uuid(),
|
||||
accessTokenTTL: z.coerce.number().default(7200),
|
||||
accessTokenMaxTTL: z.coerce.number().default(7200),
|
||||
accessTokenNumUsesLimit: z.coerce.number().default(0),
|
||||
accessTokenTrustedIps: z.unknown(),
|
||||
identityId: z.string().uuid(),
|
||||
oidcDiscoveryUrl: z.string(),
|
||||
encryptedCaCert: z.string(),
|
||||
caCertIV: z.string(),
|
||||
caCertTag: z.string(),
|
||||
boundIssuer: z.string(),
|
||||
boundAudiences: z.string(),
|
||||
boundClaims: z.unknown(),
|
||||
boundSubject: z.string().nullable().optional(),
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date()
|
||||
});
|
||||
|
||||
export type TIdentityOidcAuths = z.infer<typeof IdentityOidcAuthsSchema>;
|
||||
export type TIdentityOidcAuthsInsert = Omit<z.input<typeof IdentityOidcAuthsSchema>, TImmutableDBKeys>;
|
||||
export type TIdentityOidcAuthsUpdate = Partial<Omit<z.input<typeof IdentityOidcAuthsSchema>, TImmutableDBKeys>>;
|
@ -17,6 +17,7 @@ export * from "./certificate-secrets";
|
||||
export * from "./certificates";
|
||||
export * from "./dynamic-secret-leases";
|
||||
export * from "./dynamic-secrets";
|
||||
export * from "./external-kms";
|
||||
export * from "./git-app-install-sessions";
|
||||
export * from "./git-app-org";
|
||||
export * from "./group-project-membership-roles";
|
||||
@ -28,6 +29,7 @@ export * from "./identity-aws-auths";
|
||||
export * from "./identity-azure-auths";
|
||||
export * from "./identity-gcp-auths";
|
||||
export * from "./identity-kubernetes-auths";
|
||||
export * from "./identity-oidc-auths";
|
||||
export * from "./identity-org-memberships";
|
||||
export * from "./identity-project-additional-privilege";
|
||||
export * from "./identity-project-membership-role";
|
||||
@ -38,6 +40,7 @@ export * from "./identity-universal-auths";
|
||||
export * from "./incident-contacts";
|
||||
export * from "./integration-auths";
|
||||
export * from "./integrations";
|
||||
export * from "./internal-kms";
|
||||
export * from "./kms-key-versions";
|
||||
export * from "./kms-keys";
|
||||
export * from "./kms-root-config";
|
||||
@ -63,26 +66,35 @@ export * from "./scim-tokens";
|
||||
export * from "./secret-approval-policies";
|
||||
export * from "./secret-approval-policies-approvers";
|
||||
export * from "./secret-approval-request-secret-tags";
|
||||
export * from "./secret-approval-request-secret-tags-v2";
|
||||
export * from "./secret-approval-requests";
|
||||
export * from "./secret-approval-requests-reviewers";
|
||||
export * from "./secret-approval-requests-secrets";
|
||||
export * from "./secret-approval-requests-secrets-v2";
|
||||
export * from "./secret-blind-indexes";
|
||||
export * from "./secret-folder-versions";
|
||||
export * from "./secret-folders";
|
||||
export * from "./secret-imports";
|
||||
export * from "./secret-references";
|
||||
export * from "./secret-references-v2";
|
||||
export * from "./secret-rotation-output-v2";
|
||||
export * from "./secret-rotation-outputs";
|
||||
export * from "./secret-rotations";
|
||||
export * from "./secret-scanning-git-risks";
|
||||
export * from "./secret-sharing";
|
||||
export * from "./secret-snapshot-folders";
|
||||
export * from "./secret-snapshot-secrets";
|
||||
export * from "./secret-snapshot-secrets-v2";
|
||||
export * from "./secret-snapshots";
|
||||
export * from "./secret-tag-junction";
|
||||
export * from "./secret-tags";
|
||||
export * from "./secret-v2-tag-junction";
|
||||
export * from "./secret-version-tag-junction";
|
||||
export * from "./secret-version-v2-tag-junction";
|
||||
export * from "./secret-versions";
|
||||
export * from "./secret-versions-v2";
|
||||
export * from "./secrets";
|
||||
export * from "./secrets-v2";
|
||||
export * from "./service-tokens";
|
||||
export * from "./super-admin";
|
||||
export * from "./trusted-ips";
|
||||
|
@ -5,6 +5,8 @@
|
||||
|
||||
import { z } from "zod";
|
||||
|
||||
import { zodBuffer } from "@app/lib/zod";
|
||||
|
||||
import { TImmutableDBKeys } from "./models";
|
||||
|
||||
export const IntegrationAuthsSchema = z.object({
|
||||
@ -32,7 +34,11 @@ export const IntegrationAuthsSchema = z.object({
|
||||
updatedAt: z.date(),
|
||||
awsAssumeIamRoleArnCipherText: z.string().nullable().optional(),
|
||||
awsAssumeIamRoleArnIV: z.string().nullable().optional(),
|
||||
awsAssumeIamRoleArnTag: z.string().nullable().optional()
|
||||
awsAssumeIamRoleArnTag: z.string().nullable().optional(),
|
||||
encryptedAccess: zodBuffer.nullable().optional(),
|
||||
encryptedAccessId: zodBuffer.nullable().optional(),
|
||||
encryptedRefresh: zodBuffer.nullable().optional(),
|
||||
encryptedAwsAssumeIamRoleArn: zodBuffer.nullable().optional()
|
||||
});
|
||||
|
||||
export type TIntegrationAuths = z.infer<typeof IntegrationAuthsSchema>;
|
||||
|
21
backend/src/db/schemas/internal-kms-key-version.ts
Normal file
21
backend/src/db/schemas/internal-kms-key-version.ts
Normal file
@ -0,0 +1,21 @@
|
||||
// Code generated by automation script, DO NOT EDIT.
|
||||
// Automated by pulling database and generating zod schema
|
||||
// To update. Just run npm run generate:schema
|
||||
// Written by akhilmhdh.
|
||||
|
||||
import { z } from "zod";
|
||||
|
||||
import { zodBuffer } from "@app/lib/zod";
|
||||
|
||||
import { TImmutableDBKeys } from "./models";
|
||||
|
||||
export const InternalKmsKeyVersionSchema = z.object({
|
||||
id: z.string().uuid(),
|
||||
encryptedKey: zodBuffer,
|
||||
version: z.number(),
|
||||
internalKmsId: z.string().uuid()
|
||||
});
|
||||
|
||||
export type TInternalKmsKeyVersion = z.infer<typeof InternalKmsKeyVersionSchema>;
|
||||
export type TInternalKmsKeyVersionInsert = Omit<z.input<typeof InternalKmsKeyVersionSchema>, TImmutableDBKeys>;
|
||||
export type TInternalKmsKeyVersionUpdate = Partial<Omit<z.input<typeof InternalKmsKeyVersionSchema>, TImmutableDBKeys>>;
|
22
backend/src/db/schemas/internal-kms.ts
Normal file
22
backend/src/db/schemas/internal-kms.ts
Normal file
@ -0,0 +1,22 @@
|
||||
// Code generated by automation script, DO NOT EDIT.
|
||||
// Automated by pulling database and generating zod schema
|
||||
// To update. Just run npm run generate:schema
|
||||
// Written by akhilmhdh.
|
||||
|
||||
import { z } from "zod";
|
||||
|
||||
import { zodBuffer } from "@app/lib/zod";
|
||||
|
||||
import { TImmutableDBKeys } from "./models";
|
||||
|
||||
export const InternalKmsSchema = z.object({
|
||||
id: z.string().uuid(),
|
||||
encryptedKey: zodBuffer,
|
||||
encryptionAlgorithm: z.string(),
|
||||
version: z.number().default(1),
|
||||
kmsKeyId: z.string().uuid()
|
||||
});
|
||||
|
||||
export type TInternalKms = z.infer<typeof InternalKmsSchema>;
|
||||
export type TInternalKmsInsert = Omit<z.input<typeof InternalKmsSchema>, TImmutableDBKeys>;
|
||||
export type TInternalKmsUpdate = Partial<Omit<z.input<typeof InternalKmsSchema>, TImmutableDBKeys>>;
|
@ -5,20 +5,17 @@
|
||||
|
||||
import { z } from "zod";
|
||||
|
||||
import { zodBuffer } from "@app/lib/zod";
|
||||
|
||||
import { TImmutableDBKeys } from "./models";
|
||||
|
||||
export const KmsKeysSchema = z.object({
|
||||
id: z.string().uuid(),
|
||||
encryptedKey: zodBuffer,
|
||||
encryptionAlgorithm: z.string(),
|
||||
version: z.number().default(1),
|
||||
description: z.string().nullable().optional(),
|
||||
isDisabled: z.boolean().default(false).nullable().optional(),
|
||||
isReserved: z.boolean().default(true).nullable().optional(),
|
||||
projectId: z.string().nullable().optional(),
|
||||
orgId: z.string().uuid().nullable().optional()
|
||||
orgId: z.string().uuid(),
|
||||
slug: z.string(),
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date()
|
||||
});
|
||||
|
||||
export type TKmsKeys = z.infer<typeof KmsKeysSchema>;
|
||||
|
@ -60,6 +60,7 @@ export enum TableName {
|
||||
IdentityAzureAuth = "identity_azure_auths",
|
||||
IdentityUaClientSecret = "identity_ua_client_secrets",
|
||||
IdentityAwsAuth = "identity_aws_auths",
|
||||
IdentityOidcAuth = "identity_oidc_auths",
|
||||
IdentityOrgMembership = "identity_org_memberships",
|
||||
IdentityProjectMembership = "identity_project_memberships",
|
||||
IdentityProjectMembershipRole = "identity_project_membership_role",
|
||||
@ -89,12 +90,25 @@ export enum TableName {
|
||||
TrustedIps = "trusted_ips",
|
||||
DynamicSecret = "dynamic_secrets",
|
||||
DynamicSecretLease = "dynamic_secret_leases",
|
||||
SecretV2 = "secrets_v2",
|
||||
SecretReferenceV2 = "secret_references_v2",
|
||||
SecretVersionV2 = "secret_versions_v2",
|
||||
SecretApprovalRequestSecretV2 = "secret_approval_requests_secrets_v2",
|
||||
SecretApprovalRequestSecretTagV2 = "secret_approval_request_secret_tags_v2",
|
||||
SnapshotSecretV2 = "secret_snapshot_secrets_v2",
|
||||
// junction tables with tags
|
||||
SecretV2JnTag = "secret_v2_tag_junction",
|
||||
JnSecretTag = "secret_tag_junction",
|
||||
SecretVersionTag = "secret_version_tag_junction",
|
||||
SecretVersionV2Tag = "secret_version_v2_tag_junction",
|
||||
SecretRotationOutputV2 = "secret_rotation_output_v2",
|
||||
// KMS Service
|
||||
KmsServerRootConfig = "kms_root_config",
|
||||
KmsKey = "kms_keys",
|
||||
ExternalKms = "external_kms",
|
||||
InternalKms = "internal_kms",
|
||||
InternalKmsKeyVersion = "internal_kms_key_version",
|
||||
// @depreciated
|
||||
KmsKeyVersion = "kms_key_versions"
|
||||
}
|
||||
|
||||
@ -152,7 +166,8 @@ export enum SecretType {
|
||||
|
||||
export enum ProjectVersion {
|
||||
V1 = 1,
|
||||
V2 = 2
|
||||
V2 = 2,
|
||||
V3 = 3
|
||||
}
|
||||
|
||||
export enum ProjectUpgradeStatus {
|
||||
@ -167,5 +182,6 @@ export enum IdentityAuthMethod {
|
||||
KUBERNETES_AUTH = "kubernetes-auth",
|
||||
GCP_AUTH = "gcp-auth",
|
||||
AWS_AUTH = "aws-auth",
|
||||
AZURE_AUTH = "azure-auth"
|
||||
AZURE_AUTH = "azure-auth",
|
||||
OIDC_AUTH = "oidc-auth"
|
||||
}
|
||||
|
@ -17,7 +17,8 @@ export const OrgMembershipsSchema = z.object({
|
||||
userId: z.string().uuid().nullable().optional(),
|
||||
orgId: z.string().uuid(),
|
||||
roleId: z.string().uuid().nullable().optional(),
|
||||
projectFavorites: z.string().array().nullable().optional()
|
||||
projectFavorites: z.string().array().nullable().optional(),
|
||||
isActive: z.boolean().default(true)
|
||||
});
|
||||
|
||||
export type TOrgMemberships = z.infer<typeof OrgMembershipsSchema>;
|
||||
|
@ -5,6 +5,8 @@
|
||||
|
||||
import { z } from "zod";
|
||||
|
||||
import { zodBuffer } from "@app/lib/zod";
|
||||
|
||||
import { TImmutableDBKeys } from "./models";
|
||||
|
||||
export const OrganizationsSchema = z.object({
|
||||
@ -15,7 +17,9 @@ export const OrganizationsSchema = z.object({
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date(),
|
||||
authEnforced: z.boolean().default(false).nullable().optional(),
|
||||
scimEnabled: z.boolean().default(false).nullable().optional()
|
||||
scimEnabled: z.boolean().default(false).nullable().optional(),
|
||||
kmsDefaultKeyId: z.string().uuid().nullable().optional(),
|
||||
kmsEncryptedDataKey: zodBuffer.nullable().optional()
|
||||
});
|
||||
|
||||
export type TOrganizations = z.infer<typeof OrganizationsSchema>;
|
||||
|
@ -5,6 +5,8 @@
|
||||
|
||||
import { z } from "zod";
|
||||
|
||||
import { zodBuffer } from "@app/lib/zod";
|
||||
|
||||
import { TImmutableDBKeys } from "./models";
|
||||
|
||||
export const ProjectsSchema = z.object({
|
||||
@ -19,7 +21,9 @@ export const ProjectsSchema = z.object({
|
||||
upgradeStatus: z.string().nullable().optional(),
|
||||
pitVersionLimit: z.number().default(10),
|
||||
kmsCertificateKeyId: z.string().uuid().nullable().optional(),
|
||||
auditLogsRetentionDays: z.number().nullable().optional()
|
||||
auditLogsRetentionDays: z.number().nullable().optional(),
|
||||
kmsSecretManagerKeyId: z.string().uuid().nullable().optional(),
|
||||
kmsSecretManagerEncryptedDataKey: zodBuffer.nullable().optional()
|
||||
});
|
||||
|
||||
export type TProjects = z.infer<typeof ProjectsSchema>;
|
||||
|
@ -14,7 +14,8 @@ export const SecretApprovalPoliciesSchema = z.object({
|
||||
approvals: z.number().default(1),
|
||||
envId: z.string().uuid(),
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date()
|
||||
updatedAt: z.date(),
|
||||
enforcementLevel: z.string().default("hard")
|
||||
});
|
||||
|
||||
export type TSecretApprovalPolicies = z.infer<typeof SecretApprovalPoliciesSchema>;
|
||||
|
@ -0,0 +1,25 @@
|
||||
// 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 SecretApprovalRequestSecretTagsV2Schema = z.object({
|
||||
id: z.string().uuid(),
|
||||
secretId: z.string().uuid(),
|
||||
tagId: z.string().uuid(),
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date()
|
||||
});
|
||||
|
||||
export type TSecretApprovalRequestSecretTagsV2 = z.infer<typeof SecretApprovalRequestSecretTagsV2Schema>;
|
||||
export type TSecretApprovalRequestSecretTagsV2Insert = Omit<
|
||||
z.input<typeof SecretApprovalRequestSecretTagsV2Schema>,
|
||||
TImmutableDBKeys
|
||||
>;
|
||||
export type TSecretApprovalRequestSecretTagsV2Update = Partial<
|
||||
Omit<z.input<typeof SecretApprovalRequestSecretTagsV2Schema>, TImmutableDBKeys>
|
||||
>;
|
@ -0,0 +1,37 @@
|
||||
// Code generated by automation script, DO NOT EDIT.
|
||||
// Automated by pulling database and generating zod schema
|
||||
// To update. Just run npm run generate:schema
|
||||
// Written by akhilmhdh.
|
||||
|
||||
import { z } from "zod";
|
||||
|
||||
import { zodBuffer } from "@app/lib/zod";
|
||||
|
||||
import { TImmutableDBKeys } from "./models";
|
||||
|
||||
export const SecretApprovalRequestsSecretsV2Schema = z.object({
|
||||
id: z.string().uuid(),
|
||||
version: z.number().default(1).nullable().optional(),
|
||||
key: z.string(),
|
||||
encryptedValue: zodBuffer.nullable().optional(),
|
||||
encryptedComment: zodBuffer.nullable().optional(),
|
||||
reminderNote: z.string().nullable().optional(),
|
||||
reminderRepeatDays: z.number().nullable().optional(),
|
||||
skipMultilineEncoding: z.boolean().default(false).nullable().optional(),
|
||||
metadata: z.unknown().nullable().optional(),
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date(),
|
||||
requestId: z.string().uuid(),
|
||||
op: z.string(),
|
||||
secretId: z.string().uuid().nullable().optional(),
|
||||
secretVersion: z.string().uuid().nullable().optional()
|
||||
});
|
||||
|
||||
export type TSecretApprovalRequestsSecretsV2 = z.infer<typeof SecretApprovalRequestsSecretsV2Schema>;
|
||||
export type TSecretApprovalRequestsSecretsV2Insert = Omit<
|
||||
z.input<typeof SecretApprovalRequestsSecretsV2Schema>,
|
||||
TImmutableDBKeys
|
||||
>;
|
||||
export type TSecretApprovalRequestsSecretsV2Update = Partial<
|
||||
Omit<z.input<typeof SecretApprovalRequestsSecretsV2Schema>, TImmutableDBKeys>
|
||||
>;
|
@ -19,7 +19,8 @@ export const SecretApprovalRequestsSchema = z.object({
|
||||
updatedAt: z.date(),
|
||||
isReplicated: z.boolean().nullable().optional(),
|
||||
committerUserId: z.string().uuid(),
|
||||
statusChangedByUserId: z.string().uuid().nullable().optional()
|
||||
statusChangedByUserId: z.string().uuid().nullable().optional(),
|
||||
bypassReason: z.string().nullable().optional()
|
||||
});
|
||||
|
||||
export type TSecretApprovalRequests = z.infer<typeof SecretApprovalRequestsSchema>;
|
||||
|
20
backend/src/db/schemas/secret-references-v2.ts
Normal file
20
backend/src/db/schemas/secret-references-v2.ts
Normal file
@ -0,0 +1,20 @@
|
||||
// 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 SecretReferencesV2Schema = z.object({
|
||||
id: z.string().uuid(),
|
||||
environment: z.string(),
|
||||
secretPath: z.string(),
|
||||
secretKey: z.string(),
|
||||
secretId: z.string().uuid()
|
||||
});
|
||||
|
||||
export type TSecretReferencesV2 = z.infer<typeof SecretReferencesV2Schema>;
|
||||
export type TSecretReferencesV2Insert = Omit<z.input<typeof SecretReferencesV2Schema>, TImmutableDBKeys>;
|
||||
export type TSecretReferencesV2Update = Partial<Omit<z.input<typeof SecretReferencesV2Schema>, TImmutableDBKeys>>;
|
21
backend/src/db/schemas/secret-rotation-output-v2.ts
Normal file
21
backend/src/db/schemas/secret-rotation-output-v2.ts
Normal file
@ -0,0 +1,21 @@
|
||||
// Code generated by automation script, DO NOT EDIT.
|
||||
// Automated by pulling database and generating zod schema
|
||||
// To update. Just run npm run generate:schema
|
||||
// Written by akhilmhdh.
|
||||
|
||||
import { z } from "zod";
|
||||
|
||||
import { TImmutableDBKeys } from "./models";
|
||||
|
||||
export const SecretRotationOutputV2Schema = z.object({
|
||||
id: z.string().uuid(),
|
||||
key: z.string(),
|
||||
secretId: z.string().uuid(),
|
||||
rotationId: z.string().uuid()
|
||||
});
|
||||
|
||||
export type TSecretRotationOutputV2 = z.infer<typeof SecretRotationOutputV2Schema>;
|
||||
export type TSecretRotationOutputV2Insert = Omit<z.input<typeof SecretRotationOutputV2Schema>, TImmutableDBKeys>;
|
||||
export type TSecretRotationOutputV2Update = Partial<
|
||||
Omit<z.input<typeof SecretRotationOutputV2Schema>, TImmutableDBKeys>
|
||||
>;
|
@ -18,7 +18,10 @@ export const SecretSharingSchema = z.object({
|
||||
orgId: z.string().uuid().nullable().optional(),
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date(),
|
||||
expiresAfterViews: z.number().nullable().optional()
|
||||
expiresAfterViews: z.number().nullable().optional(),
|
||||
accessType: z.string().default("anyone"),
|
||||
name: z.string().nullable().optional(),
|
||||
lastViewedAt: z.date().nullable().optional()
|
||||
});
|
||||
|
||||
export type TSecretSharing = z.infer<typeof SecretSharingSchema>;
|
||||
|
23
backend/src/db/schemas/secret-snapshot-secrets-v2.ts
Normal file
23
backend/src/db/schemas/secret-snapshot-secrets-v2.ts
Normal file
@ -0,0 +1,23 @@
|
||||
// Code generated by automation script, DO NOT EDIT.
|
||||
// Automated by pulling database and generating zod schema
|
||||
// To update. Just run npm run generate:schema
|
||||
// Written by akhilmhdh.
|
||||
|
||||
import { z } from "zod";
|
||||
|
||||
import { TImmutableDBKeys } from "./models";
|
||||
|
||||
export const SecretSnapshotSecretsV2Schema = z.object({
|
||||
id: z.string().uuid(),
|
||||
envId: z.string().uuid(),
|
||||
secretVersionId: z.string().uuid(),
|
||||
snapshotId: z.string().uuid(),
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date()
|
||||
});
|
||||
|
||||
export type TSecretSnapshotSecretsV2 = z.infer<typeof SecretSnapshotSecretsV2Schema>;
|
||||
export type TSecretSnapshotSecretsV2Insert = Omit<z.input<typeof SecretSnapshotSecretsV2Schema>, TImmutableDBKeys>;
|
||||
export type TSecretSnapshotSecretsV2Update = Partial<
|
||||
Omit<z.input<typeof SecretSnapshotSecretsV2Schema>, TImmutableDBKeys>
|
||||
>;
|
18
backend/src/db/schemas/secret-v2-tag-junction.ts
Normal file
18
backend/src/db/schemas/secret-v2-tag-junction.ts
Normal file
@ -0,0 +1,18 @@
|
||||
// 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 SecretV2TagJunctionSchema = z.object({
|
||||
id: z.string().uuid(),
|
||||
secrets_v2Id: z.string().uuid(),
|
||||
secret_tagsId: z.string().uuid()
|
||||
});
|
||||
|
||||
export type TSecretV2TagJunction = z.infer<typeof SecretV2TagJunctionSchema>;
|
||||
export type TSecretV2TagJunctionInsert = Omit<z.input<typeof SecretV2TagJunctionSchema>, TImmutableDBKeys>;
|
||||
export type TSecretV2TagJunctionUpdate = Partial<Omit<z.input<typeof SecretV2TagJunctionSchema>, TImmutableDBKeys>>;
|
23
backend/src/db/schemas/secret-version-v2-tag-junction.ts
Normal file
23
backend/src/db/schemas/secret-version-v2-tag-junction.ts
Normal file
@ -0,0 +1,23 @@
|
||||
// Code generated by automation script, DO NOT EDIT.
|
||||
// Automated by pulling database and generating zod schema
|
||||
// To update. Just run npm run generate:schema
|
||||
// Written by akhilmhdh.
|
||||
|
||||
import { z } from "zod";
|
||||
|
||||
import { TImmutableDBKeys } from "./models";
|
||||
|
||||
export const SecretVersionV2TagJunctionSchema = z.object({
|
||||
id: z.string().uuid(),
|
||||
secret_versions_v2Id: z.string().uuid(),
|
||||
secret_tagsId: z.string().uuid()
|
||||
});
|
||||
|
||||
export type TSecretVersionV2TagJunction = z.infer<typeof SecretVersionV2TagJunctionSchema>;
|
||||
export type TSecretVersionV2TagJunctionInsert = Omit<
|
||||
z.input<typeof SecretVersionV2TagJunctionSchema>,
|
||||
TImmutableDBKeys
|
||||
>;
|
||||
export type TSecretVersionV2TagJunctionUpdate = Partial<
|
||||
Omit<z.input<typeof SecretVersionV2TagJunctionSchema>, TImmutableDBKeys>
|
||||
>;
|
33
backend/src/db/schemas/secret-versions-v2.ts
Normal file
33
backend/src/db/schemas/secret-versions-v2.ts
Normal file
@ -0,0 +1,33 @@
|
||||
// Code generated by automation script, DO NOT EDIT.
|
||||
// Automated by pulling database and generating zod schema
|
||||
// To update. Just run npm run generate:schema
|
||||
// Written by akhilmhdh.
|
||||
|
||||
import { z } from "zod";
|
||||
|
||||
import { zodBuffer } from "@app/lib/zod";
|
||||
|
||||
import { TImmutableDBKeys } from "./models";
|
||||
|
||||
export const SecretVersionsV2Schema = z.object({
|
||||
id: z.string().uuid(),
|
||||
version: z.number().default(1),
|
||||
type: z.string().default("shared"),
|
||||
key: z.string(),
|
||||
encryptedValue: zodBuffer.nullable().optional(),
|
||||
encryptedComment: zodBuffer.nullable().optional(),
|
||||
reminderNote: z.string().nullable().optional(),
|
||||
reminderRepeatDays: z.number().nullable().optional(),
|
||||
skipMultilineEncoding: z.boolean().default(false).nullable().optional(),
|
||||
metadata: z.unknown().nullable().optional(),
|
||||
envId: z.string().uuid().nullable().optional(),
|
||||
secretId: z.string().uuid(),
|
||||
folderId: z.string().uuid(),
|
||||
userId: z.string().uuid().nullable().optional(),
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date()
|
||||
});
|
||||
|
||||
export type TSecretVersionsV2 = z.infer<typeof SecretVersionsV2Schema>;
|
||||
export type TSecretVersionsV2Insert = Omit<z.input<typeof SecretVersionsV2Schema>, TImmutableDBKeys>;
|
||||
export type TSecretVersionsV2Update = Partial<Omit<z.input<typeof SecretVersionsV2Schema>, TImmutableDBKeys>>;
|
31
backend/src/db/schemas/secrets-v2.ts
Normal file
31
backend/src/db/schemas/secrets-v2.ts
Normal file
@ -0,0 +1,31 @@
|
||||
// Code generated by automation script, DO NOT EDIT.
|
||||
// Automated by pulling database and generating zod schema
|
||||
// To update. Just run npm run generate:schema
|
||||
// Written by akhilmhdh.
|
||||
|
||||
import { z } from "zod";
|
||||
|
||||
import { zodBuffer } from "@app/lib/zod";
|
||||
|
||||
import { TImmutableDBKeys } from "./models";
|
||||
|
||||
export const SecretsV2Schema = z.object({
|
||||
id: z.string().uuid(),
|
||||
version: z.number().default(1),
|
||||
type: z.string().default("shared"),
|
||||
key: z.string(),
|
||||
encryptedValue: zodBuffer.nullable().optional(),
|
||||
encryptedComment: zodBuffer.nullable().optional(),
|
||||
reminderNote: z.string().nullable().optional(),
|
||||
reminderRepeatDays: z.number().nullable().optional(),
|
||||
skipMultilineEncoding: z.boolean().default(false).nullable().optional(),
|
||||
metadata: z.unknown().nullable().optional(),
|
||||
userId: z.string().uuid().nullable().optional(),
|
||||
folderId: z.string().uuid(),
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date()
|
||||
});
|
||||
|
||||
export type TSecretsV2 = z.infer<typeof SecretsV2Schema>;
|
||||
export type TSecretsV2Insert = Omit<z.input<typeof SecretsV2Schema>, TImmutableDBKeys>;
|
||||
export type TSecretsV2Update = Partial<Omit<z.input<typeof SecretsV2Schema>, TImmutableDBKeys>>;
|
@ -33,6 +33,11 @@ export const seedData1 = {
|
||||
name: "first project",
|
||||
slug: "first-project"
|
||||
},
|
||||
projectV3: {
|
||||
id: "77fa7aed-9288-401e-a4c9-3a9430be62a4",
|
||||
name: "first project v2",
|
||||
slug: "first-project-v2"
|
||||
},
|
||||
environment: {
|
||||
name: "Development",
|
||||
slug: "dev"
|
||||
|
@ -29,7 +29,8 @@ export async function seed(knex: Knex): Promise<void> {
|
||||
role: OrgMembershipRole.Admin,
|
||||
orgId: org.id,
|
||||
status: OrgMembershipStatus.Accepted,
|
||||
userId: user.id
|
||||
userId: user.id,
|
||||
isActive: true
|
||||
}
|
||||
]);
|
||||
}
|
||||
|
50
backend/src/db/seeds/4-project-v3.ts
Normal file
50
backend/src/db/seeds/4-project-v3.ts
Normal file
@ -0,0 +1,50 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { ProjectMembershipRole, ProjectVersion, TableName } from "../schemas";
|
||||
import { seedData1 } from "../seed-data";
|
||||
|
||||
export const DEFAULT_PROJECT_ENVS = [
|
||||
{ name: "Development", slug: "dev" },
|
||||
{ name: "Staging", slug: "staging" },
|
||||
{ name: "Production", slug: "prod" }
|
||||
];
|
||||
|
||||
export async function seed(knex: Knex): Promise<void> {
|
||||
const [projectV2] = await knex(TableName.Project)
|
||||
.insert({
|
||||
name: seedData1.projectV3.name,
|
||||
orgId: seedData1.organization.id,
|
||||
slug: seedData1.projectV3.slug,
|
||||
version: ProjectVersion.V3,
|
||||
// eslint-disable-next-line
|
||||
// @ts-ignore
|
||||
id: seedData1.projectV3.id
|
||||
})
|
||||
.returning("*");
|
||||
|
||||
const projectMembershipV3 = await knex(TableName.ProjectMembership)
|
||||
.insert({
|
||||
projectId: projectV2.id,
|
||||
userId: seedData1.id
|
||||
})
|
||||
.returning("*");
|
||||
await knex(TableName.ProjectUserMembershipRole).insert({
|
||||
role: ProjectMembershipRole.Admin,
|
||||
projectMembershipId: projectMembershipV3[0].id
|
||||
});
|
||||
|
||||
// create default environments and default folders
|
||||
const projectV3Envs = await knex(TableName.Environment)
|
||||
.insert(
|
||||
DEFAULT_PROJECT_ENVS.map(({ name, slug }, index) => ({
|
||||
name,
|
||||
slug,
|
||||
projectId: seedData1.projectV3.id,
|
||||
position: index + 1
|
||||
}))
|
||||
)
|
||||
.returning("*");
|
||||
await knex(TableName.SecretFolder).insert(
|
||||
projectV3Envs.map(({ id }) => ({ name: "root", envId: id, parentId: null }))
|
||||
);
|
||||
}
|
@ -86,4 +86,15 @@ export async function seed(knex: Knex): Promise<void> {
|
||||
role: ProjectMembershipRole.Admin,
|
||||
projectMembershipId: identityProjectMembership[0].id
|
||||
});
|
||||
const identityProjectMembershipV3 = await knex(TableName.IdentityProjectMembership)
|
||||
.insert({
|
||||
identityId: seedData1.machineIdentity.id,
|
||||
projectId: seedData1.projectV3.id
|
||||
})
|
||||
.returning("*");
|
||||
|
||||
await knex(TableName.IdentityProjectMembershipRole).insert({
|
||||
role: ProjectMembershipRole.Admin,
|
||||
projectMembershipId: identityProjectMembershipV3[0].id
|
||||
});
|
||||
}
|
@ -1,6 +1,7 @@
|
||||
import { nanoid } from "nanoid";
|
||||
import { z } from "zod";
|
||||
|
||||
import { EnforcementLevel } from "@app/lib/types";
|
||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||
import { sapPubSchema } from "@app/server/routes/sanitizedSchemas";
|
||||
import { AuthMode } from "@app/services/auth/auth-type";
|
||||
@ -17,7 +18,8 @@ export const registerAccessApprovalPolicyRouter = async (server: FastifyZodProvi
|
||||
secretPath: z.string().trim().default("/"),
|
||||
environment: z.string(),
|
||||
approvers: z.string().array().min(1),
|
||||
approvals: z.number().min(1).default(1)
|
||||
approvals: z.number().min(1).default(1),
|
||||
enforcementLevel: z.nativeEnum(EnforcementLevel).default(EnforcementLevel.Hard)
|
||||
})
|
||||
.refine((data) => data.approvals <= data.approvers.length, {
|
||||
path: ["approvals"],
|
||||
@ -38,7 +40,8 @@ export const registerAccessApprovalPolicyRouter = async (server: FastifyZodProvi
|
||||
actorOrgId: req.permission.orgId,
|
||||
...req.body,
|
||||
projectSlug: req.body.projectSlug,
|
||||
name: req.body.name ?? `${req.body.environment}-${nanoid(3)}`
|
||||
name: req.body.name ?? `${req.body.environment}-${nanoid(3)}`,
|
||||
enforcementLevel: req.body.enforcementLevel
|
||||
});
|
||||
return { approval };
|
||||
}
|
||||
@ -115,7 +118,8 @@ export const registerAccessApprovalPolicyRouter = async (server: FastifyZodProvi
|
||||
.optional()
|
||||
.transform((val) => (val === "" ? "/" : val)),
|
||||
approvers: z.string().array().min(1),
|
||||
approvals: z.number().min(1).default(1)
|
||||
approvals: z.number().min(1).default(1),
|
||||
enforcementLevel: z.nativeEnum(EnforcementLevel).default(EnforcementLevel.Hard)
|
||||
})
|
||||
.refine((data) => data.approvals <= data.approvers.length, {
|
||||
path: ["approvals"],
|
||||
|
@ -99,7 +99,8 @@ export const registerAccessApprovalRequestRouter = async (server: FastifyZodProv
|
||||
approvals: z.number(),
|
||||
approvers: z.string().array(),
|
||||
secretPath: z.string().nullish(),
|
||||
envId: z.string()
|
||||
envId: z.string(),
|
||||
enforcementLevel: z.string()
|
||||
}),
|
||||
reviewers: z
|
||||
.object({
|
||||
|
289
backend/src/ee/routes/v1/external-kms-router.ts
Normal file
289
backend/src/ee/routes/v1/external-kms-router.ts
Normal file
@ -0,0 +1,289 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { ExternalKmsSchema, KmsKeysSchema } from "@app/db/schemas";
|
||||
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
||||
import {
|
||||
ExternalKmsAwsSchema,
|
||||
ExternalKmsInputSchema,
|
||||
ExternalKmsInputUpdateSchema
|
||||
} from "@app/ee/services/external-kms/providers/model";
|
||||
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||
import { AuthMode } from "@app/services/auth/auth-type";
|
||||
|
||||
const sanitizedExternalSchema = KmsKeysSchema.extend({
|
||||
external: ExternalKmsSchema.pick({
|
||||
id: true,
|
||||
status: true,
|
||||
statusDetails: true,
|
||||
provider: true
|
||||
})
|
||||
});
|
||||
|
||||
const sanitizedExternalSchemaForGetAll = KmsKeysSchema.pick({
|
||||
id: true,
|
||||
description: true,
|
||||
isDisabled: true,
|
||||
createdAt: true,
|
||||
updatedAt: true,
|
||||
slug: true
|
||||
})
|
||||
.extend({
|
||||
externalKms: ExternalKmsSchema.pick({
|
||||
provider: true,
|
||||
status: true,
|
||||
statusDetails: true
|
||||
})
|
||||
})
|
||||
.array();
|
||||
|
||||
const sanitizedExternalSchemaForGetById = KmsKeysSchema.extend({
|
||||
external: ExternalKmsSchema.pick({
|
||||
id: true,
|
||||
status: true,
|
||||
statusDetails: true,
|
||||
provider: true
|
||||
}).extend({
|
||||
providerInput: ExternalKmsAwsSchema
|
||||
})
|
||||
});
|
||||
|
||||
export const registerExternalKmsRouter = async (server: FastifyZodProvider) => {
|
||||
server.route({
|
||||
method: "POST",
|
||||
url: "/",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
schema: {
|
||||
body: z.object({
|
||||
slug: z.string().min(1).trim().toLowerCase(),
|
||||
description: z.string().trim().optional(),
|
||||
provider: ExternalKmsInputSchema
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
externalKms: sanitizedExternalSchema
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const externalKms = await server.services.externalKms.create({
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId,
|
||||
slug: req.body.slug,
|
||||
provider: req.body.provider,
|
||||
description: req.body.description
|
||||
});
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
orgId: req.permission.orgId,
|
||||
event: {
|
||||
type: EventType.CREATE_KMS,
|
||||
metadata: {
|
||||
kmsId: externalKms.id,
|
||||
provider: req.body.provider.type,
|
||||
slug: req.body.slug,
|
||||
description: req.body.description
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return { externalKms };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "PATCH",
|
||||
url: "/:id",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
schema: {
|
||||
params: z.object({
|
||||
id: z.string().trim().min(1)
|
||||
}),
|
||||
body: z.object({
|
||||
slug: z.string().min(1).trim().toLowerCase().optional(),
|
||||
description: z.string().trim().optional(),
|
||||
provider: ExternalKmsInputUpdateSchema
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
externalKms: sanitizedExternalSchema
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const externalKms = await server.services.externalKms.updateById({
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId,
|
||||
slug: req.body.slug,
|
||||
provider: req.body.provider,
|
||||
description: req.body.description,
|
||||
id: req.params.id
|
||||
});
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
orgId: req.permission.orgId,
|
||||
event: {
|
||||
type: EventType.UPDATE_KMS,
|
||||
metadata: {
|
||||
kmsId: externalKms.id,
|
||||
provider: req.body.provider.type,
|
||||
slug: req.body.slug,
|
||||
description: req.body.description
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return { externalKms };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "DELETE",
|
||||
url: "/:id",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
schema: {
|
||||
params: z.object({
|
||||
id: z.string().trim().min(1)
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
externalKms: sanitizedExternalSchema
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const externalKms = await server.services.externalKms.deleteById({
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId,
|
||||
id: req.params.id
|
||||
});
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
orgId: req.permission.orgId,
|
||||
event: {
|
||||
type: EventType.DELETE_KMS,
|
||||
metadata: {
|
||||
kmsId: externalKms.id,
|
||||
slug: externalKms.slug
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return { externalKms };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: "/:id",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
params: z.object({
|
||||
id: z.string().trim().min(1)
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
externalKms: sanitizedExternalSchemaForGetById
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const externalKms = await server.services.externalKms.findById({
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId,
|
||||
id: req.params.id
|
||||
});
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
orgId: req.permission.orgId,
|
||||
event: {
|
||||
type: EventType.GET_KMS,
|
||||
metadata: {
|
||||
kmsId: externalKms.id,
|
||||
slug: externalKms.slug
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return { externalKms };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: "/",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
response: {
|
||||
200: z.object({
|
||||
externalKmsList: sanitizedExternalSchemaForGetAll
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const externalKmsList = await server.services.externalKms.list({
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId
|
||||
});
|
||||
return { externalKmsList };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: "/slug/:slug",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
params: z.object({
|
||||
slug: z.string().trim().min(1)
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
externalKms: sanitizedExternalSchemaForGetById
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const externalKms = await server.services.externalKms.findBySlug({
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId,
|
||||
slug: req.params.slug
|
||||
});
|
||||
return { externalKms };
|
||||
}
|
||||
});
|
||||
};
|
@ -4,6 +4,7 @@ import { registerAuditLogStreamRouter } from "./audit-log-stream-router";
|
||||
import { registerCaCrlRouter } from "./certificate-authority-crl-router";
|
||||
import { registerDynamicSecretLeaseRouter } from "./dynamic-secret-lease-router";
|
||||
import { registerDynamicSecretRouter } from "./dynamic-secret-router";
|
||||
import { registerExternalKmsRouter } from "./external-kms-router";
|
||||
import { registerGroupRouter } from "./group-router";
|
||||
import { registerIdentityProjectAdditionalPrivilegeRouter } from "./identity-project-additional-privilege-router";
|
||||
import { registerLdapRouter } from "./ldap-router";
|
||||
@ -87,4 +88,8 @@ export const registerV1EERoutes = async (server: FastifyZodProvider) => {
|
||||
},
|
||||
{ prefix: "/additional-privilege" }
|
||||
);
|
||||
|
||||
await server.register(registerExternalKmsRouter, {
|
||||
prefix: "/external-kms"
|
||||
});
|
||||
};
|
||||
|
@ -52,6 +52,36 @@ export const registerOrgRoleRouter = async (server: FastifyZodProvider) => {
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: "/:organizationId/roles/:roleId",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
params: z.object({
|
||||
organizationId: z.string().trim(),
|
||||
roleId: z.string().trim()
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
role: OrgRolesSchema
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
handler: async (req) => {
|
||||
const role = await server.services.orgRole.getRole(
|
||||
req.permission.id,
|
||||
req.params.organizationId,
|
||||
req.params.roleId,
|
||||
req.permission.authMethod,
|
||||
req.permission.orgId
|
||||
);
|
||||
return { role };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "PATCH",
|
||||
url: "/:organizationId/roles/:roleId",
|
||||
@ -69,7 +99,7 @@ export const registerOrgRoleRouter = async (server: FastifyZodProvider) => {
|
||||
.trim()
|
||||
.optional()
|
||||
.refine(
|
||||
(val) => typeof val === "undefined" || Object.keys(OrgMembershipRole).includes(val),
|
||||
(val) => typeof val !== "undefined" && !Object.keys(OrgMembershipRole).includes(val),
|
||||
"Please choose a different slug, the slug you have entered is reserved."
|
||||
)
|
||||
.refine((val) => typeof val === "undefined" || slugify(val) === val, {
|
||||
@ -77,7 +107,7 @@ export const registerOrgRoleRouter = async (server: FastifyZodProvider) => {
|
||||
}),
|
||||
name: z.string().trim().optional(),
|
||||
description: z.string().trim().optional(),
|
||||
permissions: z.any().array()
|
||||
permissions: z.any().array().optional()
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
|
@ -101,7 +101,7 @@ export const registerProjectRoleRouter = async (server: FastifyZodProvider) => {
|
||||
message: "Slug must be a valid"
|
||||
}),
|
||||
name: z.string().trim().optional().describe(PROJECT_ROLE.UPDATE.name),
|
||||
permissions: ProjectPermissionSchema.array().describe(PROJECT_ROLE.UPDATE.permissions)
|
||||
permissions: ProjectPermissionSchema.array().describe(PROJECT_ROLE.UPDATE.permissions).optional()
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
@ -120,7 +120,7 @@ export const registerProjectRoleRouter = async (server: FastifyZodProvider) => {
|
||||
roleId: req.params.roleId,
|
||||
data: {
|
||||
...req.body,
|
||||
permissions: JSON.stringify(packRules(req.body.permissions))
|
||||
permissions: req.body.permissions ? JSON.stringify(packRules(req.body.permissions)) : undefined
|
||||
}
|
||||
});
|
||||
return { role };
|
||||
|
@ -4,9 +4,10 @@ import { AuditLogsSchema, SecretSnapshotsSchema } from "@app/db/schemas";
|
||||
import { EventType, UserAgentType } from "@app/ee/services/audit-log/audit-log-types";
|
||||
import { AUDIT_LOGS, PROJECTS } from "@app/lib/api-docs";
|
||||
import { getLastMidnightDateISO, removeTrailingSlash } from "@app/lib/fn";
|
||||
import { readLimit } from "@app/server/config/rateLimiter";
|
||||
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";
|
||||
import { KmsType } from "@app/services/kms/kms-types";
|
||||
|
||||
export const registerProjectRouter = async (server: FastifyZodProvider) => {
|
||||
server.route({
|
||||
@ -171,4 +172,212 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
handler: async () => ({ actors: [] })
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: "/:workspaceId/kms",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
params: z.object({
|
||||
workspaceId: z.string().trim()
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
secretManagerKmsKey: z.object({
|
||||
id: z.string(),
|
||||
slug: z.string(),
|
||||
isExternal: z.boolean()
|
||||
})
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
handler: async (req) => {
|
||||
const kmsKey = await server.services.project.getProjectKmsKeys({
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId,
|
||||
projectId: req.params.workspaceId
|
||||
});
|
||||
|
||||
return kmsKey;
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "PATCH",
|
||||
url: "/:workspaceId/kms",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
schema: {
|
||||
params: z.object({
|
||||
workspaceId: z.string().trim()
|
||||
}),
|
||||
body: z.object({
|
||||
kms: z.discriminatedUnion("type", [
|
||||
z.object({ type: z.literal(KmsType.Internal) }),
|
||||
z.object({ type: z.literal(KmsType.External), kmsId: z.string() })
|
||||
])
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
secretManagerKmsKey: z.object({
|
||||
id: z.string(),
|
||||
slug: z.string(),
|
||||
isExternal: z.boolean()
|
||||
})
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
handler: async (req) => {
|
||||
const { secretManagerKmsKey } = await server.services.project.updateProjectKmsKey({
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId,
|
||||
projectId: req.params.workspaceId,
|
||||
...req.body
|
||||
});
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
projectId: req.params.workspaceId,
|
||||
event: {
|
||||
type: EventType.UPDATE_PROJECT_KMS,
|
||||
metadata: {
|
||||
secretManagerKmsKey: {
|
||||
id: secretManagerKmsKey.id,
|
||||
slug: secretManagerKmsKey.slug
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
secretManagerKmsKey
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: "/:workspaceId/kms/backup",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
params: z.object({
|
||||
workspaceId: z.string().trim()
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
secretManager: z.string()
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
handler: async (req) => {
|
||||
const backup = await server.services.project.getProjectKmsBackup({
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId,
|
||||
projectId: req.params.workspaceId
|
||||
});
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
projectId: req.params.workspaceId,
|
||||
event: {
|
||||
type: EventType.GET_PROJECT_KMS_BACKUP,
|
||||
metadata: {}
|
||||
}
|
||||
});
|
||||
|
||||
return backup;
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "POST",
|
||||
url: "/:workspaceId/kms/backup",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
schema: {
|
||||
params: z.object({
|
||||
workspaceId: z.string().trim()
|
||||
}),
|
||||
body: z.object({
|
||||
backup: z.string().min(1)
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
secretManagerKmsKey: z.object({
|
||||
id: z.string(),
|
||||
slug: z.string(),
|
||||
isExternal: z.boolean()
|
||||
})
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
handler: async (req) => {
|
||||
const backup = await server.services.project.loadProjectKmsBackup({
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId,
|
||||
projectId: req.params.workspaceId,
|
||||
backup: req.body.backup
|
||||
});
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
projectId: req.params.workspaceId,
|
||||
event: {
|
||||
type: EventType.LOAD_PROJECT_KMS_BACKUP,
|
||||
metadata: {}
|
||||
}
|
||||
});
|
||||
|
||||
return backup;
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "POST",
|
||||
url: "/:workspaceId/migrate-v3",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
schema: {
|
||||
params: z.object({
|
||||
workspaceId: z.string().trim()
|
||||
}),
|
||||
|
||||
response: {
|
||||
200: z.object({
|
||||
message: z.string()
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
handler: async (req) => {
|
||||
const migration = await server.services.secret.startSecretV2Migration({
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId,
|
||||
projectId: req.params.workspaceId
|
||||
});
|
||||
|
||||
return migration;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
@ -186,7 +186,13 @@ export const registerScimRouter = async (server: FastifyZodProvider) => {
|
||||
})
|
||||
),
|
||||
displayName: z.string().trim(),
|
||||
active: z.boolean()
|
||||
active: z.boolean(),
|
||||
groups: z.array(
|
||||
z.object({
|
||||
value: z.string().trim(),
|
||||
display: z.string().trim()
|
||||
})
|
||||
)
|
||||
})
|
||||
}
|
||||
},
|
||||
@ -344,7 +350,12 @@ export const registerScimRouter = async (server: FastifyZodProvider) => {
|
||||
schemas: z.array(z.string()),
|
||||
id: z.string().trim(),
|
||||
displayName: z.string().trim(),
|
||||
members: z.array(z.any()).length(0),
|
||||
members: z.array(
|
||||
z.object({
|
||||
value: z.string(),
|
||||
display: z.string()
|
||||
})
|
||||
),
|
||||
meta: z.object({
|
||||
resourceType: z.string().trim()
|
||||
})
|
||||
@ -417,7 +428,7 @@ export const registerScimRouter = async (server: FastifyZodProvider) => {
|
||||
displayName: z.string().trim(),
|
||||
members: z.array(
|
||||
z.object({
|
||||
value: z.string(), // infisical orgMembershipId
|
||||
value: z.string(),
|
||||
display: z.string()
|
||||
})
|
||||
)
|
||||
@ -475,10 +486,13 @@ export const registerScimRouter = async (server: FastifyZodProvider) => {
|
||||
}),
|
||||
z.object({
|
||||
op: z.literal("add"),
|
||||
value: z.object({
|
||||
value: z.string().trim(),
|
||||
display: z.string().trim().optional()
|
||||
})
|
||||
path: z.string().trim(),
|
||||
value: z.array(
|
||||
z.object({
|
||||
value: z.string().trim(),
|
||||
display: z.string().trim().optional()
|
||||
})
|
||||
)
|
||||
})
|
||||
])
|
||||
)
|
||||
@ -569,7 +583,13 @@ export const registerScimRouter = async (server: FastifyZodProvider) => {
|
||||
})
|
||||
),
|
||||
displayName: z.string().trim(),
|
||||
active: z.boolean()
|
||||
active: z.boolean(),
|
||||
groups: z.array(
|
||||
z.object({
|
||||
value: z.string().trim(),
|
||||
display: z.string().trim()
|
||||
})
|
||||
)
|
||||
})
|
||||
}
|
||||
},
|
||||
|
@ -2,6 +2,7 @@ import { nanoid } from "nanoid";
|
||||
import { z } from "zod";
|
||||
|
||||
import { removeTrailingSlash } from "@app/lib/fn";
|
||||
import { EnforcementLevel } from "@app/lib/types";
|
||||
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||
import { sapPubSchema } from "@app/server/routes/sanitizedSchemas";
|
||||
@ -24,11 +25,13 @@ export const registerSecretApprovalPolicyRouter = async (server: FastifyZodProvi
|
||||
.string()
|
||||
.optional()
|
||||
.nullable()
|
||||
.default("/")
|
||||
.transform((val) => (val ? removeTrailingSlash(val) : val)),
|
||||
approverUserIds: z.string().array().min(1),
|
||||
approvals: z.number().min(1).default(1)
|
||||
approvers: z.string().array().min(1),
|
||||
approvals: z.number().min(1).default(1),
|
||||
enforcementLevel: z.nativeEnum(EnforcementLevel).default(EnforcementLevel.Hard)
|
||||
})
|
||||
.refine((data) => data.approvals <= data.approverUserIds.length, {
|
||||
.refine((data) => data.approvals <= data.approvers.length, {
|
||||
path: ["approvals"],
|
||||
message: "The number of approvals should be lower than the number of approvers."
|
||||
}),
|
||||
@ -47,7 +50,8 @@ export const registerSecretApprovalPolicyRouter = async (server: FastifyZodProvi
|
||||
actorOrgId: req.permission.orgId,
|
||||
projectId: req.body.workspaceId,
|
||||
...req.body,
|
||||
name: req.body.name ?? `${req.body.environment}-${nanoid(3)}`
|
||||
name: req.body.name ?? `${req.body.environment}-${nanoid(3)}`,
|
||||
enforcementLevel: req.body.enforcementLevel
|
||||
});
|
||||
return { approval };
|
||||
}
|
||||
@ -66,15 +70,17 @@ export const registerSecretApprovalPolicyRouter = async (server: FastifyZodProvi
|
||||
body: z
|
||||
.object({
|
||||
name: z.string().optional(),
|
||||
approverUserIds: z.string().array().min(1),
|
||||
approvers: z.string().array().min(1),
|
||||
approvals: z.number().min(1).default(1),
|
||||
secretPath: z
|
||||
.string()
|
||||
.optional()
|
||||
.nullable()
|
||||
.transform((val) => (val ? removeTrailingSlash(val) : val))
|
||||
.transform((val) => (val === "" ? "/" : val)),
|
||||
enforcementLevel: z.nativeEnum(EnforcementLevel).optional()
|
||||
})
|
||||
.refine((data) => data.approvals <= data.approverUserIds.length, {
|
||||
.refine((data) => data.approvals <= data.approvers.length, {
|
||||
path: ["approvals"],
|
||||
message: "The number of approvals should be lower than the number of approvers."
|
||||
}),
|
||||
|
@ -3,16 +3,14 @@ import { z } from "zod";
|
||||
import {
|
||||
SecretApprovalRequestsReviewersSchema,
|
||||
SecretApprovalRequestsSchema,
|
||||
SecretApprovalRequestsSecretsSchema,
|
||||
SecretsSchema,
|
||||
SecretTagsSchema,
|
||||
SecretVersionsSchema,
|
||||
UsersSchema
|
||||
} from "@app/db/schemas";
|
||||
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
||||
import { ApprovalStatus, RequestState } from "@app/ee/services/secret-approval-request/secret-approval-request-types";
|
||||
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||
import { secretRawSchema } from "@app/server/routes/sanitizedSchemas";
|
||||
import { AuthMode } from "@app/services/auth/auth-type";
|
||||
|
||||
const approvalRequestUser = z.object({ userId: z.string() }).merge(
|
||||
@ -49,7 +47,8 @@ export const registerSecretApprovalRequestRouter = async (server: FastifyZodProv
|
||||
name: z.string(),
|
||||
approvals: z.number(),
|
||||
approvers: z.string().array(),
|
||||
secretPath: z.string().optional().nullable()
|
||||
secretPath: z.string().optional().nullable(),
|
||||
enforcementLevel: z.string()
|
||||
}),
|
||||
committerUser: approvalRequestUser,
|
||||
commits: z.object({ op: z.string(), secretId: z.string().nullable().optional() }).array(),
|
||||
@ -116,6 +115,9 @@ export const registerSecretApprovalRequestRouter = async (server: FastifyZodProv
|
||||
params: z.object({
|
||||
id: z.string()
|
||||
}),
|
||||
body: z.object({
|
||||
bypassReason: z.string().optional()
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
approval: SecretApprovalRequestsSchema
|
||||
@ -129,7 +131,8 @@ export const registerSecretApprovalRequestRouter = async (server: FastifyZodProv
|
||||
actor: req.permission.type,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId,
|
||||
approvalId: req.params.id
|
||||
approvalId: req.params.id,
|
||||
bypassReason: req.body.bypassReason
|
||||
});
|
||||
return { approval };
|
||||
}
|
||||
@ -248,53 +251,40 @@ export const registerSecretApprovalRequestRouter = async (server: FastifyZodProv
|
||||
name: z.string(),
|
||||
approvals: z.number(),
|
||||
approvers: approvalRequestUser.array(),
|
||||
secretPath: z.string().optional().nullable()
|
||||
secretPath: z.string().optional().nullable(),
|
||||
enforcementLevel: z.string()
|
||||
}),
|
||||
environment: z.string(),
|
||||
statusChangedByUser: approvalRequestUser.optional(),
|
||||
committerUser: approvalRequestUser,
|
||||
reviewers: approvalRequestUser.extend({ status: z.string() }).array(),
|
||||
secretPath: z.string(),
|
||||
commits: SecretApprovalRequestsSecretsSchema.omit({ secretBlindIndex: true })
|
||||
.merge(
|
||||
z.object({
|
||||
tags: tagSchema,
|
||||
secret: SecretsSchema.pick({
|
||||
id: true,
|
||||
version: true,
|
||||
secretKeyIV: true,
|
||||
secretKeyTag: true,
|
||||
secretKeyCiphertext: true,
|
||||
secretValueIV: true,
|
||||
secretValueTag: true,
|
||||
secretValueCiphertext: true,
|
||||
secretCommentIV: true,
|
||||
secretCommentTag: true,
|
||||
secretCommentCiphertext: true
|
||||
commits: secretRawSchema
|
||||
.omit({ _id: true, environment: true, workspace: true, type: true, version: true })
|
||||
.extend({
|
||||
op: z.string(),
|
||||
tags: tagSchema,
|
||||
secret: z
|
||||
.object({
|
||||
id: z.string(),
|
||||
version: z.number(),
|
||||
secretKey: z.string(),
|
||||
secretValue: z.string().optional(),
|
||||
secretComment: z.string().optional()
|
||||
})
|
||||
.optional()
|
||||
.nullable(),
|
||||
secretVersion: SecretVersionsSchema.pick({
|
||||
id: true,
|
||||
version: true,
|
||||
secretKeyIV: true,
|
||||
secretKeyTag: true,
|
||||
secretKeyCiphertext: true,
|
||||
secretValueIV: true,
|
||||
secretValueTag: true,
|
||||
secretValueCiphertext: true,
|
||||
secretCommentIV: true,
|
||||
secretCommentTag: true,
|
||||
secretCommentCiphertext: true
|
||||
.optional()
|
||||
.nullable(),
|
||||
secretVersion: z
|
||||
.object({
|
||||
id: z.string(),
|
||||
version: z.number(),
|
||||
secretKey: z.string(),
|
||||
secretValue: z.string().optional(),
|
||||
secretComment: z.string().optional(),
|
||||
tags: tagSchema
|
||||
})
|
||||
.merge(
|
||||
z.object({
|
||||
tags: tagSchema
|
||||
})
|
||||
)
|
||||
.optional()
|
||||
})
|
||||
)
|
||||
.optional()
|
||||
})
|
||||
.array()
|
||||
})
|
||||
)
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { SecretRotationOutputsSchema, SecretRotationsSchema, SecretsSchema } from "@app/db/schemas";
|
||||
import { SecretRotationOutputsSchema, SecretRotationsSchema } from "@app/db/schemas";
|
||||
import { removeTrailingSlash } from "@app/lib/fn";
|
||||
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||
@ -112,18 +112,10 @@ export const registerSecretRotationRouter = async (server: FastifyZodProvider) =
|
||||
outputs: z
|
||||
.object({
|
||||
key: z.string(),
|
||||
secret: SecretsSchema.pick({
|
||||
id: true,
|
||||
version: true,
|
||||
secretKeyIV: true,
|
||||
secretKeyTag: true,
|
||||
secretKeyCiphertext: true,
|
||||
secretValueIV: true,
|
||||
secretValueTag: true,
|
||||
secretValueCiphertext: true,
|
||||
secretCommentIV: true,
|
||||
secretCommentTag: true,
|
||||
secretCommentCiphertext: true
|
||||
secret: z.object({
|
||||
secretKey: z.string(),
|
||||
id: z.string(),
|
||||
version: z.number()
|
||||
})
|
||||
})
|
||||
.array()
|
||||
|
@ -1,8 +1,8 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { SecretVersionsSchema } from "@app/db/schemas";
|
||||
import { readLimit } from "@app/server/config/rateLimiter";
|
||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||
import { secretRawSchema } from "@app/server/routes/sanitizedSchemas";
|
||||
import { AuthMode } from "@app/services/auth/auth-type";
|
||||
|
||||
export const registerSecretVersionRouter = async (server: FastifyZodProvider) => {
|
||||
@ -22,7 +22,7 @@ export const registerSecretVersionRouter = async (server: FastifyZodProvider) =>
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
secretVersions: SecretVersionsSchema.omit({ secretBlindIndex: true }).array()
|
||||
secretVersions: secretRawSchema.array()
|
||||
})
|
||||
}
|
||||
},
|
||||
|
@ -1,9 +1,10 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { SecretSnapshotsSchema, SecretTagsSchema, SecretVersionsSchema } from "@app/db/schemas";
|
||||
import { SecretSnapshotsSchema, SecretTagsSchema } from "@app/db/schemas";
|
||||
import { PROJECTS } from "@app/lib/api-docs";
|
||||
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||
import { secretRawSchema } from "@app/server/routes/sanitizedSchemas";
|
||||
import { AuthMode } from "@app/services/auth/auth-type";
|
||||
|
||||
export const registerSnapshotRouter = async (server: FastifyZodProvider) => {
|
||||
@ -27,17 +28,17 @@ export const registerSnapshotRouter = async (server: FastifyZodProvider) => {
|
||||
slug: z.string(),
|
||||
name: z.string()
|
||||
}),
|
||||
secretVersions: SecretVersionsSchema.omit({ secretBlindIndex: true })
|
||||
.merge(
|
||||
z.object({
|
||||
tags: SecretTagsSchema.pick({
|
||||
id: true,
|
||||
slug: true,
|
||||
name: true,
|
||||
color: true
|
||||
}).array()
|
||||
})
|
||||
)
|
||||
secretVersions: secretRawSchema
|
||||
.omit({ _id: true, environment: true, workspace: true, type: true })
|
||||
.extend({
|
||||
secretId: z.string(),
|
||||
tags: SecretTagsSchema.pick({
|
||||
id: true,
|
||||
slug: true,
|
||||
name: true,
|
||||
color: true
|
||||
}).array()
|
||||
})
|
||||
.array(),
|
||||
folderVersion: z.object({ id: z.string(), name: z.string() }).array(),
|
||||
createdAt: z.date(),
|
||||
|
@ -47,7 +47,8 @@ export const accessApprovalPolicyServiceFactory = ({
|
||||
approvals,
|
||||
approvers,
|
||||
projectSlug,
|
||||
environment
|
||||
environment,
|
||||
enforcementLevel
|
||||
}: TCreateAccessApprovalPolicy) => {
|
||||
const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId);
|
||||
if (!project) throw new BadRequestError({ message: "Project not found" });
|
||||
@ -94,7 +95,8 @@ export const accessApprovalPolicyServiceFactory = ({
|
||||
envId: env.id,
|
||||
approvals,
|
||||
secretPath,
|
||||
name
|
||||
name,
|
||||
enforcementLevel
|
||||
},
|
||||
tx
|
||||
);
|
||||
@ -143,7 +145,8 @@ export const accessApprovalPolicyServiceFactory = ({
|
||||
actor,
|
||||
actorOrgId,
|
||||
actorAuthMethod,
|
||||
approvals
|
||||
approvals,
|
||||
enforcementLevel
|
||||
}: TUpdateAccessApprovalPolicy) => {
|
||||
const accessApprovalPolicy = await accessApprovalPolicyDAL.findById(policyId);
|
||||
if (!accessApprovalPolicy) throw new BadRequestError({ message: "Secret approval policy not found" });
|
||||
@ -163,7 +166,8 @@ export const accessApprovalPolicyServiceFactory = ({
|
||||
{
|
||||
approvals,
|
||||
secretPath,
|
||||
name
|
||||
name,
|
||||
enforcementLevel
|
||||
},
|
||||
tx
|
||||
);
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { TProjectPermission } from "@app/lib/types";
|
||||
import { EnforcementLevel, TProjectPermission } from "@app/lib/types";
|
||||
import { ActorAuthMethod } from "@app/services/auth/auth-type";
|
||||
|
||||
import { TPermissionServiceFactory } from "../permission/permission-service";
|
||||
@ -20,6 +20,7 @@ export type TCreateAccessApprovalPolicy = {
|
||||
approvers: string[];
|
||||
projectSlug: string;
|
||||
name: string;
|
||||
enforcementLevel: EnforcementLevel;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
export type TUpdateAccessApprovalPolicy = {
|
||||
@ -28,6 +29,7 @@ export type TUpdateAccessApprovalPolicy = {
|
||||
approvers?: string[];
|
||||
secretPath?: string;
|
||||
name?: string;
|
||||
enforcementLevel?: EnforcementLevel;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
export type TDeleteAccessApprovalPolicy = {
|
||||
|
@ -48,6 +48,7 @@ export const accessApprovalRequestDALFactory = (db: TDbClient) => {
|
||||
db.ref("name").withSchema(TableName.AccessApprovalPolicy).as("policyName"),
|
||||
db.ref("approvals").withSchema(TableName.AccessApprovalPolicy).as("policyApprovals"),
|
||||
db.ref("secretPath").withSchema(TableName.AccessApprovalPolicy).as("policySecretPath"),
|
||||
db.ref("enforcementLevel").withSchema(TableName.AccessApprovalPolicy).as("policyEnforcementLevel"),
|
||||
db.ref("envId").withSchema(TableName.AccessApprovalPolicy).as("policyEnvId")
|
||||
)
|
||||
|
||||
@ -98,6 +99,7 @@ export const accessApprovalRequestDALFactory = (db: TDbClient) => {
|
||||
name: doc.policyName,
|
||||
approvals: doc.policyApprovals,
|
||||
secretPath: doc.policySecretPath,
|
||||
enforcementLevel: doc.policyEnforcementLevel,
|
||||
envId: doc.policyEnvId
|
||||
},
|
||||
privilege: doc.privilegeId
|
||||
@ -165,6 +167,7 @@ export const accessApprovalRequestDALFactory = (db: TDbClient) => {
|
||||
tx.ref("projectId").withSchema(TableName.Environment),
|
||||
tx.ref("slug").withSchema(TableName.Environment).as("environment"),
|
||||
tx.ref("secretPath").withSchema(TableName.AccessApprovalPolicy).as("policySecretPath"),
|
||||
tx.ref("enforcementLevel").withSchema(TableName.AccessApprovalPolicy).as("policyEnforcementLevel"),
|
||||
tx.ref("approvals").withSchema(TableName.AccessApprovalPolicy).as("policyApprovals"),
|
||||
tx.ref("approverId").withSchema(TableName.AccessApprovalPolicyApprover)
|
||||
);
|
||||
@ -184,7 +187,8 @@ export const accessApprovalRequestDALFactory = (db: TDbClient) => {
|
||||
id: el.policyId,
|
||||
name: el.policyName,
|
||||
approvals: el.policyApprovals,
|
||||
secretPath: el.policySecretPath
|
||||
secretPath: el.policySecretPath,
|
||||
enforcementLevel: el.policyEnforcementLevel
|
||||
}
|
||||
}),
|
||||
childrenMapper: [
|
||||
|
@ -45,6 +45,7 @@ export enum EventType {
|
||||
CREATE_SECRETS = "create-secrets",
|
||||
UPDATE_SECRET = "update-secret",
|
||||
UPDATE_SECRETS = "update-secrets",
|
||||
MOVE_SECRETS = "move-secrets",
|
||||
DELETE_SECRET = "delete-secret",
|
||||
DELETE_SECRETS = "delete-secrets",
|
||||
GET_WORKSPACE_KEY = "get-workspace-key",
|
||||
@ -78,6 +79,11 @@ export enum EventType {
|
||||
UPDATE_IDENTITY_KUBENETES_AUTH = "update-identity-kubernetes-auth",
|
||||
GET_IDENTITY_KUBERNETES_AUTH = "get-identity-kubernetes-auth",
|
||||
REVOKE_IDENTITY_KUBERNETES_AUTH = "revoke-identity-kubernetes-auth",
|
||||
LOGIN_IDENTITY_OIDC_AUTH = "login-identity-oidc-auth",
|
||||
ADD_IDENTITY_OIDC_AUTH = "add-identity-oidc-auth",
|
||||
UPDATE_IDENTITY_OIDC_AUTH = "update-identity-oidc-auth",
|
||||
GET_IDENTITY_OIDC_AUTH = "get-identity-oidc-auth",
|
||||
REVOKE_IDENTITY_OIDC_AUTH = "revoke-identity-oidc-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",
|
||||
@ -100,6 +106,7 @@ export enum EventType {
|
||||
CREATE_ENVIRONMENT = "create-environment",
|
||||
UPDATE_ENVIRONMENT = "update-environment",
|
||||
DELETE_ENVIRONMENT = "delete-environment",
|
||||
GET_ENVIRONMENT = "get-environment",
|
||||
ADD_WORKSPACE_MEMBER = "add-workspace-member",
|
||||
ADD_BATCH_WORKSPACE_MEMBER = "add-workspace-members",
|
||||
REMOVE_WORKSPACE_MEMBER = "remove-workspace-member",
|
||||
@ -123,16 +130,27 @@ export enum EventType {
|
||||
GET_CA = "get-certificate-authority",
|
||||
UPDATE_CA = "update-certificate-authority",
|
||||
DELETE_CA = "delete-certificate-authority",
|
||||
RENEW_CA = "renew-certificate-authority",
|
||||
GET_CA_CSR = "get-certificate-authority-csr",
|
||||
GET_CA_CERTS = "get-certificate-authority-certs",
|
||||
GET_CA_CERT = "get-certificate-authority-cert",
|
||||
SIGN_INTERMEDIATE = "sign-intermediate",
|
||||
IMPORT_CA_CERT = "import-certificate-authority-cert",
|
||||
GET_CA_CRL = "get-certificate-authority-crl",
|
||||
ISSUE_CERT = "issue-cert",
|
||||
SIGN_CERT = "sign-cert",
|
||||
GET_CERT = "get-cert",
|
||||
DELETE_CERT = "delete-cert",
|
||||
REVOKE_CERT = "revoke-cert",
|
||||
GET_CERT_BODY = "get-cert-body"
|
||||
GET_CERT_BODY = "get-cert-body",
|
||||
CREATE_KMS = "create-kms",
|
||||
UPDATE_KMS = "update-kms",
|
||||
DELETE_KMS = "delete-kms",
|
||||
GET_KMS = "get-kms",
|
||||
UPDATE_PROJECT_KMS = "update-project-kms",
|
||||
GET_PROJECT_KMS_BACKUP = "get-project-kms-backup",
|
||||
LOAD_PROJECT_KMS_BACKUP = "load-project-kms-backup",
|
||||
ORG_ADMIN_ACCESS_PROJECT = "org-admin-accessed-project"
|
||||
}
|
||||
|
||||
interface UserActorMetadata {
|
||||
@ -235,6 +253,17 @@ interface UpdateSecretBatchEvent {
|
||||
};
|
||||
}
|
||||
|
||||
interface MoveSecretsEvent {
|
||||
type: EventType.MOVE_SECRETS;
|
||||
metadata: {
|
||||
sourceEnvironment: string;
|
||||
sourceSecretPath: string;
|
||||
destinationEnvironment: string;
|
||||
destinationSecretPath: string;
|
||||
secretIds: string[];
|
||||
};
|
||||
}
|
||||
|
||||
interface DeleteSecretEvent {
|
||||
type: EventType.DELETE_SECRET;
|
||||
metadata: {
|
||||
@ -749,6 +778,63 @@ interface GetIdentityAzureAuthEvent {
|
||||
};
|
||||
}
|
||||
|
||||
interface LoginIdentityOidcAuthEvent {
|
||||
type: EventType.LOGIN_IDENTITY_OIDC_AUTH;
|
||||
metadata: {
|
||||
identityId: string;
|
||||
identityOidcAuthId: string;
|
||||
identityAccessTokenId: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface AddIdentityOidcAuthEvent {
|
||||
type: EventType.ADD_IDENTITY_OIDC_AUTH;
|
||||
metadata: {
|
||||
identityId: string;
|
||||
oidcDiscoveryUrl: string;
|
||||
caCert: string;
|
||||
boundIssuer: string;
|
||||
boundAudiences: string;
|
||||
boundClaims: Record<string, string>;
|
||||
boundSubject: string;
|
||||
accessTokenTTL: number;
|
||||
accessTokenMaxTTL: number;
|
||||
accessTokenNumUsesLimit: number;
|
||||
accessTokenTrustedIps: Array<TIdentityTrustedIp>;
|
||||
};
|
||||
}
|
||||
|
||||
interface DeleteIdentityOidcAuthEvent {
|
||||
type: EventType.REVOKE_IDENTITY_OIDC_AUTH;
|
||||
metadata: {
|
||||
identityId: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface UpdateIdentityOidcAuthEvent {
|
||||
type: EventType.UPDATE_IDENTITY_OIDC_AUTH;
|
||||
metadata: {
|
||||
identityId: string;
|
||||
oidcDiscoveryUrl?: string;
|
||||
caCert?: string;
|
||||
boundIssuer?: string;
|
||||
boundAudiences?: string;
|
||||
boundClaims?: Record<string, string>;
|
||||
boundSubject?: string;
|
||||
accessTokenTTL?: number;
|
||||
accessTokenMaxTTL?: number;
|
||||
accessTokenNumUsesLimit?: number;
|
||||
accessTokenTrustedIps?: Array<TIdentityTrustedIp>;
|
||||
};
|
||||
}
|
||||
|
||||
interface GetIdentityOidcAuthEvent {
|
||||
type: EventType.GET_IDENTITY_OIDC_AUTH;
|
||||
metadata: {
|
||||
identityId: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface CreateEnvironmentEvent {
|
||||
type: EventType.CREATE_ENVIRONMENT;
|
||||
metadata: {
|
||||
@ -757,6 +843,13 @@ interface CreateEnvironmentEvent {
|
||||
};
|
||||
}
|
||||
|
||||
interface GetEnvironmentEvent {
|
||||
type: EventType.GET_ENVIRONMENT;
|
||||
metadata: {
|
||||
id: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface UpdateEnvironmentEvent {
|
||||
type: EventType.UPDATE_ENVIRONMENT;
|
||||
metadata: {
|
||||
@ -1004,6 +1097,14 @@ interface DeleteCa {
|
||||
};
|
||||
}
|
||||
|
||||
interface RenewCa {
|
||||
type: EventType.RENEW_CA;
|
||||
metadata: {
|
||||
caId: string;
|
||||
dn: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface GetCaCsr {
|
||||
type: EventType.GET_CA_CSR;
|
||||
metadata: {
|
||||
@ -1012,6 +1113,14 @@ interface GetCaCsr {
|
||||
};
|
||||
}
|
||||
|
||||
interface GetCaCerts {
|
||||
type: EventType.GET_CA_CERTS;
|
||||
metadata: {
|
||||
caId: string;
|
||||
dn: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface GetCaCert {
|
||||
type: EventType.GET_CA_CERT;
|
||||
metadata: {
|
||||
@ -1054,6 +1163,15 @@ interface IssueCert {
|
||||
};
|
||||
}
|
||||
|
||||
interface SignCert {
|
||||
type: EventType.SIGN_CERT;
|
||||
metadata: {
|
||||
caId: string;
|
||||
dn: string;
|
||||
serialNumber: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface GetCert {
|
||||
type: EventType.GET_CERT;
|
||||
metadata: {
|
||||
@ -1090,6 +1208,72 @@ interface GetCertBody {
|
||||
};
|
||||
}
|
||||
|
||||
interface CreateKmsEvent {
|
||||
type: EventType.CREATE_KMS;
|
||||
metadata: {
|
||||
kmsId: string;
|
||||
provider: string;
|
||||
slug: string;
|
||||
description?: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface DeleteKmsEvent {
|
||||
type: EventType.DELETE_KMS;
|
||||
metadata: {
|
||||
kmsId: string;
|
||||
slug: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface UpdateKmsEvent {
|
||||
type: EventType.UPDATE_KMS;
|
||||
metadata: {
|
||||
kmsId: string;
|
||||
provider: string;
|
||||
slug?: string;
|
||||
description?: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface GetKmsEvent {
|
||||
type: EventType.GET_KMS;
|
||||
metadata: {
|
||||
kmsId: string;
|
||||
slug: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface UpdateProjectKmsEvent {
|
||||
type: EventType.UPDATE_PROJECT_KMS;
|
||||
metadata: {
|
||||
secretManagerKmsKey: {
|
||||
id: string;
|
||||
slug: string;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
interface GetProjectKmsBackupEvent {
|
||||
type: EventType.GET_PROJECT_KMS_BACKUP;
|
||||
metadata: Record<string, string>; // no metadata yet
|
||||
}
|
||||
|
||||
interface LoadProjectKmsBackupEvent {
|
||||
type: EventType.LOAD_PROJECT_KMS_BACKUP;
|
||||
metadata: Record<string, string>; // no metadata yet
|
||||
}
|
||||
|
||||
interface OrgAdminAccessProjectEvent {
|
||||
type: EventType.ORG_ADMIN_ACCESS_PROJECT;
|
||||
metadata: {
|
||||
userId: string;
|
||||
username: string;
|
||||
email: string;
|
||||
projectId: string;
|
||||
}; // no metadata yet
|
||||
}
|
||||
|
||||
export type Event =
|
||||
| GetSecretsEvent
|
||||
| GetSecretEvent
|
||||
@ -1097,6 +1281,7 @@ export type Event =
|
||||
| CreateSecretBatchEvent
|
||||
| UpdateSecretEvent
|
||||
| UpdateSecretBatchEvent
|
||||
| MoveSecretsEvent
|
||||
| DeleteSecretEvent
|
||||
| DeleteSecretBatchEvent
|
||||
| GetWorkspaceKeyEvent
|
||||
@ -1149,7 +1334,13 @@ export type Event =
|
||||
| DeleteIdentityAzureAuthEvent
|
||||
| UpdateIdentityAzureAuthEvent
|
||||
| GetIdentityAzureAuthEvent
|
||||
| LoginIdentityOidcAuthEvent
|
||||
| AddIdentityOidcAuthEvent
|
||||
| DeleteIdentityOidcAuthEvent
|
||||
| UpdateIdentityOidcAuthEvent
|
||||
| GetIdentityOidcAuthEvent
|
||||
| CreateEnvironmentEvent
|
||||
| GetEnvironmentEvent
|
||||
| UpdateEnvironmentEvent
|
||||
| DeleteEnvironmentEvent
|
||||
| AddWorkspaceMemberEvent
|
||||
@ -1175,13 +1366,24 @@ export type Event =
|
||||
| GetCa
|
||||
| UpdateCa
|
||||
| DeleteCa
|
||||
| RenewCa
|
||||
| GetCaCsr
|
||||
| GetCaCerts
|
||||
| GetCaCert
|
||||
| SignIntermediate
|
||||
| ImportCaCert
|
||||
| GetCaCrl
|
||||
| IssueCert
|
||||
| SignCert
|
||||
| GetCert
|
||||
| DeleteCert
|
||||
| RevokeCert
|
||||
| GetCertBody;
|
||||
| GetCertBody
|
||||
| CreateKmsEvent
|
||||
| UpdateKmsEvent
|
||||
| DeleteKmsEvent
|
||||
| GetKmsEvent
|
||||
| UpdateProjectKmsEvent
|
||||
| GetProjectKmsBackupEvent
|
||||
| LoadProjectKmsBackupEvent
|
||||
| OrgAdminAccessProjectEvent;
|
||||
|
@ -17,7 +17,7 @@ type TCertificateAuthorityCrlServiceFactoryDep = {
|
||||
certificateAuthorityDAL: Pick<TCertificateAuthorityDALFactory, "findById">;
|
||||
certificateAuthorityCrlDAL: Pick<TCertificateAuthorityCrlDALFactory, "findOne">;
|
||||
projectDAL: Pick<TProjectDALFactory, "findOne" | "updateById" | "transaction">;
|
||||
kmsService: Pick<TKmsServiceFactory, "decrypt" | "generateKmsKey">;
|
||||
kmsService: Pick<TKmsServiceFactory, "decryptWithKmsKey" | "generateKmsKey">;
|
||||
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
|
||||
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
|
||||
};
|
||||
@ -68,11 +68,11 @@ export const certificateAuthorityCrlServiceFactory = ({
|
||||
kmsService
|
||||
});
|
||||
|
||||
const decryptedCrl = await kmsService.decrypt({
|
||||
kmsId: keyId,
|
||||
cipherTextBlob: caCrl.encryptedCrl
|
||||
const kmsDecryptor = await kmsService.decryptWithKmsKey({
|
||||
kmsId: keyId
|
||||
});
|
||||
|
||||
const decryptedCrl = await kmsDecryptor({ cipherTextBlob: caCrl.encryptedCrl });
|
||||
const crl = new x509.X509Crl(decryptedCrl);
|
||||
|
||||
const base64crl = crl.toString("base64");
|
||||
|
@ -12,10 +12,7 @@ export const dynamicSecretLeaseDALFactory = (db: TDbClient) => {
|
||||
|
||||
const countLeasesForDynamicSecret = async (dynamicSecretId: string, tx?: Knex) => {
|
||||
try {
|
||||
const doc = await (tx || db.replicaNode())(TableName.DynamicSecretLease)
|
||||
.count("*")
|
||||
.where({ dynamicSecretId })
|
||||
.first();
|
||||
const doc = await (tx || db)(TableName.DynamicSecretLease).count("*").where({ dynamicSecretId }).first();
|
||||
return parseInt(doc || "0", 10);
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: "DynamicSecretCountLeases" });
|
||||
@ -24,7 +21,7 @@ export const dynamicSecretLeaseDALFactory = (db: TDbClient) => {
|
||||
|
||||
const findById = async (id: string, tx?: Knex) => {
|
||||
try {
|
||||
const doc = await (tx || db.replicaNode())(TableName.DynamicSecretLease)
|
||||
const doc = await (tx || db)(TableName.DynamicSecretLease)
|
||||
.where({ [`${TableName.DynamicSecretLease}.id` as "id"]: id })
|
||||
.first()
|
||||
.join(
|
||||
|
49
backend/src/ee/services/external-kms/external-kms-dal.ts
Normal file
49
backend/src/ee/services/external-kms/external-kms-dal.ts
Normal file
@ -0,0 +1,49 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TDbClient } from "@app/db";
|
||||
import { TableName, TKmsKeys } from "@app/db/schemas";
|
||||
import { DatabaseError } from "@app/lib/errors";
|
||||
import { ormify, selectAllTableCols } from "@app/lib/knex";
|
||||
|
||||
export type TExternalKmsDALFactory = ReturnType<typeof externalKmsDALFactory>;
|
||||
|
||||
export const externalKmsDALFactory = (db: TDbClient) => {
|
||||
const externalKmsOrm = ormify(db, TableName.ExternalKms);
|
||||
|
||||
const find = async (filter: Partial<TKmsKeys>, tx?: Knex) => {
|
||||
try {
|
||||
const result = await (tx || db.replicaNode())(TableName.ExternalKms)
|
||||
.join(TableName.KmsKey, `${TableName.KmsKey}.id`, `${TableName.ExternalKms}.kmsKeyId`)
|
||||
.where(filter)
|
||||
.select(selectAllTableCols(TableName.KmsKey))
|
||||
.select(
|
||||
db.ref("id").withSchema(TableName.ExternalKms).as("externalKmsId"),
|
||||
db.ref("provider").withSchema(TableName.ExternalKms).as("externalKmsProvider"),
|
||||
db.ref("encryptedProviderInputs").withSchema(TableName.ExternalKms).as("externalKmsEncryptedProviderInput"),
|
||||
db.ref("status").withSchema(TableName.ExternalKms).as("externalKmsStatus"),
|
||||
db.ref("statusDetails").withSchema(TableName.ExternalKms).as("externalKmsStatusDetails")
|
||||
);
|
||||
|
||||
return result.map((el) => ({
|
||||
id: el.id,
|
||||
description: el.description,
|
||||
isDisabled: el.isDisabled,
|
||||
isReserved: el.isReserved,
|
||||
orgId: el.orgId,
|
||||
slug: el.slug,
|
||||
createdAt: el.createdAt,
|
||||
updatedAt: el.updatedAt,
|
||||
externalKms: {
|
||||
id: el.externalKmsId,
|
||||
provider: el.externalKmsProvider,
|
||||
status: el.externalKmsStatus,
|
||||
statusDetails: el.externalKmsStatusDetails
|
||||
}
|
||||
}));
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: "Find" });
|
||||
}
|
||||
};
|
||||
|
||||
return { ...externalKmsOrm, find };
|
||||
};
|
332
backend/src/ee/services/external-kms/external-kms-service.ts
Normal file
332
backend/src/ee/services/external-kms/external-kms-service.ts
Normal file
@ -0,0 +1,332 @@
|
||||
import { ForbiddenError } from "@casl/ability";
|
||||
import slugify from "@sindresorhus/slugify";
|
||||
|
||||
import { BadRequestError } from "@app/lib/errors";
|
||||
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
||||
import { TKmsKeyDALFactory } from "@app/services/kms/kms-key-dal";
|
||||
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
||||
import { KmsDataKey } from "@app/services/kms/kms-types";
|
||||
|
||||
import { TLicenseServiceFactory } from "../license/license-service";
|
||||
import { OrgPermissionActions, OrgPermissionSubjects } from "../permission/org-permission";
|
||||
import { TPermissionServiceFactory } from "../permission/permission-service";
|
||||
import { TExternalKmsDALFactory } from "./external-kms-dal";
|
||||
import {
|
||||
TCreateExternalKmsDTO,
|
||||
TDeleteExternalKmsDTO,
|
||||
TGetExternalKmsByIdDTO,
|
||||
TGetExternalKmsBySlugDTO,
|
||||
TListExternalKmsDTO,
|
||||
TUpdateExternalKmsDTO
|
||||
} from "./external-kms-types";
|
||||
import { AwsKmsProviderFactory } from "./providers/aws-kms";
|
||||
import { ExternalKmsAwsSchema, KmsProviders } from "./providers/model";
|
||||
|
||||
type TExternalKmsServiceFactoryDep = {
|
||||
externalKmsDAL: TExternalKmsDALFactory;
|
||||
kmsService: Pick<TKmsServiceFactory, "getOrgKmsKeyId" | "createCipherPairWithDataKey">;
|
||||
kmsDAL: Pick<TKmsKeyDALFactory, "create" | "updateById" | "findById" | "deleteById" | "findOne">;
|
||||
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">;
|
||||
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
|
||||
};
|
||||
|
||||
export type TExternalKmsServiceFactory = ReturnType<typeof externalKmsServiceFactory>;
|
||||
|
||||
export const externalKmsServiceFactory = ({
|
||||
externalKmsDAL,
|
||||
permissionService,
|
||||
licenseService,
|
||||
kmsService,
|
||||
kmsDAL
|
||||
}: TExternalKmsServiceFactoryDep) => {
|
||||
const create = async ({
|
||||
provider,
|
||||
description,
|
||||
actor,
|
||||
slug,
|
||||
actorId,
|
||||
actorOrgId,
|
||||
actorAuthMethod
|
||||
}: TCreateExternalKmsDTO) => {
|
||||
const { permission } = await permissionService.getOrgPermission(
|
||||
actor,
|
||||
actorId,
|
||||
actorOrgId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Kms);
|
||||
const plan = await licenseService.getPlan(actorOrgId);
|
||||
if (!plan.externalKms) {
|
||||
throw new BadRequestError({
|
||||
message: "Failed to create external KMS due to plan restriction. Upgrade to the Enterprise plan."
|
||||
});
|
||||
}
|
||||
|
||||
const kmsSlug = slug ? slugify(slug) : slugify(alphaNumericNanoId(8).toLowerCase());
|
||||
|
||||
let sanitizedProviderInput = "";
|
||||
switch (provider.type) {
|
||||
case KmsProviders.Aws:
|
||||
{
|
||||
const externalKms = await AwsKmsProviderFactory({ inputs: provider.inputs });
|
||||
// if missing kms key this generate a new kms key id and returns new provider input
|
||||
const newProviderInput = await externalKms.generateInputKmsKey();
|
||||
sanitizedProviderInput = JSON.stringify(newProviderInput);
|
||||
|
||||
await externalKms.validateConnection();
|
||||
}
|
||||
break;
|
||||
default:
|
||||
throw new BadRequestError({ message: "external kms provided is invalid" });
|
||||
}
|
||||
|
||||
const { encryptor: orgDataKeyEncryptor } = await kmsService.createCipherPairWithDataKey({
|
||||
type: KmsDataKey.Organization,
|
||||
orgId: actorOrgId
|
||||
});
|
||||
|
||||
const { cipherTextBlob: encryptedProviderInputs } = orgDataKeyEncryptor({
|
||||
plainText: Buffer.from(sanitizedProviderInput, "utf8")
|
||||
});
|
||||
|
||||
const externalKms = await externalKmsDAL.transaction(async (tx) => {
|
||||
const kms = await kmsDAL.create(
|
||||
{
|
||||
isReserved: false,
|
||||
description,
|
||||
slug: kmsSlug,
|
||||
orgId: actorOrgId
|
||||
},
|
||||
tx
|
||||
);
|
||||
const externalKmsCfg = await externalKmsDAL.create(
|
||||
{
|
||||
provider: provider.type,
|
||||
encryptedProviderInputs,
|
||||
kmsKeyId: kms.id
|
||||
},
|
||||
tx
|
||||
);
|
||||
return { ...kms, external: externalKmsCfg };
|
||||
});
|
||||
|
||||
return externalKms;
|
||||
};
|
||||
|
||||
const updateById = async ({
|
||||
provider,
|
||||
description,
|
||||
actor,
|
||||
id: kmsId,
|
||||
slug,
|
||||
actorId,
|
||||
actorOrgId,
|
||||
actorAuthMethod
|
||||
}: TUpdateExternalKmsDTO) => {
|
||||
const kmsDoc = await kmsDAL.findById(kmsId);
|
||||
const { permission } = await permissionService.getOrgPermission(
|
||||
actor,
|
||||
actorId,
|
||||
kmsDoc.orgId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Kms);
|
||||
|
||||
const plan = await licenseService.getPlan(kmsDoc.orgId);
|
||||
if (!plan.externalKms) {
|
||||
throw new BadRequestError({
|
||||
message: "Failed to update external KMS due to plan restriction. Upgrade to the Enterprise plan."
|
||||
});
|
||||
}
|
||||
|
||||
const kmsSlug = slug ? slugify(slug) : undefined;
|
||||
|
||||
const externalKmsDoc = await externalKmsDAL.findOne({ kmsKeyId: kmsDoc.id });
|
||||
if (!externalKmsDoc) throw new BadRequestError({ message: "External kms not found" });
|
||||
|
||||
let sanitizedProviderInput = "";
|
||||
const { encryptor: orgDataKeyEncryptor, decryptor: orgDataKeyDecryptor } =
|
||||
await kmsService.createCipherPairWithDataKey({
|
||||
type: KmsDataKey.Organization,
|
||||
orgId: actorOrgId
|
||||
});
|
||||
if (provider) {
|
||||
const decryptedProviderInputBlob = orgDataKeyDecryptor({
|
||||
cipherTextBlob: externalKmsDoc.encryptedProviderInputs
|
||||
});
|
||||
|
||||
switch (provider.type) {
|
||||
case KmsProviders.Aws:
|
||||
{
|
||||
const decryptedProviderInput = await ExternalKmsAwsSchema.parseAsync(
|
||||
JSON.parse(decryptedProviderInputBlob.toString("utf8"))
|
||||
);
|
||||
const updatedProviderInput = { ...decryptedProviderInput, ...provider.inputs };
|
||||
const externalKms = await AwsKmsProviderFactory({ inputs: updatedProviderInput });
|
||||
await externalKms.validateConnection();
|
||||
sanitizedProviderInput = JSON.stringify(updatedProviderInput);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
throw new BadRequestError({ message: "external kms provided is invalid" });
|
||||
}
|
||||
}
|
||||
|
||||
let encryptedProviderInputs: Buffer | undefined;
|
||||
if (sanitizedProviderInput) {
|
||||
const { cipherTextBlob } = orgDataKeyEncryptor({
|
||||
plainText: Buffer.from(sanitizedProviderInput, "utf8")
|
||||
});
|
||||
encryptedProviderInputs = cipherTextBlob;
|
||||
}
|
||||
|
||||
const externalKms = await externalKmsDAL.transaction(async (tx) => {
|
||||
const kms = await kmsDAL.updateById(
|
||||
kmsDoc.id,
|
||||
{
|
||||
description,
|
||||
slug: kmsSlug
|
||||
},
|
||||
tx
|
||||
);
|
||||
if (encryptedProviderInputs) {
|
||||
const externalKmsCfg = await externalKmsDAL.updateById(
|
||||
externalKmsDoc.id,
|
||||
{
|
||||
encryptedProviderInputs
|
||||
},
|
||||
tx
|
||||
);
|
||||
return { ...kms, external: externalKmsCfg };
|
||||
}
|
||||
return { ...kms, external: externalKmsDoc };
|
||||
});
|
||||
|
||||
return externalKms;
|
||||
};
|
||||
|
||||
const deleteById = async ({ actor, id: kmsId, actorId, actorOrgId, actorAuthMethod }: TDeleteExternalKmsDTO) => {
|
||||
const kmsDoc = await kmsDAL.findById(kmsId);
|
||||
const { permission } = await permissionService.getOrgPermission(
|
||||
actor,
|
||||
actorId,
|
||||
kmsDoc.orgId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Delete, OrgPermissionSubjects.Kms);
|
||||
|
||||
const externalKmsDoc = await externalKmsDAL.findOne({ kmsKeyId: kmsDoc.id });
|
||||
if (!externalKmsDoc) throw new BadRequestError({ message: "External kms not found" });
|
||||
|
||||
const externalKms = await externalKmsDAL.transaction(async (tx) => {
|
||||
const kms = await kmsDAL.deleteById(kmsDoc.id, tx);
|
||||
return { ...kms, external: externalKmsDoc };
|
||||
});
|
||||
|
||||
return externalKms;
|
||||
};
|
||||
|
||||
const list = async ({ actor, actorId, actorOrgId, actorAuthMethod }: TListExternalKmsDTO) => {
|
||||
const { permission } = await permissionService.getOrgPermission(
|
||||
actor,
|
||||
actorId,
|
||||
actorOrgId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Kms);
|
||||
|
||||
const externalKmsDocs = await externalKmsDAL.find({ orgId: actorOrgId });
|
||||
|
||||
return externalKmsDocs;
|
||||
};
|
||||
|
||||
const findById = async ({ actor, actorId, actorOrgId, actorAuthMethod, id: kmsId }: TGetExternalKmsByIdDTO) => {
|
||||
const kmsDoc = await kmsDAL.findById(kmsId);
|
||||
const { permission } = await permissionService.getOrgPermission(
|
||||
actor,
|
||||
actorId,
|
||||
kmsDoc.orgId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Kms);
|
||||
|
||||
const externalKmsDoc = await externalKmsDAL.findOne({ kmsKeyId: kmsDoc.id });
|
||||
if (!externalKmsDoc) throw new BadRequestError({ message: "External kms not found" });
|
||||
|
||||
const { decryptor: orgDataKeyDecryptor } = await kmsService.createCipherPairWithDataKey({
|
||||
type: KmsDataKey.Organization,
|
||||
orgId: actorOrgId
|
||||
});
|
||||
|
||||
const decryptedProviderInputBlob = orgDataKeyDecryptor({
|
||||
cipherTextBlob: externalKmsDoc.encryptedProviderInputs
|
||||
});
|
||||
switch (externalKmsDoc.provider) {
|
||||
case KmsProviders.Aws: {
|
||||
const decryptedProviderInput = await ExternalKmsAwsSchema.parseAsync(
|
||||
JSON.parse(decryptedProviderInputBlob.toString("utf8"))
|
||||
);
|
||||
return { ...kmsDoc, external: { ...externalKmsDoc, providerInput: decryptedProviderInput } };
|
||||
}
|
||||
default:
|
||||
throw new BadRequestError({ message: "external kms provided is invalid" });
|
||||
}
|
||||
};
|
||||
|
||||
const findBySlug = async ({
|
||||
actor,
|
||||
actorId,
|
||||
actorOrgId,
|
||||
actorAuthMethod,
|
||||
slug: kmsSlug
|
||||
}: TGetExternalKmsBySlugDTO) => {
|
||||
const kmsDoc = await kmsDAL.findOne({ slug: kmsSlug, orgId: actorOrgId });
|
||||
const { permission } = await permissionService.getOrgPermission(
|
||||
actor,
|
||||
actorId,
|
||||
kmsDoc.orgId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Kms);
|
||||
|
||||
const externalKmsDoc = await externalKmsDAL.findOne({ kmsKeyId: kmsDoc.id });
|
||||
if (!externalKmsDoc) throw new BadRequestError({ message: "External kms not found" });
|
||||
|
||||
const { decryptor: orgDataKeyDecryptor } = await kmsService.createCipherPairWithDataKey({
|
||||
type: KmsDataKey.Organization,
|
||||
orgId: actorOrgId
|
||||
});
|
||||
|
||||
const decryptedProviderInputBlob = orgDataKeyDecryptor({
|
||||
cipherTextBlob: externalKmsDoc.encryptedProviderInputs
|
||||
});
|
||||
|
||||
switch (externalKmsDoc.provider) {
|
||||
case KmsProviders.Aws: {
|
||||
const decryptedProviderInput = await ExternalKmsAwsSchema.parseAsync(
|
||||
JSON.parse(decryptedProviderInputBlob.toString("utf8"))
|
||||
);
|
||||
return { ...kmsDoc, external: { ...externalKmsDoc, providerInput: decryptedProviderInput } };
|
||||
}
|
||||
default:
|
||||
throw new BadRequestError({ message: "external kms provided is invalid" });
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
create,
|
||||
updateById,
|
||||
deleteById,
|
||||
list,
|
||||
findById,
|
||||
findBySlug
|
||||
};
|
||||
};
|
30
backend/src/ee/services/external-kms/external-kms-types.ts
Normal file
30
backend/src/ee/services/external-kms/external-kms-types.ts
Normal file
@ -0,0 +1,30 @@
|
||||
import { TOrgPermission } from "@app/lib/types";
|
||||
|
||||
import { TExternalKmsInputSchema, TExternalKmsInputUpdateSchema } from "./providers/model";
|
||||
|
||||
export type TCreateExternalKmsDTO = {
|
||||
slug?: string;
|
||||
description?: string;
|
||||
provider: TExternalKmsInputSchema;
|
||||
} & Omit<TOrgPermission, "orgId">;
|
||||
|
||||
export type TUpdateExternalKmsDTO = {
|
||||
id: string;
|
||||
slug?: string;
|
||||
description?: string;
|
||||
provider?: TExternalKmsInputUpdateSchema;
|
||||
} & Omit<TOrgPermission, "orgId">;
|
||||
|
||||
export type TDeleteExternalKmsDTO = {
|
||||
id: string;
|
||||
} & Omit<TOrgPermission, "orgId">;
|
||||
|
||||
export type TListExternalKmsDTO = Omit<TOrgPermission, "orgId">;
|
||||
|
||||
export type TGetExternalKmsByIdDTO = {
|
||||
id: string;
|
||||
} & Omit<TOrgPermission, "orgId">;
|
||||
|
||||
export type TGetExternalKmsBySlugDTO = {
|
||||
slug: string;
|
||||
} & Omit<TOrgPermission, "orgId">;
|
111
backend/src/ee/services/external-kms/providers/aws-kms.ts
Normal file
111
backend/src/ee/services/external-kms/providers/aws-kms.ts
Normal file
@ -0,0 +1,111 @@
|
||||
import { CreateKeyCommand, DecryptCommand, DescribeKeyCommand, EncryptCommand, KMSClient } from "@aws-sdk/client-kms";
|
||||
import { AssumeRoleCommand, STSClient } from "@aws-sdk/client-sts";
|
||||
import { randomUUID } from "crypto";
|
||||
|
||||
import { ExternalKmsAwsSchema, KmsAwsCredentialType, TExternalKmsAwsSchema, TExternalKmsProviderFns } from "./model";
|
||||
|
||||
const getAwsKmsClient = async (providerInputs: TExternalKmsAwsSchema) => {
|
||||
if (providerInputs.credential.type === KmsAwsCredentialType.AssumeRole) {
|
||||
const awsCredential = providerInputs.credential.data;
|
||||
const stsClient = new STSClient({
|
||||
region: providerInputs.awsRegion
|
||||
});
|
||||
const command = new AssumeRoleCommand({
|
||||
RoleArn: awsCredential.assumeRoleArn,
|
||||
RoleSessionName: `infisical-kms-${randomUUID()}`,
|
||||
DurationSeconds: 900, // 15mins
|
||||
ExternalId: awsCredential.externalId
|
||||
});
|
||||
const response = await stsClient.send(command);
|
||||
if (!response.Credentials?.AccessKeyId || !response.Credentials?.SecretAccessKey)
|
||||
throw new Error("Failed to assume role");
|
||||
|
||||
const kmsClient = new KMSClient({
|
||||
region: providerInputs.awsRegion,
|
||||
credentials: {
|
||||
accessKeyId: response.Credentials.AccessKeyId,
|
||||
secretAccessKey: response.Credentials.SecretAccessKey,
|
||||
sessionToken: response.Credentials.SessionToken,
|
||||
expiration: response.Credentials.Expiration
|
||||
}
|
||||
});
|
||||
return kmsClient;
|
||||
}
|
||||
const awsCredential = providerInputs.credential.data;
|
||||
const kmsClient = new KMSClient({
|
||||
region: providerInputs.awsRegion,
|
||||
credentials: {
|
||||
accessKeyId: awsCredential.accessKey,
|
||||
secretAccessKey: awsCredential.secretKey
|
||||
}
|
||||
});
|
||||
return kmsClient;
|
||||
};
|
||||
|
||||
type AwsKmsProviderArgs = {
|
||||
inputs: unknown;
|
||||
};
|
||||
type TAwsKmsProviderFactoryReturn = TExternalKmsProviderFns & {
|
||||
generateInputKmsKey: () => Promise<TExternalKmsAwsSchema>;
|
||||
};
|
||||
|
||||
export const AwsKmsProviderFactory = async ({ inputs }: AwsKmsProviderArgs): Promise<TAwsKmsProviderFactoryReturn> => {
|
||||
let providerInputs = await ExternalKmsAwsSchema.parseAsync(inputs);
|
||||
let awsClient = await getAwsKmsClient(providerInputs);
|
||||
|
||||
const generateInputKmsKey = async () => {
|
||||
if (providerInputs.kmsKeyId) return providerInputs;
|
||||
|
||||
const command = new CreateKeyCommand({ Tags: [{ TagKey: "author", TagValue: "infisical" }] });
|
||||
const kmsKey = await awsClient.send(command);
|
||||
|
||||
if (!kmsKey.KeyMetadata?.KeyId) throw new Error("Failed to generate kms key");
|
||||
|
||||
const updatedProviderInputs = await ExternalKmsAwsSchema.parseAsync({
|
||||
...providerInputs,
|
||||
kmsKeyId: kmsKey.KeyMetadata?.KeyId
|
||||
});
|
||||
|
||||
providerInputs = updatedProviderInputs;
|
||||
awsClient = await getAwsKmsClient(providerInputs);
|
||||
|
||||
return updatedProviderInputs;
|
||||
};
|
||||
|
||||
const validateConnection = async () => {
|
||||
const command = new DescribeKeyCommand({
|
||||
KeyId: providerInputs.kmsKeyId
|
||||
});
|
||||
const isConnected = await awsClient.send(command).then(() => true);
|
||||
return isConnected;
|
||||
};
|
||||
|
||||
const encrypt = async (data: Buffer) => {
|
||||
const command = new EncryptCommand({
|
||||
KeyId: providerInputs.kmsKeyId,
|
||||
Plaintext: data
|
||||
});
|
||||
const encryptionCommand = await awsClient.send(command);
|
||||
if (!encryptionCommand.CiphertextBlob) throw new Error("encryption failed");
|
||||
|
||||
return { encryptedBlob: Buffer.from(encryptionCommand.CiphertextBlob) };
|
||||
};
|
||||
|
||||
const decrypt = async (encryptedBlob: Buffer) => {
|
||||
const command = new DecryptCommand({
|
||||
KeyId: providerInputs.kmsKeyId,
|
||||
CiphertextBlob: encryptedBlob
|
||||
});
|
||||
const decryptionCommand = await awsClient.send(command);
|
||||
if (!decryptionCommand.Plaintext) throw new Error("decryption failed");
|
||||
|
||||
return { data: Buffer.from(decryptionCommand.Plaintext) };
|
||||
};
|
||||
|
||||
return {
|
||||
generateInputKmsKey,
|
||||
validateConnection,
|
||||
encrypt,
|
||||
decrypt
|
||||
};
|
||||
};
|
61
backend/src/ee/services/external-kms/providers/model.ts
Normal file
61
backend/src/ee/services/external-kms/providers/model.ts
Normal file
@ -0,0 +1,61 @@
|
||||
import { z } from "zod";
|
||||
|
||||
export enum KmsProviders {
|
||||
Aws = "aws"
|
||||
}
|
||||
|
||||
export enum KmsAwsCredentialType {
|
||||
AssumeRole = "assume-role",
|
||||
AccessKey = "access-key"
|
||||
}
|
||||
|
||||
export const ExternalKmsAwsSchema = z.object({
|
||||
credential: z
|
||||
.discriminatedUnion("type", [
|
||||
z.object({
|
||||
type: z.literal(KmsAwsCredentialType.AccessKey),
|
||||
data: z.object({
|
||||
accessKey: z.string().trim().min(1).describe("AWS user account access key"),
|
||||
secretKey: z.string().trim().min(1).describe("AWS user account secret key")
|
||||
})
|
||||
}),
|
||||
z.object({
|
||||
type: z.literal(KmsAwsCredentialType.AssumeRole),
|
||||
data: z.object({
|
||||
assumeRoleArn: z.string().trim().min(1).describe("AWS user role to be assumed by infisical"),
|
||||
externalId: z
|
||||
.string()
|
||||
.trim()
|
||||
.min(1)
|
||||
.optional()
|
||||
.describe("AWS assume role external id for furthur security in authentication")
|
||||
})
|
||||
})
|
||||
])
|
||||
.describe("AWS credential information to connect"),
|
||||
awsRegion: z.string().min(1).trim().describe("AWS region to connect"),
|
||||
kmsKeyId: z
|
||||
.string()
|
||||
.trim()
|
||||
.optional()
|
||||
.describe("A pre existing AWS KMS key id to be used for encryption. If not provided a kms key will be generated.")
|
||||
});
|
||||
export type TExternalKmsAwsSchema = z.infer<typeof ExternalKmsAwsSchema>;
|
||||
|
||||
// The root schema of the JSON
|
||||
export const ExternalKmsInputSchema = z.discriminatedUnion("type", [
|
||||
z.object({ type: z.literal(KmsProviders.Aws), inputs: ExternalKmsAwsSchema })
|
||||
]);
|
||||
export type TExternalKmsInputSchema = z.infer<typeof ExternalKmsInputSchema>;
|
||||
|
||||
export const ExternalKmsInputUpdateSchema = z.discriminatedUnion("type", [
|
||||
z.object({ type: z.literal(KmsProviders.Aws), inputs: ExternalKmsAwsSchema.partial() })
|
||||
]);
|
||||
export type TExternalKmsInputUpdateSchema = z.infer<typeof ExternalKmsInputUpdateSchema>;
|
||||
|
||||
// generic function shared by all provider
|
||||
export type TExternalKmsProviderFns = {
|
||||
validateConnection: () => Promise<boolean>;
|
||||
encrypt: (data: Buffer) => Promise<{ encryptedBlob: Buffer }>;
|
||||
decrypt: (encryptedBlob: Buffer) => Promise<{ data: Buffer }>;
|
||||
};
|
@ -336,31 +336,36 @@ export const removeUsersFromGroupByUserIds = async ({
|
||||
)
|
||||
);
|
||||
|
||||
// TODO: this part can be optimized
|
||||
for await (const userId of userIds) {
|
||||
const t = await userGroupMembershipDAL.filterProjectsByUserMembership(userId, group.id, projectIds, tx);
|
||||
const projectsToDeleteKeyFor = projectIds.filter((p) => !t.has(p));
|
||||
const promises: Array<Promise<void>> = [];
|
||||
for (const userId of userIds) {
|
||||
promises.push(
|
||||
(async () => {
|
||||
const t = await userGroupMembershipDAL.filterProjectsByUserMembership(userId, group.id, projectIds, tx);
|
||||
const projectsToDeleteKeyFor = projectIds.filter((p) => !t.has(p));
|
||||
|
||||
if (projectsToDeleteKeyFor.length) {
|
||||
await projectKeyDAL.delete(
|
||||
{
|
||||
receiverId: userId,
|
||||
$in: {
|
||||
projectId: projectsToDeleteKeyFor
|
||||
}
|
||||
},
|
||||
tx
|
||||
);
|
||||
}
|
||||
if (projectsToDeleteKeyFor.length) {
|
||||
await projectKeyDAL.delete(
|
||||
{
|
||||
receiverId: userId,
|
||||
$in: {
|
||||
projectId: projectsToDeleteKeyFor
|
||||
}
|
||||
},
|
||||
tx
|
||||
);
|
||||
}
|
||||
|
||||
await userGroupMembershipDAL.delete(
|
||||
{
|
||||
groupId: group.id,
|
||||
userId
|
||||
},
|
||||
tx
|
||||
await userGroupMembershipDAL.delete(
|
||||
{
|
||||
groupId: group.id,
|
||||
userId
|
||||
},
|
||||
tx
|
||||
);
|
||||
})()
|
||||
);
|
||||
}
|
||||
await Promise.all(promises);
|
||||
}
|
||||
|
||||
if (membersToRemoveFromGroupPending.length) {
|
||||
|
@ -162,11 +162,60 @@ export const userGroupMembershipDALFactory = (db: TDbClient) => {
|
||||
}
|
||||
};
|
||||
|
||||
const findGroupMembershipsByUserIdInOrg = async (userId: string, orgId: string) => {
|
||||
try {
|
||||
const docs = await db
|
||||
.replicaNode()(TableName.UserGroupMembership)
|
||||
.join(TableName.Groups, `${TableName.UserGroupMembership}.groupId`, `${TableName.Groups}.id`)
|
||||
.join(TableName.OrgMembership, `${TableName.UserGroupMembership}.userId`, `${TableName.OrgMembership}.userId`)
|
||||
.join(TableName.Users, `${TableName.UserGroupMembership}.userId`, `${TableName.Users}.id`)
|
||||
.where(`${TableName.UserGroupMembership}.userId`, userId)
|
||||
.where(`${TableName.Groups}.orgId`, orgId)
|
||||
.select(
|
||||
db.ref("id").withSchema(TableName.UserGroupMembership),
|
||||
db.ref("groupId").withSchema(TableName.UserGroupMembership),
|
||||
db.ref("name").withSchema(TableName.Groups).as("groupName"),
|
||||
db.ref("id").withSchema(TableName.OrgMembership).as("orgMembershipId"),
|
||||
db.ref("firstName").withSchema(TableName.Users).as("firstName"),
|
||||
db.ref("lastName").withSchema(TableName.Users).as("lastName")
|
||||
);
|
||||
|
||||
return docs;
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: "Find group memberships by user id in org" });
|
||||
}
|
||||
};
|
||||
|
||||
const findGroupMembershipsByGroupIdInOrg = async (groupId: string, orgId: string) => {
|
||||
try {
|
||||
const docs = await db
|
||||
.replicaNode()(TableName.UserGroupMembership)
|
||||
.join(TableName.Groups, `${TableName.UserGroupMembership}.groupId`, `${TableName.Groups}.id`)
|
||||
.join(TableName.OrgMembership, `${TableName.UserGroupMembership}.userId`, `${TableName.OrgMembership}.userId`)
|
||||
.join(TableName.Users, `${TableName.UserGroupMembership}.userId`, `${TableName.Users}.id`)
|
||||
.where(`${TableName.Groups}.id`, groupId)
|
||||
.where(`${TableName.Groups}.orgId`, orgId)
|
||||
.select(
|
||||
db.ref("id").withSchema(TableName.UserGroupMembership),
|
||||
db.ref("groupId").withSchema(TableName.UserGroupMembership),
|
||||
db.ref("name").withSchema(TableName.Groups).as("groupName"),
|
||||
db.ref("id").withSchema(TableName.OrgMembership).as("orgMembershipId"),
|
||||
db.ref("firstName").withSchema(TableName.Users).as("firstName"),
|
||||
db.ref("lastName").withSchema(TableName.Users).as("lastName")
|
||||
);
|
||||
return docs;
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: "Find group memberships by group id in org" });
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
...userGroupMembershipOrm,
|
||||
filterProjectsByUserMembership,
|
||||
findUserGroupMembershipsInProject,
|
||||
findGroupMembersNotInProject,
|
||||
deletePendingUserGroupMembershipsByUserIds
|
||||
deletePendingUserGroupMembershipsByUserIds,
|
||||
findGroupMembershipsByUserIdInOrg,
|
||||
findGroupMembershipsByGroupIdInOrg
|
||||
};
|
||||
};
|
||||
|
@ -449,7 +449,8 @@ export const ldapConfigServiceFactory = ({
|
||||
userId: userAlias.userId,
|
||||
orgId,
|
||||
role: OrgMembershipRole.Member,
|
||||
status: OrgMembershipStatus.Accepted
|
||||
status: OrgMembershipStatus.Accepted,
|
||||
isActive: true
|
||||
},
|
||||
tx
|
||||
);
|
||||
@ -534,7 +535,8 @@ export const ldapConfigServiceFactory = ({
|
||||
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
|
||||
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
|
||||
isActive: true
|
||||
},
|
||||
tx
|
||||
);
|
||||
|
@ -39,7 +39,8 @@ export const getDefaultOnPremFeatures = (): TFeatureSet => ({
|
||||
secretApproval: false,
|
||||
secretRotation: true,
|
||||
caCrl: false,
|
||||
instanceUserManagement: false
|
||||
instanceUserManagement: false,
|
||||
externalKms: false
|
||||
});
|
||||
|
||||
export const setupLicenceRequestWithStore = (baseURL: string, refreshUrl: string, licenseKey: string) => {
|
||||
|
@ -218,6 +218,8 @@ export const licenseServiceFactory = ({
|
||||
} else if (instanceType === InstanceType.EnterpriseOnPrem) {
|
||||
const usedSeats = await licenseDAL.countOfOrgMembers(null, tx);
|
||||
const usedIdentitySeats = await licenseDAL.countOrgUsersAndIdentities(null, tx);
|
||||
onPremFeatures.membersUsed = usedSeats;
|
||||
onPremFeatures.identitiesUsed = usedIdentitySeats;
|
||||
await licenseServerOnPremApi.request.patch(`/api/license/v1/license`, {
|
||||
usedSeats,
|
||||
usedIdentitySeats
|
||||
|
@ -30,9 +30,9 @@ export type TFeatureSet = {
|
||||
workspacesUsed: 0;
|
||||
dynamicSecret: false;
|
||||
memberLimit: null;
|
||||
membersUsed: 0;
|
||||
membersUsed: number;
|
||||
identityLimit: null;
|
||||
identitiesUsed: 0;
|
||||
identitiesUsed: number;
|
||||
environmentLimit: null;
|
||||
environmentsUsed: 0;
|
||||
secretVersioning: true;
|
||||
@ -57,6 +57,7 @@ export type TFeatureSet = {
|
||||
secretRotation: true;
|
||||
caCrl: false;
|
||||
instanceUserManagement: false;
|
||||
externalKms: false;
|
||||
};
|
||||
|
||||
export type TOrgPlansTableDTO = {
|
||||
|
@ -193,7 +193,8 @@ export const oidcConfigServiceFactory = ({
|
||||
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
|
||||
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
|
||||
isActive: true
|
||||
},
|
||||
tx
|
||||
);
|
||||
@ -266,7 +267,8 @@ export const oidcConfigServiceFactory = ({
|
||||
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
|
||||
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
|
||||
isActive: true
|
||||
},
|
||||
tx
|
||||
);
|
||||
|
@ -9,6 +9,10 @@ export enum OrgPermissionActions {
|
||||
Delete = "delete"
|
||||
}
|
||||
|
||||
export enum OrgPermissionAdminConsoleAction {
|
||||
AccessAllProjects = "access-all-projects"
|
||||
}
|
||||
|
||||
export enum OrgPermissionSubjects {
|
||||
Workspace = "workspace",
|
||||
Role = "role",
|
||||
@ -21,7 +25,9 @@ export enum OrgPermissionSubjects {
|
||||
Groups = "groups",
|
||||
Billing = "billing",
|
||||
SecretScanning = "secret-scanning",
|
||||
Identity = "identity"
|
||||
Identity = "identity",
|
||||
Kms = "kms",
|
||||
AdminConsole = "organization-admin-console"
|
||||
}
|
||||
|
||||
export type OrgPermissionSet =
|
||||
@ -37,7 +43,9 @@ export type OrgPermissionSet =
|
||||
| [OrgPermissionActions, OrgPermissionSubjects.Groups]
|
||||
| [OrgPermissionActions, OrgPermissionSubjects.SecretScanning]
|
||||
| [OrgPermissionActions, OrgPermissionSubjects.Billing]
|
||||
| [OrgPermissionActions, OrgPermissionSubjects.Identity];
|
||||
| [OrgPermissionActions, OrgPermissionSubjects.Identity]
|
||||
| [OrgPermissionActions, OrgPermissionSubjects.Kms]
|
||||
| [OrgPermissionAdminConsoleAction, OrgPermissionSubjects.AdminConsole];
|
||||
|
||||
const buildAdminPermission = () => {
|
||||
const { can, build } = new AbilityBuilder<MongoAbility<OrgPermissionSet>>(createMongoAbility);
|
||||
@ -100,6 +108,13 @@ const buildAdminPermission = () => {
|
||||
can(OrgPermissionActions.Edit, OrgPermissionSubjects.Identity);
|
||||
can(OrgPermissionActions.Delete, OrgPermissionSubjects.Identity);
|
||||
|
||||
can(OrgPermissionActions.Read, OrgPermissionSubjects.Kms);
|
||||
can(OrgPermissionActions.Create, OrgPermissionSubjects.Kms);
|
||||
can(OrgPermissionActions.Edit, OrgPermissionSubjects.Kms);
|
||||
can(OrgPermissionActions.Delete, OrgPermissionSubjects.Kms);
|
||||
|
||||
can(OrgPermissionAdminConsoleAction.AccessAllProjects, OrgPermissionSubjects.AdminConsole);
|
||||
|
||||
return build({ conditionsMatcher });
|
||||
};
|
||||
|
||||
|
@ -109,6 +109,9 @@ export const permissionServiceFactory = ({
|
||||
authMethod: ActorAuthMethod,
|
||||
userOrgId?: string
|
||||
) => {
|
||||
// when token is scoped, ensure the passed org id is same as user org id
|
||||
if (userOrgId && userOrgId !== orgId)
|
||||
throw new BadRequestError({ message: "Invalid user token. Scoped to different organization." });
|
||||
const membership = await permissionDAL.getOrgPermission(userId, orgId);
|
||||
if (!membership) throw new UnauthorizedError({ name: "User not in org" });
|
||||
if (membership.role === OrgMembershipRole.Custom && !membership.permissions) {
|
||||
|
@ -23,12 +23,14 @@ export enum ProjectPermissionSub {
|
||||
IpAllowList = "ip-allowlist",
|
||||
Project = "workspace",
|
||||
Secrets = "secrets",
|
||||
SecretFolders = "secret-folders",
|
||||
SecretRollback = "secret-rollback",
|
||||
SecretApproval = "secret-approval",
|
||||
SecretRotation = "secret-rotation",
|
||||
Identity = "identity",
|
||||
CertificateAuthorities = "certificate-authorities",
|
||||
Certificates = "certificates"
|
||||
Certificates = "certificates",
|
||||
Kms = "kms"
|
||||
}
|
||||
|
||||
type SubjectFields = {
|
||||
@ -41,6 +43,10 @@ export type ProjectPermissionSet =
|
||||
ProjectPermissionActions,
|
||||
ProjectPermissionSub.Secrets | (ForcedSubject<ProjectPermissionSub.Secrets> & SubjectFields)
|
||||
]
|
||||
| [
|
||||
ProjectPermissionActions,
|
||||
ProjectPermissionSub.SecretFolders | (ForcedSubject<ProjectPermissionSub.SecretFolders> & SubjectFields)
|
||||
]
|
||||
| [ProjectPermissionActions, ProjectPermissionSub.Role]
|
||||
| [ProjectPermissionActions, ProjectPermissionSub.Tags]
|
||||
| [ProjectPermissionActions, ProjectPermissionSub.Member]
|
||||
@ -60,7 +66,8 @@ export type ProjectPermissionSet =
|
||||
| [ProjectPermissionActions.Delete, ProjectPermissionSub.Project]
|
||||
| [ProjectPermissionActions.Edit, ProjectPermissionSub.Project]
|
||||
| [ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback]
|
||||
| [ProjectPermissionActions.Create, ProjectPermissionSub.SecretRollback];
|
||||
| [ProjectPermissionActions.Create, ProjectPermissionSub.SecretRollback]
|
||||
| [ProjectPermissionActions.Edit, ProjectPermissionSub.Kms];
|
||||
|
||||
const buildAdminPermissionRules = () => {
|
||||
const { can, rules } = new AbilityBuilder<MongoAbility<ProjectPermissionSet>>(createMongoAbility);
|
||||
@ -157,6 +164,8 @@ const buildAdminPermissionRules = () => {
|
||||
can(ProjectPermissionActions.Edit, ProjectPermissionSub.Project);
|
||||
can(ProjectPermissionActions.Delete, ProjectPermissionSub.Project);
|
||||
|
||||
can(ProjectPermissionActions.Edit, ProjectPermissionSub.Kms);
|
||||
|
||||
return rules;
|
||||
};
|
||||
|
||||
|
@ -370,7 +370,8 @@ export const samlConfigServiceFactory = ({
|
||||
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
|
||||
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
|
||||
isActive: true
|
||||
},
|
||||
tx
|
||||
);
|
||||
@ -457,7 +458,8 @@ export const samlConfigServiceFactory = ({
|
||||
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
|
||||
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
|
||||
isActive: true
|
||||
},
|
||||
tx
|
||||
);
|
||||
|
@ -32,12 +32,19 @@ export const parseScimFilter = (filterToParse: string | undefined) => {
|
||||
return { [attributeName]: parsedValue.replace(/"/g, "") };
|
||||
};
|
||||
|
||||
export function extractScimValueFromPath(path: string): string | null {
|
||||
const regex = /members\[value eq "([^"]+)"\]/;
|
||||
const match = path.match(regex);
|
||||
return match ? match[1] : null;
|
||||
}
|
||||
|
||||
export const buildScimUser = ({
|
||||
orgMembershipId,
|
||||
username,
|
||||
email,
|
||||
firstName,
|
||||
lastName,
|
||||
groups = [],
|
||||
active
|
||||
}: {
|
||||
orgMembershipId: string;
|
||||
@ -45,6 +52,10 @@ export const buildScimUser = ({
|
||||
email?: string | null;
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
groups?: {
|
||||
value: string;
|
||||
display: string;
|
||||
}[];
|
||||
active: boolean;
|
||||
}): TScimUser => {
|
||||
const scimUser = {
|
||||
@ -67,7 +78,7 @@ export const buildScimUser = ({
|
||||
]
|
||||
: [],
|
||||
active,
|
||||
groups: [],
|
||||
groups,
|
||||
meta: {
|
||||
resourceType: "User",
|
||||
location: null
|
||||
|
@ -2,13 +2,14 @@ import { ForbiddenError } from "@casl/ability";
|
||||
import slugify from "@sindresorhus/slugify";
|
||||
import jwt from "jsonwebtoken";
|
||||
|
||||
import { OrgMembershipRole, OrgMembershipStatus, TableName, TGroups, TOrgMemberships, TUsers } from "@app/db/schemas";
|
||||
import { OrgMembershipRole, OrgMembershipStatus, TableName, TOrgMemberships, TUsers } from "@app/db/schemas";
|
||||
import { TGroupDALFactory } from "@app/ee/services/group/group-dal";
|
||||
import { addUsersToGroupByUserIds, removeUsersFromGroupByUserIds } from "@app/ee/services/group/group-fns";
|
||||
import { TUserGroupMembershipDALFactory } from "@app/ee/services/group/user-group-membership-dal";
|
||||
import { TScimDALFactory } from "@app/ee/services/scim/scim-dal";
|
||||
import { getConfig } from "@app/lib/config/env";
|
||||
import { BadRequestError, ScimRequestError, UnauthorizedError } from "@app/lib/errors";
|
||||
import { logger } from "@app/lib/logger";
|
||||
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
||||
import { TOrgPermission } from "@app/lib/types";
|
||||
import { AuthTokenType } from "@app/services/auth/auth-type";
|
||||
@ -30,7 +31,14 @@ import { UserAliasType } from "@app/services/user-alias/user-alias-types";
|
||||
import { TLicenseServiceFactory } from "../license/license-service";
|
||||
import { OrgPermissionActions, OrgPermissionSubjects } from "../permission/org-permission";
|
||||
import { TPermissionServiceFactory } from "../permission/permission-service";
|
||||
import { buildScimGroup, buildScimGroupList, buildScimUser, buildScimUserList, parseScimFilter } from "./scim-fns";
|
||||
import {
|
||||
buildScimGroup,
|
||||
buildScimGroupList,
|
||||
buildScimUser,
|
||||
buildScimUserList,
|
||||
extractScimValueFromPath,
|
||||
parseScimFilter
|
||||
} from "./scim-fns";
|
||||
import {
|
||||
TCreateScimGroupDTO,
|
||||
TCreateScimTokenDTO,
|
||||
@ -44,6 +52,7 @@ import {
|
||||
TListScimUsers,
|
||||
TListScimUsersDTO,
|
||||
TReplaceScimUserDTO,
|
||||
TScimGroup,
|
||||
TScimTokenJwtPayload,
|
||||
TUpdateScimGroupNamePatchDTO,
|
||||
TUpdateScimGroupNamePutDTO,
|
||||
@ -61,17 +70,23 @@ type TScimServiceFactoryDep = {
|
||||
TOrgDALFactory,
|
||||
"createMembership" | "findById" | "findMembership" | "deleteMembershipById" | "transaction" | "updateMembershipById"
|
||||
>;
|
||||
orgMembershipDAL: Pick<TOrgMembershipDALFactory, "find" | "findOne" | "create" | "updateById">;
|
||||
orgMembershipDAL: Pick<TOrgMembershipDALFactory, "find" | "findOne" | "create" | "updateById" | "findById">;
|
||||
projectDAL: Pick<TProjectDALFactory, "find" | "findProjectGhostUser">;
|
||||
projectMembershipDAL: Pick<TProjectMembershipDALFactory, "find" | "delete" | "findProjectMembershipsByUserId">;
|
||||
groupDAL: Pick<
|
||||
TGroupDALFactory,
|
||||
"create" | "findOne" | "findAllGroupMembers" | "update" | "delete" | "findGroups" | "transaction"
|
||||
"create" | "findOne" | "findAllGroupMembers" | "delete" | "findGroups" | "transaction" | "updateById" | "update"
|
||||
>;
|
||||
groupProjectDAL: Pick<TGroupProjectDALFactory, "find">;
|
||||
userGroupMembershipDAL: Pick<
|
||||
TUserGroupMembershipDALFactory,
|
||||
"find" | "transaction" | "insertMany" | "filterProjectsByUserMembership" | "delete"
|
||||
| "find"
|
||||
| "transaction"
|
||||
| "insertMany"
|
||||
| "filterProjectsByUserMembership"
|
||||
| "delete"
|
||||
| "findGroupMembershipsByUserIdInOrg"
|
||||
| "findGroupMembershipsByGroupIdInOrg"
|
||||
>;
|
||||
projectKeyDAL: Pick<TProjectKeyDALFactory, "find" | "findLatestProjectKey" | "insertMany" | "delete">;
|
||||
projectBotDAL: Pick<TProjectBotDALFactory, "findOne">;
|
||||
@ -197,14 +212,14 @@ export const scimServiceFactory = ({
|
||||
findOpts
|
||||
);
|
||||
|
||||
const scimUsers = users.map(({ id, externalId, username, firstName, lastName, email }) =>
|
||||
const scimUsers = users.map(({ id, externalId, username, firstName, lastName, email, isActive }) =>
|
||||
buildScimUser({
|
||||
orgMembershipId: id ?? "",
|
||||
username: externalId ?? username,
|
||||
firstName: firstName ?? "",
|
||||
lastName: lastName ?? "",
|
||||
email,
|
||||
active: true
|
||||
active: isActive
|
||||
})
|
||||
);
|
||||
|
||||
@ -240,13 +255,22 @@ export const scimServiceFactory = ({
|
||||
status: 403
|
||||
});
|
||||
|
||||
const groupMembershipsInOrg = await userGroupMembershipDAL.findGroupMembershipsByUserIdInOrg(
|
||||
membership.userId,
|
||||
orgId
|
||||
);
|
||||
|
||||
return buildScimUser({
|
||||
orgMembershipId: membership.id,
|
||||
username: membership.externalId ?? membership.username,
|
||||
email: membership.email ?? "",
|
||||
firstName: membership.firstName as string,
|
||||
lastName: membership.lastName as string,
|
||||
active: true
|
||||
active: membership.isActive,
|
||||
groups: groupMembershipsInOrg.map((group) => ({
|
||||
value: group.groupId,
|
||||
display: group.groupName
|
||||
}))
|
||||
});
|
||||
};
|
||||
|
||||
@ -296,7 +320,8 @@ export const scimServiceFactory = ({
|
||||
inviteEmail: email,
|
||||
orgId,
|
||||
role: OrgMembershipRole.Member,
|
||||
status: user.isAccepted ? OrgMembershipStatus.Accepted : OrgMembershipStatus.Invited // if user is fully completed, then set status to accepted, otherwise set it to invited so we can update it later
|
||||
status: user.isAccepted ? OrgMembershipStatus.Accepted : OrgMembershipStatus.Invited, // if user is fully completed, then set status to accepted, otherwise set it to invited so we can update it later
|
||||
isActive: true
|
||||
},
|
||||
tx
|
||||
);
|
||||
@ -364,7 +389,8 @@ export const scimServiceFactory = ({
|
||||
inviteEmail: email,
|
||||
orgId,
|
||||
role: OrgMembershipRole.Member,
|
||||
status: user.isAccepted ? OrgMembershipStatus.Accepted : OrgMembershipStatus.Invited // if user is fully completed, then set status to accepted, otherwise set it to invited so we can update it later
|
||||
status: user.isAccepted ? OrgMembershipStatus.Accepted : OrgMembershipStatus.Invited, // if user is fully completed, then set status to accepted, otherwise set it to invited so we can update it later
|
||||
isActive: true
|
||||
},
|
||||
tx
|
||||
);
|
||||
@ -401,7 +427,7 @@ export const scimServiceFactory = ({
|
||||
firstName: createdUser.firstName as string,
|
||||
lastName: createdUser.lastName as string,
|
||||
email: createdUser.email ?? "",
|
||||
active: true
|
||||
active: createdOrgMembership.isActive
|
||||
});
|
||||
};
|
||||
|
||||
@ -445,14 +471,8 @@ export const scimServiceFactory = ({
|
||||
});
|
||||
|
||||
if (!active) {
|
||||
await deleteOrgMembershipFn({
|
||||
orgMembershipId: membership.id,
|
||||
orgId: membership.orgId,
|
||||
orgDAL,
|
||||
projectMembershipDAL,
|
||||
projectKeyDAL,
|
||||
userAliasDAL,
|
||||
licenseService
|
||||
await orgMembershipDAL.updateById(membership.id, {
|
||||
isActive: false
|
||||
});
|
||||
}
|
||||
|
||||
@ -491,17 +511,14 @@ export const scimServiceFactory = ({
|
||||
status: 403
|
||||
});
|
||||
|
||||
if (!active) {
|
||||
await deleteOrgMembershipFn({
|
||||
orgMembershipId: membership.id,
|
||||
orgId: membership.orgId,
|
||||
orgDAL,
|
||||
projectMembershipDAL,
|
||||
projectKeyDAL,
|
||||
userAliasDAL,
|
||||
licenseService
|
||||
});
|
||||
}
|
||||
await orgMembershipDAL.updateById(membership.id, {
|
||||
isActive: active
|
||||
});
|
||||
|
||||
const groupMembershipsInOrg = await userGroupMembershipDAL.findGroupMembershipsByUserIdInOrg(
|
||||
membership.userId,
|
||||
orgId
|
||||
);
|
||||
|
||||
return buildScimUser({
|
||||
orgMembershipId: membership.id,
|
||||
@ -509,7 +526,11 @@ export const scimServiceFactory = ({
|
||||
email: membership.email,
|
||||
firstName: membership.firstName as string,
|
||||
lastName: membership.lastName as string,
|
||||
active
|
||||
active,
|
||||
groups: groupMembershipsInOrg.map((group) => ({
|
||||
value: group.groupId,
|
||||
display: group.groupName
|
||||
}))
|
||||
});
|
||||
};
|
||||
|
||||
@ -577,13 +598,20 @@ export const scimServiceFactory = ({
|
||||
}
|
||||
);
|
||||
|
||||
const scimGroups = groups.map((group) =>
|
||||
buildScimGroup({
|
||||
const scimGroups: TScimGroup[] = [];
|
||||
|
||||
for await (const group of groups) {
|
||||
const members = await userGroupMembershipDAL.findGroupMembershipsByGroupIdInOrg(group.id, orgId);
|
||||
const scimGroup = buildScimGroup({
|
||||
groupId: group.id,
|
||||
name: group.name,
|
||||
members: [] // does this need to be populated?
|
||||
})
|
||||
);
|
||||
members: members.map((member) => ({
|
||||
value: member.orgMembershipId,
|
||||
display: `${member.firstName ?? ""} ${member.lastName ?? ""}`
|
||||
}))
|
||||
});
|
||||
scimGroups.push(scimGroup);
|
||||
}
|
||||
|
||||
return buildScimGroupList({
|
||||
scimGroups,
|
||||
@ -817,7 +845,6 @@ export const scimServiceFactory = ({
|
||||
});
|
||||
};
|
||||
|
||||
// TODO: add support for add/remove op
|
||||
const updateScimGroupNamePatch = async ({ groupId, orgId, operations }: TUpdateScimGroupNamePatchDTO) => {
|
||||
const plan = await licenseService.getPlan(orgId);
|
||||
if (!plan.groups)
|
||||
@ -840,27 +867,64 @@ export const scimServiceFactory = ({
|
||||
status: 403
|
||||
});
|
||||
|
||||
let group: TGroups | undefined;
|
||||
let group = await groupDAL.findOne({
|
||||
id: groupId,
|
||||
orgId
|
||||
});
|
||||
|
||||
if (!group) {
|
||||
throw new ScimRequestError({
|
||||
detail: "Group Not Found",
|
||||
status: 404
|
||||
});
|
||||
}
|
||||
|
||||
for await (const operation of operations) {
|
||||
switch (operation.op) {
|
||||
case "replace": {
|
||||
await groupDAL.update(
|
||||
{
|
||||
id: groupId,
|
||||
orgId
|
||||
},
|
||||
{
|
||||
name: operation.value.displayName
|
||||
}
|
||||
);
|
||||
group = await groupDAL.updateById(group.id, {
|
||||
name: operation.value.displayName
|
||||
});
|
||||
break;
|
||||
}
|
||||
case "add": {
|
||||
// TODO
|
||||
try {
|
||||
const orgMemberships = await orgMembershipDAL.find({
|
||||
$in: {
|
||||
id: operation.value.map((member) => member.value)
|
||||
}
|
||||
});
|
||||
|
||||
await addUsersToGroupByUserIds({
|
||||
group,
|
||||
userIds: orgMemberships.map((membership) => membership.userId as string),
|
||||
userDAL,
|
||||
userGroupMembershipDAL,
|
||||
orgDAL,
|
||||
groupProjectDAL,
|
||||
projectKeyDAL,
|
||||
projectDAL,
|
||||
projectBotDAL
|
||||
});
|
||||
} catch {
|
||||
logger.info("Repeat SCIM user-group add operation");
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
case "remove": {
|
||||
// TODO
|
||||
const orgMembershipId = extractScimValueFromPath(operation.path);
|
||||
if (!orgMembershipId) throw new ScimRequestError({ detail: "Invalid path value", status: 400 });
|
||||
const orgMembership = await orgMembershipDAL.findById(orgMembershipId);
|
||||
if (!orgMembership) throw new ScimRequestError({ detail: "Org Membership Not Found", status: 400 });
|
||||
await removeUsersFromGroupByUserIds({
|
||||
group,
|
||||
userIds: [orgMembership.userId as string],
|
||||
userDAL,
|
||||
userGroupMembershipDAL,
|
||||
groupProjectDAL,
|
||||
projectKeyDAL
|
||||
});
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
@ -872,17 +936,15 @@ export const scimServiceFactory = ({
|
||||
}
|
||||
}
|
||||
|
||||
if (!group) {
|
||||
throw new ScimRequestError({
|
||||
detail: "Group Not Found",
|
||||
status: 404
|
||||
});
|
||||
}
|
||||
const members = await userGroupMembershipDAL.findGroupMembershipsByGroupIdInOrg(group.id, orgId);
|
||||
|
||||
return buildScimGroup({
|
||||
groupId: group.id,
|
||||
name: group.name,
|
||||
members: []
|
||||
members: members.map((member) => ({
|
||||
value: member.orgMembershipId,
|
||||
display: `${member.firstName ?? ""} ${member.lastName ?? ""}`
|
||||
}))
|
||||
});
|
||||
};
|
||||
|
||||
|
@ -125,10 +125,11 @@ type TRemoveOp = {
|
||||
|
||||
type TAddOp = {
|
||||
op: "add";
|
||||
path: string;
|
||||
value: {
|
||||
value: string;
|
||||
display?: string;
|
||||
};
|
||||
}[];
|
||||
};
|
||||
|
||||
export type TDeleteScimGroupDTO = {
|
||||
@ -157,7 +158,10 @@ export type TScimUser = {
|
||||
type: string;
|
||||
}[];
|
||||
active: boolean;
|
||||
groups: string[];
|
||||
groups: {
|
||||
value: string;
|
||||
display: string;
|
||||
}[];
|
||||
meta: {
|
||||
resourceType: string;
|
||||
location: null;
|
||||
|
@ -45,12 +45,13 @@ export const secretApprovalPolicyServiceFactory = ({
|
||||
actorOrgId,
|
||||
actorAuthMethod,
|
||||
approvals,
|
||||
approverUserIds,
|
||||
approvers,
|
||||
projectId,
|
||||
secretPath,
|
||||
environment
|
||||
environment,
|
||||
enforcementLevel
|
||||
}: TCreateSapDTO) => {
|
||||
if (approvals > approverUserIds.length)
|
||||
if (approvals > approvers.length)
|
||||
throw new BadRequestError({ message: "Approvals cannot be greater than approvers" });
|
||||
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
@ -73,12 +74,13 @@ export const secretApprovalPolicyServiceFactory = ({
|
||||
envId: env.id,
|
||||
approvals,
|
||||
secretPath,
|
||||
name
|
||||
name,
|
||||
enforcementLevel
|
||||
},
|
||||
tx
|
||||
);
|
||||
await secretApprovalPolicyApproverDAL.insertMany(
|
||||
approverUserIds.map((approverUserId) => ({
|
||||
approvers.map((approverUserId) => ({
|
||||
approverUserId,
|
||||
policyId: doc.id
|
||||
})),
|
||||
@ -90,7 +92,7 @@ export const secretApprovalPolicyServiceFactory = ({
|
||||
};
|
||||
|
||||
const updateSecretApprovalPolicy = async ({
|
||||
approverUserIds,
|
||||
approvers,
|
||||
secretPath,
|
||||
name,
|
||||
actorId,
|
||||
@ -98,7 +100,8 @@ export const secretApprovalPolicyServiceFactory = ({
|
||||
actorOrgId,
|
||||
actorAuthMethod,
|
||||
approvals,
|
||||
secretPolicyId
|
||||
secretPolicyId,
|
||||
enforcementLevel
|
||||
}: TUpdateSapDTO) => {
|
||||
const secretApprovalPolicy = await secretApprovalPolicyDAL.findById(secretPolicyId);
|
||||
if (!secretApprovalPolicy) throw new BadRequestError({ message: "Secret approval policy not found" });
|
||||
@ -118,14 +121,15 @@ export const secretApprovalPolicyServiceFactory = ({
|
||||
{
|
||||
approvals,
|
||||
secretPath,
|
||||
name
|
||||
name,
|
||||
enforcementLevel
|
||||
},
|
||||
tx
|
||||
);
|
||||
if (approverUserIds) {
|
||||
if (approvers) {
|
||||
await secretApprovalPolicyApproverDAL.delete({ policyId: doc.id }, tx);
|
||||
await secretApprovalPolicyApproverDAL.insertMany(
|
||||
approverUserIds.map((approverUserId) => ({
|
||||
approvers.map((approverUserId) => ({
|
||||
approverUserId,
|
||||
policyId: doc.id
|
||||
})),
|
||||
|
@ -1,20 +1,22 @@
|
||||
import { TProjectPermission } from "@app/lib/types";
|
||||
import { EnforcementLevel, TProjectPermission } from "@app/lib/types";
|
||||
|
||||
export type TCreateSapDTO = {
|
||||
approvals: number;
|
||||
secretPath?: string | null;
|
||||
environment: string;
|
||||
approverUserIds: string[];
|
||||
approvers: string[];
|
||||
projectId: string;
|
||||
name: string;
|
||||
enforcementLevel: EnforcementLevel;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
export type TUpdateSapDTO = {
|
||||
secretPolicyId: string;
|
||||
approvals?: number;
|
||||
secretPath?: string | null;
|
||||
approverUserIds: string[];
|
||||
approvers: string[];
|
||||
name?: string;
|
||||
enforcementLevel?: EnforcementLevel;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
export type TDeleteSapDTO = {
|
||||
|
@ -94,6 +94,8 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => {
|
||||
tx.ref("projectId").withSchema(TableName.Environment),
|
||||
tx.ref("slug").withSchema(TableName.Environment).as("environment"),
|
||||
tx.ref("secretPath").withSchema(TableName.SecretApprovalPolicy).as("policySecretPath"),
|
||||
tx.ref("envId").withSchema(TableName.SecretApprovalPolicy).as("policyEnvId"),
|
||||
tx.ref("enforcementLevel").withSchema(TableName.SecretApprovalPolicy).as("policyEnforcementLevel"),
|
||||
tx.ref("approvals").withSchema(TableName.SecretApprovalPolicy).as("policyApprovals")
|
||||
);
|
||||
|
||||
@ -128,7 +130,9 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => {
|
||||
id: el.policyId,
|
||||
name: el.policyName,
|
||||
approvals: el.policyApprovals,
|
||||
secretPath: el.policySecretPath
|
||||
secretPath: el.policySecretPath,
|
||||
enforcementLevel: el.policyEnforcementLevel,
|
||||
envId: el.policyEnvId
|
||||
}
|
||||
}),
|
||||
childrenMapper: [
|
||||
@ -282,6 +286,7 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => {
|
||||
`DENSE_RANK() OVER (partition by ${TableName.Environment}."projectId" ORDER BY ${TableName.SecretApprovalRequest}."id" DESC) as rank`
|
||||
),
|
||||
db.ref("secretPath").withSchema(TableName.SecretApprovalPolicy).as("policySecretPath"),
|
||||
db.ref("enforcementLevel").withSchema(TableName.SecretApprovalPolicy).as("policyEnforcementLevel"),
|
||||
db.ref("approvals").withSchema(TableName.SecretApprovalPolicy).as("policyApprovals"),
|
||||
db.ref("approverUserId").withSchema(TableName.SecretApprovalPolicyApprover),
|
||||
db.ref("email").withSchema("committerUser").as("committerUserEmail"),
|
||||
@ -308,7 +313,8 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => {
|
||||
id: el.policyId,
|
||||
name: el.policyName,
|
||||
approvals: el.policyApprovals,
|
||||
secretPath: el.policySecretPath
|
||||
secretPath: el.policySecretPath,
|
||||
enforcementLevel: el.policyEnforcementLevel
|
||||
},
|
||||
committerUser: {
|
||||
userId: el.committerUserId,
|
||||
@ -350,5 +356,161 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => {
|
||||
}
|
||||
};
|
||||
|
||||
return { ...secretApprovalRequestOrm, findById, findProjectRequestCount, findByProjectId };
|
||||
const findByProjectIdBridgeSecretV2 = async (
|
||||
{ status, limit = 20, offset = 0, projectId, committer, environment, userId }: TFindQueryFilter,
|
||||
tx?: Knex
|
||||
) => {
|
||||
try {
|
||||
// akhilmhdh: If ever u wanted a 1 to so many relationship connected with pagination
|
||||
// this is the place u wanna look at.
|
||||
const query = (tx || db.replicaNode())(TableName.SecretApprovalRequest)
|
||||
.join(TableName.SecretFolder, `${TableName.SecretApprovalRequest}.folderId`, `${TableName.SecretFolder}.id`)
|
||||
.join(TableName.Environment, `${TableName.SecretFolder}.envId`, `${TableName.Environment}.id`)
|
||||
.join(
|
||||
TableName.SecretApprovalPolicy,
|
||||
`${TableName.SecretApprovalRequest}.policyId`,
|
||||
`${TableName.SecretApprovalPolicy}.id`
|
||||
)
|
||||
.join(
|
||||
TableName.SecretApprovalPolicyApprover,
|
||||
`${TableName.SecretApprovalPolicy}.id`,
|
||||
`${TableName.SecretApprovalPolicyApprover}.policyId`
|
||||
)
|
||||
.join<TUsers>(
|
||||
db(TableName.Users).as("committerUser"),
|
||||
`${TableName.SecretApprovalRequest}.committerUserId`,
|
||||
`committerUser.id`
|
||||
)
|
||||
.leftJoin(
|
||||
TableName.SecretApprovalRequestReviewer,
|
||||
`${TableName.SecretApprovalRequest}.id`,
|
||||
`${TableName.SecretApprovalRequestReviewer}.requestId`
|
||||
)
|
||||
.leftJoin<TSecretApprovalRequestsSecrets>(
|
||||
TableName.SecretApprovalRequestSecretV2,
|
||||
`${TableName.SecretApprovalRequestSecretV2}.requestId`,
|
||||
`${TableName.SecretApprovalRequest}.id`
|
||||
)
|
||||
.where(
|
||||
stripUndefinedInWhere({
|
||||
projectId,
|
||||
[`${TableName.Environment}.slug` as "slug"]: environment,
|
||||
[`${TableName.SecretApprovalRequest}.status`]: status,
|
||||
committerUserId: committer
|
||||
})
|
||||
)
|
||||
.andWhere(
|
||||
(bd) =>
|
||||
void bd
|
||||
.where(`${TableName.SecretApprovalPolicyApprover}.approverUserId`, userId)
|
||||
.orWhere(`${TableName.SecretApprovalRequest}.committerUserId`, userId)
|
||||
)
|
||||
.select(selectAllTableCols(TableName.SecretApprovalRequest))
|
||||
.select(
|
||||
db.ref("projectId").withSchema(TableName.Environment),
|
||||
db.ref("slug").withSchema(TableName.Environment).as("environment"),
|
||||
db.ref("id").withSchema(TableName.SecretApprovalRequestReviewer).as("reviewerId"),
|
||||
db.ref("reviewerUserId").withSchema(TableName.SecretApprovalRequestReviewer),
|
||||
db.ref("status").withSchema(TableName.SecretApprovalRequestReviewer).as("reviewerStatus"),
|
||||
db.ref("id").withSchema(TableName.SecretApprovalPolicy).as("policyId"),
|
||||
db.ref("name").withSchema(TableName.SecretApprovalPolicy).as("policyName"),
|
||||
db.ref("op").withSchema(TableName.SecretApprovalRequestSecretV2).as("commitOp"),
|
||||
db.ref("secretId").withSchema(TableName.SecretApprovalRequestSecretV2).as("commitSecretId"),
|
||||
db.ref("id").withSchema(TableName.SecretApprovalRequestSecretV2).as("commitId"),
|
||||
db.raw(
|
||||
`DENSE_RANK() OVER (partition by ${TableName.Environment}."projectId" ORDER BY ${TableName.SecretApprovalRequest}."id" DESC) as rank`
|
||||
),
|
||||
db.ref("secretPath").withSchema(TableName.SecretApprovalPolicy).as("policySecretPath"),
|
||||
db.ref("approvals").withSchema(TableName.SecretApprovalPolicy).as("policyApprovals"),
|
||||
db.ref("enforcementLevel").withSchema(TableName.SecretApprovalPolicy).as("policyEnforcementLevel"),
|
||||
db.ref("approverUserId").withSchema(TableName.SecretApprovalPolicyApprover),
|
||||
db.ref("email").withSchema("committerUser").as("committerUserEmail"),
|
||||
db.ref("username").withSchema("committerUser").as("committerUserUsername"),
|
||||
db.ref("firstName").withSchema("committerUser").as("committerUserFirstName"),
|
||||
db.ref("lastName").withSchema("committerUser").as("committerUserLastName")
|
||||
)
|
||||
.orderBy("createdAt", "desc");
|
||||
|
||||
const docs = await (tx || db)
|
||||
.with("w", query)
|
||||
.select("*")
|
||||
.from<Awaited<typeof query>[number]>("w")
|
||||
.where("w.rank", ">=", offset)
|
||||
.andWhere("w.rank", "<", offset + limit);
|
||||
const formatedDoc = sqlNestRelationships({
|
||||
data: docs,
|
||||
key: "id",
|
||||
parentMapper: (el) => ({
|
||||
...SecretApprovalRequestsSchema.parse(el),
|
||||
environment: el.environment,
|
||||
projectId: el.projectId,
|
||||
policy: {
|
||||
id: el.policyId,
|
||||
name: el.policyName,
|
||||
approvals: el.policyApprovals,
|
||||
secretPath: el.policySecretPath,
|
||||
enforcementLevel: el.policyEnforcementLevel
|
||||
},
|
||||
committerUser: {
|
||||
userId: el.committerUserId,
|
||||
email: el.committerUserEmail,
|
||||
firstName: el.committerUserFirstName,
|
||||
lastName: el.committerUserLastName,
|
||||
username: el.committerUserUsername
|
||||
}
|
||||
}),
|
||||
childrenMapper: [
|
||||
{
|
||||
key: "reviewerId",
|
||||
label: "reviewers" as const,
|
||||
mapper: ({ reviewerUserId, reviewerStatus: s }) =>
|
||||
reviewerUserId ? { userId: reviewerUserId, status: s } : undefined
|
||||
},
|
||||
{
|
||||
key: "approverUserId",
|
||||
label: "approvers" as const,
|
||||
mapper: ({ approverUserId }) => approverUserId
|
||||
},
|
||||
{
|
||||
key: "commitId",
|
||||
label: "commits" as const,
|
||||
mapper: ({ commitSecretId: secretId, commitId: id, commitOp: op }) => ({
|
||||
op,
|
||||
id,
|
||||
secretId
|
||||
})
|
||||
}
|
||||
]
|
||||
});
|
||||
return formatedDoc.map((el) => ({
|
||||
...el,
|
||||
policy: { ...el.policy, approvers: el.approvers }
|
||||
}));
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: "FindSAR" });
|
||||
}
|
||||
};
|
||||
|
||||
const deleteByProjectId = async (projectId: string, tx?: Knex) => {
|
||||
try {
|
||||
const query = await (tx || db)(TableName.SecretApprovalRequest)
|
||||
.join(TableName.SecretFolder, `${TableName.SecretApprovalRequest}.folderId`, `${TableName.SecretFolder}.id`)
|
||||
.join(TableName.Environment, `${TableName.SecretFolder}.envId`, `${TableName.Environment}.id`)
|
||||
.where({ projectId })
|
||||
.delete();
|
||||
|
||||
return query;
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: "DeleteByProjectId" });
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
...secretApprovalRequestOrm,
|
||||
findById,
|
||||
findProjectRequestCount,
|
||||
findByProjectId,
|
||||
findByProjectIdBridgeSecretV2,
|
||||
deleteByProjectId
|
||||
};
|
||||
};
|
||||
|
@ -3,6 +3,7 @@ import { Knex } from "knex";
|
||||
import { TDbClient } from "@app/db";
|
||||
import {
|
||||
SecretApprovalRequestsSecretsSchema,
|
||||
SecretApprovalRequestsSecretsV2Schema,
|
||||
TableName,
|
||||
TSecretApprovalRequestsSecrets,
|
||||
TSecretTags
|
||||
@ -15,6 +16,8 @@ export type TSecretApprovalRequestSecretDALFactory = ReturnType<typeof secretApp
|
||||
export const secretApprovalRequestSecretDALFactory = (db: TDbClient) => {
|
||||
const secretApprovalRequestSecretOrm = ormify(db, TableName.SecretApprovalRequestSecret);
|
||||
const secretApprovalRequestSecretTagOrm = ormify(db, TableName.SecretApprovalRequestSecretTag);
|
||||
const secretApprovalRequestSecretV2TagOrm = ormify(db, TableName.SecretApprovalRequestSecretTagV2);
|
||||
const secretApprovalRequestSecretV2Orm = ormify(db, TableName.SecretApprovalRequestSecretV2);
|
||||
|
||||
const bulkUpdateNoVersionIncrement = async (data: TSecretApprovalRequestsSecrets[], tx?: Knex) => {
|
||||
try {
|
||||
@ -221,10 +224,197 @@ export const secretApprovalRequestSecretDALFactory = (db: TDbClient) => {
|
||||
throw new DatabaseError({ error, name: "FindByRequestId" });
|
||||
}
|
||||
};
|
||||
|
||||
const findByRequestIdBridgeSecretV2 = async (requestId: string, tx?: Knex) => {
|
||||
try {
|
||||
const doc = await (tx || db.replicaNode())({
|
||||
secVerTag: TableName.SecretTag
|
||||
})
|
||||
.from(TableName.SecretApprovalRequestSecretV2)
|
||||
.where({ requestId })
|
||||
.leftJoin(
|
||||
TableName.SecretApprovalRequestSecretTagV2,
|
||||
`${TableName.SecretApprovalRequestSecretV2}.id`,
|
||||
`${TableName.SecretApprovalRequestSecretTagV2}.secretId`
|
||||
)
|
||||
.leftJoin(
|
||||
TableName.SecretTag,
|
||||
`${TableName.SecretApprovalRequestSecretTagV2}.tagId`,
|
||||
`${TableName.SecretTag}.id`
|
||||
)
|
||||
.leftJoin(TableName.SecretV2, `${TableName.SecretApprovalRequestSecretV2}.secretId`, `${TableName.SecretV2}.id`)
|
||||
.leftJoin(
|
||||
TableName.SecretVersionV2,
|
||||
`${TableName.SecretVersionV2}.id`,
|
||||
`${TableName.SecretApprovalRequestSecretV2}.secretVersion`
|
||||
)
|
||||
.leftJoin(
|
||||
TableName.SecretVersionV2Tag,
|
||||
`${TableName.SecretVersionV2Tag}.${TableName.SecretVersionV2}Id`,
|
||||
`${TableName.SecretVersionV2}.id`
|
||||
)
|
||||
.leftJoin<TSecretTags>(
|
||||
db.ref(TableName.SecretTag).as("secVerTag"),
|
||||
`${TableName.SecretVersionV2Tag}.${TableName.SecretTag}Id`,
|
||||
db.ref("id").withSchema("secVerTag")
|
||||
)
|
||||
.select(selectAllTableCols(TableName.SecretApprovalRequestSecretV2))
|
||||
.select({
|
||||
secVerTagId: "secVerTag.id",
|
||||
secVerTagColor: "secVerTag.color",
|
||||
secVerTagSlug: "secVerTag.slug",
|
||||
secVerTagName: "secVerTag.name"
|
||||
})
|
||||
.select(
|
||||
db.ref("id").withSchema(TableName.SecretTag).as("tagId"),
|
||||
db.ref("id").withSchema(TableName.SecretApprovalRequestSecretTagV2).as("tagJnId"),
|
||||
db.ref("color").withSchema(TableName.SecretTag).as("tagColor"),
|
||||
db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug"),
|
||||
db.ref("name").withSchema(TableName.SecretTag).as("tagName")
|
||||
)
|
||||
.select(
|
||||
db.ref("version").withSchema(TableName.SecretV2).as("orgSecVersion"),
|
||||
db.ref("key").withSchema(TableName.SecretV2).as("orgSecKey"),
|
||||
db.ref("encryptedValue").withSchema(TableName.SecretV2).as("orgSecValue"),
|
||||
db.ref("encryptedComment").withSchema(TableName.SecretV2).as("orgSecComment")
|
||||
)
|
||||
.select(
|
||||
db.ref("version").withSchema(TableName.SecretVersionV2).as("secVerVersion"),
|
||||
db.ref("key").withSchema(TableName.SecretVersionV2).as("secVerKey"),
|
||||
db.ref("encryptedValue").withSchema(TableName.SecretVersionV2).as("secVerValue"),
|
||||
db.ref("encryptedComment").withSchema(TableName.SecretVersionV2).as("secVerComment")
|
||||
);
|
||||
const formatedDoc = sqlNestRelationships({
|
||||
data: doc,
|
||||
key: "id",
|
||||
parentMapper: (data) => SecretApprovalRequestsSecretsV2Schema.omit({ secretVersion: true }).parse(data),
|
||||
childrenMapper: [
|
||||
{
|
||||
key: "tagJnId",
|
||||
label: "tags" as const,
|
||||
mapper: ({ tagId: id, tagName: name, tagSlug: slug, tagColor: color }) => ({
|
||||
id,
|
||||
name,
|
||||
slug,
|
||||
color
|
||||
})
|
||||
},
|
||||
{
|
||||
key: "secretId",
|
||||
label: "secret" as const,
|
||||
mapper: ({ orgSecVersion, orgSecKey, orgSecValue, orgSecComment, secretId }) =>
|
||||
secretId
|
||||
? {
|
||||
id: secretId,
|
||||
version: orgSecVersion,
|
||||
key: orgSecKey,
|
||||
encryptedValue: orgSecValue,
|
||||
encryptedComment: orgSecComment
|
||||
}
|
||||
: undefined
|
||||
},
|
||||
{
|
||||
key: "secretVersion",
|
||||
label: "secretVersion" as const,
|
||||
mapper: ({ secretVersion, secVerVersion, secVerKey, secVerValue, secVerComment }) =>
|
||||
secretVersion
|
||||
? {
|
||||
version: secVerVersion,
|
||||
id: secretVersion,
|
||||
key: secVerKey,
|
||||
encryptedValue: secVerValue,
|
||||
encryptedComment: secVerComment
|
||||
}
|
||||
: undefined,
|
||||
childrenMapper: [
|
||||
{
|
||||
key: "secVerTagId",
|
||||
label: "tags" as const,
|
||||
mapper: ({ secVerTagId: id, secVerTagName: name, secVerTagSlug: slug, secVerTagColor: color }) => ({
|
||||
// eslint-disable-next-line
|
||||
id,
|
||||
// eslint-disable-next-line
|
||||
name,
|
||||
// eslint-disable-next-line
|
||||
slug,
|
||||
// eslint-disable-next-line
|
||||
color
|
||||
})
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
});
|
||||
return formatedDoc?.map(({ secret, secretVersion, ...el }) => ({
|
||||
...el,
|
||||
secret: secret?.[0],
|
||||
secretVersion: secretVersion?.[0]
|
||||
}));
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: "FindByRequestId" });
|
||||
}
|
||||
};
|
||||
// special query for migration to v2 secret
|
||||
const findByProjectId = async (projectId: string, tx?: Knex) => {
|
||||
try {
|
||||
const docs = await (tx || db)(TableName.SecretApprovalRequestSecret)
|
||||
.join(
|
||||
TableName.SecretApprovalRequest,
|
||||
`${TableName.SecretApprovalRequest}.id`,
|
||||
`${TableName.SecretApprovalRequestSecret}.requestId`
|
||||
)
|
||||
.join(TableName.SecretFolder, `${TableName.SecretApprovalRequest}.folderId`, `${TableName.SecretFolder}.id`)
|
||||
.join(TableName.Environment, `${TableName.SecretFolder}.envId`, `${TableName.Environment}.id`)
|
||||
.leftJoin(
|
||||
TableName.SecretApprovalRequestSecretTag,
|
||||
`${TableName.SecretApprovalRequestSecret}.id`,
|
||||
`${TableName.SecretApprovalRequestSecretTag}.secretId`
|
||||
)
|
||||
.where({ projectId })
|
||||
.select(selectAllTableCols(TableName.SecretApprovalRequestSecret))
|
||||
.select(
|
||||
db.ref("id").withSchema(TableName.SecretApprovalRequestSecretTag).as("secretApprovalTagId"),
|
||||
db.ref("secretId").withSchema(TableName.SecretApprovalRequestSecretTag).as("secretApprovalTagSecretId"),
|
||||
db.ref("tagId").withSchema(TableName.SecretApprovalRequestSecretTag).as("secretApprovalTagSecretTagId"),
|
||||
db.ref("createdAt").withSchema(TableName.SecretApprovalRequestSecretTag).as("secretApprovalTagCreatedAt"),
|
||||
db.ref("updatedAt").withSchema(TableName.SecretApprovalRequestSecretTag).as("secretApprovalTagUpdatedAt")
|
||||
);
|
||||
const formatedDoc = sqlNestRelationships({
|
||||
data: docs,
|
||||
key: "id",
|
||||
parentMapper: (data) => SecretApprovalRequestsSecretsSchema.parse(data),
|
||||
childrenMapper: [
|
||||
{
|
||||
key: "secretApprovalTagId",
|
||||
label: "tags" as const,
|
||||
mapper: ({
|
||||
secretApprovalTagSecretId,
|
||||
secretApprovalTagId,
|
||||
secretApprovalTagUpdatedAt,
|
||||
secretApprovalTagCreatedAt
|
||||
}) => ({
|
||||
secretApprovalTagSecretId,
|
||||
secretApprovalTagId,
|
||||
secretApprovalTagUpdatedAt,
|
||||
secretApprovalTagCreatedAt
|
||||
})
|
||||
}
|
||||
]
|
||||
});
|
||||
return formatedDoc;
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: "FindByRequestId" });
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
...secretApprovalRequestSecretOrm,
|
||||
insertV2Bridge: secretApprovalRequestSecretV2Orm.insertMany,
|
||||
findByRequestId,
|
||||
findByRequestIdBridgeSecretV2,
|
||||
bulkUpdateNoVersionIncrement,
|
||||
insertApprovalSecretTags: secretApprovalRequestSecretTagOrm.insertMany
|
||||
findByProjectId,
|
||||
insertApprovalSecretTags: secretApprovalRequestSecretTagOrm.insertMany,
|
||||
insertApprovalSecretV2Tags: secretApprovalRequestSecretV2TagOrm.insertMany
|
||||
};
|
||||
};
|
||||
|
@ -5,17 +5,25 @@ import {
|
||||
SecretEncryptionAlgo,
|
||||
SecretKeyEncoding,
|
||||
SecretType,
|
||||
TSecretApprovalRequestsSecretsInsert
|
||||
TSecretApprovalRequestsSecretsInsert,
|
||||
TSecretApprovalRequestsSecretsV2Insert
|
||||
} from "@app/db/schemas";
|
||||
import { getConfig } from "@app/lib/config/env";
|
||||
import { decryptSymmetric128BitHexKeyUTF8 } from "@app/lib/crypto";
|
||||
import { BadRequestError, UnauthorizedError } from "@app/lib/errors";
|
||||
import { groupBy, pick, unique } from "@app/lib/fn";
|
||||
import { setKnexStringValue } from "@app/lib/knex";
|
||||
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
||||
import { EnforcementLevel } from "@app/lib/types";
|
||||
import { ActorType } from "@app/services/auth/auth-type";
|
||||
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
||||
import { KmsDataKey } from "@app/services/kms/kms-types";
|
||||
import { TProjectDALFactory } from "@app/services/project/project-dal";
|
||||
import { TProjectBotServiceFactory } from "@app/services/project-bot/project-bot-service";
|
||||
import { TProjectEnvDALFactory } from "@app/services/project-env/project-env-dal";
|
||||
import { TSecretDALFactory } from "@app/services/secret/secret-dal";
|
||||
import {
|
||||
decryptSecretWithBot,
|
||||
fnSecretBlindIndexCheck,
|
||||
fnSecretBlindIndexCheckV2,
|
||||
fnSecretBulkDelete,
|
||||
@ -30,6 +38,17 @@ import { TSecretVersionTagDALFactory } from "@app/services/secret/secret-version
|
||||
import { TSecretBlindIndexDALFactory } from "@app/services/secret-blind-index/secret-blind-index-dal";
|
||||
import { TSecretFolderDALFactory } from "@app/services/secret-folder/secret-folder-dal";
|
||||
import { TSecretTagDALFactory } from "@app/services/secret-tag/secret-tag-dal";
|
||||
import { TSecretV2BridgeDALFactory } from "@app/services/secret-v2-bridge/secret-v2-bridge-dal";
|
||||
import {
|
||||
fnSecretBulkDelete as fnSecretV2BridgeBulkDelete,
|
||||
fnSecretBulkInsert as fnSecretV2BridgeBulkInsert,
|
||||
fnSecretBulkUpdate as fnSecretV2BridgeBulkUpdate,
|
||||
getAllNestedSecretReferences as getAllNestedSecretReferencesV2Bridge
|
||||
} from "@app/services/secret-v2-bridge/secret-v2-bridge-fns";
|
||||
import { TSecretVersionV2DALFactory } from "@app/services/secret-v2-bridge/secret-version-dal";
|
||||
import { TSecretVersionV2TagDALFactory } from "@app/services/secret-v2-bridge/secret-version-tag-dal";
|
||||
import { SmtpTemplates, TSmtpService } from "@app/services/smtp/smtp-service";
|
||||
import { TUserDALFactory } from "@app/services/user/user-dal";
|
||||
|
||||
import { TPermissionServiceFactory } from "../permission/permission-service";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub } from "../permission/project-permission";
|
||||
@ -42,6 +61,7 @@ import {
|
||||
RequestState,
|
||||
TApprovalRequestCountDTO,
|
||||
TGenerateSecretApprovalRequestDTO,
|
||||
TGenerateSecretApprovalRequestV2BridgeDTO,
|
||||
TListApprovalsDTO,
|
||||
TMergeSecretApprovalRequestDTO,
|
||||
TReviewRequestDTO,
|
||||
@ -57,13 +77,26 @@ type TSecretApprovalRequestServiceFactoryDep = {
|
||||
secretApprovalRequestReviewerDAL: TSecretApprovalRequestReviewerDALFactory;
|
||||
folderDAL: Pick<TSecretFolderDALFactory, "findBySecretPath" | "findSecretPathByFolderIds">;
|
||||
secretDAL: TSecretDALFactory;
|
||||
secretTagDAL: Pick<TSecretTagDALFactory, "findManyTagsById" | "saveTagsToSecret" | "deleteTagsManySecret">;
|
||||
secretTagDAL: Pick<
|
||||
TSecretTagDALFactory,
|
||||
"findManyTagsById" | "saveTagsToSecret" | "deleteTagsManySecret" | "saveTagsToSecretV2" | "deleteTagsToSecretV2"
|
||||
>;
|
||||
secretBlindIndexDAL: Pick<TSecretBlindIndexDALFactory, "findOne">;
|
||||
snapshotService: Pick<TSecretSnapshotServiceFactory, "performSnapshot">;
|
||||
secretVersionDAL: Pick<TSecretVersionDALFactory, "findLatestVersionMany" | "insertMany">;
|
||||
secretVersionTagDAL: Pick<TSecretVersionTagDALFactory, "insertMany">;
|
||||
projectDAL: Pick<TProjectDALFactory, "checkProjectUpgradeStatus">;
|
||||
smtpService: Pick<TSmtpService, "sendMail">;
|
||||
userDAL: Pick<TUserDALFactory, "find" | "findOne">;
|
||||
projectEnvDAL: Pick<TProjectEnvDALFactory, "findOne">;
|
||||
projectDAL: Pick<TProjectDALFactory, "checkProjectUpgradeStatus" | "findById" | "findProjectById">;
|
||||
secretQueueService: Pick<TSecretQueueFactory, "syncSecrets" | "removeSecretReminder">;
|
||||
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey" | "encryptWithInputKey" | "decryptWithInputKey">;
|
||||
secretV2BridgeDAL: Pick<
|
||||
TSecretV2BridgeDALFactory,
|
||||
"insertMany" | "upsertSecretReferences" | "findBySecretKeys" | "bulkUpdate" | "deleteMany"
|
||||
>;
|
||||
secretVersionV2BridgeDAL: Pick<TSecretVersionV2DALFactory, "insertMany" | "findLatestVersionMany">;
|
||||
secretVersionTagV2BridgeDAL: Pick<TSecretVersionV2TagDALFactory, "insertMany">;
|
||||
};
|
||||
|
||||
export type TSecretApprovalRequestServiceFactory = ReturnType<typeof secretApprovalRequestServiceFactory>;
|
||||
@ -82,7 +115,14 @@ export const secretApprovalRequestServiceFactory = ({
|
||||
snapshotService,
|
||||
secretVersionDAL,
|
||||
secretQueueService,
|
||||
projectBotService
|
||||
projectBotService,
|
||||
smtpService,
|
||||
userDAL,
|
||||
projectEnvDAL,
|
||||
kmsService,
|
||||
secretV2BridgeDAL,
|
||||
secretVersionV2BridgeDAL,
|
||||
secretVersionTagV2BridgeDAL
|
||||
}: TSecretApprovalRequestServiceFactoryDep) => {
|
||||
const requestCount = async ({ projectId, actor, actorId, actorOrgId, actorAuthMethod }: TApprovalRequestCountDTO) => {
|
||||
if (actor === ActorType.SERVICE) throw new BadRequestError({ message: "Cannot use service token" });
|
||||
@ -114,6 +154,19 @@ export const secretApprovalRequestServiceFactory = ({
|
||||
if (actor === ActorType.SERVICE) throw new BadRequestError({ message: "Cannot use service token" });
|
||||
|
||||
await permissionService.getProjectPermission(actor, actorId, projectId, actorAuthMethod, actorOrgId);
|
||||
|
||||
const { shouldUseSecretV2Bridge } = await projectBotService.getBotKey(projectId);
|
||||
if (shouldUseSecretV2Bridge) {
|
||||
return secretApprovalRequestDAL.findByProjectIdBridgeSecretV2({
|
||||
projectId,
|
||||
committer,
|
||||
environment,
|
||||
status,
|
||||
userId: actorId,
|
||||
limit,
|
||||
offset
|
||||
});
|
||||
}
|
||||
const approvals = await secretApprovalRequestDAL.findByProjectId({
|
||||
projectId,
|
||||
committer,
|
||||
@ -138,11 +191,14 @@ export const secretApprovalRequestServiceFactory = ({
|
||||
const secretApprovalRequest = await secretApprovalRequestDAL.findById(id);
|
||||
if (!secretApprovalRequest) throw new BadRequestError({ message: "Secret approval request not found" });
|
||||
|
||||
const { projectId } = secretApprovalRequest;
|
||||
const { botKey, shouldUseSecretV2Bridge } = await projectBotService.getBotKey(projectId);
|
||||
|
||||
const { policy } = secretApprovalRequest;
|
||||
const { hasRole } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
actorId,
|
||||
secretApprovalRequest.projectId,
|
||||
projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
@ -154,7 +210,75 @@ export const secretApprovalRequestServiceFactory = ({
|
||||
throw new UnauthorizedError({ message: "User has no access" });
|
||||
}
|
||||
|
||||
const secrets = await secretApprovalRequestSecretDAL.findByRequestId(secretApprovalRequest.id);
|
||||
let secrets;
|
||||
if (shouldUseSecretV2Bridge) {
|
||||
const { decryptor: secretManagerDecryptor } = await kmsService.createCipherPairWithDataKey({
|
||||
type: KmsDataKey.SecretManager,
|
||||
projectId
|
||||
});
|
||||
const encrypedSecrets = await secretApprovalRequestSecretDAL.findByRequestIdBridgeSecretV2(
|
||||
secretApprovalRequest.id
|
||||
);
|
||||
secrets = encrypedSecrets.map((el) => ({
|
||||
...el,
|
||||
secretKey: el.key,
|
||||
id: el.id,
|
||||
version: el.version,
|
||||
secretValue: el.encryptedValue
|
||||
? secretManagerDecryptor({ cipherTextBlob: el.encryptedValue }).toString()
|
||||
: undefined,
|
||||
secretComment: el.encryptedComment
|
||||
? secretManagerDecryptor({ cipherTextBlob: el.encryptedComment }).toString()
|
||||
: undefined,
|
||||
secret: el.secret
|
||||
? {
|
||||
secretKey: el.secret.key,
|
||||
id: el.secret.id,
|
||||
version: el.secret.version,
|
||||
secretValue: el.secret.encryptedValue
|
||||
? secretManagerDecryptor({ cipherTextBlob: el.secret.encryptedValue }).toString()
|
||||
: undefined,
|
||||
secretComment: el.secret.encryptedComment
|
||||
? secretManagerDecryptor({ cipherTextBlob: el.secret.encryptedComment }).toString()
|
||||
: undefined
|
||||
}
|
||||
: undefined,
|
||||
secretVersion: el.secretVersion
|
||||
? {
|
||||
secretKey: el.secretVersion.key,
|
||||
id: el.secretVersion.id,
|
||||
version: el.secretVersion.version,
|
||||
secretValue: el.secretVersion.encryptedValue
|
||||
? secretManagerDecryptor({ cipherTextBlob: el.secretVersion.encryptedValue }).toString()
|
||||
: undefined,
|
||||
secretComment: el.secretVersion.encryptedComment
|
||||
? secretManagerDecryptor({ cipherTextBlob: el.secretVersion.encryptedComment }).toString()
|
||||
: undefined
|
||||
}
|
||||
: undefined
|
||||
}));
|
||||
} else {
|
||||
if (!botKey) throw new BadRequestError({ message: "Bot key not found" });
|
||||
const encrypedSecrets = await secretApprovalRequestSecretDAL.findByRequestId(secretApprovalRequest.id);
|
||||
secrets = encrypedSecrets.map((el) => ({
|
||||
...el,
|
||||
...decryptSecretWithBot(el, botKey),
|
||||
secret: el.secret
|
||||
? {
|
||||
id: el.secret.id,
|
||||
version: el.secret.version,
|
||||
...decryptSecretWithBot(el.secret, botKey)
|
||||
}
|
||||
: undefined,
|
||||
secretVersion: el.secretVersion
|
||||
? {
|
||||
id: el.secretVersion.id,
|
||||
version: el.secretVersion.version,
|
||||
...decryptSecretWithBot(el.secretVersion, botKey)
|
||||
}
|
||||
: undefined
|
||||
}));
|
||||
}
|
||||
const secretPath = await folderDAL.findSecretPathByFolderIds(secretApprovalRequest.projectId, [
|
||||
secretApprovalRequest.folderId
|
||||
]);
|
||||
@ -257,7 +381,8 @@ export const secretApprovalRequestServiceFactory = ({
|
||||
actor,
|
||||
actorId,
|
||||
actorOrgId,
|
||||
actorAuthMethod
|
||||
actorAuthMethod,
|
||||
bypassReason
|
||||
}: TMergeSecretApprovalRequestDTO) => {
|
||||
const secretApprovalRequest = await secretApprovalRequestDAL.findById(approvalId);
|
||||
if (!secretApprovalRequest) throw new BadRequestError({ message: "Secret approval request not found" });
|
||||
@ -288,45 +413,167 @@ export const secretApprovalRequestServiceFactory = ({
|
||||
secretApprovalRequest.policy.approvers.filter(
|
||||
({ userId: approverId }) => reviewers[approverId.toString()] === ApprovalStatus.APPROVED
|
||||
).length;
|
||||
const isSoftEnforcement = secretApprovalRequest.policy.enforcementLevel === EnforcementLevel.Soft;
|
||||
|
||||
if (!hasMinApproval) throw new BadRequestError({ message: "Doesn't have minimum approvals needed" });
|
||||
const secretApprovalSecrets = await secretApprovalRequestSecretDAL.findByRequestId(secretApprovalRequest.id);
|
||||
if (!secretApprovalSecrets) throw new BadRequestError({ message: "No secrets found" });
|
||||
if (!hasMinApproval && !isSoftEnforcement)
|
||||
throw new BadRequestError({ message: "Doesn't have minimum approvals needed" });
|
||||
|
||||
const conflicts: Array<{ secretId: string; op: SecretOperations }> = [];
|
||||
let secretCreationCommits = secretApprovalSecrets.filter(({ op }) => op === SecretOperations.Create);
|
||||
if (secretCreationCommits.length) {
|
||||
const { secsGroupedByBlindIndex: conflictGroupByBlindIndex } = await fnSecretBlindIndexCheckV2({
|
||||
folderId,
|
||||
secretDAL,
|
||||
inputSecrets: secretCreationCommits.map(({ secretBlindIndex }) => {
|
||||
if (!secretBlindIndex) {
|
||||
throw new BadRequestError({
|
||||
message: "Missing secret blind index"
|
||||
});
|
||||
}
|
||||
return { secretBlindIndex };
|
||||
})
|
||||
});
|
||||
secretCreationCommits
|
||||
.filter(({ secretBlindIndex }) => conflictGroupByBlindIndex[secretBlindIndex || ""])
|
||||
.forEach((el) => {
|
||||
conflicts.push({ op: SecretOperations.Create, secretId: el.id });
|
||||
});
|
||||
secretCreationCommits = secretCreationCommits.filter(
|
||||
({ secretBlindIndex }) => !conflictGroupByBlindIndex[secretBlindIndex || ""]
|
||||
const { botKey, shouldUseSecretV2Bridge } = await projectBotService.getBotKey(projectId);
|
||||
let mergeStatus;
|
||||
if (shouldUseSecretV2Bridge) {
|
||||
// this cycle if for bridged secrets
|
||||
const secretApprovalSecrets = await secretApprovalRequestSecretDAL.findByRequestIdBridgeSecretV2(
|
||||
secretApprovalRequest.id
|
||||
);
|
||||
}
|
||||
if (!secretApprovalSecrets) throw new BadRequestError({ message: "No secrets found" });
|
||||
|
||||
let secretUpdationCommits = secretApprovalSecrets.filter(({ op }) => op === SecretOperations.Update);
|
||||
if (secretUpdationCommits.length) {
|
||||
const { secsGroupedByBlindIndex: conflictGroupByBlindIndex } = await fnSecretBlindIndexCheckV2({
|
||||
folderId,
|
||||
secretDAL,
|
||||
userId: "",
|
||||
inputSecrets: secretUpdationCommits
|
||||
.filter(({ secretBlindIndex, secret }) => secret && secret.secretBlindIndex !== secretBlindIndex)
|
||||
.map(({ secretBlindIndex }) => {
|
||||
const { decryptor: secretManagerDecryptor } = await kmsService.createCipherPairWithDataKey({
|
||||
type: KmsDataKey.SecretManager,
|
||||
projectId
|
||||
});
|
||||
|
||||
const conflicts: Array<{ secretId: string; op: SecretOperations }> = [];
|
||||
let secretCreationCommits = secretApprovalSecrets.filter(({ op }) => op === SecretOperations.Create);
|
||||
if (secretCreationCommits.length) {
|
||||
const secrets = await secretV2BridgeDAL.findBySecretKeys(
|
||||
folderId,
|
||||
secretCreationCommits.map((el) => ({
|
||||
key: el.key,
|
||||
type: SecretType.Shared
|
||||
}))
|
||||
);
|
||||
const creationConflictSecretsGroupByKey = groupBy(secrets, (i) => i.key);
|
||||
secretCreationCommits
|
||||
.filter(({ key }) => creationConflictSecretsGroupByKey[key])
|
||||
.forEach((el) => {
|
||||
conflicts.push({ op: SecretOperations.Create, secretId: el.id });
|
||||
});
|
||||
secretCreationCommits = secretCreationCommits.filter(({ key }) => !creationConflictSecretsGroupByKey[key]);
|
||||
}
|
||||
|
||||
let secretUpdationCommits = secretApprovalSecrets.filter(({ op }) => op === SecretOperations.Update);
|
||||
if (secretUpdationCommits.length) {
|
||||
const secrets = await secretV2BridgeDAL.findBySecretKeys(
|
||||
folderId,
|
||||
secretCreationCommits.map((el) => ({
|
||||
key: el.key,
|
||||
type: SecretType.Shared
|
||||
}))
|
||||
);
|
||||
const updationConflictSecretsGroupByKey = groupBy(secrets, (i) => i.key);
|
||||
secretUpdationCommits
|
||||
.filter(({ key, secretId }) => updationConflictSecretsGroupByKey[key] || !secretId)
|
||||
.forEach((el) => {
|
||||
conflicts.push({ op: SecretOperations.Update, secretId: el.id });
|
||||
});
|
||||
|
||||
secretUpdationCommits = secretUpdationCommits.filter(
|
||||
({ key, secretId }) => Boolean(secretId) && !updationConflictSecretsGroupByKey[key]
|
||||
);
|
||||
}
|
||||
|
||||
const secretDeletionCommits = secretApprovalSecrets.filter(({ op }) => op === SecretOperations.Delete);
|
||||
mergeStatus = await secretApprovalRequestDAL.transaction(async (tx) => {
|
||||
const newSecrets = secretCreationCommits.length
|
||||
? await fnSecretV2BridgeBulkInsert({
|
||||
tx,
|
||||
folderId,
|
||||
inputSecrets: secretCreationCommits.map((el) => ({
|
||||
tagIds: el?.tags.map(({ id }) => id),
|
||||
version: 1,
|
||||
encryptedComment: el.encryptedComment,
|
||||
encryptedValue: el.encryptedValue,
|
||||
skipMultilineEncoding: el.skipMultilineEncoding,
|
||||
key: el.key,
|
||||
references: el.encryptedValue
|
||||
? getAllNestedSecretReferencesV2Bridge(
|
||||
secretManagerDecryptor({
|
||||
cipherTextBlob: el.encryptedValue
|
||||
}).toString()
|
||||
)
|
||||
: [],
|
||||
type: SecretType.Shared
|
||||
})),
|
||||
secretDAL: secretV2BridgeDAL,
|
||||
secretVersionDAL: secretVersionV2BridgeDAL,
|
||||
secretTagDAL,
|
||||
secretVersionTagDAL: secretVersionTagV2BridgeDAL
|
||||
})
|
||||
: [];
|
||||
const updatedSecrets = secretUpdationCommits.length
|
||||
? await fnSecretV2BridgeBulkUpdate({
|
||||
folderId,
|
||||
tx,
|
||||
inputSecrets: secretUpdationCommits.map((el) => {
|
||||
const encryptedValue =
|
||||
typeof el.encryptedValue !== "undefined"
|
||||
? {
|
||||
encryptedValue: el.encryptedValue as Buffer,
|
||||
references: el.encryptedValue
|
||||
? getAllNestedSecretReferencesV2Bridge(
|
||||
secretManagerDecryptor({
|
||||
cipherTextBlob: el.encryptedValue
|
||||
}).toString()
|
||||
)
|
||||
: []
|
||||
}
|
||||
: {};
|
||||
return {
|
||||
filter: { id: el.secretId as string, type: SecretType.Shared },
|
||||
data: {
|
||||
reminderRepeatDays: el.reminderRepeatDays,
|
||||
encryptedComment: el.encryptedComment,
|
||||
reminderNote: el.reminderNote,
|
||||
skipMultilineEncoding: el.skipMultilineEncoding,
|
||||
key: el.key,
|
||||
tagIds: el?.tags.map(({ id }) => id),
|
||||
...encryptedValue
|
||||
}
|
||||
};
|
||||
}),
|
||||
secretDAL: secretV2BridgeDAL,
|
||||
secretVersionDAL: secretVersionV2BridgeDAL,
|
||||
secretTagDAL,
|
||||
secretVersionTagDAL: secretVersionTagV2BridgeDAL
|
||||
})
|
||||
: [];
|
||||
const deletedSecret = secretDeletionCommits.length
|
||||
? await fnSecretV2BridgeBulkDelete({
|
||||
projectId,
|
||||
folderId,
|
||||
tx,
|
||||
actorId: "",
|
||||
secretDAL: secretV2BridgeDAL,
|
||||
secretQueueService,
|
||||
inputSecrets: secretDeletionCommits.map(({ key }) => ({ secretKey: key, type: SecretType.Shared }))
|
||||
})
|
||||
: [];
|
||||
const updatedSecretApproval = await secretApprovalRequestDAL.updateById(
|
||||
secretApprovalRequest.id,
|
||||
{
|
||||
conflicts: JSON.stringify(conflicts),
|
||||
hasMerged: true,
|
||||
status: RequestState.Closed,
|
||||
statusChangedByUserId: actorId
|
||||
},
|
||||
tx
|
||||
);
|
||||
return {
|
||||
secrets: { created: newSecrets, updated: updatedSecrets, deleted: deletedSecret },
|
||||
approval: updatedSecretApproval
|
||||
};
|
||||
});
|
||||
} else {
|
||||
const secretApprovalSecrets = await secretApprovalRequestSecretDAL.findByRequestId(secretApprovalRequest.id);
|
||||
if (!secretApprovalSecrets) throw new BadRequestError({ message: "No secrets found" });
|
||||
|
||||
const conflicts: Array<{ secretId: string; op: SecretOperations }> = [];
|
||||
let secretCreationCommits = secretApprovalSecrets.filter(({ op }) => op === SecretOperations.Create);
|
||||
if (secretCreationCommits.length) {
|
||||
const { secsGroupedByBlindIndex: conflictGroupByBlindIndex } = await fnSecretBlindIndexCheckV2({
|
||||
folderId,
|
||||
secretDAL,
|
||||
inputSecrets: secretCreationCommits.map(({ secretBlindIndex }) => {
|
||||
if (!secretBlindIndex) {
|
||||
throw new BadRequestError({
|
||||
message: "Missing secret blind index"
|
||||
@ -334,80 +581,56 @@ export const secretApprovalRequestServiceFactory = ({
|
||||
}
|
||||
return { secretBlindIndex };
|
||||
})
|
||||
});
|
||||
secretUpdationCommits
|
||||
.filter(
|
||||
({ secretBlindIndex, secretId }) =>
|
||||
(secretBlindIndex && conflictGroupByBlindIndex[secretBlindIndex]) || !secretId
|
||||
)
|
||||
.forEach((el) => {
|
||||
conflicts.push({ op: SecretOperations.Update, secretId: el.id });
|
||||
});
|
||||
secretCreationCommits
|
||||
.filter(({ secretBlindIndex }) => conflictGroupByBlindIndex[secretBlindIndex || ""])
|
||||
.forEach((el) => {
|
||||
conflicts.push({ op: SecretOperations.Create, secretId: el.id });
|
||||
});
|
||||
secretCreationCommits = secretCreationCommits.filter(
|
||||
({ secretBlindIndex }) => !conflictGroupByBlindIndex[secretBlindIndex || ""]
|
||||
);
|
||||
}
|
||||
|
||||
secretUpdationCommits = secretUpdationCommits.filter(
|
||||
({ secretBlindIndex, secretId }) =>
|
||||
Boolean(secretId) && (secretBlindIndex ? !conflictGroupByBlindIndex[secretBlindIndex] : true)
|
||||
);
|
||||
}
|
||||
let secretUpdationCommits = secretApprovalSecrets.filter(({ op }) => op === SecretOperations.Update);
|
||||
if (secretUpdationCommits.length) {
|
||||
const { secsGroupedByBlindIndex: conflictGroupByBlindIndex } = await fnSecretBlindIndexCheckV2({
|
||||
folderId,
|
||||
secretDAL,
|
||||
userId: "",
|
||||
inputSecrets: secretUpdationCommits
|
||||
.filter(({ secretBlindIndex, secret }) => secret && secret.secretBlindIndex !== secretBlindIndex)
|
||||
.map(({ secretBlindIndex }) => {
|
||||
if (!secretBlindIndex) {
|
||||
throw new BadRequestError({
|
||||
message: "Missing secret blind index"
|
||||
});
|
||||
}
|
||||
return { secretBlindIndex };
|
||||
})
|
||||
});
|
||||
secretUpdationCommits
|
||||
.filter(
|
||||
({ secretBlindIndex, secretId }) =>
|
||||
(secretBlindIndex && conflictGroupByBlindIndex[secretBlindIndex]) || !secretId
|
||||
)
|
||||
.forEach((el) => {
|
||||
conflicts.push({ op: SecretOperations.Update, secretId: el.id });
|
||||
});
|
||||
|
||||
const secretDeletionCommits = secretApprovalSecrets.filter(({ op }) => op === SecretOperations.Delete);
|
||||
const botKey = await projectBotService.getBotKey(projectId).catch(() => null);
|
||||
const mergeStatus = await secretApprovalRequestDAL.transaction(async (tx) => {
|
||||
const newSecrets = secretCreationCommits.length
|
||||
? await fnSecretBulkInsert({
|
||||
tx,
|
||||
folderId,
|
||||
inputSecrets: secretCreationCommits.map((el) => ({
|
||||
...pick(el, [
|
||||
"secretCommentCiphertext",
|
||||
"secretCommentTag",
|
||||
"secretCommentIV",
|
||||
"secretValueIV",
|
||||
"secretValueTag",
|
||||
"secretValueCiphertext",
|
||||
"secretKeyCiphertext",
|
||||
"secretKeyTag",
|
||||
"secretKeyIV",
|
||||
"metadata",
|
||||
"skipMultilineEncoding",
|
||||
"secretReminderNote",
|
||||
"secretReminderRepeatDays",
|
||||
"algorithm",
|
||||
"keyEncoding",
|
||||
"secretBlindIndex"
|
||||
]),
|
||||
tags: el?.tags.map(({ id }) => id),
|
||||
version: 1,
|
||||
type: SecretType.Shared,
|
||||
references: botKey
|
||||
? getAllNestedSecretReferences(
|
||||
decryptSymmetric128BitHexKeyUTF8({
|
||||
ciphertext: el.secretValueCiphertext,
|
||||
iv: el.secretValueIV,
|
||||
tag: el.secretValueTag,
|
||||
key: botKey
|
||||
})
|
||||
)
|
||||
: undefined
|
||||
})),
|
||||
secretDAL,
|
||||
secretVersionDAL,
|
||||
secretTagDAL,
|
||||
secretVersionTagDAL
|
||||
})
|
||||
: [];
|
||||
const updatedSecrets = secretUpdationCommits.length
|
||||
? await fnSecretBulkUpdate({
|
||||
folderId,
|
||||
projectId,
|
||||
tx,
|
||||
inputSecrets: secretUpdationCommits.map((el) => ({
|
||||
filter: {
|
||||
id: el.secretId as string, // this null check is already checked at top on conflict strategy
|
||||
type: SecretType.Shared
|
||||
},
|
||||
data: {
|
||||
tags: el?.tags.map(({ id }) => id),
|
||||
secretUpdationCommits = secretUpdationCommits.filter(
|
||||
({ secretBlindIndex, secretId }) =>
|
||||
Boolean(secretId) && (secretBlindIndex ? !conflictGroupByBlindIndex[secretBlindIndex] : true)
|
||||
);
|
||||
}
|
||||
|
||||
const secretDeletionCommits = secretApprovalSecrets.filter(({ op }) => op === SecretOperations.Delete);
|
||||
mergeStatus = await secretApprovalRequestDAL.transaction(async (tx) => {
|
||||
const newSecrets = secretCreationCommits.length
|
||||
? await fnSecretBulkInsert({
|
||||
tx,
|
||||
folderId,
|
||||
inputSecrets: secretCreationCommits.map((el) => ({
|
||||
...pick(el, [
|
||||
"secretCommentCiphertext",
|
||||
"secretCommentTag",
|
||||
@ -422,8 +645,13 @@ export const secretApprovalRequestServiceFactory = ({
|
||||
"skipMultilineEncoding",
|
||||
"secretReminderNote",
|
||||
"secretReminderRepeatDays",
|
||||
"algorithm",
|
||||
"keyEncoding",
|
||||
"secretBlindIndex"
|
||||
]),
|
||||
tags: el?.tags.map(({ id }) => id),
|
||||
version: 1,
|
||||
type: SecretType.Shared,
|
||||
references: botKey
|
||||
? getAllNestedSecretReferences(
|
||||
decryptSymmetric128BitHexKeyUTF8({
|
||||
@ -434,47 +662,94 @@ export const secretApprovalRequestServiceFactory = ({
|
||||
})
|
||||
)
|
||||
: undefined
|
||||
}
|
||||
})),
|
||||
secretDAL,
|
||||
secretVersionDAL,
|
||||
secretTagDAL,
|
||||
secretVersionTagDAL
|
||||
})
|
||||
: [];
|
||||
const deletedSecret = secretDeletionCommits.length
|
||||
? await fnSecretBulkDelete({
|
||||
projectId,
|
||||
folderId,
|
||||
tx,
|
||||
actorId: "",
|
||||
secretDAL,
|
||||
secretQueueService,
|
||||
inputSecrets: secretDeletionCommits.map(({ secretBlindIndex }) => {
|
||||
if (!secretBlindIndex) {
|
||||
throw new BadRequestError({
|
||||
message: "Missing secret blind index"
|
||||
});
|
||||
}
|
||||
return { secretBlindIndex, type: SecretType.Shared };
|
||||
})),
|
||||
secretDAL,
|
||||
secretVersionDAL,
|
||||
secretTagDAL,
|
||||
secretVersionTagDAL
|
||||
})
|
||||
})
|
||||
: [];
|
||||
const updatedSecretApproval = await secretApprovalRequestDAL.updateById(
|
||||
secretApprovalRequest.id,
|
||||
{
|
||||
conflicts: JSON.stringify(conflicts),
|
||||
hasMerged: true,
|
||||
status: RequestState.Closed,
|
||||
statusChangedByUserId: actorId
|
||||
},
|
||||
tx
|
||||
);
|
||||
return {
|
||||
secrets: { created: newSecrets, updated: updatedSecrets, deleted: deletedSecret },
|
||||
approval: updatedSecretApproval
|
||||
};
|
||||
});
|
||||
: [];
|
||||
const updatedSecrets = secretUpdationCommits.length
|
||||
? await fnSecretBulkUpdate({
|
||||
folderId,
|
||||
projectId,
|
||||
tx,
|
||||
inputSecrets: secretUpdationCommits.map((el) => ({
|
||||
filter: {
|
||||
id: el.secretId as string, // this null check is already checked at top on conflict strategy
|
||||
type: SecretType.Shared
|
||||
},
|
||||
data: {
|
||||
tags: el?.tags.map(({ id }) => id),
|
||||
...pick(el, [
|
||||
"secretCommentCiphertext",
|
||||
"secretCommentTag",
|
||||
"secretCommentIV",
|
||||
"secretValueIV",
|
||||
"secretValueTag",
|
||||
"secretValueCiphertext",
|
||||
"secretKeyCiphertext",
|
||||
"secretKeyTag",
|
||||
"secretKeyIV",
|
||||
"metadata",
|
||||
"skipMultilineEncoding",
|
||||
"secretReminderNote",
|
||||
"secretReminderRepeatDays",
|
||||
"secretBlindIndex"
|
||||
]),
|
||||
references: botKey
|
||||
? getAllNestedSecretReferences(
|
||||
decryptSymmetric128BitHexKeyUTF8({
|
||||
ciphertext: el.secretValueCiphertext,
|
||||
iv: el.secretValueIV,
|
||||
tag: el.secretValueTag,
|
||||
key: botKey
|
||||
})
|
||||
)
|
||||
: undefined
|
||||
}
|
||||
})),
|
||||
secretDAL,
|
||||
secretVersionDAL,
|
||||
secretTagDAL,
|
||||
secretVersionTagDAL
|
||||
})
|
||||
: [];
|
||||
const deletedSecret = secretDeletionCommits.length
|
||||
? await fnSecretBulkDelete({
|
||||
projectId,
|
||||
folderId,
|
||||
tx,
|
||||
actorId: "",
|
||||
secretDAL,
|
||||
secretQueueService,
|
||||
inputSecrets: secretDeletionCommits.map(({ secretBlindIndex }) => {
|
||||
if (!secretBlindIndex) {
|
||||
throw new BadRequestError({
|
||||
message: "Missing secret blind index"
|
||||
});
|
||||
}
|
||||
return { secretBlindIndex, type: SecretType.Shared };
|
||||
})
|
||||
})
|
||||
: [];
|
||||
const updatedSecretApproval = await secretApprovalRequestDAL.updateById(
|
||||
secretApprovalRequest.id,
|
||||
{
|
||||
conflicts: JSON.stringify(conflicts),
|
||||
hasMerged: true,
|
||||
status: RequestState.Closed,
|
||||
statusChangedByUserId: actorId
|
||||
},
|
||||
tx
|
||||
);
|
||||
return {
|
||||
secrets: { created: newSecrets, updated: updatedSecrets, deleted: deletedSecret },
|
||||
approval: updatedSecretApproval
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
await snapshotService.performSnapshot(folderId);
|
||||
const [folder] = await folderDAL.findSecretPathByFolderIds(projectId, [folderId]);
|
||||
if (!folder) throw new BadRequestError({ message: "Folder not found" });
|
||||
@ -485,6 +760,35 @@ export const secretApprovalRequestServiceFactory = ({
|
||||
actorId,
|
||||
actor
|
||||
});
|
||||
|
||||
if (isSoftEnforcement) {
|
||||
const cfg = getConfig();
|
||||
const project = await projectDAL.findProjectById(projectId);
|
||||
const env = await projectEnvDAL.findOne({ id: policy.envId });
|
||||
const requestedByUser = await userDAL.findOne({ id: actorId });
|
||||
const approverUsers = await userDAL.find({
|
||||
$in: {
|
||||
id: policy.approvers.map((approver: { userId: string }) => approver.userId)
|
||||
}
|
||||
});
|
||||
|
||||
await smtpService.sendMail({
|
||||
recipients: approverUsers.filter((approver) => approver.email).map((approver) => approver.email!),
|
||||
subjectLine: "Infisical Secret Change Policy Bypassed",
|
||||
|
||||
substitutions: {
|
||||
projectName: project.name,
|
||||
requesterFullName: `${requestedByUser.firstName} ${requestedByUser.lastName}`,
|
||||
requesterEmail: requestedByUser.email,
|
||||
bypassReason,
|
||||
secretPath: policy.secretPath,
|
||||
environment: env.name,
|
||||
approvalUrl: `${cfg.SITE_URL}/project/${project.id}/approval`
|
||||
},
|
||||
template: SmtpTemplates.AccessSecretRequestBypassed
|
||||
});
|
||||
}
|
||||
|
||||
return mergeStatus;
|
||||
};
|
||||
|
||||
@ -734,8 +1038,262 @@ export const secretApprovalRequestServiceFactory = ({
|
||||
});
|
||||
return secretApprovalRequest;
|
||||
};
|
||||
|
||||
const generateSecretApprovalRequestV2Bridge = async ({
|
||||
data,
|
||||
actorId,
|
||||
actor,
|
||||
actorOrgId,
|
||||
actorAuthMethod,
|
||||
policy,
|
||||
projectId,
|
||||
secretPath,
|
||||
environment
|
||||
}: TGenerateSecretApprovalRequestV2BridgeDTO) => {
|
||||
if (actor === ActorType.SERVICE || actor === ActorType.Machine)
|
||||
throw new BadRequestError({ message: "Cannot use service token or machine token over protected branches" });
|
||||
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
actorId,
|
||||
projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Read,
|
||||
subject(ProjectPermissionSub.Secrets, { environment, secretPath })
|
||||
);
|
||||
|
||||
const folder = await folderDAL.findBySecretPath(projectId, environment, secretPath);
|
||||
if (!folder)
|
||||
throw new BadRequestError({
|
||||
message: "Folder not found for the given environment slug & secret path",
|
||||
name: "GenSecretApproval"
|
||||
});
|
||||
const folderId = folder.id;
|
||||
|
||||
const commits: Omit<TSecretApprovalRequestsSecretsV2Insert, "requestId">[] = [];
|
||||
const commitTagIds: Record<string, string[]> = {};
|
||||
|
||||
const { encryptor: secretManagerEncryptor } = await kmsService.createCipherPairWithDataKey({
|
||||
type: KmsDataKey.SecretManager,
|
||||
projectId
|
||||
});
|
||||
|
||||
// for created secret approval change
|
||||
const createdSecrets = data[SecretOperations.Create];
|
||||
if (createdSecrets && createdSecrets?.length) {
|
||||
const secrets = await secretV2BridgeDAL.findBySecretKeys(
|
||||
folderId,
|
||||
createdSecrets.map((el) => ({
|
||||
key: el.secretKey,
|
||||
type: SecretType.Shared
|
||||
}))
|
||||
);
|
||||
if (secrets.length)
|
||||
throw new BadRequestError({ message: `Secret already exist: ${secrets.map((el) => el.key).join(",")}` });
|
||||
|
||||
commits.push(
|
||||
...createdSecrets.map((createdSecret) => ({
|
||||
op: SecretOperations.Create,
|
||||
version: 1,
|
||||
encryptedComment: setKnexStringValue(
|
||||
createdSecret.secretComment,
|
||||
(value) => secretManagerEncryptor({ plainText: Buffer.from(value) }).cipherTextBlob
|
||||
),
|
||||
encryptedValue: setKnexStringValue(
|
||||
createdSecret.secretValue,
|
||||
(value) => secretManagerEncryptor({ plainText: Buffer.from(value) }).cipherTextBlob
|
||||
),
|
||||
skipMultilineEncoding: createdSecret.skipMultilineEncoding,
|
||||
key: createdSecret.secretKey,
|
||||
type: SecretType.Shared
|
||||
}))
|
||||
);
|
||||
createdSecrets.forEach(({ tagIds, secretKey }) => {
|
||||
if (tagIds?.length) commitTagIds[secretKey] = tagIds;
|
||||
});
|
||||
}
|
||||
// not secret approval for update operations
|
||||
const secretsToUpdate = data[SecretOperations.Update];
|
||||
if (secretsToUpdate && secretsToUpdate?.length) {
|
||||
const secretsToUpdateStoredInDB = await secretV2BridgeDAL.findBySecretKeys(
|
||||
folderId,
|
||||
secretsToUpdate.map((el) => ({
|
||||
key: el.secretKey,
|
||||
type: SecretType.Shared
|
||||
}))
|
||||
);
|
||||
if (secretsToUpdateStoredInDB.length !== secretsToUpdate.length)
|
||||
throw new BadRequestError({
|
||||
message: `Secret not exist: ${secretsToUpdateStoredInDB.map((el) => el.key).join(",")}`
|
||||
});
|
||||
|
||||
// now find any secret that needs to update its name
|
||||
// same process as above
|
||||
const secretsWithNewName = secretsToUpdate.filter(({ newSecretName }) => Boolean(newSecretName));
|
||||
if (secretsWithNewName.length) {
|
||||
const secrets = await secretV2BridgeDAL.findBySecretKeys(
|
||||
folderId,
|
||||
secretsWithNewName.map((el) => ({
|
||||
key: el.secretKey,
|
||||
type: SecretType.Shared
|
||||
}))
|
||||
);
|
||||
if (secrets.length)
|
||||
throw new BadRequestError({
|
||||
message: `Secret not exist: ${secretsToUpdateStoredInDB.map((el) => el.key).join(",")}`
|
||||
});
|
||||
}
|
||||
|
||||
const updatingSecretsGroupByKey = groupBy(secretsToUpdateStoredInDB, (el) => el.key);
|
||||
const latestSecretVersions = await secretVersionV2BridgeDAL.findLatestVersionMany(
|
||||
folderId,
|
||||
secretsToUpdateStoredInDB.map(({ id }) => id)
|
||||
);
|
||||
commits.push(
|
||||
...secretsToUpdate.map(
|
||||
({
|
||||
newSecretName,
|
||||
secretKey,
|
||||
tagIds,
|
||||
secretValue,
|
||||
reminderRepeatDays,
|
||||
reminderNote,
|
||||
secretComment,
|
||||
metadata,
|
||||
skipMultilineEncoding
|
||||
}) => {
|
||||
const secretId = updatingSecretsGroupByKey[secretKey][0].id;
|
||||
if (tagIds?.length) commitTagIds[secretKey] = tagIds;
|
||||
return {
|
||||
...latestSecretVersions[secretId],
|
||||
key: newSecretName || secretKey,
|
||||
encryptedComment: setKnexStringValue(
|
||||
secretComment,
|
||||
(value) => secretManagerEncryptor({ plainText: Buffer.from(value) }).cipherTextBlob
|
||||
),
|
||||
encryptedValue: setKnexStringValue(
|
||||
secretValue,
|
||||
(value) => secretManagerEncryptor({ plainText: Buffer.from(value) }).cipherTextBlob
|
||||
),
|
||||
reminderRepeatDays,
|
||||
reminderNote,
|
||||
metadata,
|
||||
skipMultilineEncoding,
|
||||
op: SecretOperations.Update as const,
|
||||
secret: secretId,
|
||||
secretVersion: latestSecretVersions[secretId].id,
|
||||
version: updatingSecretsGroupByKey[secretKey][0].version || 1
|
||||
};
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
// deleted secrets
|
||||
const deletedSecrets = data[SecretOperations.Delete];
|
||||
if (deletedSecrets && deletedSecrets.length) {
|
||||
const secretsToDeleteInDB = await secretV2BridgeDAL.findBySecretKeys(
|
||||
folderId,
|
||||
deletedSecrets.map((el) => ({
|
||||
key: el.secretKey,
|
||||
type: SecretType.Shared
|
||||
}))
|
||||
);
|
||||
if (secretsToDeleteInDB.length !== deletedSecrets.length)
|
||||
throw new BadRequestError({
|
||||
message: `Secret not exist: ${secretsToDeleteInDB.map((el) => el.key).join(",")}`
|
||||
});
|
||||
const secretsGroupedByKey = groupBy(secretsToDeleteInDB, (i) => i.key);
|
||||
const deletedSecretIds = deletedSecrets.map((el) => secretsGroupedByKey[el.secretKey][0].id);
|
||||
const latestSecretVersions = await secretVersionV2BridgeDAL.findLatestVersionMany(folderId, deletedSecretIds);
|
||||
commits.push(
|
||||
...deletedSecrets.map(({ secretKey }) => {
|
||||
const secretId = secretsGroupedByKey[secretKey][0].id;
|
||||
return {
|
||||
op: SecretOperations.Delete as const,
|
||||
...latestSecretVersions[secretId],
|
||||
key: secretKey,
|
||||
secret: secretId,
|
||||
secretVersion: latestSecretVersions[secretId].id
|
||||
};
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
if (!commits.length) throw new BadRequestError({ message: "Empty commits" });
|
||||
|
||||
const tagIds = unique(Object.values(commitTagIds).flat());
|
||||
const tags = tagIds.length ? await secretTagDAL.findManyTagsById(projectId, tagIds) : [];
|
||||
if (tagIds.length !== tags.length) throw new BadRequestError({ message: "Tag not found" });
|
||||
|
||||
const secretApprovalRequest = await secretApprovalRequestDAL.transaction(async (tx) => {
|
||||
const doc = await secretApprovalRequestDAL.create(
|
||||
{
|
||||
folderId,
|
||||
slug: alphaNumericNanoId(),
|
||||
policyId: policy.id,
|
||||
status: "open",
|
||||
hasMerged: false,
|
||||
committerUserId: actorId
|
||||
},
|
||||
tx
|
||||
);
|
||||
const approvalCommits = await secretApprovalRequestSecretDAL.insertV2Bridge(
|
||||
commits.map(
|
||||
({
|
||||
version,
|
||||
op,
|
||||
key,
|
||||
encryptedComment,
|
||||
skipMultilineEncoding,
|
||||
metadata,
|
||||
reminderNote,
|
||||
reminderRepeatDays,
|
||||
encryptedValue,
|
||||
secretId,
|
||||
secretVersion
|
||||
}) => ({
|
||||
version,
|
||||
requestId: doc.id,
|
||||
op,
|
||||
secretId,
|
||||
metadata,
|
||||
secretVersion,
|
||||
skipMultilineEncoding,
|
||||
encryptedValue,
|
||||
reminderRepeatDays,
|
||||
reminderNote,
|
||||
encryptedComment,
|
||||
key
|
||||
})
|
||||
),
|
||||
tx
|
||||
);
|
||||
|
||||
const commitsGroupByKey = groupBy(approvalCommits, (i) => i.key);
|
||||
if (tagIds.length) {
|
||||
await secretApprovalRequestSecretDAL.insertApprovalSecretV2Tags(
|
||||
Object.keys(commitTagIds).flatMap((blindIndex) =>
|
||||
commitTagIds[blindIndex]
|
||||
? commitTagIds[blindIndex].map((tagId) => ({
|
||||
secretId: commitsGroupByKey[blindIndex][0].id,
|
||||
tagId
|
||||
}))
|
||||
: []
|
||||
),
|
||||
tx
|
||||
);
|
||||
}
|
||||
return { ...doc, commits: approvalCommits };
|
||||
});
|
||||
return secretApprovalRequest;
|
||||
};
|
||||
|
||||
return {
|
||||
generateSecretApprovalRequest,
|
||||
generateSecretApprovalRequestV2Bridge,
|
||||
mergeSecretApprovalRequest,
|
||||
reviewApproval,
|
||||
updateApprovalStatus,
|
||||
|
@ -26,6 +26,23 @@ export type TApprovalUpdateSecret = Partial<TApprovalCreateSecret> & {
|
||||
tagIds?: string[];
|
||||
};
|
||||
|
||||
export type TApprovalCreateSecretV2Bridge = {
|
||||
secretKey: string;
|
||||
secretValue?: string;
|
||||
secretComment?: string;
|
||||
reminderNote?: string | null;
|
||||
reminderRepeatDays?: number | null;
|
||||
skipMultilineEncoding?: boolean;
|
||||
metadata?: Record<string, string>;
|
||||
tagIds?: string[];
|
||||
};
|
||||
|
||||
export type TApprovalUpdateSecretV2Bridge = Partial<TApprovalCreateSecretV2Bridge> & {
|
||||
secretKey: string;
|
||||
newSecretName?: string;
|
||||
tagIds?: string[];
|
||||
};
|
||||
|
||||
export type TGenerateSecretApprovalRequestDTO = {
|
||||
environment: string;
|
||||
secretPath: string;
|
||||
@ -37,8 +54,20 @@ export type TGenerateSecretApprovalRequestDTO = {
|
||||
};
|
||||
} & TProjectPermission;
|
||||
|
||||
export type TGenerateSecretApprovalRequestV2BridgeDTO = {
|
||||
environment: string;
|
||||
secretPath: string;
|
||||
policy: TSecretApprovalPolicies;
|
||||
data: {
|
||||
[SecretOperations.Create]?: TApprovalCreateSecretV2Bridge[];
|
||||
[SecretOperations.Update]?: TApprovalUpdateSecretV2Bridge[];
|
||||
[SecretOperations.Delete]?: { secretKey: string }[];
|
||||
};
|
||||
} & TProjectPermission;
|
||||
|
||||
export type TMergeSecretApprovalRequestDTO = {
|
||||
approvalId: string;
|
||||
bypassReason?: string;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
export type TStatusChangeDTO = {
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { SecretType, TSecrets } from "@app/db/schemas";
|
||||
import { SecretType, TSecrets, TSecretsV2 } from "@app/db/schemas";
|
||||
import { TSecretApprovalPolicyServiceFactory } from "@app/ee/services/secret-approval-policy/secret-approval-policy-service";
|
||||
import { TSecretApprovalRequestDALFactory } from "@app/ee/services/secret-approval-request/secret-approval-request-dal";
|
||||
import { TSecretApprovalRequestSecretDALFactory } from "@app/ee/services/secret-approval-request/secret-approval-request-secret-dal";
|
||||
@ -10,6 +10,8 @@ import { logger } from "@app/lib/logger";
|
||||
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
||||
import { QueueName, TQueueServiceFactory } from "@app/queue";
|
||||
import { ActorType } from "@app/services/auth/auth-type";
|
||||
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
||||
import { KmsDataKey } from "@app/services/kms/kms-types";
|
||||
import { TProjectBotServiceFactory } from "@app/services/project-bot/project-bot-service";
|
||||
import { TSecretDALFactory } from "@app/services/secret/secret-dal";
|
||||
import { fnSecretBulkInsert, fnSecretBulkUpdate } from "@app/services/secret/secret-fns";
|
||||
@ -17,12 +19,20 @@ import { TSecretQueueFactory, uniqueSecretQueueKey } from "@app/services/secret/
|
||||
import { SecretOperations } from "@app/services/secret/secret-types";
|
||||
import { TSecretVersionDALFactory } from "@app/services/secret/secret-version-dal";
|
||||
import { TSecretVersionTagDALFactory } from "@app/services/secret/secret-version-tag-dal";
|
||||
import { TSecretBlindIndexDALFactory } from "@app/services/secret-blind-index/secret-blind-index-dal";
|
||||
import { TSecretFolderDALFactory } from "@app/services/secret-folder/secret-folder-dal";
|
||||
import { ReservedFolders } from "@app/services/secret-folder/secret-folder-types";
|
||||
import { TSecretImportDALFactory } from "@app/services/secret-import/secret-import-dal";
|
||||
import { fnSecretsFromImports } from "@app/services/secret-import/secret-import-fns";
|
||||
import { fnSecretsFromImports, fnSecretsV2FromImports } from "@app/services/secret-import/secret-import-fns";
|
||||
import { TSecretTagDALFactory } from "@app/services/secret-tag/secret-tag-dal";
|
||||
import { TSecretV2BridgeDALFactory } from "@app/services/secret-v2-bridge/secret-v2-bridge-dal";
|
||||
import {
|
||||
fnSecretBulkInsert as fnSecretV2BridgeBulkInsert,
|
||||
fnSecretBulkUpdate as fnSecretV2BridgeBulkUpdate,
|
||||
getAllNestedSecretReferences,
|
||||
getAllNestedSecretReferences as getAllNestedSecretReferencesV2Bridge
|
||||
} from "@app/services/secret-v2-bridge/secret-v2-bridge-fns";
|
||||
import { TSecretVersionV2DALFactory } from "@app/services/secret-v2-bridge/secret-version-dal";
|
||||
import { TSecretVersionV2TagDALFactory } from "@app/services/secret-v2-bridge/secret-version-tag-dal";
|
||||
|
||||
import { MAX_REPLICATION_DEPTH } from "./secret-replication-constants";
|
||||
|
||||
@ -32,24 +42,42 @@ type TSecretReplicationServiceFactoryDep = {
|
||||
"find" | "findByBlindIndexes" | "insertMany" | "bulkUpdate" | "delete" | "upsertSecretReferences" | "transaction"
|
||||
>;
|
||||
secretVersionDAL: Pick<TSecretVersionDALFactory, "find" | "insertMany" | "update" | "findLatestVersionMany">;
|
||||
secretV2BridgeDAL: Pick<
|
||||
TSecretV2BridgeDALFactory,
|
||||
"find" | "findBySecretKeys" | "insertMany" | "bulkUpdate" | "delete" | "upsertSecretReferences" | "transaction"
|
||||
>;
|
||||
secretVersionV2BridgeDAL: Pick<
|
||||
TSecretVersionV2DALFactory,
|
||||
"find" | "insertMany" | "update" | "findLatestVersionMany"
|
||||
>;
|
||||
secretImportDAL: Pick<TSecretImportDALFactory, "find" | "updateById" | "findByFolderIds">;
|
||||
folderDAL: Pick<
|
||||
TSecretFolderDALFactory,
|
||||
"findSecretPathByFolderIds" | "findBySecretPath" | "create" | "findOne" | "findByManySecretPath"
|
||||
>;
|
||||
secretVersionTagDAL: Pick<TSecretVersionTagDALFactory, "find" | "insertMany">;
|
||||
secretVersionV2TagBridgeDAL: Pick<TSecretVersionV2TagDALFactory, "find" | "insertMany">;
|
||||
secretQueueService: Pick<TSecretQueueFactory, "syncSecrets" | "replicateSecrets">;
|
||||
queueService: Pick<TQueueServiceFactory, "start" | "listen" | "queue" | "stopJobById">;
|
||||
secretApprovalPolicyService: Pick<TSecretApprovalPolicyServiceFactory, "getSecretApprovalPolicy">;
|
||||
keyStore: Pick<TKeyStoreFactory, "acquireLock" | "setItemWithExpiry" | "getItem">;
|
||||
secretBlindIndexDAL: Pick<TSecretBlindIndexDALFactory, "findOne">;
|
||||
secretTagDAL: Pick<TSecretTagDALFactory, "findManyTagsById" | "saveTagsToSecret" | "deleteTagsManySecret" | "find">;
|
||||
secretTagDAL: Pick<
|
||||
TSecretTagDALFactory,
|
||||
| "findManyTagsById"
|
||||
| "saveTagsToSecret"
|
||||
| "deleteTagsManySecret"
|
||||
| "find"
|
||||
| "saveTagsToSecretV2"
|
||||
| "deleteTagsToSecretV2"
|
||||
>;
|
||||
secretApprovalRequestDAL: Pick<TSecretApprovalRequestDALFactory, "create" | "transaction">;
|
||||
secretApprovalRequestSecretDAL: Pick<
|
||||
TSecretApprovalRequestSecretDALFactory,
|
||||
"insertMany" | "insertApprovalSecretTags"
|
||||
"insertMany" | "insertApprovalSecretTags" | "insertV2Bridge"
|
||||
>;
|
||||
|
||||
projectBotService: Pick<TProjectBotServiceFactory, "getBotKey">;
|
||||
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
|
||||
};
|
||||
|
||||
export type TSecretReplicationServiceFactory = ReturnType<typeof secretReplicationServiceFactory>;
|
||||
@ -90,9 +118,13 @@ export const secretReplicationServiceFactory = ({
|
||||
secretApprovalRequestSecretDAL,
|
||||
secretApprovalRequestDAL,
|
||||
secretQueueService,
|
||||
projectBotService
|
||||
projectBotService,
|
||||
secretVersionV2TagBridgeDAL,
|
||||
secretVersionV2BridgeDAL,
|
||||
secretV2BridgeDAL,
|
||||
kmsService
|
||||
}: TSecretReplicationServiceFactoryDep) => {
|
||||
const getReplicatedSecrets = (
|
||||
const $getReplicatedSecrets = (
|
||||
botKey: string,
|
||||
localSecrets: TSecrets[],
|
||||
importedSecrets: { secrets: TSecrets[] }[]
|
||||
@ -119,6 +151,25 @@ export const secretReplicationServiceFactory = ({
|
||||
return secrets;
|
||||
};
|
||||
|
||||
const $getReplicatedSecretsV2 = (
|
||||
localSecrets: (TSecretsV2 & { secretKey: string; secretValue?: string })[],
|
||||
importedSecrets: { secrets: (TSecretsV2 & { secretKey: string; secretValue?: string })[] }[]
|
||||
) => {
|
||||
const deDupe = new Set<string>();
|
||||
const secrets = [...localSecrets];
|
||||
|
||||
for (let i = importedSecrets.length - 1; i >= 0; i = -1) {
|
||||
importedSecrets[i].secrets.forEach((el) => {
|
||||
if (deDupe.has(el.key)) {
|
||||
return;
|
||||
}
|
||||
deDupe.add(el.key);
|
||||
secrets.push(el);
|
||||
});
|
||||
}
|
||||
return secrets;
|
||||
};
|
||||
|
||||
// IMPORTANT NOTE BEFORE READING THE FUNCTION
|
||||
// SOURCE - Where secrets are copied from
|
||||
// DESTINATION - Where the replicated imports that points to SOURCE from Destination
|
||||
@ -139,6 +190,7 @@ export const secretReplicationServiceFactory = ({
|
||||
|
||||
const folder = await folderDAL.findBySecretPath(projectId, environmentSlug, secretPath);
|
||||
if (!folder) return;
|
||||
const { botKey, shouldUseSecretV2Bridge } = await projectBotService.getBotKey(projectId);
|
||||
|
||||
// the the replicated imports made to the source. These are the destinations
|
||||
const destinationSecretImports = await secretImportDAL.find({
|
||||
@ -191,8 +243,270 @@ export const secretReplicationServiceFactory = ({
|
||||
: destinationReplicatedSecretImports;
|
||||
if (!destinationReplicatedSecretImports.length) return;
|
||||
|
||||
const botKey = await projectBotService.getBotKey(projectId);
|
||||
if (shouldUseSecretV2Bridge) {
|
||||
const { decryptor: secretManagerDecryptor } = await kmsService.createCipherPairWithDataKey({
|
||||
type: KmsDataKey.SecretManager,
|
||||
projectId
|
||||
});
|
||||
|
||||
// these are the secrets to be added in replicated folders
|
||||
const sourceLocalSecrets = await secretV2BridgeDAL.find({ folderId: folder.id, type: SecretType.Shared });
|
||||
const sourceSecretImports = await secretImportDAL.find({ folderId: folder.id });
|
||||
const sourceImportedSecrets = await fnSecretsV2FromImports({
|
||||
allowedImports: sourceSecretImports,
|
||||
secretDAL: secretV2BridgeDAL,
|
||||
folderDAL,
|
||||
secretImportDAL,
|
||||
decryptor: (value) => (value ? secretManagerDecryptor({ cipherTextBlob: value }).toString() : undefined)
|
||||
});
|
||||
// secrets that gets replicated across imports
|
||||
const sourceDecryptedLocalSecrets = sourceLocalSecrets.map((el) => ({
|
||||
...el,
|
||||
secretKey: el.key,
|
||||
secretValue: el.encryptedValue
|
||||
? secretManagerDecryptor({ cipherTextBlob: el.encryptedValue }).toString()
|
||||
: undefined
|
||||
}));
|
||||
const sourceSecrets = $getReplicatedSecretsV2(sourceDecryptedLocalSecrets, sourceImportedSecrets);
|
||||
const sourceSecretsGroupByKey = groupBy(sourceSecrets, (i) => i.key);
|
||||
|
||||
const lock = await keyStore.acquireLock(
|
||||
[getReplicationKeyLockPrefix(projectId, environmentSlug, secretPath)],
|
||||
5000
|
||||
);
|
||||
|
||||
try {
|
||||
/* eslint-disable no-await-in-loop */
|
||||
for (const destinationSecretImport of destinationReplicatedSecretImports) {
|
||||
try {
|
||||
const hasJobCompleted = await keyStore.getItem(
|
||||
keystoreReplicationSuccessKey(job.id as string, destinationSecretImport.id),
|
||||
KeyStorePrefixes.SecretReplication
|
||||
);
|
||||
if (hasJobCompleted) {
|
||||
logger.info(
|
||||
{ jobId: job.id, importId: destinationSecretImport.id },
|
||||
"Skipping this job as this has been successfully replicated."
|
||||
);
|
||||
// eslint-disable-next-line
|
||||
continue;
|
||||
}
|
||||
|
||||
const [destinationFolder] = await folderDAL.findSecretPathByFolderIds(projectId, [
|
||||
destinationSecretImport.folderId
|
||||
]);
|
||||
if (!destinationFolder) throw new BadRequestError({ message: "Imported folder not found" });
|
||||
|
||||
let destinationReplicationFolder = await folderDAL.findOne({
|
||||
parentId: destinationFolder.id,
|
||||
name: getReplicationFolderName(destinationSecretImport.id),
|
||||
isReserved: true
|
||||
});
|
||||
if (!destinationReplicationFolder) {
|
||||
destinationReplicationFolder = await folderDAL.create({
|
||||
parentId: destinationFolder.id,
|
||||
name: getReplicationFolderName(destinationSecretImport.id),
|
||||
envId: destinationFolder.envId,
|
||||
isReserved: true
|
||||
});
|
||||
}
|
||||
const destinationReplicationFolderId = destinationReplicationFolder.id;
|
||||
|
||||
const destinationLocalSecretsFromDB = await secretV2BridgeDAL.find({
|
||||
folderId: destinationReplicationFolderId
|
||||
});
|
||||
const destinationLocalSecrets = destinationLocalSecretsFromDB.map((el) => ({
|
||||
...el,
|
||||
secretKey: el.key,
|
||||
secretValue: el.encryptedValue
|
||||
? secretManagerDecryptor({ cipherTextBlob: el.encryptedValue }).toString()
|
||||
: undefined
|
||||
}));
|
||||
|
||||
const destinationLocalSecretsGroupedByKey = groupBy(destinationLocalSecrets, (i) => i.key);
|
||||
|
||||
const locallyCreatedSecrets = sourceSecrets
|
||||
.filter(({ key }) => !destinationLocalSecretsGroupedByKey[key]?.[0])
|
||||
.map((el) => ({ ...el, operation: SecretOperations.Create })); // rewrite update ops to create
|
||||
|
||||
const locallyUpdatedSecrets = sourceSecrets
|
||||
.filter(
|
||||
({ key, secretKey, secretValue }) =>
|
||||
destinationLocalSecretsGroupedByKey[key]?.[0] &&
|
||||
// if key or value changed
|
||||
(destinationLocalSecretsGroupedByKey[key]?.[0]?.secretKey !== secretKey ||
|
||||
destinationLocalSecretsGroupedByKey[key]?.[0]?.secretValue !== secretValue)
|
||||
)
|
||||
.map((el) => ({ ...el, operation: SecretOperations.Update })); // rewrite update ops to create
|
||||
|
||||
const locallyDeletedSecrets = destinationLocalSecrets
|
||||
.filter(({ key }) => !sourceSecretsGroupByKey[key]?.[0])
|
||||
.map((el) => ({ ...el, operation: SecretOperations.Delete }));
|
||||
|
||||
const isEmtpy =
|
||||
locallyCreatedSecrets.length + locallyUpdatedSecrets.length + locallyDeletedSecrets.length === 0;
|
||||
// eslint-disable-next-line
|
||||
if (isEmtpy) continue;
|
||||
|
||||
const policy = await secretApprovalPolicyService.getSecretApprovalPolicy(
|
||||
projectId,
|
||||
destinationFolder.environmentSlug,
|
||||
destinationFolder.path
|
||||
);
|
||||
// this means it should be a approval request rather than direct replication
|
||||
if (policy && actor === ActorType.USER) {
|
||||
const localSecretsLatestVersions = destinationLocalSecrets.map(({ id }) => id);
|
||||
const latestSecretVersions = await secretVersionV2BridgeDAL.findLatestVersionMany(
|
||||
destinationReplicationFolderId,
|
||||
localSecretsLatestVersions
|
||||
);
|
||||
await secretApprovalRequestDAL.transaction(async (tx) => {
|
||||
const approvalRequestDoc = await secretApprovalRequestDAL.create(
|
||||
{
|
||||
folderId: destinationReplicationFolderId,
|
||||
slug: alphaNumericNanoId(),
|
||||
policyId: policy.id,
|
||||
status: "open",
|
||||
hasMerged: false,
|
||||
committerUserId: actorId,
|
||||
isReplicated: true
|
||||
},
|
||||
tx
|
||||
);
|
||||
const commits = locallyCreatedSecrets
|
||||
.concat(locallyUpdatedSecrets)
|
||||
.concat(locallyDeletedSecrets)
|
||||
.map((doc) => {
|
||||
const { operation } = doc;
|
||||
const localSecret = destinationLocalSecretsGroupedByKey[doc.key]?.[0];
|
||||
|
||||
return {
|
||||
op: operation,
|
||||
requestId: approvalRequestDoc.id,
|
||||
metadata: doc.metadata,
|
||||
key: doc.key,
|
||||
encryptedValue: doc.encryptedValue,
|
||||
encryptedComment: doc.encryptedComment,
|
||||
skipMultilineEncoding: doc.skipMultilineEncoding,
|
||||
// except create operation other two needs the secret id and version id
|
||||
...(operation !== SecretOperations.Create
|
||||
? { secretId: localSecret.id, secretVersion: latestSecretVersions[localSecret.id].id }
|
||||
: {})
|
||||
};
|
||||
});
|
||||
const approvalCommits = await secretApprovalRequestSecretDAL.insertV2Bridge(commits, tx);
|
||||
|
||||
return { ...approvalRequestDoc, commits: approvalCommits };
|
||||
});
|
||||
} else {
|
||||
await secretDAL.transaction(async (tx) => {
|
||||
if (locallyCreatedSecrets.length) {
|
||||
await fnSecretV2BridgeBulkInsert({
|
||||
folderId: destinationReplicationFolderId,
|
||||
secretVersionDAL: secretVersionV2BridgeDAL,
|
||||
secretDAL: secretV2BridgeDAL,
|
||||
tx,
|
||||
secretTagDAL,
|
||||
secretVersionTagDAL: secretVersionV2TagBridgeDAL,
|
||||
inputSecrets: locallyCreatedSecrets.map((doc) => {
|
||||
return {
|
||||
type: doc.type,
|
||||
metadata: doc.metadata,
|
||||
key: doc.key,
|
||||
encryptedValue: doc.encryptedValue,
|
||||
encryptedComment: doc.encryptedComment,
|
||||
skipMultilineEncoding: doc.skipMultilineEncoding,
|
||||
references: doc.secretValue ? getAllNestedSecretReferencesV2Bridge(doc.secretValue) : []
|
||||
};
|
||||
})
|
||||
});
|
||||
}
|
||||
if (locallyUpdatedSecrets.length) {
|
||||
await fnSecretV2BridgeBulkUpdate({
|
||||
folderId: destinationReplicationFolderId,
|
||||
secretVersionDAL: secretVersionV2BridgeDAL,
|
||||
secretDAL: secretV2BridgeDAL,
|
||||
tx,
|
||||
secretTagDAL,
|
||||
secretVersionTagDAL: secretVersionV2TagBridgeDAL,
|
||||
inputSecrets: locallyUpdatedSecrets.map((doc) => {
|
||||
return {
|
||||
filter: {
|
||||
folderId: destinationReplicationFolderId,
|
||||
id: destinationLocalSecretsGroupedByKey[doc.key][0].id
|
||||
},
|
||||
data: {
|
||||
type: doc.type,
|
||||
metadata: doc.metadata,
|
||||
key: doc.key,
|
||||
encryptedValue: doc.encryptedValue as Buffer,
|
||||
encryptedComment: doc.encryptedComment,
|
||||
skipMultilineEncoding: doc.skipMultilineEncoding,
|
||||
references: doc.secretValue ? getAllNestedSecretReferencesV2Bridge(doc.secretValue) : []
|
||||
}
|
||||
};
|
||||
})
|
||||
});
|
||||
}
|
||||
if (locallyDeletedSecrets.length) {
|
||||
await secretDAL.delete(
|
||||
{
|
||||
$in: {
|
||||
id: locallyDeletedSecrets.map(({ id }) => id)
|
||||
},
|
||||
folderId: destinationReplicationFolderId
|
||||
},
|
||||
tx
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
await secretQueueService.syncSecrets({
|
||||
projectId,
|
||||
secretPath: destinationFolder.path,
|
||||
environmentSlug: destinationFolder.environmentSlug,
|
||||
actorId,
|
||||
actor,
|
||||
_depth: depth + 1,
|
||||
_deDupeReplicationQueue: deDupeReplicationQueue,
|
||||
_deDupeQueue: deDupeQueue
|
||||
});
|
||||
}
|
||||
|
||||
// this is used to avoid multiple times generating secret approval by failed one
|
||||
await keyStore.setItemWithExpiry(
|
||||
keystoreReplicationSuccessKey(job.id as string, destinationSecretImport.id),
|
||||
SECRET_IMPORT_SUCCESS_LOCK,
|
||||
1,
|
||||
KeyStorePrefixes.SecretReplication
|
||||
);
|
||||
|
||||
await secretImportDAL.updateById(destinationSecretImport.id, {
|
||||
lastReplicated: new Date(),
|
||||
replicationStatus: null,
|
||||
isReplicationSuccess: true
|
||||
});
|
||||
} catch (err) {
|
||||
logger.error(
|
||||
err,
|
||||
`Failed to replicate secret with import id=[${destinationSecretImport.id}] env=[${destinationSecretImport.importEnv.slug}] path=[${destinationSecretImport.importPath}]`
|
||||
);
|
||||
await secretImportDAL.updateById(destinationSecretImport.id, {
|
||||
lastReplicated: new Date(),
|
||||
replicationStatus: (err as Error)?.message.slice(0, 500),
|
||||
isReplicationSuccess: false
|
||||
});
|
||||
}
|
||||
}
|
||||
/* eslint-enable no-await-in-loop */
|
||||
} finally {
|
||||
await lock.release();
|
||||
logger.info(job.data, "Replication finished");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (!botKey) throw new BadRequestError({ message: "Bot not found" });
|
||||
// these are the secrets to be added in replicated folders
|
||||
const sourceLocalSecrets = await secretDAL.find({ folderId: folder.id, type: SecretType.Shared });
|
||||
const sourceSecretImports = await secretImportDAL.find({ folderId: folder.id });
|
||||
@ -203,7 +517,7 @@ export const secretReplicationServiceFactory = ({
|
||||
secretImportDAL
|
||||
});
|
||||
// secrets that gets replicated across imports
|
||||
const sourceSecrets = getReplicatedSecrets(botKey, sourceLocalSecrets, sourceImportedSecrets);
|
||||
const sourceSecrets = $getReplicatedSecrets(botKey, sourceLocalSecrets, sourceImportedSecrets);
|
||||
const sourceSecretsGroupByBlindIndex = groupBy(sourceSecrets, (i) => i.secretBlindIndex as string);
|
||||
|
||||
const lock = await keyStore.acquireLock(
|
||||
@ -372,7 +686,8 @@ export const secretReplicationServiceFactory = ({
|
||||
secretCommentIV: doc.secretCommentIV,
|
||||
secretCommentTag: doc.secretCommentTag,
|
||||
secretCommentCiphertext: doc.secretCommentCiphertext,
|
||||
skipMultilineEncoding: doc.skipMultilineEncoding
|
||||
skipMultilineEncoding: doc.skipMultilineEncoding,
|
||||
references: getAllNestedSecretReferences(doc.secretValue)
|
||||
};
|
||||
})
|
||||
});
|
||||
@ -407,7 +722,8 @@ export const secretReplicationServiceFactory = ({
|
||||
secretCommentIV: doc.secretCommentIV,
|
||||
secretCommentTag: doc.secretCommentTag,
|
||||
secretCommentCiphertext: doc.secretCommentCiphertext,
|
||||
skipMultilineEncoding: doc.skipMultilineEncoding
|
||||
skipMultilineEncoding: doc.skipMultilineEncoding,
|
||||
references: getAllNestedSecretReferences(doc.secretValue)
|
||||
}
|
||||
};
|
||||
})
|
||||
|
@ -10,6 +10,7 @@ export type TSecretRotationDALFactory = ReturnType<typeof secretRotationDALFacto
|
||||
export const secretRotationDALFactory = (db: TDbClient) => {
|
||||
const secretRotationOrm = ormify(db, TableName.SecretRotation);
|
||||
const secretRotationOutputOrm = ormify(db, TableName.SecretRotationOutput);
|
||||
const secretRotationOutputV2Orm = ormify(db, TableName.SecretRotationOutputV2);
|
||||
|
||||
const findQuery = (filter: TFindFilter<TSecretRotations & { projectId: string }>, tx: Knex) =>
|
||||
tx(TableName.SecretRotation)
|
||||
@ -31,13 +32,7 @@ export const secretRotationDALFactory = (db: TDbClient) => {
|
||||
.select(tx.ref("version").withSchema(TableName.Secret).as("secVersion"))
|
||||
.select(tx.ref("secretKeyIV").withSchema(TableName.Secret))
|
||||
.select(tx.ref("secretKeyTag").withSchema(TableName.Secret))
|
||||
.select(tx.ref("secretKeyCiphertext").withSchema(TableName.Secret))
|
||||
.select(tx.ref("secretValueIV").withSchema(TableName.Secret))
|
||||
.select(tx.ref("secretValueTag").withSchema(TableName.Secret))
|
||||
.select(tx.ref("secretValueCiphertext").withSchema(TableName.Secret))
|
||||
.select(tx.ref("secretCommentIV").withSchema(TableName.Secret))
|
||||
.select(tx.ref("secretCommentTag").withSchema(TableName.Secret))
|
||||
.select(tx.ref("secretCommentCiphertext").withSchema(TableName.Secret));
|
||||
.select(tx.ref("secretKeyCiphertext").withSchema(TableName.Secret));
|
||||
|
||||
const find = async (filter: TFindFilter<TSecretRotations & { projectId: string }>, tx?: Knex) => {
|
||||
try {
|
||||
@ -54,33 +49,65 @@ export const secretRotationDALFactory = (db: TDbClient) => {
|
||||
{
|
||||
key: "secId",
|
||||
label: "outputs" as const,
|
||||
mapper: ({
|
||||
secId,
|
||||
outputKey,
|
||||
secVersion,
|
||||
secretKeyIV,
|
||||
secretKeyTag,
|
||||
secretKeyCiphertext,
|
||||
secretValueTag,
|
||||
secretValueIV,
|
||||
secretValueCiphertext,
|
||||
secretCommentIV,
|
||||
secretCommentTag,
|
||||
secretCommentCiphertext
|
||||
}) => ({
|
||||
mapper: ({ secId, outputKey, secVersion, secretKeyIV, secretKeyTag, secretKeyCiphertext }) => ({
|
||||
key: outputKey,
|
||||
secret: {
|
||||
id: secId,
|
||||
version: secVersion,
|
||||
secretKeyIV,
|
||||
secretKeyTag,
|
||||
secretKeyCiphertext,
|
||||
secretValueTag,
|
||||
secretValueIV,
|
||||
secretValueCiphertext,
|
||||
secretCommentIV,
|
||||
secretCommentTag,
|
||||
secretCommentCiphertext
|
||||
secretKeyCiphertext
|
||||
}
|
||||
})
|
||||
}
|
||||
]
|
||||
});
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: "SecretRotationFind" });
|
||||
}
|
||||
};
|
||||
|
||||
const findQuerySecretV2 = (filter: TFindFilter<TSecretRotations & { projectId: string }>, tx: Knex) =>
|
||||
tx(TableName.SecretRotation)
|
||||
.where(filter)
|
||||
.join(TableName.Environment, `${TableName.SecretRotation}.envId`, `${TableName.Environment}.id`)
|
||||
.leftJoin(
|
||||
TableName.SecretRotationOutputV2,
|
||||
`${TableName.SecretRotation}.id`,
|
||||
`${TableName.SecretRotationOutputV2}.rotationId`
|
||||
)
|
||||
.join(TableName.SecretV2, `${TableName.SecretRotationOutputV2}.secretId`, `${TableName.SecretV2}.id`)
|
||||
.select(selectAllTableCols(TableName.SecretRotation))
|
||||
.select(tx.ref("name").withSchema(TableName.Environment).as("envName"))
|
||||
.select(tx.ref("slug").withSchema(TableName.Environment).as("envSlug"))
|
||||
.select(tx.ref("id").withSchema(TableName.Environment).as("envId"))
|
||||
.select(tx.ref("projectId").withSchema(TableName.Environment))
|
||||
.select(tx.ref("key").withSchema(TableName.SecretRotationOutputV2).as("outputKey"))
|
||||
.select(tx.ref("id").withSchema(TableName.SecretV2).as("secId"))
|
||||
.select(tx.ref("version").withSchema(TableName.SecretV2).as("secVersion"))
|
||||
.select(tx.ref("key").withSchema(TableName.SecretV2).as("secretKey"));
|
||||
|
||||
const findSecretV2 = async (filter: TFindFilter<TSecretRotations & { projectId: string }>, tx?: Knex) => {
|
||||
try {
|
||||
const data = await findQuerySecretV2(filter, tx || db.replicaNode());
|
||||
return sqlNestRelationships({
|
||||
data,
|
||||
key: "id",
|
||||
parentMapper: (el) => ({
|
||||
...SecretRotationsSchema.parse(el),
|
||||
projectId: el.projectId,
|
||||
environment: { id: el.envId, name: el.envName, slug: el.envSlug }
|
||||
}),
|
||||
childrenMapper: [
|
||||
{
|
||||
key: "secId",
|
||||
label: "outputs" as const,
|
||||
mapper: ({ secId, outputKey, secVersion, secretKey }) => ({
|
||||
key: outputKey,
|
||||
secret: {
|
||||
id: secId,
|
||||
version: secVersion,
|
||||
secretKey
|
||||
}
|
||||
})
|
||||
}
|
||||
@ -114,12 +141,19 @@ export const secretRotationDALFactory = (db: TDbClient) => {
|
||||
};
|
||||
|
||||
const findRotationOutputsByRotationId = async (rotationId: string) => secretRotationOutputOrm.find({ rotationId });
|
||||
const findRotationOutputsV2ByRotationId = async (rotationId: string) =>
|
||||
secretRotationOutputV2Orm.find({ rotationId });
|
||||
|
||||
// special query
|
||||
|
||||
return {
|
||||
...secretRotationOrm,
|
||||
find,
|
||||
findSecretV2,
|
||||
findById,
|
||||
secretOutputInsertMany: secretRotationOutputOrm.insertMany,
|
||||
findRotationOutputsByRotationId
|
||||
secretOutputV2InsertMany: secretRotationOutputV2Orm.insertMany,
|
||||
findRotationOutputsByRotationId,
|
||||
findRotationOutputsV2ByRotationId
|
||||
};
|
||||
};
|
||||
|
@ -17,9 +17,13 @@ import { BadRequestError } from "@app/lib/errors";
|
||||
import { logger } from "@app/lib/logger";
|
||||
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
||||
import { QueueJobs, QueueName, TQueueServiceFactory } from "@app/queue";
|
||||
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
||||
import { KmsDataKey } from "@app/services/kms/kms-types";
|
||||
import { TProjectBotServiceFactory } from "@app/services/project-bot/project-bot-service";
|
||||
import { TSecretDALFactory } from "@app/services/secret/secret-dal";
|
||||
import { TSecretVersionDALFactory } from "@app/services/secret/secret-version-dal";
|
||||
import { TSecretV2BridgeDALFactory } from "@app/services/secret-v2-bridge/secret-v2-bridge-dal";
|
||||
import { TSecretVersionV2DALFactory } from "@app/services/secret-v2-bridge/secret-version-dal";
|
||||
import { TTelemetryServiceFactory } from "@app/services/telemetry/telemetry-service";
|
||||
import { PostHogEventTypes } from "@app/services/telemetry/telemetry-types";
|
||||
|
||||
@ -47,8 +51,11 @@ type TSecretRotationQueueFactoryDep = {
|
||||
secretRotationDAL: TSecretRotationDALFactory;
|
||||
projectBotService: Pick<TProjectBotServiceFactory, "getBotKey">;
|
||||
secretDAL: Pick<TSecretDALFactory, "bulkUpdate" | "find">;
|
||||
secretV2BridgeDAL: Pick<TSecretV2BridgeDALFactory, "bulkUpdate" | "find">;
|
||||
secretVersionDAL: Pick<TSecretVersionDALFactory, "insertMany" | "findLatestVersionMany">;
|
||||
secretVersionV2BridgeDAL: Pick<TSecretVersionV2DALFactory, "insertMany" | "findLatestVersionMany">;
|
||||
telemetryService: Pick<TTelemetryServiceFactory, "sendPostHogEvents">;
|
||||
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
|
||||
};
|
||||
|
||||
// These error should stop the repeatable job and ask user to reconfigure rotation
|
||||
@ -70,7 +77,10 @@ export const secretRotationQueueFactory = ({
|
||||
projectBotService,
|
||||
secretDAL,
|
||||
secretVersionDAL,
|
||||
telemetryService
|
||||
telemetryService,
|
||||
secretV2BridgeDAL,
|
||||
secretVersionV2BridgeDAL,
|
||||
kmsService
|
||||
}: TSecretRotationQueueFactoryDep) => {
|
||||
const addToQueue = async (rotationId: string, interval: number) => {
|
||||
const appCfg = getConfig();
|
||||
@ -111,7 +121,13 @@ export const secretRotationQueueFactory = ({
|
||||
try {
|
||||
if (!rotationProvider || !secretRotation) throw new DisableRotationErrors({ message: "Provider not found" });
|
||||
|
||||
const rotationOutputs = await secretRotationDAL.findRotationOutputsByRotationId(rotationId);
|
||||
const { botKey, shouldUseSecretV2Bridge } = await projectBotService.getBotKey(secretRotation.projectId);
|
||||
let rotationOutputs;
|
||||
if (shouldUseSecretV2Bridge) {
|
||||
rotationOutputs = await secretRotationDAL.findRotationOutputsV2ByRotationId(rotationId);
|
||||
} else {
|
||||
rotationOutputs = await secretRotationDAL.findRotationOutputsByRotationId(rotationId);
|
||||
}
|
||||
if (!rotationOutputs.length) throw new DisableRotationErrors({ message: "Secrets not found" });
|
||||
|
||||
// deep copy
|
||||
@ -267,62 +283,112 @@ export const secretRotationQueueFactory = ({
|
||||
internal: newCredential.internal
|
||||
});
|
||||
const encVarData = infisicalSymmetricEncypt(JSON.stringify(variables));
|
||||
const key = await projectBotService.getBotKey(secretRotation.projectId);
|
||||
const encryptedSecrets = rotationOutputs.map(({ key: outputKey, secretId }) => ({
|
||||
secretId,
|
||||
value: encryptSymmetric128BitHexKeyUTF8(
|
||||
typeof newCredential.outputs[outputKey] === "object"
|
||||
? JSON.stringify(newCredential.outputs[outputKey])
|
||||
: String(newCredential.outputs[outputKey]),
|
||||
key
|
||||
)
|
||||
}));
|
||||
// map the final values to output keys in the board
|
||||
await secretRotationDAL.transaction(async (tx) => {
|
||||
await secretRotationDAL.updateById(
|
||||
rotationId,
|
||||
{
|
||||
encryptedData: encVarData.ciphertext,
|
||||
encryptedDataIV: encVarData.iv,
|
||||
encryptedDataTag: encVarData.tag,
|
||||
keyEncoding: encVarData.encoding,
|
||||
algorithm: encVarData.algorithm,
|
||||
lastRotatedAt: new Date(),
|
||||
statusMessage: "Rotated successfull",
|
||||
status: "success"
|
||||
},
|
||||
tx
|
||||
);
|
||||
const updatedSecrets = await secretDAL.bulkUpdate(
|
||||
encryptedSecrets.map(({ secretId, value }) => ({
|
||||
// this secret id is validated when user is inserted
|
||||
filter: { id: secretId, type: SecretType.Shared },
|
||||
data: {
|
||||
secretValueCiphertext: value.ciphertext,
|
||||
secretValueIV: value.iv,
|
||||
secretValueTag: value.tag
|
||||
}
|
||||
})),
|
||||
tx
|
||||
);
|
||||
await secretVersionDAL.insertMany(
|
||||
updatedSecrets.map(({ id, updatedAt, createdAt, ...el }) => {
|
||||
if (!el.secretBlindIndex) throw new BadRequestError({ message: "Missing blind index" });
|
||||
return {
|
||||
...el,
|
||||
secretId: id,
|
||||
secretBlindIndex: el.secretBlindIndex
|
||||
};
|
||||
}),
|
||||
tx
|
||||
);
|
||||
const { encryptor: secretManagerEncryptor } = await kmsService.createCipherPairWithDataKey({
|
||||
type: KmsDataKey.SecretManager,
|
||||
projectId: secretRotation.projectId
|
||||
});
|
||||
|
||||
const numberOfSecretsRotated = rotationOutputs.length;
|
||||
if (shouldUseSecretV2Bridge) {
|
||||
const encryptedSecrets = rotationOutputs.map(({ key: outputKey, secretId }) => ({
|
||||
secretId,
|
||||
value:
|
||||
typeof newCredential.outputs[outputKey] === "object"
|
||||
? JSON.stringify(newCredential.outputs[outputKey])
|
||||
: String(newCredential.outputs[outputKey])
|
||||
}));
|
||||
// map the final values to output keys in the board
|
||||
await secretRotationDAL.transaction(async (tx) => {
|
||||
await secretRotationDAL.updateById(
|
||||
rotationId,
|
||||
{
|
||||
encryptedData: encVarData.ciphertext,
|
||||
encryptedDataIV: encVarData.iv,
|
||||
encryptedDataTag: encVarData.tag,
|
||||
keyEncoding: encVarData.encoding,
|
||||
algorithm: encVarData.algorithm,
|
||||
lastRotatedAt: new Date(),
|
||||
statusMessage: "Rotated successfull",
|
||||
status: "success"
|
||||
},
|
||||
tx
|
||||
);
|
||||
const updatedSecrets = await secretV2BridgeDAL.bulkUpdate(
|
||||
encryptedSecrets.map(({ secretId, value }) => ({
|
||||
// this secret id is validated when user is inserted
|
||||
filter: { id: secretId, type: SecretType.Shared },
|
||||
data: {
|
||||
encryptedValue: secretManagerEncryptor({ plainText: Buffer.from(value) }).cipherTextBlob
|
||||
}
|
||||
})),
|
||||
tx
|
||||
);
|
||||
await secretVersionV2BridgeDAL.insertMany(
|
||||
updatedSecrets.map(({ id, updatedAt, createdAt, ...el }) => ({
|
||||
...el,
|
||||
secretId: id
|
||||
})),
|
||||
tx
|
||||
);
|
||||
});
|
||||
} else {
|
||||
if (!botKey) throw new BadRequestError({ message: "Bot not found" });
|
||||
const encryptedSecrets = rotationOutputs.map(({ key: outputKey, secretId }) => ({
|
||||
secretId,
|
||||
value: encryptSymmetric128BitHexKeyUTF8(
|
||||
typeof newCredential.outputs[outputKey] === "object"
|
||||
? JSON.stringify(newCredential.outputs[outputKey])
|
||||
: String(newCredential.outputs[outputKey]),
|
||||
botKey
|
||||
)
|
||||
}));
|
||||
// map the final values to output keys in the board
|
||||
await secretRotationDAL.transaction(async (tx) => {
|
||||
await secretRotationDAL.updateById(
|
||||
rotationId,
|
||||
{
|
||||
encryptedData: encVarData.ciphertext,
|
||||
encryptedDataIV: encVarData.iv,
|
||||
encryptedDataTag: encVarData.tag,
|
||||
keyEncoding: encVarData.encoding,
|
||||
algorithm: encVarData.algorithm,
|
||||
lastRotatedAt: new Date(),
|
||||
statusMessage: "Rotated successfull",
|
||||
status: "success"
|
||||
},
|
||||
tx
|
||||
);
|
||||
const updatedSecrets = await secretDAL.bulkUpdate(
|
||||
encryptedSecrets.map(({ secretId, value }) => ({
|
||||
// this secret id is validated when user is inserted
|
||||
filter: { id: secretId, type: SecretType.Shared },
|
||||
data: {
|
||||
secretValueCiphertext: value.ciphertext,
|
||||
secretValueIV: value.iv,
|
||||
secretValueTag: value.tag
|
||||
}
|
||||
})),
|
||||
tx
|
||||
);
|
||||
await secretVersionDAL.insertMany(
|
||||
updatedSecrets.map(({ id, updatedAt, createdAt, ...el }) => {
|
||||
if (!el.secretBlindIndex) throw new BadRequestError({ message: "Missing blind index" });
|
||||
return {
|
||||
...el,
|
||||
secretId: id,
|
||||
secretBlindIndex: el.secretBlindIndex
|
||||
};
|
||||
}),
|
||||
tx
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
await telemetryService.sendPostHogEvents({
|
||||
event: PostHogEventTypes.SecretRotated,
|
||||
distinctId: "",
|
||||
properties: {
|
||||
numberOfSecrets: encryptedSecrets.length,
|
||||
numberOfSecrets: numberOfSecretsRotated,
|
||||
environment: secretRotation.environment.slug,
|
||||
secretPath: secretRotation.secretPath,
|
||||
workspaceId: secretRotation.projectId
|
||||
|
@ -1,12 +1,15 @@
|
||||
import { ForbiddenError, subject } from "@casl/ability";
|
||||
import Ajv from "ajv";
|
||||
|
||||
import { infisicalSymmetricEncypt } from "@app/lib/crypto/encryption";
|
||||
import { ProjectVersion } from "@app/db/schemas";
|
||||
import { decryptSymmetric128BitHexKeyUTF8, infisicalSymmetricEncypt } from "@app/lib/crypto/encryption";
|
||||
import { BadRequestError } from "@app/lib/errors";
|
||||
import { TProjectPermission } from "@app/lib/types";
|
||||
import { TProjectDALFactory } from "@app/services/project/project-dal";
|
||||
import { TProjectBotServiceFactory } from "@app/services/project-bot/project-bot-service";
|
||||
import { TSecretDALFactory } from "@app/services/secret/secret-dal";
|
||||
import { TSecretFolderDALFactory } from "@app/services/secret-folder/secret-folder-dal";
|
||||
import { TSecretV2BridgeDALFactory } from "@app/services/secret-v2-bridge/secret-v2-bridge-dal";
|
||||
|
||||
import { TLicenseServiceFactory } from "../license/license-service";
|
||||
import { TPermissionServiceFactory } from "../permission/permission-service";
|
||||
@ -22,9 +25,11 @@ type TSecretRotationServiceFactoryDep = {
|
||||
projectDAL: Pick<TProjectDALFactory, "findById">;
|
||||
folderDAL: Pick<TSecretFolderDALFactory, "findBySecretPath">;
|
||||
secretDAL: Pick<TSecretDALFactory, "find">;
|
||||
secretV2BridgeDAL: Pick<TSecretV2BridgeDALFactory, "find">;
|
||||
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
|
||||
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
|
||||
secretRotationQueue: TSecretRotationQueueFactory;
|
||||
projectBotService: Pick<TProjectBotServiceFactory, "getBotKey">;
|
||||
};
|
||||
|
||||
export type TSecretRotationServiceFactory = ReturnType<typeof secretRotationServiceFactory>;
|
||||
@ -37,7 +42,9 @@ export const secretRotationServiceFactory = ({
|
||||
licenseService,
|
||||
projectDAL,
|
||||
folderDAL,
|
||||
secretDAL
|
||||
secretDAL,
|
||||
projectBotService,
|
||||
secretV2BridgeDAL
|
||||
}: TSecretRotationServiceFactoryDep) => {
|
||||
const getProviderTemplates = async ({
|
||||
actor,
|
||||
@ -92,15 +99,25 @@ export const secretRotationServiceFactory = ({
|
||||
ProjectPermissionActions.Edit,
|
||||
subject(ProjectPermissionSub.Secrets, { environment, secretPath })
|
||||
);
|
||||
|
||||
const selectedSecrets = await secretDAL.find({
|
||||
folderId: folder.id,
|
||||
$in: { id: Object.values(outputs) }
|
||||
});
|
||||
if (selectedSecrets.length !== Object.values(outputs).length)
|
||||
throw new BadRequestError({ message: "Secrets not found" });
|
||||
|
||||
const project = await projectDAL.findById(projectId);
|
||||
const shouldUseBridge = project.version === ProjectVersion.V3;
|
||||
|
||||
if (shouldUseBridge) {
|
||||
const selectedSecrets = await secretV2BridgeDAL.find({
|
||||
folderId: folder.id,
|
||||
$in: { id: Object.values(outputs) }
|
||||
});
|
||||
if (selectedSecrets.length !== Object.values(outputs).length)
|
||||
throw new BadRequestError({ message: "Secrets not found" });
|
||||
} else {
|
||||
const selectedSecrets = await secretDAL.find({
|
||||
folderId: folder.id,
|
||||
$in: { id: Object.values(outputs) }
|
||||
});
|
||||
if (selectedSecrets.length !== Object.values(outputs).length)
|
||||
throw new BadRequestError({ message: "Secrets not found" });
|
||||
}
|
||||
|
||||
const plan = await licenseService.getPlan(project.orgId);
|
||||
if (!plan.secretRotation)
|
||||
throw new BadRequestError({
|
||||
@ -148,10 +165,18 @@ export const secretRotationServiceFactory = ({
|
||||
},
|
||||
tx
|
||||
);
|
||||
const outputSecretMapping = await secretRotationDAL.secretOutputInsertMany(
|
||||
Object.entries(outputs).map(([key, secretId]) => ({ key, secretId, rotationId: doc.id })),
|
||||
tx
|
||||
);
|
||||
let outputSecretMapping;
|
||||
if (shouldUseBridge) {
|
||||
outputSecretMapping = await secretRotationDAL.secretOutputV2InsertMany(
|
||||
Object.entries(outputs).map(([key, secretId]) => ({ key, secretId, rotationId: doc.id })),
|
||||
tx
|
||||
);
|
||||
} else {
|
||||
outputSecretMapping = await secretRotationDAL.secretOutputInsertMany(
|
||||
Object.entries(outputs).map(([key, secretId]) => ({ key, secretId, rotationId: doc.id })),
|
||||
tx
|
||||
);
|
||||
}
|
||||
return { ...doc, outputs: outputSecretMapping, environment: folder.environment };
|
||||
});
|
||||
await secretRotationQueue.addToQueue(secretRotation.id, secretRotation.interval);
|
||||
@ -167,8 +192,30 @@ export const secretRotationServiceFactory = ({
|
||||
actorOrgId
|
||||
);
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.SecretRotation);
|
||||
const doc = await secretRotationDAL.find({ projectId });
|
||||
return doc;
|
||||
const { botKey, shouldUseSecretV2Bridge } = await projectBotService.getBotKey(projectId);
|
||||
if (shouldUseSecretV2Bridge) {
|
||||
const docs = await secretRotationDAL.findSecretV2({ projectId });
|
||||
return docs;
|
||||
}
|
||||
|
||||
if (!botKey) throw new BadRequestError({ message: "bot not found" });
|
||||
const docs = await secretRotationDAL.find({ projectId });
|
||||
return docs.map((el) => ({
|
||||
...el,
|
||||
outputs: el.outputs.map((output) => ({
|
||||
...output,
|
||||
secret: {
|
||||
id: output.secret.id,
|
||||
version: output.secret.version,
|
||||
secretKey: decryptSymmetric128BitHexKeyUTF8({
|
||||
ciphertext: output.secret.secretKeyCiphertext,
|
||||
iv: output.secret.secretKeyIV,
|
||||
tag: output.secret.secretKeyTag,
|
||||
key: botKey
|
||||
})
|
||||
}
|
||||
}))
|
||||
}));
|
||||
};
|
||||
|
||||
const restartById = async ({ actor, actorId, actorOrgId, actorAuthMethod, rotationId }: TRestartDTO) => {
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user